@vibesdotdev/terminal-client 0.0.0-stage → 1.0.0-rc.2
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 +18 -0
- package/README.md +102 -2
- package/dist/ansi/index.d.ts +11 -0
- package/dist/ansi/index.d.ts.map +1 -0
- package/dist/ansi/index.js +11 -0
- package/dist/ansi/index.js.map +1 -0
- package/dist/ansi/keymap.d.ts +77 -0
- package/dist/ansi/keymap.d.ts.map +1 -0
- package/dist/ansi/keymap.js +257 -0
- package/dist/ansi/keymap.js.map +1 -0
- package/dist/api-client-plugin.d.ts +4 -0
- package/dist/api-client-plugin.d.ts.map +1 -0
- package/dist/api-client-plugin.js +16 -0
- package/dist/api-client-plugin.js.map +1 -0
- package/dist/client/http-session.d.ts +44 -0
- package/dist/client/http-session.d.ts.map +1 -0
- package/dist/client/http-session.js +457 -0
- package/dist/client/http-session.js.map +1 -0
- package/dist/client/index.d.ts +8 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +8 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client.d.ts +56 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +169 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/pty-bridge/index.d.ts +15 -0
- package/dist/pty-bridge/index.d.ts.map +1 -0
- package/dist/pty-bridge/index.js +15 -0
- package/dist/pty-bridge/index.js.map +1 -0
- package/dist/pty-bridge/protocol.d.ts +6 -0
- package/dist/pty-bridge/protocol.d.ts.map +1 -0
- package/dist/pty-bridge/protocol.js +6 -0
- package/dist/pty-bridge/protocol.js.map +1 -0
- package/dist/pty-bridge/server.d.ts +7 -0
- package/dist/pty-bridge/server.d.ts.map +1 -0
- package/dist/pty-bridge/server.js +9 -0
- package/dist/pty-bridge/server.js.map +1 -0
- package/dist/remote-session.d.ts +51 -0
- package/dist/remote-session.d.ts.map +1 -0
- package/dist/remote-session.js +326 -0
- package/dist/remote-session.js.map +1 -0
- package/dist/schemas/http.d.ts +61 -0
- package/dist/schemas/http.d.ts.map +1 -0
- package/dist/schemas/http.js +45 -0
- package/dist/schemas/http.js.map +1 -0
- package/dist/schemas/remote.d.ts +25 -0
- package/dist/schemas/remote.d.ts.map +1 -0
- package/dist/schemas/remote.js +23 -0
- package/dist/schemas/remote.js.map +1 -0
- package/dist/schemas/session.d.ts +26 -0
- package/dist/schemas/session.d.ts.map +1 -0
- package/dist/schemas/session.js +17 -0
- package/dist/schemas/session.js.map +1 -0
- package/dist/schemas/store.d.ts +29 -0
- package/dist/schemas/store.d.ts.map +1 -0
- package/dist/schemas/store.js +22 -0
- package/dist/schemas/store.js.map +1 -0
- package/dist/schemas/terminal.d.ts +296 -0
- package/dist/schemas/terminal.d.ts.map +1 -0
- package/dist/schemas/terminal.js +243 -0
- package/dist/schemas/terminal.js.map +1 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +9 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/session-store.d.ts +176 -0
- package/dist/server/session-store.d.ts.map +1 -0
- package/dist/server/session-store.js +462 -0
- package/dist/server/session-store.js.map +1 -0
- package/dist/server/sse-stream.d.ts +32 -0
- package/dist/server/sse-stream.d.ts.map +1 -0
- package/dist/server/sse-stream.js +87 -0
- package/dist/server/sse-stream.js.map +1 -0
- package/dist/svelte/HostPicker.svelte +256 -0
- package/dist/svelte/HostPicker.svelte.d.ts +30 -0
- package/dist/svelte/HostPicker.svelte.d.ts.map +1 -0
- package/dist/svelte/README.md +52 -0
- package/dist/svelte/ReconnectBanner.svelte +88 -0
- package/dist/svelte/ReconnectBanner.svelte.d.ts +16 -0
- package/dist/svelte/ReconnectBanner.svelte.d.ts.map +1 -0
- package/dist/svelte/SessionTabs.svelte +192 -0
- package/dist/svelte/SessionTabs.svelte.d.ts +17 -0
- package/dist/svelte/SessionTabs.svelte.d.ts.map +1 -0
- package/dist/svelte/Terminal.svelte +567 -0
- package/dist/svelte/Terminal.svelte.d.ts +25 -0
- package/dist/svelte/Terminal.svelte.d.ts.map +1 -0
- package/dist/svelte/TerminalMobileToolbar.svelte +56 -0
- package/dist/svelte/TerminalMobileToolbar.svelte.d.ts +14 -0
- package/dist/svelte/TerminalMobileToolbar.svelte.d.ts.map +1 -0
- package/dist/svelte/TerminalWorkspace.svelte +562 -0
- package/dist/svelte/TerminalWorkspace.svelte.d.ts +25 -0
- package/dist/svelte/TerminalWorkspace.svelte.d.ts.map +1 -0
- package/dist/svelte/ambient.d.ts +9 -0
- package/dist/svelte/docs/terminal-svelte.docs.descriptor.d.ts +4 -0
- package/dist/svelte/docs/terminal-svelte.docs.descriptor.d.ts.map +1 -0
- package/dist/svelte/docs/terminal-svelte.docs.descriptor.js +74 -0
- package/dist/svelte/index.d.ts +27 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +26 -0
- package/dist/svelte/styles.css +8 -0
- package/dist/svelte/terminal-session-authority.d.ts +17 -0
- package/dist/svelte/terminal-session-authority.d.ts.map +1 -0
- package/dist/svelte/terminal-session-authority.js +25 -0
- package/dist/svelte/terminal-svelte.plugin.d.ts +19 -0
- package/dist/svelte/terminal-svelte.plugin.d.ts.map +1 -0
- package/dist/svelte/terminal-svelte.plugin.js +42 -0
- package/dist/svelte/terminal-theme.d.ts +11 -0
- package/dist/svelte/terminal-theme.d.ts.map +1 -0
- package/dist/svelte/terminal-theme.js +116 -0
- package/dist/svelte/tool-widgets/TerminalToolCard.svelte +65 -0
- package/dist/svelte/tool-widgets/TerminalToolCard.svelte.d.ts +5 -0
- package/dist/svelte/tool-widgets/TerminalToolCard.svelte.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/terminal-tool-card.d.ts +25 -0
- package/dist/svelte/tool-widgets/terminal-tool-card.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/terminal-tool-card.js +116 -0
- package/dist/svelte/types.d.ts +160 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +51 -0
- package/dist/svelte/workspace.d.ts +3 -0
- package/dist/svelte/workspace.d.ts.map +1 -0
- package/dist/svelte/workspace.js +2 -0
- package/dist/terminal-client.plugin.d.ts +3 -0
- package/dist/terminal-client.plugin.d.ts.map +1 -0
- package/dist/terminal-client.plugin.js +3 -0
- package/dist/terminal-client.plugin.js.map +1 -0
- package/dist/terminal-runtime.d.ts +30 -0
- package/dist/terminal-runtime.d.ts.map +1 -0
- package/dist/terminal-runtime.js +673 -0
- package/dist/terminal-runtime.js.map +1 -0
- package/dist/terminal.consumer.plugin.d.ts +14 -0
- package/dist/terminal.consumer.plugin.d.ts.map +1 -0
- package/dist/terminal.consumer.plugin.js +31 -0
- package/dist/terminal.consumer.plugin.js.map +1 -0
- package/dist/tools/terminal.impl.consumer.d.ts +11 -0
- package/dist/tools/terminal.impl.consumer.d.ts.map +1 -0
- package/dist/tools/terminal.impl.consumer.js +20 -0
- package/dist/tools/terminal.impl.consumer.js.map +1 -0
- package/dist/tools/terminal.schemas.d.ts +3 -0
- package/dist/tools/terminal.schemas.d.ts.map +1 -0
- package/dist/tools/terminal.schemas.js +3 -0
- package/dist/tools/terminal.schemas.js.map +1 -0
- package/dist/tools/terminal.unified.descriptor.d.ts +218 -0
- package/dist/tools/terminal.unified.descriptor.d.ts.map +1 -0
- package/dist/tools/terminal.unified.descriptor.js +36 -0
- package/dist/tools/terminal.unified.descriptor.js.map +1 -0
- package/dist/tui/index.d.ts +9 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +8 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/terminal-open.descriptor.d.ts +9 -0
- package/dist/tui/terminal-open.descriptor.d.ts.map +1 -0
- package/dist/tui/terminal-open.descriptor.js +18 -0
- package/dist/tui/terminal-open.descriptor.js.map +1 -0
- package/dist/tui/terminal-open.impl.consumer.d.ts +9 -0
- package/dist/tui/terminal-open.impl.consumer.d.ts.map +1 -0
- package/dist/tui/terminal-open.impl.consumer.js +13 -0
- package/dist/tui/terminal-open.impl.consumer.js.map +1 -0
- package/dist/tui/terminal-registry.descriptor.d.ts +11 -0
- package/dist/tui/terminal-registry.descriptor.d.ts.map +1 -0
- package/dist/tui/terminal-registry.descriptor.js +17 -0
- package/dist/tui/terminal-registry.descriptor.js.map +1 -0
- package/dist/tui/terminal-registry.impl.consumer.d.ts +71 -0
- package/dist/tui/terminal-registry.impl.consumer.d.ts.map +1 -0
- package/dist/tui/terminal-registry.impl.consumer.js +190 -0
- package/dist/tui/terminal-registry.impl.consumer.js.map +1 -0
- package/dist/tui/terminal-tui.plugin.d.ts +10 -0
- package/dist/tui/terminal-tui.plugin.d.ts.map +1 -0
- package/dist/tui/terminal-tui.plugin.js +32 -0
- package/dist/tui/terminal-tui.plugin.js.map +1 -0
- package/dist/tui/terminal.descriptor.d.ts +10 -0
- package/dist/tui/terminal.descriptor.d.ts.map +1 -0
- package/dist/tui/terminal.descriptor.js +25 -0
- package/dist/tui/terminal.descriptor.js.map +1 -0
- package/dist/tui/terminal.impl.consumer.d.ts +14 -0
- package/dist/tui/terminal.impl.consumer.d.ts.map +1 -0
- package/dist/tui/terminal.impl.consumer.js +189 -0
- package/dist/tui/terminal.impl.consumer.js.map +1 -0
- package/dist/tui/terminal.state-key.d.ts +30 -0
- package/dist/tui/terminal.state-key.d.ts.map +1 -0
- package/dist/tui/terminal.state-key.js +9 -0
- package/dist/tui/terminal.state-key.js.map +1 -0
- package/dist/tui/terminal.state.d.ts +152 -0
- package/dist/tui/terminal.state.d.ts.map +1 -0
- package/dist/tui/terminal.state.js +149 -0
- package/dist/tui/terminal.state.js.map +1 -0
- package/dist/workspace.d.ts +155 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +292 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +228 -4
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Unified Tool Implementation (consumer)
|
|
3
|
+
*
|
|
4
|
+
* `execute` / `spawn` call `runtime/execution` (pipes). `session` / `open` open
|
|
5
|
+
* a PTY via the per-runtime `TerminalSessionStore`. One store per runtime;
|
|
6
|
+
* PTY ownership is keyed on the caller's participant/connection. A spawned
|
|
7
|
+
* process is held per owner too: its `procId` is a handle `read`, `wait`,
|
|
8
|
+
* `list` and `close` accept, with its stdout/stderr kept in a backlog ring.
|
|
9
|
+
*/
|
|
10
|
+
import { BaseRuntimeClient } from '@vibesdotdev/kernel/client';
|
|
11
|
+
import { CapabilityError, CaughtFailureSchema, composeChildEnv, isCapabilityError, settle, valueFreeFailure } from '@vibesdotdev/kernel';
|
|
12
|
+
import { getLogger } from '@vibesdotdev/logging';
|
|
13
|
+
import { resolveSequence } from '@vibesdotdev/terminal-client/ansi';
|
|
14
|
+
import * as z from 'zod/v4';
|
|
15
|
+
import { openLocalSession } from "./client.js";
|
|
16
|
+
import { createTerminalSessionStore } from "./server/session-store.js";
|
|
17
|
+
import { TerminalInputSchema } from "./tools/terminal.schemas.js";
|
|
18
|
+
/** Max concurrent PTY sessions per owner before `open` is rejected. */
|
|
19
|
+
const MAX_SESSIONS_PER_OWNER = 8;
|
|
20
|
+
/** Consecutive `wait` calls that saw no new output allowed on one session before the tool pushes back. */
|
|
21
|
+
const MAX_CONSECUTIVE_WAITS = 3;
|
|
22
|
+
/** Output chunks a spawned process keeps for `read`/`wait`, as the PTY backlog ring does. */
|
|
23
|
+
const MAX_SPAWN_BACKLOG = 256;
|
|
24
|
+
/** How long a wait that saw the exit gives the pipes to deliver their last output. */
|
|
25
|
+
const SPAWN_DRAIN_JOIN_MS = 1_000;
|
|
26
|
+
const ContextIdentitySchema = z.string().trim().min(1).max(512);
|
|
27
|
+
function createState(runtime) {
|
|
28
|
+
const produced = new Map();
|
|
29
|
+
const store = createTerminalSessionStore(runtime, async (opts) => {
|
|
30
|
+
const session = await openLocalSession(runtime, {
|
|
31
|
+
command: opts.command,
|
|
32
|
+
args: opts.args,
|
|
33
|
+
cwd: opts.cwd,
|
|
34
|
+
env: opts.env,
|
|
35
|
+
cols: opts.cols,
|
|
36
|
+
rows: opts.rows
|
|
37
|
+
});
|
|
38
|
+
produced.set(session.id, 0);
|
|
39
|
+
// Attached first — before the store's backlog listener — so `produced`
|
|
40
|
+
// is always >= the number of chunks retained in the backlog ring.
|
|
41
|
+
const unsubscribe = session.onOutput(() => {
|
|
42
|
+
produced.set(session.id, (produced.get(session.id) ?? 0) + 1);
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
id: session.id,
|
|
46
|
+
environmentId: session.environmentId,
|
|
47
|
+
write: session.write.bind(session),
|
|
48
|
+
resize: session.resize.bind(session),
|
|
49
|
+
signal: session.signal.bind(session),
|
|
50
|
+
onOutput: session.onOutput.bind(session),
|
|
51
|
+
kill() { unsubscribe(); session.kill(); },
|
|
52
|
+
exited: () => session.exited().finally(() => { unsubscribe(); })
|
|
53
|
+
};
|
|
54
|
+
}, { maxSessions: MAX_SESSIONS_PER_OWNER });
|
|
55
|
+
return { closed: false, store, produced, waitStreak: new Map(), spawned: new Map() };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Keep a spawned process's stream in its backlog ring until the stream ends.
|
|
59
|
+
* Disposal joins owned work before the tool's own disposal kills the process,
|
|
60
|
+
* so the read stops when disposal begins instead of holding it for a live process.
|
|
61
|
+
*/
|
|
62
|
+
function drainInto(runtime, procId, entry, stream) {
|
|
63
|
+
if (stream === null)
|
|
64
|
+
return;
|
|
65
|
+
const reader = stream.getReader();
|
|
66
|
+
let reading = true;
|
|
67
|
+
entry.stopDrains.push(() => reading ? reader.cancel() : Promise.resolve());
|
|
68
|
+
const lifecycle = runtime.lifecycle;
|
|
69
|
+
const drained = settle(() => lifecycle.own(async () => {
|
|
70
|
+
const stop = () => void settle(() => reader.cancel());
|
|
71
|
+
lifecycle.signal.addEventListener('abort', stop, { once: true });
|
|
72
|
+
try {
|
|
73
|
+
for (;;) {
|
|
74
|
+
const next = await reader.read();
|
|
75
|
+
if (next.done)
|
|
76
|
+
return;
|
|
77
|
+
entry.produced += 1;
|
|
78
|
+
entry.backlog.push(next.value);
|
|
79
|
+
if (entry.backlog.length > MAX_SPAWN_BACKLOG)
|
|
80
|
+
entry.backlog.shift();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
lifecycle.signal.removeEventListener('abort', stop);
|
|
85
|
+
reading = false;
|
|
86
|
+
reader.releaseLock();
|
|
87
|
+
}
|
|
88
|
+
})).then((read) => {
|
|
89
|
+
if (read.ok)
|
|
90
|
+
return;
|
|
91
|
+
getLogger('terminal.tool', runtime).warn('spawned process output stopped being read', {
|
|
92
|
+
procId,
|
|
93
|
+
failure: valueFreeFailure(read.cause)
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
entry.drains.push(drained);
|
|
97
|
+
}
|
|
98
|
+
/** Record a spawned process's exit on its entry when the handle reports it. */
|
|
99
|
+
function observeExit(runtime, procId, entry) {
|
|
100
|
+
void settle(() => entry.handle.exited).then((exit) => {
|
|
101
|
+
entry.exited = true;
|
|
102
|
+
if (exit.ok) {
|
|
103
|
+
entry.exitCode = exit.value.code;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
getLogger('terminal.tool', runtime).warn('spawned process exit could not be observed', {
|
|
107
|
+
procId,
|
|
108
|
+
failure: valueFreeFailure(exit.cause)
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** Bound participant custody survives reconnects; unbound MCP custody is connection-local. */
|
|
113
|
+
function ownerOf(ctx) {
|
|
114
|
+
const participant = ctx?.caller?.participantId?.trim();
|
|
115
|
+
if (participant)
|
|
116
|
+
return `participant:${participant}`;
|
|
117
|
+
const md = ctx?.metadata;
|
|
118
|
+
for (const key of ['mcp.transportSessionId', 'mcp.connectionKey']) {
|
|
119
|
+
const value = ContextIdentitySchema.safeParse(md?.[key]);
|
|
120
|
+
if (value.success)
|
|
121
|
+
return `${key}:${value.data}`;
|
|
122
|
+
}
|
|
123
|
+
if (md?.['mcp.transportMode'] !== undefined) {
|
|
124
|
+
throw new CapabilityError('Terminal caller identity is unavailable.', 'AUTH_REQUIRED', {
|
|
125
|
+
reason: 'terminal-caller-identity-missing',
|
|
126
|
+
retryable: false
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const agent = ctx?.caller?.agentId?.trim();
|
|
130
|
+
if (agent)
|
|
131
|
+
return `agent:${agent}`;
|
|
132
|
+
// Direct surfaces may provide their contextual session or authenticated user.
|
|
133
|
+
for (const key of ['sessionId', 'codeSessionId', 'userId', 'agentId', 'roomId']) {
|
|
134
|
+
const value = ContextIdentitySchema.safeParse(md?.[key]);
|
|
135
|
+
if (value.success)
|
|
136
|
+
return `${key}:${value.data}`;
|
|
137
|
+
}
|
|
138
|
+
throw new CapabilityError('Terminal caller identity is unavailable.', 'AUTH_REQUIRED', {
|
|
139
|
+
reason: 'terminal-caller-identity-missing',
|
|
140
|
+
retryable: false
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// ANSI escape-sequence matcher (CSI / OSC / SS2-3 / single escapes). Mirrors the
|
|
144
|
+
// widely-used `ansi-regex` pattern; used to strip control sequences for `read`.
|
|
145
|
+
// The three control chars (ESC/CSI/BEL) are built at runtime via String.fromCharCode
|
|
146
|
+
// so the regex source carries no literal control character (satisfies no-control-regex
|
|
147
|
+
// without a lint suppression).
|
|
148
|
+
const ANSI_ESC = String.fromCharCode(0x1b);
|
|
149
|
+
const ANSI_CSI = String.fromCharCode(0x9b);
|
|
150
|
+
const ANSI_BEL = String.fromCharCode(0x07);
|
|
151
|
+
const ANSI_REGEX = new RegExp(`[${ANSI_ESC}${ANSI_CSI}][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?${ANSI_BEL})|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))`, 'g');
|
|
152
|
+
/** Parse a key spec like "ctrl+c", "alt+x", "shift+Tab", or plain "Enter". */
|
|
153
|
+
function parseChord(spec) {
|
|
154
|
+
const parts = spec
|
|
155
|
+
.split('+')
|
|
156
|
+
.map((part) => part.trim())
|
|
157
|
+
.filter((part) => part.length > 0);
|
|
158
|
+
const key = parts.length > 0 ? parts[parts.length - 1] : spec;
|
|
159
|
+
const chord = { key };
|
|
160
|
+
for (const modifier of parts.slice(0, -1)) {
|
|
161
|
+
const name = modifier.toLowerCase();
|
|
162
|
+
if (name === 'ctrl' || name === 'control')
|
|
163
|
+
chord.ctrl = true;
|
|
164
|
+
else if (name === 'alt' || name === 'option')
|
|
165
|
+
chord.alt = true;
|
|
166
|
+
else if (name === 'shift')
|
|
167
|
+
chord.shift = true;
|
|
168
|
+
else if (name === 'meta' || name === 'cmd' || name === 'super' || name === 'win')
|
|
169
|
+
chord.meta = true;
|
|
170
|
+
}
|
|
171
|
+
return chord;
|
|
172
|
+
}
|
|
173
|
+
function buildWriteBytes(input) {
|
|
174
|
+
const parts = [];
|
|
175
|
+
if (input.keys && input.keys.length > 0) {
|
|
176
|
+
for (const key of input.keys)
|
|
177
|
+
parts.push(resolveSequence(parseChord(key)));
|
|
178
|
+
}
|
|
179
|
+
if (input.text !== undefined)
|
|
180
|
+
parts.push(input.text);
|
|
181
|
+
const enter = input.enter ?? input.text !== undefined;
|
|
182
|
+
if (enter)
|
|
183
|
+
parts.push('\r');
|
|
184
|
+
return new TextEncoder().encode(parts.join(''));
|
|
185
|
+
}
|
|
186
|
+
function notFound() {
|
|
187
|
+
return new CapabilityError('The terminal session was not found.', 'NOT_FOUND', {
|
|
188
|
+
reason: 'terminal-session-not-found',
|
|
189
|
+
retryable: false
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function collectBacklog(stored, total, since, stripAnsi) {
|
|
193
|
+
const dropped = total - stored.backlog.length;
|
|
194
|
+
let chunks = [];
|
|
195
|
+
if (since < total) {
|
|
196
|
+
const startIndex = Math.max(since, dropped) - dropped;
|
|
197
|
+
chunks = stored.backlog.slice(startIndex);
|
|
198
|
+
}
|
|
199
|
+
const decoder = new TextDecoder();
|
|
200
|
+
let output = chunks.map((chunk) => decoder.decode(chunk, { stream: true })).join('');
|
|
201
|
+
output += decoder.decode();
|
|
202
|
+
if (stripAnsi)
|
|
203
|
+
output = output.replace(ANSI_REGEX, '');
|
|
204
|
+
return { output, cursor: total };
|
|
205
|
+
}
|
|
206
|
+
async function resolveExecution(runtime) {
|
|
207
|
+
return runtime.query('runtime/execution').resolve();
|
|
208
|
+
}
|
|
209
|
+
function scopedCwd(input, ctx) {
|
|
210
|
+
if (input.cwd !== undefined)
|
|
211
|
+
return input.cwd;
|
|
212
|
+
const md = ctx?.metadata ?? {};
|
|
213
|
+
for (const key of ['workspaceRoot', 'workspacePath']) {
|
|
214
|
+
const value = ContextIdentitySchema.safeParse(md[key]);
|
|
215
|
+
if (value.success)
|
|
216
|
+
return value.data;
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
async function dispatch(input, state, runtime, ctx) {
|
|
221
|
+
if (input.action === 'capabilities' || input.action === 'execute' || input.action === 'spawn') {
|
|
222
|
+
const exec = await resolveExecution(runtime);
|
|
223
|
+
const caps = exec.getCapabilities();
|
|
224
|
+
if (input.action === 'capabilities') {
|
|
225
|
+
return {
|
|
226
|
+
action: 'capabilities',
|
|
227
|
+
executionId: exec.id,
|
|
228
|
+
environmentId: exec.environmentId,
|
|
229
|
+
pty: caps.pty,
|
|
230
|
+
shell: caps.shell,
|
|
231
|
+
capabilities: caps
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (!caps.shell) {
|
|
235
|
+
throw new CapabilityError('The selected execution provider has no shell.', 'UNAVAILABLE', {
|
|
236
|
+
reason: 'terminal-shell-unavailable',
|
|
237
|
+
retryable: false
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const cwd = scopedCwd(input, ctx);
|
|
241
|
+
const command = input.command[0];
|
|
242
|
+
const args = input.command.slice(1);
|
|
243
|
+
// Exactly the composed environment (rc02-rc03 C15): the host's base set
|
|
244
|
+
// plus the variables the call declares.
|
|
245
|
+
const env = composeChildEnv(runtime, {
|
|
246
|
+
env: {
|
|
247
|
+
GIT_CONFIG_COUNT: '1',
|
|
248
|
+
GIT_CONFIG_KEY_0: 'commit.gpgsign',
|
|
249
|
+
GIT_CONFIG_VALUE_0: 'false',
|
|
250
|
+
...input.env
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
if (input.action === 'execute') {
|
|
254
|
+
const result = await exec.execute({
|
|
255
|
+
command,
|
|
256
|
+
args,
|
|
257
|
+
cwd,
|
|
258
|
+
env,
|
|
259
|
+
envMode: 'replace',
|
|
260
|
+
timeout: input.timeoutMs
|
|
261
|
+
});
|
|
262
|
+
return {
|
|
263
|
+
action: 'execute',
|
|
264
|
+
command: input.command,
|
|
265
|
+
cwd,
|
|
266
|
+
executionId: exec.id,
|
|
267
|
+
environmentId: exec.environmentId,
|
|
268
|
+
exitCode: result.exitCode,
|
|
269
|
+
stdout: result.stdout,
|
|
270
|
+
stderr: result.stderr,
|
|
271
|
+
durationMs: result.durationMs,
|
|
272
|
+
timedOut: result.timedOut,
|
|
273
|
+
pty: caps.pty,
|
|
274
|
+
shell: caps.shell
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// The handle is held for its owner, so `read`/`wait`/`close` can reach it.
|
|
278
|
+
const owner = ownerOf(ctx);
|
|
279
|
+
const handle = await exec.spawn({
|
|
280
|
+
command,
|
|
281
|
+
args,
|
|
282
|
+
cwd,
|
|
283
|
+
env,
|
|
284
|
+
envMode: 'replace',
|
|
285
|
+
stdout: 'pipe',
|
|
286
|
+
stderr: 'pipe',
|
|
287
|
+
// `close` (or disposal) must end what the process started, not only the wrapper.
|
|
288
|
+
killTree: true
|
|
289
|
+
});
|
|
290
|
+
if (state.closed) {
|
|
291
|
+
await handle.kill();
|
|
292
|
+
throw new CapabilityError('The terminal runtime is disposed.', 'UNAVAILABLE', { reason: 'terminal-runtime-disposed' });
|
|
293
|
+
}
|
|
294
|
+
const procId = String(handle.pid);
|
|
295
|
+
const entry = {
|
|
296
|
+
owner,
|
|
297
|
+
command: input.command.join(' '),
|
|
298
|
+
startedAt: runtime.lifecycle.now(),
|
|
299
|
+
handle,
|
|
300
|
+
backlog: [],
|
|
301
|
+
produced: 0,
|
|
302
|
+
exitCode: null,
|
|
303
|
+
exited: false,
|
|
304
|
+
drains: [],
|
|
305
|
+
stopDrains: []
|
|
306
|
+
};
|
|
307
|
+
state.spawned.set(procId, entry);
|
|
308
|
+
drainInto(runtime, procId, entry, handle.stdout);
|
|
309
|
+
drainInto(runtime, procId, entry, handle.stderr);
|
|
310
|
+
observeExit(runtime, procId, entry);
|
|
311
|
+
const present = input.present === true && !caps.pty
|
|
312
|
+
? { kind: 'log', procId }
|
|
313
|
+
: undefined;
|
|
314
|
+
const output = {
|
|
315
|
+
action: 'spawn',
|
|
316
|
+
command: input.command,
|
|
317
|
+
cwd,
|
|
318
|
+
executionId: exec.id,
|
|
319
|
+
environmentId: exec.environmentId,
|
|
320
|
+
procId,
|
|
321
|
+
running: true,
|
|
322
|
+
pty: caps.pty,
|
|
323
|
+
shell: caps.shell
|
|
324
|
+
};
|
|
325
|
+
if (present !== undefined)
|
|
326
|
+
output.present = present;
|
|
327
|
+
return output;
|
|
328
|
+
}
|
|
329
|
+
const owner = ownerOf(ctx);
|
|
330
|
+
const { store, produced, waitStreak } = state;
|
|
331
|
+
// Any action other than `wait` is real progress on that session.
|
|
332
|
+
if (input.action !== 'wait' && 'sessionId' in input && store.getOwnedSession(input.sessionId, owner)) {
|
|
333
|
+
waitStreak.delete(input.sessionId);
|
|
334
|
+
}
|
|
335
|
+
// A `procId` from `spawn` is a handle these actions accept like a session id.
|
|
336
|
+
const spawned = 'sessionId' in input ? ownedSpawn(state, input.sessionId, owner) : undefined;
|
|
337
|
+
if (spawned !== undefined && 'sessionId' in input) {
|
|
338
|
+
return dispatchSpawned(input, input.sessionId, spawned, state, runtime);
|
|
339
|
+
}
|
|
340
|
+
switch (input.action) {
|
|
341
|
+
case 'open':
|
|
342
|
+
case 'session': {
|
|
343
|
+
const command = Array.isArray(input.command)
|
|
344
|
+
? (input.command[0] ?? (runtime.getEnv('SHELL') || '/bin/sh'))
|
|
345
|
+
: (input.command ?? (runtime.getEnv('SHELL') || '/bin/sh'));
|
|
346
|
+
const args = Array.isArray(input.command)
|
|
347
|
+
? [...input.command.slice(1), ...(input.args ?? [])]
|
|
348
|
+
: input.args;
|
|
349
|
+
const cwd = scopedCwd(input, ctx);
|
|
350
|
+
const { id } = await store.openSession(owner, {
|
|
351
|
+
command,
|
|
352
|
+
args,
|
|
353
|
+
cwd,
|
|
354
|
+
env: input.env,
|
|
355
|
+
cols: input.cols,
|
|
356
|
+
rows: input.rows
|
|
357
|
+
});
|
|
358
|
+
const exec = await resolveExecution(runtime);
|
|
359
|
+
const caps = exec.getCapabilities();
|
|
360
|
+
const shouldPresent = input.present !== false;
|
|
361
|
+
const output = {
|
|
362
|
+
action: input.action,
|
|
363
|
+
sessionId: id,
|
|
364
|
+
cwd,
|
|
365
|
+
executionId: exec.id,
|
|
366
|
+
environmentId: exec.environmentId,
|
|
367
|
+
pty: caps.pty,
|
|
368
|
+
shell: caps.shell,
|
|
369
|
+
capabilities: caps
|
|
370
|
+
};
|
|
371
|
+
if (shouldPresent)
|
|
372
|
+
output.present = { kind: 'terminal', sessionId: id };
|
|
373
|
+
return output;
|
|
374
|
+
}
|
|
375
|
+
case 'list': {
|
|
376
|
+
// Cheap GC: drop cursor entries for sessions the store no longer tracks
|
|
377
|
+
// (killed, natural exit, or idle-reaped) so `produced` cannot leak.
|
|
378
|
+
for (const id of produced.keys()) {
|
|
379
|
+
if (!store.hasSession(id))
|
|
380
|
+
produced.delete(id);
|
|
381
|
+
}
|
|
382
|
+
const sessions = store.listSessions(owner).map((meta) => ({
|
|
383
|
+
id: meta.id,
|
|
384
|
+
command: meta.command,
|
|
385
|
+
createdAt: meta.startedAt,
|
|
386
|
+
status: meta.exited ? 'exited' : 'running'
|
|
387
|
+
}));
|
|
388
|
+
for (const [procId, entry] of state.spawned) {
|
|
389
|
+
if (entry.owner !== owner)
|
|
390
|
+
continue;
|
|
391
|
+
sessions.push({
|
|
392
|
+
id: procId,
|
|
393
|
+
command: entry.command,
|
|
394
|
+
createdAt: entry.startedAt,
|
|
395
|
+
status: entry.exited ? 'exited' : 'running'
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return { action: 'list', sessions };
|
|
399
|
+
}
|
|
400
|
+
case 'read': {
|
|
401
|
+
const stored = store.getOwnedSession(input.sessionId, owner);
|
|
402
|
+
if (!stored)
|
|
403
|
+
throw notFound();
|
|
404
|
+
// Same cheap sweep as `list` — prune cursors for vanished sessions.
|
|
405
|
+
for (const id of produced.keys()) {
|
|
406
|
+
if (!store.hasSession(id))
|
|
407
|
+
produced.delete(id);
|
|
408
|
+
}
|
|
409
|
+
const total = produced.get(input.sessionId) ?? stored.backlog.length;
|
|
410
|
+
const { output } = collectBacklog(stored, total, input.since ?? 0, input.stripAnsi ?? true);
|
|
411
|
+
// A read is real activity: refresh the idle clock so an actively polled
|
|
412
|
+
// but SSE-unattached session is not reaped as idle.
|
|
413
|
+
store.touchSession(input.sessionId, owner);
|
|
414
|
+
return { action: 'read', sessionId: input.sessionId, output, cursor: total };
|
|
415
|
+
}
|
|
416
|
+
case 'write': {
|
|
417
|
+
const stored = store.getOwnedSession(input.sessionId, owner);
|
|
418
|
+
if (!stored)
|
|
419
|
+
throw notFound();
|
|
420
|
+
const bytes = buildWriteBytes(input);
|
|
421
|
+
const ok = store.writeToSession(input.sessionId, owner, bytes);
|
|
422
|
+
if (ok)
|
|
423
|
+
store.touchSession(input.sessionId, owner);
|
|
424
|
+
return {
|
|
425
|
+
action: 'write',
|
|
426
|
+
sessionId: input.sessionId,
|
|
427
|
+
bytesWritten: ok ? bytes.length : 0
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
case 'resize': {
|
|
431
|
+
const ok = store.resizeSession(input.sessionId, owner, input.cols, input.rows);
|
|
432
|
+
if (!ok)
|
|
433
|
+
throw notFound();
|
|
434
|
+
store.touchSession(input.sessionId, owner);
|
|
435
|
+
return { action: 'resize', sessionId: input.sessionId, resized: true };
|
|
436
|
+
}
|
|
437
|
+
case 'wait': {
|
|
438
|
+
const stored = store.getOwnedSession(input.sessionId, owner);
|
|
439
|
+
if (!stored)
|
|
440
|
+
throw notFound();
|
|
441
|
+
const streak = (waitStreak.get(input.sessionId) ?? 0) + 1;
|
|
442
|
+
if (streak > MAX_CONSECUTIVE_WAITS) {
|
|
443
|
+
waitStreak.delete(input.sessionId);
|
|
444
|
+
throw new CapabilityError('The terminal wait limit has been reached.', 'RATE_LIMITED', {
|
|
445
|
+
reason: 'terminal-wait-limit-reached',
|
|
446
|
+
retryable: false
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
waitStreak.set(input.sessionId, streak);
|
|
450
|
+
store.touchSession(input.sessionId, owner);
|
|
451
|
+
const producedBefore = produced.get(input.sessionId) ?? stored.backlog.length;
|
|
452
|
+
const waitOutcome = await runtime.lifecycle
|
|
453
|
+
.wait(async () => stored.session.exited(), {
|
|
454
|
+
timeoutMs: input.timeoutMs ?? 30000,
|
|
455
|
+
reason: 'terminal wait'
|
|
456
|
+
})
|
|
457
|
+
.then((exited) => ({ kind: 'exit', exited }), (cause) => ({
|
|
458
|
+
kind: 'failure',
|
|
459
|
+
failure: CaughtFailureSchema.parse(cause)
|
|
460
|
+
}));
|
|
461
|
+
let result;
|
|
462
|
+
if (waitOutcome.kind === 'failure') {
|
|
463
|
+
if (!isWaitTimeout(waitOutcome.failure))
|
|
464
|
+
throw waitOutcome.failure;
|
|
465
|
+
result = { exitCode: null, timedOut: true };
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
result = { exitCode: waitOutcome.exited.exitCode, timedOut: false };
|
|
469
|
+
waitStreak.delete(input.sessionId);
|
|
470
|
+
}
|
|
471
|
+
const total = produced.get(input.sessionId) ?? stored.backlog.length;
|
|
472
|
+
// A wait that saw new output is progress, not a stalled poll.
|
|
473
|
+
if (total > producedBefore)
|
|
474
|
+
waitStreak.delete(input.sessionId);
|
|
475
|
+
const { output, cursor } = collectBacklog(stored, total, input.since ?? 0, input.stripAnsi ?? true);
|
|
476
|
+
return {
|
|
477
|
+
action: 'wait',
|
|
478
|
+
sessionId: input.sessionId,
|
|
479
|
+
...result,
|
|
480
|
+
output,
|
|
481
|
+
cursor
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
case 'close': {
|
|
485
|
+
const ok = store.killSession(input.sessionId, owner);
|
|
486
|
+
if (!ok)
|
|
487
|
+
throw notFound();
|
|
488
|
+
// Only drop the cursor once ownership is confirmed — a non-owner must
|
|
489
|
+
// never mutate the owner's read state.
|
|
490
|
+
produced.delete(input.sessionId);
|
|
491
|
+
waitStreak.delete(input.sessionId);
|
|
492
|
+
return { action: 'close', sessionId: input.sessionId, closed: true };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/** The caller's spawned process under `procId`; another owner's process is not found. */
|
|
497
|
+
function ownedSpawn(state, procId, owner) {
|
|
498
|
+
const entry = state.spawned.get(procId);
|
|
499
|
+
return entry?.owner === owner ? entry : undefined;
|
|
500
|
+
}
|
|
501
|
+
function spawnedOutput(entry, since, stripAnsi) {
|
|
502
|
+
return collectBacklog(entry, entry.produced, since ?? 0, stripAnsi ?? true);
|
|
503
|
+
}
|
|
504
|
+
/** `read` / `wait` / `close` on a spawned process; it has no PTY to `write` to or `resize`. */
|
|
505
|
+
async function dispatchSpawned(input, procId, entry, state, runtime) {
|
|
506
|
+
const { waitStreak } = state;
|
|
507
|
+
if (input.action !== 'wait')
|
|
508
|
+
waitStreak.delete(procId);
|
|
509
|
+
switch (input.action) {
|
|
510
|
+
case 'read': {
|
|
511
|
+
const { output, cursor } = spawnedOutput(entry, input.since, input.stripAnsi);
|
|
512
|
+
return { action: 'read', sessionId: procId, output, cursor };
|
|
513
|
+
}
|
|
514
|
+
case 'wait': {
|
|
515
|
+
const streak = (waitStreak.get(procId) ?? 0) + 1;
|
|
516
|
+
if (streak > MAX_CONSECUTIVE_WAITS) {
|
|
517
|
+
waitStreak.delete(procId);
|
|
518
|
+
throw new CapabilityError('The terminal wait limit has been reached.', 'RATE_LIMITED', {
|
|
519
|
+
reason: 'terminal-wait-limit-reached',
|
|
520
|
+
retryable: false
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
waitStreak.set(procId, streak);
|
|
524
|
+
const producedBefore = entry.produced;
|
|
525
|
+
const waitOutcome = await runtime.lifecycle
|
|
526
|
+
.wait(async () => entry.handle.exited, {
|
|
527
|
+
timeoutMs: input.timeoutMs ?? 30000,
|
|
528
|
+
reason: 'terminal wait'
|
|
529
|
+
})
|
|
530
|
+
.then((exited) => ({ kind: 'exit', exited }), (cause) => ({ kind: 'failure', failure: CaughtFailureSchema.parse(cause) }));
|
|
531
|
+
let result;
|
|
532
|
+
if (waitOutcome.kind === 'failure') {
|
|
533
|
+
if (!isWaitTimeout(waitOutcome.failure))
|
|
534
|
+
throw waitOutcome.failure;
|
|
535
|
+
result = { exitCode: null, timedOut: true };
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
result = { exitCode: waitOutcome.exited.code, timedOut: false };
|
|
539
|
+
waitStreak.delete(procId);
|
|
540
|
+
// An ended process's last output is still in its pipes: answer once both are read. A
|
|
541
|
+
// descendant can hold a pipe open past the exit, so the join is bounded.
|
|
542
|
+
const joined = await settle(() => runtime.lifecycle.wait(async () => {
|
|
543
|
+
await Promise.all(entry.drains);
|
|
544
|
+
}, { timeoutMs: SPAWN_DRAIN_JOIN_MS, reason: 'terminal spawned output' }));
|
|
545
|
+
if (!joined.ok && !isWaitTimeout(joined.cause))
|
|
546
|
+
throw joined.cause;
|
|
547
|
+
}
|
|
548
|
+
// A wait that saw new output is progress, not a stalled poll.
|
|
549
|
+
if (entry.produced > producedBefore)
|
|
550
|
+
waitStreak.delete(procId);
|
|
551
|
+
const { output, cursor } = spawnedOutput(entry, input.since, input.stripAnsi);
|
|
552
|
+
return { action: 'wait', sessionId: procId, ...result, output, cursor };
|
|
553
|
+
}
|
|
554
|
+
case 'close': {
|
|
555
|
+
if (!entry.exited)
|
|
556
|
+
await entry.handle.kill();
|
|
557
|
+
await Promise.all(entry.stopDrains.map(stop => stop()));
|
|
558
|
+
state.spawned.delete(procId);
|
|
559
|
+
return { action: 'close', sessionId: procId, closed: true };
|
|
560
|
+
}
|
|
561
|
+
case 'write':
|
|
562
|
+
case 'resize':
|
|
563
|
+
throw new CapabilityError(`A spawned process has no terminal to ${input.action}.`, 'INVALID_INPUT', {
|
|
564
|
+
reason: 'terminal-spawned-process-not-interactive',
|
|
565
|
+
retryable: false,
|
|
566
|
+
details: { procId, action: input.action }
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/** The kernel owns one terminal client and its lazy tool state per runtime. */
|
|
571
|
+
export class TerminalClient extends BaseRuntimeClient {
|
|
572
|
+
state = null;
|
|
573
|
+
sequence = 0;
|
|
574
|
+
remoteAbortCleanup;
|
|
575
|
+
stopRemoteSessions = () => {
|
|
576
|
+
// Start session cleanup on abort; RPC closes its connection and drains
|
|
577
|
+
// explicitly owned remote references. The finalizer joins every outcome.
|
|
578
|
+
this.remoteAbortCleanup = Promise.allSettled([...this.remoteSessions].map(async (close) => { await close(); }));
|
|
579
|
+
};
|
|
580
|
+
prepareRemoteSession() {
|
|
581
|
+
this.runtime.lifecycle.signal.addEventListener('abort', this.stopRemoteSessions, { once: true });
|
|
582
|
+
}
|
|
583
|
+
remoteSessions = new Set();
|
|
584
|
+
nextRemoteSessionId(hostId) {
|
|
585
|
+
return `remote:${hostId}:${++this.sequence}`;
|
|
586
|
+
}
|
|
587
|
+
ownRemoteSession(close) {
|
|
588
|
+
this.remoteSessions.add(close);
|
|
589
|
+
return () => this.remoteSessions.delete(close);
|
|
590
|
+
}
|
|
591
|
+
/** Open the HTTP transport on this runtime; hosts select the semantic client once. */
|
|
592
|
+
async openHttpSession(options) {
|
|
593
|
+
if (this.runtime.lifecycle.signal.aborted) {
|
|
594
|
+
throw new CapabilityError('The terminal runtime is disposed.', 'UNAVAILABLE', { reason: 'terminal-runtime-disposed' });
|
|
595
|
+
}
|
|
596
|
+
this.prepareRemoteSession();
|
|
597
|
+
const opening = import("./client/http-session.js").then(({ openHttpSession }) => openHttpSession(this.runtime, options));
|
|
598
|
+
let closing = false;
|
|
599
|
+
let cleanup;
|
|
600
|
+
const close = () => {
|
|
601
|
+
closing = true;
|
|
602
|
+
cleanup ??= (async () => {
|
|
603
|
+
const outcome = await settle(() => opening);
|
|
604
|
+
// The opening caller owns its refusal; cleanup owns only acquired sessions.
|
|
605
|
+
if (outcome.ok) {
|
|
606
|
+
outcome.value.session.kill();
|
|
607
|
+
await outcome.value.session.exited();
|
|
608
|
+
}
|
|
609
|
+
})();
|
|
610
|
+
return cleanup;
|
|
611
|
+
};
|
|
612
|
+
const release = this.ownRemoteSession(close);
|
|
613
|
+
try {
|
|
614
|
+
const opened = await opening;
|
|
615
|
+
if (closing || this.runtime.lifecycle.signal.aborted) {
|
|
616
|
+
await close();
|
|
617
|
+
throw new CapabilityError('The terminal runtime is disposed.', 'UNAVAILABLE', { reason: 'terminal-runtime-disposed' });
|
|
618
|
+
}
|
|
619
|
+
void opened.session.exited().then(release, release);
|
|
620
|
+
return opened;
|
|
621
|
+
}
|
|
622
|
+
catch (cause) {
|
|
623
|
+
release();
|
|
624
|
+
if (this.runtime.lifecycle.signal.aborted) {
|
|
625
|
+
throw new CapabilityError('The terminal runtime is disposed.', 'UNAVAILABLE', { reason: 'terminal-runtime-disposed' });
|
|
626
|
+
}
|
|
627
|
+
throw cause;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
execute(input, ctx) {
|
|
631
|
+
if (this.runtime.lifecycle.signal.aborted) {
|
|
632
|
+
throw new CapabilityError('The terminal runtime is disposed.', 'UNAVAILABLE', { reason: 'terminal-runtime-disposed' });
|
|
633
|
+
}
|
|
634
|
+
this.state ??= createState(this.runtime);
|
|
635
|
+
return dispatch(TerminalInputSchema.parse(input), this.state, this.runtime, ctx);
|
|
636
|
+
}
|
|
637
|
+
async onDispose() {
|
|
638
|
+
this.runtime.lifecycle.signal.removeEventListener('abort', this.stopRemoteSessions);
|
|
639
|
+
const abortCleanup = this.remoteAbortCleanup;
|
|
640
|
+
this.remoteAbortCleanup = undefined;
|
|
641
|
+
const state = this.state;
|
|
642
|
+
this.state = null;
|
|
643
|
+
if (state)
|
|
644
|
+
state.closed = true;
|
|
645
|
+
const cleanups = abortCleanup ? [] : [...this.remoteSessions];
|
|
646
|
+
for (const entry of state?.spawned.values() ?? []) {
|
|
647
|
+
if (!entry.exited)
|
|
648
|
+
cleanups.push(() => entry.handle.kill());
|
|
649
|
+
cleanups.push(...entry.stopDrains);
|
|
650
|
+
cleanups.push(async () => { await Promise.all(entry.drains); });
|
|
651
|
+
}
|
|
652
|
+
if (state)
|
|
653
|
+
cleanups.push(() => state.store.dispose());
|
|
654
|
+
const outcomes = await Promise.allSettled(cleanups.map(close => Promise.resolve().then(close)));
|
|
655
|
+
state?.produced.clear();
|
|
656
|
+
state?.waitStreak.clear();
|
|
657
|
+
state?.spawned.clear();
|
|
658
|
+
this.remoteSessions.clear();
|
|
659
|
+
this.sequence = 0;
|
|
660
|
+
const abortOutcomes = await abortCleanup ?? [];
|
|
661
|
+
const failures = [...abortOutcomes, ...outcomes].filter(result => result.status === 'rejected').map(result => result.reason);
|
|
662
|
+
if (failures.length === 1)
|
|
663
|
+
throw failures[0];
|
|
664
|
+
if (failures.length > 1)
|
|
665
|
+
throw new AggregateError(failures, 'Terminal client cleanup failed.');
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
export default TerminalClient;
|
|
669
|
+
/** `lifecycle.wait` rejects a lapsed deadline as `CapabilityError` code `TIMEOUT`, reason `wait-timeout`. */
|
|
670
|
+
function isWaitTimeout(error) {
|
|
671
|
+
return isCapabilityError(error) && error.code === 'TIMEOUT' && error.reason === 'wait-timeout';
|
|
672
|
+
}
|
|
673
|
+
//# sourceMappingURL=terminal-runtime.js.map
|