@myagentroam/node 0.11.74 → 0.12.1
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/dist/code-navigation/index.js +2 -0
- package/dist/code-navigation/languages/c-family.js +9 -0
- package/dist/code-navigation/languages/c-preprocessor.js +30 -0
- package/dist/code-navigation/languages/c.js +8 -0
- package/dist/code-navigation/languages/cpp.js +10 -0
- package/dist/code-navigation/languages/csharp.js +18 -0
- package/dist/code-navigation/languages/css.js +7 -0
- package/dist/code-navigation/languages/ecmascript.js +44 -0
- package/dist/code-navigation/languages/file-target.js +9 -0
- package/dist/code-navigation/languages/go.js +32 -0
- package/dist/code-navigation/languages/html.js +7 -0
- package/dist/code-navigation/languages/jvm.js +17 -0
- package/dist/code-navigation/languages/less.js +7 -0
- package/dist/code-navigation/languages/markdown.js +113 -0
- package/dist/code-navigation/languages/markup-reference.js +27 -0
- package/dist/code-navigation/languages/objective-c.js +11 -0
- package/dist/code-navigation/languages/objective-cpp.js +10 -0
- package/dist/code-navigation/languages/python.js +17 -0
- package/dist/code-navigation/languages/rust.js +18 -0
- package/dist/code-navigation/languages/sass.js +7 -0
- package/dist/code-navigation/languages/scss.js +7 -0
- package/dist/code-navigation/languages/stylesheet-reference.js +37 -0
- package/dist/code-navigation/languages/vue.js +75 -0
- package/dist/code-navigation/registry.js +72 -0
- package/dist/code-navigation/source-ranges.js +17 -0
- package/dist/code-navigation/symbol-maps.js +257 -0
- package/dist/code-navigation/symbol-matcher.js +48 -0
- package/dist/code-navigation/symbol-navigation.js +1 -0
- package/dist/code-navigation/types.js +1 -0
- package/dist/ctags-binary.js +3 -3
- package/dist/marctl/bin/marctl.mjs +6 -0
- package/dist/marctl/bin/marnodes.mjs +6 -0
- package/dist/marctl/bin/marscp.mjs +6 -0
- package/dist/marctl/bin/marssh.mjs +6 -0
- package/dist/marctl/package.json +1 -0
- package/dist/marctl/src/cli.mjs +271 -0
- package/dist/marctl/src/client.mjs +291 -0
- package/dist/marctl/src/config.mjs +46 -0
- package/dist/marctl/src/direct-control.mjs +225 -0
- package/dist/marctl/src/direct-errors.mjs +48 -0
- package/dist/marctl/src/direct-process.mjs +374 -0
- package/dist/marctl/src/direct-transport.mjs +235 -0
- package/dist/marctl/src/direct.mjs +421 -0
- package/dist/marctl/src/errors.mjs +11 -0
- package/dist/marctl/src/i18n/en-US.mjs +285 -0
- package/dist/marctl/src/i18n/index.mjs +20 -0
- package/dist/marctl/src/i18n/zh-CN.mjs +269 -0
- package/dist/marctl/src/scp-channel.mjs +99 -0
- package/dist/marctl/src/scp-copy.mjs +46 -0
- package/dist/marctl/src/scp-endpoint.mjs +29 -0
- package/dist/marctl/src/scp-protocol.mjs +2 -0
- package/dist/marctl/src/scp-worker.cjs +266 -0
- package/dist/marctl/src/scp-workers.mjs +179 -0
- package/dist/marctl/src/scp.mjs +64 -0
- package/dist/marctl/src/session-args.mjs +233 -0
- package/dist/marctl/src/session-input.mjs +43 -0
- package/dist/marctl/src/session-prepare.mjs +87 -0
- package/dist/marctl/src/session-target.mjs +26 -0
- package/dist/marctl/src/session-wait.mjs +232 -0
- package/dist/marctl/src/sessions.mjs +93 -0
- package/dist/marctl/src/terminal-frame.mjs +48 -0
- package/dist/marctl/src/terminal-interaction.mjs +177 -0
- package/dist/marctl/src/terminal-race.mjs +117 -0
- package/dist/marctl/src/terminal-relay-sender.mjs +129 -0
- package/dist/marctl/src/terminal-relay.mjs +305 -0
- package/dist/marctl/src/terminal.mjs +152 -0
- package/dist/marctl.js +1 -1
- package/dist/marnodes.js +1 -1
- package/dist/marscp.js +1 -1
- package/dist/marssh.js +1 -1
- package/dist/ripgrep-binary.js +3 -3
- package/dist/runner/mar-agent/agent-operation-presentation.js +22 -10
- package/dist/runner/mar-agent/hosted-conversation-runtime.js +3 -2
- package/dist/runner/mar-agent/runtime-extensions.js +33 -0
- package/dist/runner/mar-agent/session-runtime.js +5 -4
- package/dist/service/code-navigation/file-reference-parser.js +1 -1
- package/dist/service/node-terminal-channel.js +1 -1
- package/dist/session/native-session-projection-service.js +1 -2
- package/dist/session/session-catalog-service.js +74 -25
- package/dist/workspace/workspace-code-navigation-service.js +2 -2
- package/package.json +11 -9
- package/vendor/ctags/NOTICE +0 -22
- package/vendor/ctags/darwin-arm64/ctags +0 -0
- package/vendor/ctags/darwin-x64/ctags +0 -0
- package/vendor/ctags/licenses/COPYING +0 -340
- package/vendor/ctags/licenses/COPYING.MinGW-w64-runtime.txt +0 -240
- package/vendor/ctags/licenses/LICENSE.janssen +0 -19
- package/vendor/ctags/licenses/LICENSE.libyaml +0 -19
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/COPYING +0 -340
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/RECEIPT.yml +0 -27
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libiconv.yml +0 -24
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libjansson.yml +0 -19
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libxml2.yml +0 -20
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libyaml.yml +0 -7
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libz.yml +0 -13
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/COPYING +0 -340
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/RECEIPT.yml +0 -27
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libiconv.yml +0 -24
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libjansson.yml +0 -19
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libxml2.yml +0 -20
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libyaml.yml +0 -7
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libz.yml +0 -13
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/COPYING +0 -340
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/RECEIPT.yml +0 -27
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libiconv.yml +0 -24
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libjansson.yml +0 -19
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libxml2.yml +0 -20
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libyaml.yml +0 -7
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libz.yml +0 -13
- package/vendor/ctags/licenses/official-snapshot/linux-x64/COPYING +0 -340
- package/vendor/ctags/licenses/official-snapshot/linux-x64/RECEIPT.yml +0 -27
- package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libiconv.yml +0 -24
- package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libjansson.yml +0 -19
- package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libxml2.yml +0 -20
- package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libyaml.yml +0 -7
- package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libz.yml +0 -13
- package/vendor/ctags/licenses/win32-x64/COPYING +0 -340
- package/vendor/ctags/licenses/win32-x64/COPYING.MinGW-w64-runtime.txt +0 -240
- package/vendor/ctags/licenses/win32-x64/Copyright.libxml2 +0 -23
- package/vendor/ctags/licenses/win32-x64/LICENCE.pcre2 +0 -94
- package/vendor/ctags/licenses/win32-x64/LICENSE.janssen +0 -19
- package/vendor/ctags/licenses/win32-x64/LICENSE.libyaml +0 -19
- package/vendor/ctags/linux-arm64/ctags +0 -0
- package/vendor/ctags/linux-x64/ctags +0 -0
- package/vendor/ctags/manifest.json +0 -42
- package/vendor/ctags/source/universal-ctags-ab95af198f8eaeb6d50dc96e3adf52e769d3e5f0.tar.gz +0 -0
- package/vendor/ctags/win32-x64/ctags.exe +0 -0
- package/vendor/ripgrep/LICENSE-MIT +0 -21
- package/vendor/ripgrep/UNLICENSE +0 -24
- package/vendor/ripgrep/darwin-arm64/rg +0 -0
- package/vendor/ripgrep/darwin-x64/rg +0 -0
- package/vendor/ripgrep/linux-arm64/rg +0 -0
- package/vendor/ripgrep/linux-x64/rg +0 -0
- package/vendor/ripgrep/manifest.json +0 -42
- package/vendor/ripgrep/win32-arm64/rg.exe +0 -0
- package/vendor/ripgrep/win32-x64/rg.exe +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { MarctlError } from './errors.mjs';
|
|
2
|
+
import { sessionTarget } from './session-target.mjs';
|
|
3
|
+
import {
|
|
4
|
+
isAgentNetworkAction,
|
|
5
|
+
prepareAgentNetworkRequest,
|
|
6
|
+
prepareSessionRequest
|
|
7
|
+
} from './session-prepare.mjs';
|
|
8
|
+
import { waitForSessionRun } from './session-wait.mjs';
|
|
9
|
+
|
|
10
|
+
export async function runSessionCommand({
|
|
11
|
+
args,
|
|
12
|
+
client,
|
|
13
|
+
env,
|
|
14
|
+
stdin,
|
|
15
|
+
WebSocketClass,
|
|
16
|
+
translate,
|
|
17
|
+
signal
|
|
18
|
+
}) {
|
|
19
|
+
args = { ...args, ...(args.images === undefined ? {} : { images: [...args.images] }) };
|
|
20
|
+
const targeted = sessionTarget[args.action] !== 'workspace';
|
|
21
|
+
const sessionId =
|
|
22
|
+
args.sessionId ??
|
|
23
|
+
(sessionTarget[args.action] === 'current' ? env.MAR_CTL_SESSION_ID : undefined);
|
|
24
|
+
if (targeted && !sessionId && sessionTarget[args.action] !== 'optional')
|
|
25
|
+
throw new MarctlError(translate('errors.sessionContextRequired'), {
|
|
26
|
+
code: 'MARCTL_SESSION_CONTEXT_REQUIRED'
|
|
27
|
+
});
|
|
28
|
+
if (args.action === 'wait' && args.runId !== undefined)
|
|
29
|
+
return waitForSessionRun({
|
|
30
|
+
client,
|
|
31
|
+
sessionId,
|
|
32
|
+
runId: args.runId,
|
|
33
|
+
timeoutMs: args.timeoutMs ?? 30000,
|
|
34
|
+
until: args.until,
|
|
35
|
+
WebSocketClass,
|
|
36
|
+
translate,
|
|
37
|
+
signal
|
|
38
|
+
});
|
|
39
|
+
const network = isAgentNetworkAction(args.action);
|
|
40
|
+
const body = network
|
|
41
|
+
? await prepareAgentNetworkRequest({ args, stdin, translate, sessionId })
|
|
42
|
+
: await prepareSessionRequest({ args, env, stdin, translate, sessionId, targeted });
|
|
43
|
+
const clientMessageId = network ? body.command.input.clientMessageId : body.clientMessageId;
|
|
44
|
+
try {
|
|
45
|
+
const requestedWait = args.timeoutMs ?? 30000;
|
|
46
|
+
const effectiveWait = requestedWait === 0 ? 0 : Math.max(10000, requestedWait);
|
|
47
|
+
const result = await client.sessionCommand(
|
|
48
|
+
body,
|
|
49
|
+
args.action === 'wait' ? { timeoutMs: effectiveWait + 10000, signal } : undefined
|
|
50
|
+
);
|
|
51
|
+
return {
|
|
52
|
+
...result,
|
|
53
|
+
...(typeof result?.session?.id === 'string' ? { sessionId: result.session.id } : {}),
|
|
54
|
+
...(typeof result?.run?.id === 'string' ? { runId: result.run.id } : {}),
|
|
55
|
+
...(clientMessageId === undefined ? {} : { clientMessageId })
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (clientMessageId !== undefined) error.clientMessageId = clientMessageId;
|
|
59
|
+
if (
|
|
60
|
+
[
|
|
61
|
+
'start',
|
|
62
|
+
'send',
|
|
63
|
+
'config',
|
|
64
|
+
'rename',
|
|
65
|
+
'prompt.set',
|
|
66
|
+
'prompt.clear',
|
|
67
|
+
'approval.decide',
|
|
68
|
+
'schedule.create',
|
|
69
|
+
'schedule.update',
|
|
70
|
+
'stop',
|
|
71
|
+
'goal.set',
|
|
72
|
+
'goal.clear'
|
|
73
|
+
].includes(args.action) &&
|
|
74
|
+
['MARCTL_REQUEST_TIMEOUT', 'MARCTL_SERVER_UNREACHABLE', 'MARCTL_RESPONSE_INVALID'].includes(
|
|
75
|
+
error.code
|
|
76
|
+
)
|
|
77
|
+
) {
|
|
78
|
+
error.code = 'MARCTL_OUTCOME_UNKNOWN';
|
|
79
|
+
error.message = translate('errors.sessionOutcomeUnknown');
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function sessionExitCode(error) {
|
|
86
|
+
if (error.code === 'MARCTL_SESSION_INPUT_INVALID') return 2;
|
|
87
|
+
if ([401, 403].includes(error.statusCode)) return 3;
|
|
88
|
+
if (error.code?.includes('UNSUPPORTED')) return 4;
|
|
89
|
+
if (error.code === 'MARCTL_WAIT_TIMEOUT' || error.code === 'MARCTL_REQUEST_TIMEOUT') return 5;
|
|
90
|
+
if (['MARCTL_OUTCOME_UNKNOWN', 'MARCTL_WAIT_UNKNOWN'].includes(error.code)) return 6;
|
|
91
|
+
if (error.code === 'MARCTL_WAIT_CANCELLED') return 130;
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { MarctlError } from './errors.mjs';
|
|
2
|
+
|
|
3
|
+
const MAX_INPUT_BYTES = 65536;
|
|
4
|
+
|
|
5
|
+
export function parseFrame(raw, isBinary, terminalId, translate) {
|
|
6
|
+
if (isBinary) throw new MarctlError(translate('errors.terminalBinaryFrame'));
|
|
7
|
+
let frame;
|
|
8
|
+
try {
|
|
9
|
+
frame = JSON.parse(rawToString(raw));
|
|
10
|
+
} catch {
|
|
11
|
+
throw new MarctlError(translate('errors.terminalInvalidJson'));
|
|
12
|
+
}
|
|
13
|
+
if (frame === null || typeof frame !== 'object' || frame.terminalId !== terminalId)
|
|
14
|
+
throw new MarctlError(translate('errors.terminalFrameInvalid'));
|
|
15
|
+
|
|
16
|
+
if (frame.type === 'attach') {
|
|
17
|
+
if (!Number.isSafeInteger(frame.afterSequence) || frame.afterSequence < 0)
|
|
18
|
+
throw new MarctlError(translate('errors.terminalFrameInvalid'));
|
|
19
|
+
return frame;
|
|
20
|
+
}
|
|
21
|
+
if (frame.type === 'output') {
|
|
22
|
+
if (
|
|
23
|
+
!Number.isSafeInteger(frame.sequence) ||
|
|
24
|
+
frame.sequence < 1 ||
|
|
25
|
+
typeof frame.data !== 'string' ||
|
|
26
|
+
Buffer.byteLength(frame.data) > MAX_INPUT_BYTES
|
|
27
|
+
)
|
|
28
|
+
throw new MarctlError(translate('errors.terminalFrameInvalid'));
|
|
29
|
+
return frame;
|
|
30
|
+
}
|
|
31
|
+
if (frame.type === 'exit') {
|
|
32
|
+
if (frame.exitCode !== null && !Number.isSafeInteger(frame.exitCode))
|
|
33
|
+
throw new MarctlError(translate('errors.terminalFrameInvalid'));
|
|
34
|
+
return frame;
|
|
35
|
+
}
|
|
36
|
+
if (frame.type === 'error' && typeof frame.code === 'string' && frame.code.length > 0)
|
|
37
|
+
return frame;
|
|
38
|
+
throw new MarctlError(translate('errors.terminalFrameInvalid'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rawToString(raw) {
|
|
42
|
+
if (typeof raw === 'string') return raw;
|
|
43
|
+
if (Buffer.isBuffer(raw)) return raw.toString('utf8');
|
|
44
|
+
if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');
|
|
45
|
+
if (ArrayBuffer.isView(raw))
|
|
46
|
+
return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString('utf8');
|
|
47
|
+
return String(raw);
|
|
48
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { MarctlError } from './errors.mjs';
|
|
2
|
+
import { DirectRejectedError } from './direct-errors.mjs';
|
|
3
|
+
import { parseFrame } from './terminal-frame.mjs';
|
|
4
|
+
|
|
5
|
+
const MAX_INPUT_BYTES = 65536;
|
|
6
|
+
const DEFAULT_ROWS = 24;
|
|
7
|
+
const DEFAULT_COLS = 80;
|
|
8
|
+
|
|
9
|
+
export function splitInput(text, maxBytes = MAX_INPUT_BYTES) {
|
|
10
|
+
const value = Buffer.isBuffer(text) ? text.toString('utf8') : String(text);
|
|
11
|
+
const chunks = [];
|
|
12
|
+
let chunk = '';
|
|
13
|
+
let chunkBytes = 0;
|
|
14
|
+
|
|
15
|
+
for (const character of value) {
|
|
16
|
+
const characterBytes = Buffer.byteLength(character);
|
|
17
|
+
if (chunk !== '' && chunkBytes + characterBytes > maxBytes) {
|
|
18
|
+
chunks.push(chunk);
|
|
19
|
+
chunk = '';
|
|
20
|
+
chunkBytes = 0;
|
|
21
|
+
}
|
|
22
|
+
if (characterBytes > maxBytes) continue;
|
|
23
|
+
chunk += character;
|
|
24
|
+
chunkBytes += characterBytes;
|
|
25
|
+
}
|
|
26
|
+
if (chunk !== '') chunks.push(chunk);
|
|
27
|
+
return chunks;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function runAttachedTerminal({
|
|
31
|
+
transport,
|
|
32
|
+
terminalId,
|
|
33
|
+
commandMode,
|
|
34
|
+
stdin,
|
|
35
|
+
stdout,
|
|
36
|
+
signal,
|
|
37
|
+
translate
|
|
38
|
+
}) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let settled = false;
|
|
41
|
+
let inputEnabled = false;
|
|
42
|
+
let rawModeEnabled = false;
|
|
43
|
+
let inputTasks = 0;
|
|
44
|
+
|
|
45
|
+
const cleanup = () => {
|
|
46
|
+
transport.removeListener?.('message', onMessage);
|
|
47
|
+
transport.removeListener?.('error', onError);
|
|
48
|
+
transport.removeListener?.('close', onClose);
|
|
49
|
+
stdin.removeListener?.('data', onInput);
|
|
50
|
+
stdout.removeListener?.('resize', onResize);
|
|
51
|
+
signal?.removeEventListener('abort', onAbort);
|
|
52
|
+
if (rawModeEnabled) {
|
|
53
|
+
try {
|
|
54
|
+
stdin.setRawMode(false);
|
|
55
|
+
} catch {
|
|
56
|
+
// The terminal may already be gone.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
stdin.pause?.();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const finish = (error, exitCode) => {
|
|
63
|
+
if (settled) return;
|
|
64
|
+
settled = true;
|
|
65
|
+
cleanup();
|
|
66
|
+
sendCloseFrame(transport, terminalId);
|
|
67
|
+
transport.close?.();
|
|
68
|
+
if (error === undefined) resolve(exitCode);
|
|
69
|
+
else reject(error);
|
|
70
|
+
};
|
|
71
|
+
const fail = (key) => finish(new MarctlError(translate(key)));
|
|
72
|
+
const send = (frame) => {
|
|
73
|
+
return transport.send(JSON.stringify(frame));
|
|
74
|
+
};
|
|
75
|
+
const onInput = async (data) => {
|
|
76
|
+
inputTasks += 1;
|
|
77
|
+
stdin.pause?.();
|
|
78
|
+
try {
|
|
79
|
+
for (const chunk of splitInput(data)) {
|
|
80
|
+
if (settled) return;
|
|
81
|
+
await send({ type: 'input', terminalId, data: chunk });
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
fail('errors.terminalInputFailed');
|
|
85
|
+
} finally {
|
|
86
|
+
inputTasks -= 1;
|
|
87
|
+
if (!settled && inputTasks === 0) stdin.resume?.();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const onResize = () => {
|
|
91
|
+
try {
|
|
92
|
+
const { rows, cols } = getDimensions(stdout);
|
|
93
|
+
Promise.resolve(send({ type: 'resize', terminalId, rows, cols })).catch(() =>
|
|
94
|
+
fail('errors.terminalResizeFailed')
|
|
95
|
+
);
|
|
96
|
+
} catch {
|
|
97
|
+
fail('errors.terminalResizeFailed');
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const onMessage = (raw, isBinary) => {
|
|
101
|
+
try {
|
|
102
|
+
const frame = parseFrame(raw, isBinary, terminalId, translate);
|
|
103
|
+
if (frame.type === 'output') stdout.write(frame.data);
|
|
104
|
+
else if (frame.type === 'exit') {
|
|
105
|
+
finish(undefined, frame.exitCode === null ? 1 : frame.exitCode);
|
|
106
|
+
} else if (frame.type === 'error') fail('errors.terminalRemoteError');
|
|
107
|
+
else fail('errors.terminalUnexpectedFrame');
|
|
108
|
+
} catch (error) {
|
|
109
|
+
finish(
|
|
110
|
+
error instanceof MarctlError
|
|
111
|
+
? error
|
|
112
|
+
: new MarctlError(translate('errors.terminalFrameInvalid'))
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const onError = (error) =>
|
|
117
|
+
finish(
|
|
118
|
+
error instanceof DirectRejectedError
|
|
119
|
+
? new MarctlError(`${translate('errors.terminalConnectionFailed')} [${error.code}]`, {
|
|
120
|
+
code: error.code
|
|
121
|
+
})
|
|
122
|
+
: new MarctlError(translate('errors.terminalConnectionFailed'))
|
|
123
|
+
);
|
|
124
|
+
const onClose = () => finish(new MarctlError(translate('errors.terminalConnectionLost')));
|
|
125
|
+
const onAbort = () => finish(undefined, signal.reason === 'SIGTERM' ? 143 : 130);
|
|
126
|
+
const startInput = () => {
|
|
127
|
+
if (settled || inputEnabled) return;
|
|
128
|
+
inputEnabled = true;
|
|
129
|
+
try {
|
|
130
|
+
if (!commandMode) {
|
|
131
|
+
if (typeof stdin.setRawMode !== 'function') {
|
|
132
|
+
fail('errors.terminalInteractiveRequired');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
stdin.setRawMode(true);
|
|
136
|
+
rawModeEnabled = true;
|
|
137
|
+
}
|
|
138
|
+
stdin.resume?.();
|
|
139
|
+
stdin.on?.('data', onInput);
|
|
140
|
+
stdout.on?.('resize', onResize);
|
|
141
|
+
onResize();
|
|
142
|
+
} catch {
|
|
143
|
+
fail(commandMode ? 'errors.terminalSessionFailed' : 'errors.terminalInteractiveInit');
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
transport.on?.('message', onMessage);
|
|
147
|
+
transport.on?.('error', onError);
|
|
148
|
+
transport.on?.('close', onClose);
|
|
149
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
150
|
+
if (signal?.aborted) {
|
|
151
|
+
onAbort();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
transport.activate?.();
|
|
155
|
+
startInput();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function sendCloseFrame(socket, terminalId) {
|
|
160
|
+
if (!terminalId || !isSocketOpen(socket)) return;
|
|
161
|
+
try {
|
|
162
|
+
Promise.resolve(socket.send(JSON.stringify({ type: 'close', terminalId }))).catch(() => {});
|
|
163
|
+
} catch {
|
|
164
|
+
// The DELETE request below remains the authoritative cleanup path.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isSocketOpen(socket) {
|
|
169
|
+
return socket.readyState === undefined || socket.readyState === 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function getDimensions(stdout) {
|
|
173
|
+
const rows = Number.isSafeInteger(stdout.rows) && stdout.rows > 0 ? stdout.rows : DEFAULT_ROWS;
|
|
174
|
+
const cols =
|
|
175
|
+
Number.isSafeInteger(stdout.columns) && stdout.columns > 0 ? stdout.columns : DEFAULT_COLS;
|
|
176
|
+
return { rows: Math.min(rows, 1000), cols: Math.min(cols, 1000) };
|
|
177
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { prepareRelayTransport } from './terminal-relay.mjs';
|
|
2
|
+
import { DirectFallbackError, DirectRejectedError } from './direct-errors.mjs';
|
|
3
|
+
|
|
4
|
+
export async function raceTerminalConnections({
|
|
5
|
+
openDirectTerminal,
|
|
6
|
+
directOptions,
|
|
7
|
+
client,
|
|
8
|
+
WebSocketClass,
|
|
9
|
+
nodeId,
|
|
10
|
+
terminalId,
|
|
11
|
+
translate,
|
|
12
|
+
connectionTimeoutMs,
|
|
13
|
+
signal
|
|
14
|
+
}) {
|
|
15
|
+
if (signal?.aborted) throw new DirectFallbackError('DIRECT_RACE_CANCELLED');
|
|
16
|
+
const abort = new AbortController();
|
|
17
|
+
const relayAbort = new AbortController();
|
|
18
|
+
const preparing = new Set(['direct', 'relay']);
|
|
19
|
+
let start;
|
|
20
|
+
const preparation = new Promise((resolve) => {
|
|
21
|
+
start = resolve;
|
|
22
|
+
});
|
|
23
|
+
const prepared = (candidate) => {
|
|
24
|
+
preparing.delete(candidate);
|
|
25
|
+
if (preparing.size === 0) start();
|
|
26
|
+
return preparation;
|
|
27
|
+
};
|
|
28
|
+
let selected;
|
|
29
|
+
let announce;
|
|
30
|
+
const first = new Promise((resolve) => {
|
|
31
|
+
announce = resolve;
|
|
32
|
+
});
|
|
33
|
+
const select = (candidate) => {
|
|
34
|
+
if (selected) return false;
|
|
35
|
+
selected = candidate;
|
|
36
|
+
announce(candidate);
|
|
37
|
+
if (candidate === 'relay') abort.abort();
|
|
38
|
+
else relayAbort.abort();
|
|
39
|
+
return true;
|
|
40
|
+
};
|
|
41
|
+
const cancel = () => {
|
|
42
|
+
abort.abort();
|
|
43
|
+
relayAbort.abort();
|
|
44
|
+
if (selected) return;
|
|
45
|
+
selected = 'cancel';
|
|
46
|
+
start();
|
|
47
|
+
announce('cancel');
|
|
48
|
+
};
|
|
49
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
50
|
+
const direct = openDirectTerminal({
|
|
51
|
+
...directOptions,
|
|
52
|
+
signal: abort.signal,
|
|
53
|
+
onPrepared: () => prepared('direct'),
|
|
54
|
+
onRejected: () => select('direct'),
|
|
55
|
+
onIceConnected: () => {
|
|
56
|
+
if (!select('direct')) throw new Error('DIRECT_RACE_CANCELLED');
|
|
57
|
+
},
|
|
58
|
+
onTransportReady: () => {
|
|
59
|
+
if (selected !== 'direct') throw new Error('DIRECT_RACE_CANCELLED');
|
|
60
|
+
}
|
|
61
|
+
}).then(
|
|
62
|
+
(value) => {
|
|
63
|
+
if (selected === 'cancel' || signal?.aborted) value.close();
|
|
64
|
+
return { value };
|
|
65
|
+
},
|
|
66
|
+
(error) => {
|
|
67
|
+
prepared('direct');
|
|
68
|
+
if (error instanceof DirectRejectedError) select('direct');
|
|
69
|
+
return { error };
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
const relay = prepareRelayTransport({
|
|
73
|
+
client,
|
|
74
|
+
WebSocketClass,
|
|
75
|
+
nodeId,
|
|
76
|
+
terminalId,
|
|
77
|
+
translate,
|
|
78
|
+
connectionTimeoutMs,
|
|
79
|
+
signal: relayAbort.signal,
|
|
80
|
+
onPrepared: () => prepared('relay')
|
|
81
|
+
}).then(
|
|
82
|
+
(value) => {
|
|
83
|
+
if (!select('relay')) value.close();
|
|
84
|
+
return { value };
|
|
85
|
+
},
|
|
86
|
+
(error) => {
|
|
87
|
+
prepared('relay');
|
|
88
|
+
return { error };
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
const winner = await Promise.race([
|
|
92
|
+
first,
|
|
93
|
+
Promise.all([direct, relay]).then(() => selected ?? 'none')
|
|
94
|
+
]);
|
|
95
|
+
if (winner === 'cancel' || signal?.aborted) {
|
|
96
|
+
signal?.removeEventListener('abort', cancel);
|
|
97
|
+
abort.abort();
|
|
98
|
+
relayAbort.abort();
|
|
99
|
+
throw new DirectFallbackError('DIRECT_RACE_CANCELLED');
|
|
100
|
+
}
|
|
101
|
+
if (winner === 'direct') {
|
|
102
|
+
const result = await direct;
|
|
103
|
+
signal?.removeEventListener('abort', cancel);
|
|
104
|
+
if (result.error) throw result.error;
|
|
105
|
+
return result.value;
|
|
106
|
+
}
|
|
107
|
+
if (winner === 'relay') {
|
|
108
|
+
const result = await relay;
|
|
109
|
+
signal?.removeEventListener('abort', cancel);
|
|
110
|
+
if (result.error) throw result.error;
|
|
111
|
+
return result.value.activate(signal);
|
|
112
|
+
}
|
|
113
|
+
const [directResult, relayResult] = await Promise.all([direct, relay]);
|
|
114
|
+
signal?.removeEventListener('abort', cancel);
|
|
115
|
+
if (directResult.error instanceof DirectRejectedError) throw directResult.error;
|
|
116
|
+
throw relayResult.error ?? directResult.error;
|
|
117
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const MAX_PENDING_BYTES = 2 * 1024 * 1024;
|
|
2
|
+
const MAX_SOCKET_BYTES = 256 * 1024;
|
|
3
|
+
|
|
4
|
+
/** Shared sender for CLI input and the Node's aggregate Terminal relay output. */
|
|
5
|
+
export class TerminalRelaySender {
|
|
6
|
+
#socket;
|
|
7
|
+
#onPendingChange;
|
|
8
|
+
#pending = [];
|
|
9
|
+
#pendingBytes = 0;
|
|
10
|
+
#inFlight;
|
|
11
|
+
#timer;
|
|
12
|
+
#immediate;
|
|
13
|
+
#closed = false;
|
|
14
|
+
|
|
15
|
+
constructor(socket, { onPendingChange = () => {} } = {}) {
|
|
16
|
+
this.#socket = socket;
|
|
17
|
+
this.#onPendingChange = onPendingChange;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
send(value, key = '') {
|
|
21
|
+
if (this.#closed || this.#socket.readyState !== 1)
|
|
22
|
+
return Promise.reject(new Error('TERMINAL_CONNECTION_LOST'));
|
|
23
|
+
const bytes = Buffer.byteLength(value, 'utf8');
|
|
24
|
+
if (this.#pendingBytes + bytes > MAX_PENDING_BYTES)
|
|
25
|
+
return Promise.reject(
|
|
26
|
+
Object.assign(new Error('TERMINAL_RELAY_BUFFER_EXCEEDED'), {
|
|
27
|
+
code: 'TERMINAL_RELAY_BUFFER_EXCEEDED'
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
const result = new Promise((resolve, reject) => {
|
|
31
|
+
this.#pending.push({ value, key, bytes, resolve, reject });
|
|
32
|
+
this.#pendingBytes += bytes;
|
|
33
|
+
});
|
|
34
|
+
this.#pump();
|
|
35
|
+
this.#notify();
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
cancel(key) {
|
|
40
|
+
if (this.#inFlight?.key === key) {
|
|
41
|
+
this.#inFlight.cancelled = true;
|
|
42
|
+
this.#inFlight.reject(new Error('TERMINAL_ATTACH_INVALID'));
|
|
43
|
+
}
|
|
44
|
+
const kept = [];
|
|
45
|
+
for (const item of this.#pending) {
|
|
46
|
+
if (item.key !== key) kept.push(item);
|
|
47
|
+
else {
|
|
48
|
+
this.#pendingBytes -= item.bytes;
|
|
49
|
+
item.reject(new Error('TERMINAL_ATTACH_INVALID'));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.#pending = kept;
|
|
53
|
+
if (kept.length === 0) this.#clearScheduled();
|
|
54
|
+
this.#notify();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
close(error = new Error('TERMINAL_CONNECTION_LOST')) {
|
|
58
|
+
if (this.#closed) return;
|
|
59
|
+
this.#closed = true;
|
|
60
|
+
this.#clearScheduled();
|
|
61
|
+
this.#inFlight?.reject(error);
|
|
62
|
+
this.#inFlight = undefined;
|
|
63
|
+
for (const item of this.#pending) item.reject(error);
|
|
64
|
+
this.#pending = [];
|
|
65
|
+
this.#pendingBytes = 0;
|
|
66
|
+
this.#notify();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#pump() {
|
|
70
|
+
if (
|
|
71
|
+
this.#closed ||
|
|
72
|
+
this.#inFlight !== undefined ||
|
|
73
|
+
this.#timer !== undefined ||
|
|
74
|
+
this.#immediate !== undefined ||
|
|
75
|
+
this.#pending.length === 0
|
|
76
|
+
)
|
|
77
|
+
return;
|
|
78
|
+
if (this.#socket.readyState !== 1) {
|
|
79
|
+
this.close();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const buffered = this.#socket.bufferedAmount ?? 0;
|
|
83
|
+
if (buffered > 0 && buffered + this.#pending[0].bytes > MAX_SOCKET_BYTES) {
|
|
84
|
+
// Poll only while the transport is congested; there is no throughput timer.
|
|
85
|
+
this.#timer = setTimeout(() => {
|
|
86
|
+
this.#timer = undefined;
|
|
87
|
+
this.#pump();
|
|
88
|
+
this.#notify();
|
|
89
|
+
}, 20);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const item = this.#pending.shift();
|
|
93
|
+
this.#inFlight = item;
|
|
94
|
+
try {
|
|
95
|
+
this.#socket.send(item.value, (error) => {
|
|
96
|
+
if (this.#inFlight !== item) return;
|
|
97
|
+
if (error) {
|
|
98
|
+
this.close(error);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.#inFlight = undefined;
|
|
102
|
+
this.#pendingBytes -= item.bytes;
|
|
103
|
+
// Even a synchronous write callback must yield before the next frame.
|
|
104
|
+
this.#immediate = setImmediate(() => {
|
|
105
|
+
this.#immediate = undefined;
|
|
106
|
+
this.#pump();
|
|
107
|
+
this.#notify();
|
|
108
|
+
});
|
|
109
|
+
item.resolve();
|
|
110
|
+
this.#notify();
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.close(error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#notify() {
|
|
118
|
+
const keys = new Set(this.#pending.map((item) => item.key));
|
|
119
|
+
if (this.#inFlight !== undefined && !this.#inFlight.cancelled) keys.add(this.#inFlight.key);
|
|
120
|
+
this.#onPendingChange(keys);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#clearScheduled() {
|
|
124
|
+
clearTimeout(this.#timer);
|
|
125
|
+
this.#timer = undefined;
|
|
126
|
+
clearImmediate(this.#immediate);
|
|
127
|
+
this.#immediate = undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|