@myagentroam/marctl 0.10.1 → 0.10.3
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/package.json +1 -1
- package/src/cli.mjs +2 -18
- package/src/client.mjs +8 -0
- package/src/config.mjs +11 -129
- package/src/direct-process.mjs +351 -0
- package/src/direct.mjs +23 -17
- package/src/i18n/en-US.mjs +8 -28
- package/src/i18n/zh-CN.mjs +7 -27
- package/src/terminal.mjs +4 -2
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -38,7 +38,7 @@ export async function main(
|
|
|
38
38
|
return 0;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const config = await loadConfig({ env,
|
|
41
|
+
const config = await loadConfig({ env, translate });
|
|
42
42
|
const marctlClient = client ?? new MarctlClient({ ...config, translate });
|
|
43
43
|
if (args.command === 'nodes') {
|
|
44
44
|
const nodes = await marctlClient.listNodes();
|
|
@@ -110,7 +110,6 @@ export function executableArgs(executable, args) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
export function parseArgs(argv, translate = createTranslator()) {
|
|
113
|
-
let envFile;
|
|
114
113
|
let command;
|
|
115
114
|
let nodeId;
|
|
116
115
|
const remoteArgs = [];
|
|
@@ -133,18 +132,6 @@ export function parseArgs(argv, translate = createTranslator()) {
|
|
|
133
132
|
version = true;
|
|
134
133
|
continue;
|
|
135
134
|
}
|
|
136
|
-
if (!separator && argument === '--env-file') {
|
|
137
|
-
envFile = argv[index + 1];
|
|
138
|
-
if (!envFile || envFile.startsWith('-'))
|
|
139
|
-
throw new MarctlError(translate('errors.envFileValueRequired'));
|
|
140
|
-
index += 1;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (!separator && argument.startsWith('--env-file=')) {
|
|
144
|
-
envFile = argument.slice('--env-file='.length);
|
|
145
|
-
if (!envFile) throw new MarctlError(translate('errors.envFileValueRequired'));
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
135
|
if (!separator && argument === '--') {
|
|
149
136
|
separator = true;
|
|
150
137
|
continue;
|
|
@@ -186,12 +173,10 @@ export function parseArgs(argv, translate = createTranslator()) {
|
|
|
186
173
|
|
|
187
174
|
if (help) return { help: true, ...(command === undefined ? {} : { command }) };
|
|
188
175
|
if (version || command === 'version') return { version: true };
|
|
189
|
-
if (command === 'nodes' && nodeId === undefined && remoteArgs.length === 0)
|
|
190
|
-
return { command, ...(envFile === undefined ? {} : { envFile }) };
|
|
176
|
+
if (command === 'nodes' && nodeId === undefined && remoteArgs.length === 0) return { command };
|
|
191
177
|
if (command === 'scp' && nodeId !== undefined && remoteArgs.length === 1)
|
|
192
178
|
return {
|
|
193
179
|
command,
|
|
194
|
-
...(envFile === undefined ? {} : { envFile }),
|
|
195
180
|
source: nodeId,
|
|
196
181
|
destination: remoteArgs[0],
|
|
197
182
|
preserve
|
|
@@ -199,7 +184,6 @@ export function parseArgs(argv, translate = createTranslator()) {
|
|
|
199
184
|
if (command === 'ssh' && nodeId !== undefined && nodeId !== '')
|
|
200
185
|
return {
|
|
201
186
|
command,
|
|
202
|
-
...(envFile === undefined ? {} : { envFile }),
|
|
203
187
|
nodeId,
|
|
204
188
|
...(remoteArgs.length === 0
|
|
205
189
|
? {}
|
package/src/client.mjs
CHANGED
|
@@ -86,6 +86,14 @@ export class MarctlClient {
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
async openDirectTerminal(options) {
|
|
90
|
+
const { openDirectTerminalProcess } = await import('./direct-process.mjs');
|
|
91
|
+
return openDirectTerminalProcess({
|
|
92
|
+
...options,
|
|
93
|
+
connection: { serverUrl: this.#serverUrl, key: this.#key }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
async createAttachTicket({ nodeId, terminalId, afterSequence }) {
|
|
90
98
|
validateNodeId(nodeId, this.#translate);
|
|
91
99
|
validateTerminalId(terminalId, this.#translate);
|
package/src/config.mjs
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import { lstat, readFile } from 'node:fs/promises';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
1
|
import { createTranslator } from './i18n/index.mjs';
|
|
5
2
|
|
|
6
|
-
export const CONFIG_KEYS = Object.freeze(['MAR_CTL_SERVER_URL', 'MAR_CTL_KEY']);
|
|
7
|
-
|
|
8
3
|
export class MarctlError extends Error {
|
|
9
4
|
constructor(message, options = {}) {
|
|
10
5
|
super(message, options);
|
|
@@ -12,56 +7,17 @@ export class MarctlError extends Error {
|
|
|
12
7
|
}
|
|
13
8
|
}
|
|
14
9
|
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function loadConfig({
|
|
28
|
-
env = process.env,
|
|
29
|
-
envFile,
|
|
30
|
-
platform = process.platform,
|
|
31
|
-
homeDirectory = os.homedir(),
|
|
32
|
-
translate = createTranslator(env)
|
|
33
|
-
} = {}) {
|
|
34
|
-
if (env.MAR_CTL_CONTEXT_REQUIRED === '1') {
|
|
35
|
-
const serverUrl = nonEmptyString(env.MAR_CTL_SERVER_URL);
|
|
36
|
-
const token = nonEmptyString(env.MAR_CTL_USER_TOKEN);
|
|
37
|
-
if (
|
|
38
|
-
serverUrl === undefined ||
|
|
39
|
-
token === undefined ||
|
|
40
|
-
!/^marctl_user_[A-Za-z0-9_-]{43}$/u.test(token)
|
|
41
|
-
)
|
|
42
|
-
throw new MarctlError(translate('errors.userContextUnavailable'));
|
|
43
|
-
return Object.freeze({
|
|
44
|
-
serverUrl: validateServerUrl(serverUrl, translate),
|
|
45
|
-
key: token
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
const configPath = envFile ?? defaultConfigPath({ env, platform, homeDirectory });
|
|
49
|
-
const values = await readConfigFile(configPath, { platform, translate });
|
|
50
|
-
|
|
51
|
-
const serverUrlValue = nonEmptyString(values.MAR_CTL_SERVER_URL);
|
|
52
|
-
if (serverUrlValue === undefined)
|
|
53
|
-
throw new MarctlError(translate('errors.configServerUrlMissing'));
|
|
54
|
-
|
|
55
|
-
const keyValue = nonEmptyString(values.MAR_CTL_KEY);
|
|
56
|
-
if (keyValue === undefined) throw new MarctlError(translate('errors.configKeyMissing'));
|
|
57
|
-
// eslint-disable-next-line no-control-regex -- Reject control characters explicitly at the input boundary.
|
|
58
|
-
if (/[\u0000-\u001f\u007f]/u.test(keyValue))
|
|
59
|
-
throw new MarctlError(translate('errors.configKeyInvalid'));
|
|
60
|
-
|
|
61
|
-
return Object.freeze({
|
|
62
|
-
serverUrl: validateServerUrl(serverUrlValue, translate),
|
|
63
|
-
key: keyValue
|
|
64
|
-
});
|
|
10
|
+
export async function loadConfig({ env = process.env, translate = createTranslator(env) } = {}) {
|
|
11
|
+
const serverUrl = nonEmptyString(env.MAR_CTL_SERVER_URL);
|
|
12
|
+
const token = nonEmptyString(env.MAR_CTL_USER_TOKEN);
|
|
13
|
+
if (
|
|
14
|
+
env.MAR_CTL_CONTEXT_REQUIRED !== '1' ||
|
|
15
|
+
serverUrl === undefined ||
|
|
16
|
+
token === undefined ||
|
|
17
|
+
!/^marctl_user_[A-Za-z0-9_-]{43}$/u.test(token)
|
|
18
|
+
)
|
|
19
|
+
throw new MarctlError(translate('errors.userContextUnavailable'));
|
|
20
|
+
return Object.freeze({ serverUrl: validateServerUrl(serverUrl, translate), key: token });
|
|
65
21
|
}
|
|
66
22
|
|
|
67
23
|
export function validateServerUrl(value, translate = createTranslator()) {
|
|
@@ -86,80 +42,6 @@ export function validateServerUrl(value, translate = createTranslator()) {
|
|
|
86
42
|
return url.origin;
|
|
87
43
|
}
|
|
88
44
|
|
|
89
|
-
async function readConfigFile(filePath, { platform, translate }) {
|
|
90
|
-
let before;
|
|
91
|
-
try {
|
|
92
|
-
before = await lstat(filePath);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
if (error?.code === 'ENOENT') throw new MarctlError(translate('errors.envFileMissing'));
|
|
95
|
-
throw new MarctlError(translate('errors.envFileRead'));
|
|
96
|
-
}
|
|
97
|
-
assertSecureConfigFile(before, platform, translate);
|
|
98
|
-
|
|
99
|
-
let content;
|
|
100
|
-
try {
|
|
101
|
-
content = await readFile(filePath, 'utf8');
|
|
102
|
-
} catch {
|
|
103
|
-
throw new MarctlError(translate('errors.envFileRead'));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let after;
|
|
107
|
-
try {
|
|
108
|
-
after = await lstat(filePath);
|
|
109
|
-
} catch {
|
|
110
|
-
throw new MarctlError(translate('errors.envFileRead'));
|
|
111
|
-
}
|
|
112
|
-
assertSecureConfigFile(after, platform, translate);
|
|
113
|
-
if (before.dev !== after.dev || before.ino !== after.ino)
|
|
114
|
-
throw new MarctlError(translate('errors.envFileRead'));
|
|
115
|
-
|
|
116
|
-
const values = {};
|
|
117
|
-
for (const rawLine of content.replace(/^\uFEFF/u, '').split(/\r?\n/u)) {
|
|
118
|
-
const line = rawLine.trim();
|
|
119
|
-
if (line === '' || line.startsWith('#')) continue;
|
|
120
|
-
|
|
121
|
-
const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/u.exec(line);
|
|
122
|
-
if (match === null) throw new MarctlError(translate('errors.envFileEntry'));
|
|
123
|
-
|
|
124
|
-
const [, name, rawValue] = match;
|
|
125
|
-
if (!CONFIG_KEYS.includes(name)) throw new MarctlError(translate('errors.envFileUnsupported'));
|
|
126
|
-
values[name] = parseEnvValue(rawValue, translate);
|
|
127
|
-
}
|
|
128
|
-
return values;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function parseEnvValue(rawValue, translate) {
|
|
132
|
-
const value = rawValue.trim();
|
|
133
|
-
if (value.startsWith('"') || value.startsWith("'")) {
|
|
134
|
-
const quote = value[0];
|
|
135
|
-
if (!value.endsWith(quote) || value.length < 2)
|
|
136
|
-
throw new MarctlError(translate('errors.envFileQuoted'));
|
|
137
|
-
const body = value.slice(1, -1);
|
|
138
|
-
if (quote === "'") return body;
|
|
139
|
-
return body.replace(/\\(["\\nrt])/gu, (_match, escaped) => {
|
|
140
|
-
if (escaped === 'n') return '\n';
|
|
141
|
-
if (escaped === 'r') return '\r';
|
|
142
|
-
if (escaped === 't') return '\t';
|
|
143
|
-
return escaped;
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
return value;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function assertSecureConfigFile(stats, platform, translate) {
|
|
150
|
-
if (stats.isSymbolicLink()) throw new MarctlError(translate('errors.envFileSymlink'));
|
|
151
|
-
if (!stats.isFile()) throw new MarctlError(translate('errors.envFileType'));
|
|
152
|
-
if (platform === 'win32') return;
|
|
153
|
-
|
|
154
|
-
const permissions = stats.mode & 0o7777;
|
|
155
|
-
const ownerUid = typeof process.getuid === 'function' ? process.getuid() : undefined;
|
|
156
|
-
if (
|
|
157
|
-
(ownerUid !== undefined && stats.uid !== ownerUid) ||
|
|
158
|
-
(permissions !== 0o600 && permissions !== 0o400)
|
|
159
|
-
)
|
|
160
|
-
throw new MarctlError(translate('errors.envFilePermissions'));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
45
|
function nonEmptyString(value) {
|
|
164
46
|
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
165
47
|
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { DirectFallbackError, DirectUnsafeError } from './direct.mjs';
|
|
5
|
+
|
|
6
|
+
const CLEANUP_TIMEOUT_MS = 1000;
|
|
7
|
+
const MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
8
|
+
const ipcBytes = new WeakMap();
|
|
9
|
+
|
|
10
|
+
function sendIpc(target, message, onError = () => {}, onSent = () => {}) {
|
|
11
|
+
const bytes = Buffer.byteLength(JSON.stringify(message));
|
|
12
|
+
const pending = ipcBytes.get(target) ?? 0;
|
|
13
|
+
if (!target.connected || pending + bytes > MAX_BUFFER_BYTES) {
|
|
14
|
+
onError(new Error('TERMINAL_CONNECTION_LOST'));
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
ipcBytes.set(target, pending + bytes);
|
|
18
|
+
try {
|
|
19
|
+
target.send(message, (error) => {
|
|
20
|
+
ipcBytes.set(target, (ipcBytes.get(target) ?? 0) - bytes);
|
|
21
|
+
if (error) onError(error);
|
|
22
|
+
else onSent();
|
|
23
|
+
});
|
|
24
|
+
return true;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
ipcBytes.set(target, (ipcBytes.get(target) ?? 0) - bytes);
|
|
27
|
+
onError(error);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function openDirectTerminalProcess({
|
|
33
|
+
connection,
|
|
34
|
+
nodeId,
|
|
35
|
+
terminalId,
|
|
36
|
+
manifest,
|
|
37
|
+
afterSequence = 0,
|
|
38
|
+
timeoutMs = 15000,
|
|
39
|
+
heartbeatMs = 10000,
|
|
40
|
+
onPrepared,
|
|
41
|
+
onIceConnected,
|
|
42
|
+
onTransportReady,
|
|
43
|
+
signal,
|
|
44
|
+
forkImpl = fork
|
|
45
|
+
}) {
|
|
46
|
+
if (signal?.aborted) return Promise.reject(new DirectFallbackError('DIRECT_RACE_CANCELLED'));
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
let child;
|
|
49
|
+
try {
|
|
50
|
+
child = forkImpl(new URL('./direct-process.mjs', import.meta.url), [], {
|
|
51
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
52
|
+
execArgv: []
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
reject(new DirectFallbackError('DIRECT_UNAVAILABLE'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let settled = false;
|
|
59
|
+
let opening = false;
|
|
60
|
+
let stopping = false;
|
|
61
|
+
let exited = false;
|
|
62
|
+
let cleanupTimer;
|
|
63
|
+
const stop = () => {
|
|
64
|
+
if (stopping) return;
|
|
65
|
+
stopping = true;
|
|
66
|
+
clearTimeout(setupTimer);
|
|
67
|
+
signal?.removeEventListener('abort', onAbort);
|
|
68
|
+
if (exited) return;
|
|
69
|
+
sendIpc(child, { type: 'abort' });
|
|
70
|
+
// Native cleanup may block. Only this connection's process is terminated.
|
|
71
|
+
cleanupTimer = setTimeout(() => child.kill('SIGKILL'), CLEANUP_TIMEOUT_MS);
|
|
72
|
+
};
|
|
73
|
+
const transport = new IsolatedDirectTransport(child, stop);
|
|
74
|
+
const fail = (error) => {
|
|
75
|
+
if (settled) {
|
|
76
|
+
transport.fail(error);
|
|
77
|
+
} else {
|
|
78
|
+
settled = true;
|
|
79
|
+
reject(error);
|
|
80
|
+
stop();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const setupError = () =>
|
|
84
|
+
opening
|
|
85
|
+
? new DirectUnsafeError('DIRECT_OPEN_CANCEL_UNCONFIRMED')
|
|
86
|
+
: new DirectFallbackError('DIRECT_UNAVAILABLE');
|
|
87
|
+
const setupTimer = setTimeout(() => fail(setupError()), timeoutMs);
|
|
88
|
+
const onAbort = () =>
|
|
89
|
+
fail(
|
|
90
|
+
opening
|
|
91
|
+
? new DirectUnsafeError('DIRECT_OPEN_CANCEL_UNCONFIRMED')
|
|
92
|
+
: new DirectFallbackError('DIRECT_RACE_CANCELLED')
|
|
93
|
+
);
|
|
94
|
+
child.on('message', (message) => {
|
|
95
|
+
if (message.type === 'callback') {
|
|
96
|
+
const callback = { prepared: onPrepared, ice: onIceConnected, transport: onTransportReady }[
|
|
97
|
+
message.event
|
|
98
|
+
];
|
|
99
|
+
if (message.event === 'opening') opening = true;
|
|
100
|
+
Promise.resolve()
|
|
101
|
+
.then(() => {
|
|
102
|
+
if (stopping) throw new DirectFallbackError('DIRECT_RACE_CANCELLED');
|
|
103
|
+
return callback?.();
|
|
104
|
+
})
|
|
105
|
+
.then(
|
|
106
|
+
() =>
|
|
107
|
+
sendIpc(child, { type: 'callback.result', id: message.id }, () => fail(setupError())),
|
|
108
|
+
() => sendIpc(child, { type: 'callback.result', id: message.id, failed: true })
|
|
109
|
+
);
|
|
110
|
+
} else if (message.type === 'attached' && !settled) {
|
|
111
|
+
settled = true;
|
|
112
|
+
clearTimeout(setupTimer);
|
|
113
|
+
resolve(transport);
|
|
114
|
+
} else if (message.type === 'failure') {
|
|
115
|
+
const error = settled
|
|
116
|
+
? new Error(message.code ?? 'TERMINAL_CONNECTION_LOST')
|
|
117
|
+
: message.name === 'DirectUnsafeError' ||
|
|
118
|
+
(opening && message.name !== 'DirectFallbackError')
|
|
119
|
+
? new DirectUnsafeError(message.code)
|
|
120
|
+
: new DirectFallbackError(message.code);
|
|
121
|
+
fail(error);
|
|
122
|
+
} else if (message.type === 'frame') {
|
|
123
|
+
transport.frame(message.frame, message.isBinary);
|
|
124
|
+
} else if (message.type === 'send.result') {
|
|
125
|
+
transport.written(message.id, message.code);
|
|
126
|
+
} else if (message.type === 'finished') {
|
|
127
|
+
clearTimeout(cleanupTimer);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
child.on('error', () => fail(setupError()));
|
|
131
|
+
child.on('disconnect', () => {
|
|
132
|
+
if (!stopping) fail(setupError());
|
|
133
|
+
});
|
|
134
|
+
child.on('exit', () => {
|
|
135
|
+
exited = true;
|
|
136
|
+
clearTimeout(cleanupTimer);
|
|
137
|
+
clearTimeout(setupTimer);
|
|
138
|
+
signal?.removeEventListener('abort', onAbort);
|
|
139
|
+
if (!stopping) fail(setupError());
|
|
140
|
+
});
|
|
141
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
142
|
+
sendIpc(
|
|
143
|
+
child,
|
|
144
|
+
{
|
|
145
|
+
type: 'start',
|
|
146
|
+
options: { connection, nodeId, terminalId, manifest, afterSequence, timeoutMs, heartbeatMs }
|
|
147
|
+
},
|
|
148
|
+
() => fail(setupError())
|
|
149
|
+
);
|
|
150
|
+
if (signal?.aborted) onAbort();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class IsolatedDirectTransport extends EventEmitter {
|
|
155
|
+
#child;
|
|
156
|
+
#stop;
|
|
157
|
+
#active = false;
|
|
158
|
+
#closed = false;
|
|
159
|
+
#events = [];
|
|
160
|
+
#bytes = 0;
|
|
161
|
+
#writes = new Map();
|
|
162
|
+
#writeBytes = 0;
|
|
163
|
+
#nextId = 0;
|
|
164
|
+
|
|
165
|
+
constructor(child, stop) {
|
|
166
|
+
super();
|
|
167
|
+
this.#child = child;
|
|
168
|
+
this.#stop = stop;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
activate() {
|
|
172
|
+
if (this.#active) return;
|
|
173
|
+
this.#active = true;
|
|
174
|
+
const events = this.#events;
|
|
175
|
+
this.#events = [];
|
|
176
|
+
this.#bytes = 0;
|
|
177
|
+
for (const [type, value, isBinary] of events) this.#emit(type, value, isBinary);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
frame(frame, isBinary = false) {
|
|
181
|
+
if (this.#closed) return;
|
|
182
|
+
if (!this.#active) {
|
|
183
|
+
this.#bytes += Buffer.byteLength(frame);
|
|
184
|
+
if (this.#bytes > MAX_BUFFER_BYTES)
|
|
185
|
+
return this.fail(new Error('TERMINAL_DIRECT_BUFFER_EXCEEDED'));
|
|
186
|
+
}
|
|
187
|
+
this.#emit('message', frame, isBinary);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
send(frame) {
|
|
191
|
+
if (this.#closed) return Promise.reject(new Error('TERMINAL_CONNECTION_LOST'));
|
|
192
|
+
const value = typeof frame === 'string' ? frame : JSON.stringify(frame);
|
|
193
|
+
const bytes = Buffer.byteLength(value);
|
|
194
|
+
if (this.#writeBytes + bytes > MAX_BUFFER_BYTES)
|
|
195
|
+
return Promise.reject(new Error('TERMINAL_DIRECT_BUFFER_EXCEEDED'));
|
|
196
|
+
const id = ++this.#nextId;
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
this.#writeBytes += bytes;
|
|
199
|
+
this.#writes.set(id, { resolve, reject, bytes });
|
|
200
|
+
sendIpc(this.#child, { type: 'send', id, frame: value }, () =>
|
|
201
|
+
this.fail(new Error('TERMINAL_CONNECTION_LOST'))
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
written(id, code) {
|
|
207
|
+
const write = this.#writes.get(id);
|
|
208
|
+
if (!write) return;
|
|
209
|
+
this.#writes.delete(id);
|
|
210
|
+
this.#writeBytes -= write.bytes;
|
|
211
|
+
if (code) write.reject(new Error(code));
|
|
212
|
+
else write.resolve();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fail(error) {
|
|
216
|
+
if (this.#closed) return;
|
|
217
|
+
this.#emit(
|
|
218
|
+
'error',
|
|
219
|
+
new Error(
|
|
220
|
+
error instanceof DirectFallbackError || error instanceof DirectUnsafeError
|
|
221
|
+
? 'TERMINAL_CONNECTION_LOST'
|
|
222
|
+
: error.message
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
this.close();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
close() {
|
|
229
|
+
if (this.#closed) return;
|
|
230
|
+
this.#closed = true;
|
|
231
|
+
for (const { reject } of this.#writes.values()) reject(new Error('TERMINAL_CONNECTION_LOST'));
|
|
232
|
+
this.#writes.clear();
|
|
233
|
+
this.#writeBytes = 0;
|
|
234
|
+
this.#stop();
|
|
235
|
+
this.#emit('close');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#emit(type, value, isBinary = false) {
|
|
239
|
+
if (!this.#active) this.#events.push([type, value, isBinary]);
|
|
240
|
+
else if (type !== 'error' || this.listenerCount('error') > 0) this.emit(type, value, isBinary);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function runChild() {
|
|
245
|
+
const controller = new AbortController();
|
|
246
|
+
const callbacks = new Map();
|
|
247
|
+
let nextId = 0;
|
|
248
|
+
let transport;
|
|
249
|
+
let native;
|
|
250
|
+
let cleaning = false;
|
|
251
|
+
const cleanup = () => {
|
|
252
|
+
if (cleaning) return;
|
|
253
|
+
cleaning = true;
|
|
254
|
+
setImmediate(() => {
|
|
255
|
+
try {
|
|
256
|
+
native?.cleanup();
|
|
257
|
+
} catch {
|
|
258
|
+
/* Process exit also releases native resources. */
|
|
259
|
+
}
|
|
260
|
+
sendIpc(
|
|
261
|
+
process,
|
|
262
|
+
{ type: 'finished' },
|
|
263
|
+
() => process.exit(0),
|
|
264
|
+
() => process.exit(0)
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
const callback = (event) =>
|
|
269
|
+
new Promise((resolve, reject) => {
|
|
270
|
+
const id = ++nextId;
|
|
271
|
+
callbacks.set(id, { resolve, reject });
|
|
272
|
+
sendIpc(process, { type: 'callback', id, event }, reject);
|
|
273
|
+
});
|
|
274
|
+
const abort = () => {
|
|
275
|
+
controller.abort();
|
|
276
|
+
for (const { reject } of callbacks.values())
|
|
277
|
+
reject(new DirectFallbackError('DIRECT_RACE_CANCELLED'));
|
|
278
|
+
callbacks.clear();
|
|
279
|
+
if (transport) transport.close();
|
|
280
|
+
};
|
|
281
|
+
process.on('disconnect', () => {
|
|
282
|
+
abort();
|
|
283
|
+
cleanup();
|
|
284
|
+
});
|
|
285
|
+
process.on('message', async (message) => {
|
|
286
|
+
if (message.type === 'callback.result') {
|
|
287
|
+
const pending = callbacks.get(message.id);
|
|
288
|
+
callbacks.delete(message.id);
|
|
289
|
+
if (message.failed) pending?.reject(new DirectFallbackError('DIRECT_RACE_CANCELLED'));
|
|
290
|
+
else pending?.resolve();
|
|
291
|
+
} else if (message.type === 'abort') {
|
|
292
|
+
abort();
|
|
293
|
+
} else if (message.type === 'send') {
|
|
294
|
+
try {
|
|
295
|
+
await transport.send(message.frame);
|
|
296
|
+
sendIpc(process, { type: 'send.result', id: message.id });
|
|
297
|
+
} catch {
|
|
298
|
+
sendIpc(process, { type: 'send.result', id: message.id, code: 'TERMINAL_INPUT_FAILED' });
|
|
299
|
+
}
|
|
300
|
+
} else if (message.type === 'start') {
|
|
301
|
+
try {
|
|
302
|
+
const { MarctlClient } = await import('./client.mjs');
|
|
303
|
+
const { openDirectTerminal } = await import('./direct.mjs');
|
|
304
|
+
const { default: WebSocketClass } = await import('ws');
|
|
305
|
+
native = await import('node-datachannel');
|
|
306
|
+
const { connection, ...options } = message.options;
|
|
307
|
+
let iceSelection;
|
|
308
|
+
transport = await openDirectTerminal({
|
|
309
|
+
...options,
|
|
310
|
+
client: new MarctlClient(connection),
|
|
311
|
+
WebSocketClass,
|
|
312
|
+
peerConnectionFactory: (name, config) => new native.PeerConnection(name, config),
|
|
313
|
+
cleanupNative: cleanup,
|
|
314
|
+
signal: controller.signal,
|
|
315
|
+
onPrepared: () => callback('prepared'),
|
|
316
|
+
onIceConnected: () => {
|
|
317
|
+
iceSelection = callback('ice');
|
|
318
|
+
iceSelection.catch(() => controller.abort());
|
|
319
|
+
},
|
|
320
|
+
onTransportReady: async () => {
|
|
321
|
+
await iceSelection;
|
|
322
|
+
await callback('transport');
|
|
323
|
+
},
|
|
324
|
+
beforeOpen: () => callback('opening')
|
|
325
|
+
});
|
|
326
|
+
sendIpc(process, { type: 'attached' });
|
|
327
|
+
transport.on('message', (frame, isBinary) =>
|
|
328
|
+
sendIpc(process, { type: 'frame', frame: String(frame), isBinary }, abort)
|
|
329
|
+
);
|
|
330
|
+
transport.on('error', (error) => {
|
|
331
|
+
sendIpc(process, { type: 'failure', code: error.message });
|
|
332
|
+
abort();
|
|
333
|
+
});
|
|
334
|
+
transport.on('close', () => {
|
|
335
|
+
sendIpc(process, { type: 'failure', code: 'TERMINAL_DIRECT_CLOSED' });
|
|
336
|
+
cleanup();
|
|
337
|
+
});
|
|
338
|
+
transport.activate();
|
|
339
|
+
} catch (error) {
|
|
340
|
+
sendIpc(process, {
|
|
341
|
+
type: 'failure',
|
|
342
|
+
name: error instanceof DirectUnsafeError ? 'DirectUnsafeError' : 'DirectFallbackError',
|
|
343
|
+
code: error.code ?? 'DIRECT_UNAVAILABLE'
|
|
344
|
+
});
|
|
345
|
+
cleanup();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (process.send && process.argv[1] === fileURLToPath(import.meta.url)) runChild();
|
package/src/direct.mjs
CHANGED
|
@@ -22,21 +22,26 @@ export class DirectUnsafeError extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export async function openDirectTerminal({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
export async function openDirectTerminal(options) {
|
|
26
|
+
const {
|
|
27
|
+
client,
|
|
28
|
+
WebSocketClass,
|
|
29
|
+
nodeId,
|
|
30
|
+
terminalId,
|
|
31
|
+
afterSequence = 0,
|
|
32
|
+
timeoutMs = DIRECT_SETUP_TIMEOUT_MS,
|
|
33
|
+
heartbeatMs = DIRECT_HEARTBEAT_MS,
|
|
34
|
+
peerConnectionFactory,
|
|
35
|
+
onPrepared,
|
|
36
|
+
onIceConnected,
|
|
37
|
+
onTransportReady,
|
|
38
|
+
beforeOpen,
|
|
39
|
+
cleanupNative: cleanupNativeImpl,
|
|
40
|
+
signal
|
|
41
|
+
} = options;
|
|
42
|
+
let { manifest } = options;
|
|
43
|
+
if (peerConnectionFactory === undefined && typeof client.openDirectTerminal === 'function')
|
|
44
|
+
return client.openDirectTerminal(options);
|
|
40
45
|
if (manifest === undefined && typeof client.getTerminalManifest !== 'function')
|
|
41
46
|
throw new DirectFallbackError('DIRECT_MANIFEST_UNAVAILABLE');
|
|
42
47
|
|
|
@@ -50,7 +55,7 @@ export async function openDirectTerminal({
|
|
|
50
55
|
let unsafeFallback = false;
|
|
51
56
|
let transportControlCleanup;
|
|
52
57
|
let transportNegotiationActive = true;
|
|
53
|
-
let cleanupNative;
|
|
58
|
+
let cleanupNative = cleanupNativeImpl;
|
|
54
59
|
try {
|
|
55
60
|
manifest ??= await client.getTerminalManifest(nodeId);
|
|
56
61
|
if (
|
|
@@ -82,7 +87,7 @@ export async function openDirectTerminal({
|
|
|
82
87
|
|
|
83
88
|
const native =
|
|
84
89
|
peerConnectionFactory === undefined ? await import('node-datachannel') : undefined;
|
|
85
|
-
cleanupNative
|
|
90
|
+
cleanupNative ??= native?.cleanup;
|
|
86
91
|
const createPeer =
|
|
87
92
|
peerConnectionFactory ?? ((name, options) => new native.PeerConnection(name, options));
|
|
88
93
|
peer = createPeer(`marctl-${transportSessionId}`, { iceServers: [] });
|
|
@@ -252,6 +257,7 @@ export async function openDirectTerminal({
|
|
|
252
257
|
heartbeatMs,
|
|
253
258
|
cleanupNative
|
|
254
259
|
});
|
|
260
|
+
if (beforeOpen) await beforeOpen();
|
|
255
261
|
await withTimeout(
|
|
256
262
|
transport.waitForAttachment(terminalPrepared.grant),
|
|
257
263
|
timeoutMs,
|
package/src/i18n/en-US.mjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
const configurationHelp = `
|
|
2
|
-
MAR-launched terminals and Workspace Agent Runs
|
|
3
|
-
scheduled runs use the task owner.
|
|
4
|
-
|
|
5
|
-
the system home directory is used when HOME is unset), or select a file with --env-file.
|
|
6
|
-
The file must contain MAR_CTL_SERVER_URL and MAR_CTL_KEY. Never pass a key as an argument.`;
|
|
1
|
+
const configurationHelp = `Authorization:
|
|
2
|
+
Node access is available only in MAR-launched terminals and Workspace Agent Runs;
|
|
3
|
+
scheduled runs use the task owner. MAR supplies short-lived user credentials.
|
|
4
|
+
External terminals and persistent administrator keys are not supported.`;
|
|
7
5
|
|
|
8
6
|
export default Object.freeze({
|
|
9
7
|
help: `marctl — MyAgentRoam command-line interface
|
|
@@ -20,7 +18,6 @@ Commands:
|
|
|
20
18
|
help Show help for the CLI or a command.
|
|
21
19
|
|
|
22
20
|
Options:
|
|
23
|
-
--env-file PATH Use the specified management configuration file.
|
|
24
21
|
-h, --help Show help and exit.
|
|
25
22
|
-V, --version Print the CLI package version and exit.
|
|
26
23
|
|
|
@@ -40,23 +37,21 @@ ${configurationHelp}
|
|
|
40
37
|
|
|
41
38
|
Notes:
|
|
42
39
|
Help and version work offline without configuration or authorization.
|
|
43
|
-
Put local help/version options before a Node or file endpoint
|
|
40
|
+
Put local help/version options before a Node or file endpoint.
|
|
44
41
|
Each alias shows only its own command help. Use marctl help <command> for details.
|
|
45
42
|
Remote access uses authorized Node Terminal transport, not system SSH or SCP.`,
|
|
46
43
|
commandHelp: Object.freeze({
|
|
47
44
|
nodes: `{program} — List accessible Nodes
|
|
48
45
|
|
|
49
46
|
Usage:
|
|
50
|
-
{program} [
|
|
47
|
+
{program} [options]
|
|
51
48
|
|
|
52
49
|
Options:
|
|
53
|
-
--env-file PATH Use the specified management configuration file.
|
|
54
50
|
-h, --help Show help and exit.
|
|
55
51
|
-V, --version Print the CLI package version and exit.
|
|
56
52
|
|
|
57
53
|
Examples:
|
|
58
54
|
{program}
|
|
59
|
-
{program} --env-file ~/.marctl/production.env
|
|
60
55
|
|
|
61
56
|
Output:
|
|
62
57
|
Tab-separated columns: ID, NAME, STATUS, PLATFORM.
|
|
@@ -66,14 +61,13 @@ ${configurationHelp}`,
|
|
|
66
61
|
ssh: `{program} — Open a terminal or execute a remote command
|
|
67
62
|
|
|
68
63
|
Usage:
|
|
69
|
-
{program} [
|
|
64
|
+
{program} [options] <node> [command [arg...]]
|
|
70
65
|
|
|
71
66
|
Arguments:
|
|
72
67
|
<node> An authorized Node ID or an exact, unique Node name.
|
|
73
68
|
[command ...] A command for the remote shell; omit it for an interactive shell.
|
|
74
69
|
|
|
75
70
|
Options (before <node>):
|
|
76
|
-
--env-file PATH Use the specified management configuration file.
|
|
77
71
|
-h, --help Show help and exit.
|
|
78
72
|
-V, --version Print the CLI package version and exit.
|
|
79
73
|
|
|
@@ -96,7 +90,7 @@ ${configurationHelp}`,
|
|
|
96
90
|
scp: `{program} — Copy a regular file
|
|
97
91
|
|
|
98
92
|
Usage:
|
|
99
|
-
{program} [
|
|
93
|
+
{program} [options] <source> <destination>
|
|
100
94
|
|
|
101
95
|
Arguments:
|
|
102
96
|
<source> A local file or an authorized Node file endpoint.
|
|
@@ -108,7 +102,6 @@ Arguments:
|
|
|
108
102
|
Options:
|
|
109
103
|
-p Preserve source permissions and access/modification times.
|
|
110
104
|
-q Suppress progress output (already the default).
|
|
111
|
-
--env-file PATH Use the specified management configuration file.
|
|
112
105
|
-h, --help Show help and exit; place before the first endpoint.
|
|
113
106
|
-V, --version Print the CLI package version and exit; place before the first endpoint.
|
|
114
107
|
-- Treat remaining arguments as literal paths.
|
|
@@ -156,21 +149,9 @@ or Server connection is required.`
|
|
|
156
149
|
disconnected: 'disconnected',
|
|
157
150
|
errors: Object.freeze({
|
|
158
151
|
userContextUnavailable: 'This Node session has no valid marctl user authorization.',
|
|
159
|
-
configServerUrlMissing: 'MAR_CTL_SERVER_URL is not configured.',
|
|
160
|
-
configKeyMissing: 'MAR_CTL_KEY is not configured.',
|
|
161
|
-
configKeyInvalid: 'MAR_CTL_KEY contains invalid characters.',
|
|
162
152
|
serverUrlInvalid: 'MAR_CTL_SERVER_URL is invalid.',
|
|
163
153
|
serverUrlTransport: 'MAR_CTL_SERVER_URL must use HTTPS or loopback HTTP.',
|
|
164
154
|
serverUrlOrigin: 'MAR_CTL_SERVER_URL must be an origin without credentials or query data.',
|
|
165
|
-
envFileMissing: 'The requested environment file was not found.',
|
|
166
|
-
envFileRead: 'The environment file could not be read.',
|
|
167
|
-
envFileSymlink: 'The environment file must not be a symbolic link. Use chmod 600.',
|
|
168
|
-
envFileType: 'The environment file must be a regular file. Use chmod 600.',
|
|
169
|
-
envFilePermissions:
|
|
170
|
-
'The environment file must be owned by the current user and have mode 0600 or 0400. Use chmod 600.',
|
|
171
|
-
envFileEntry: 'Invalid environment entry.',
|
|
172
|
-
envFileUnsupported: 'Unsupported environment variable.',
|
|
173
|
-
envFileQuoted: 'Invalid quoted environment value.',
|
|
174
155
|
fetchUnavailable: 'Fetch is unavailable in this Node.js runtime.',
|
|
175
156
|
serverInvalidNodes: 'The server returned an invalid node list.',
|
|
176
157
|
serverInvalidManifest: 'The server returned an invalid node manifest.',
|
|
@@ -212,7 +193,6 @@ or Server connection is required.`
|
|
|
212
193
|
terminalDirectUnsafeFallback:
|
|
213
194
|
'Direct terminal setup could not be safely canceled after OPEN; relay fallback was skipped.',
|
|
214
195
|
invalidArguments: 'Invalid command arguments.',
|
|
215
|
-
envFileValueRequired: 'A value is required for --env-file.',
|
|
216
196
|
sshOptionsUnsupported: 'SSH options are not supported; use marssh <host>.',
|
|
217
197
|
scpOptionsUnsupported: 'This SCP option is not supported by the Node Terminal transport.',
|
|
218
198
|
commandFailed: 'The command failed.'
|
package/src/i18n/zh-CN.mjs
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
const configurationHelp =
|
|
2
|
-
MAR 打开的终端和普通 Workspace Agent Run
|
|
3
|
-
|
|
4
|
-
独立使用时读取 ~/.marctl/default.env(Windows 为 %HOME%\\.marctl\\default.env,
|
|
5
|
-
HOME 未设置时使用系统主目录),也可用 --env-file 指定文件。
|
|
6
|
-
文件须包含 MAR_CTL_SERVER_URL 和 MAR_CTL_KEY。不要通过命令参数传递密钥。`;
|
|
1
|
+
const configurationHelp = `授权:
|
|
2
|
+
仅 MAR 打开的终端和普通 Workspace Agent Run 可访问节点,定时任务使用任务 owner 的权限。
|
|
3
|
+
MAR 自动提供短期用户凭证;不支持外部终端或持久管理员密钥。`;
|
|
7
4
|
|
|
8
5
|
export default Object.freeze({
|
|
9
6
|
help: `marctl — MyAgentRoam 命令行工具
|
|
@@ -20,7 +17,6 @@ export default Object.freeze({
|
|
|
20
17
|
help 显示工具或指定子命令的帮助。
|
|
21
18
|
|
|
22
19
|
选项:
|
|
23
|
-
--env-file PATH 使用指定的管理配置文件。
|
|
24
20
|
-h, --help 显示帮助后退出。
|
|
25
21
|
-V, --version 显示 CLI 包版本后退出。
|
|
26
22
|
|
|
@@ -40,23 +36,21 @@ ${configurationHelp}
|
|
|
40
36
|
|
|
41
37
|
说明:
|
|
42
38
|
帮助和版本查询离线可用,无需配置或授权。
|
|
43
|
-
|
|
39
|
+
本机帮助、版本选项须放在节点或文件端点前。
|
|
44
40
|
快捷命令仅显示对应操作的帮助。用 marctl help <子命令> 查看详细说明。
|
|
45
41
|
远程访问使用授权的 Node Terminal 传输,不是系统 SSH 或 SCP 协议。`,
|
|
46
42
|
commandHelp: Object.freeze({
|
|
47
43
|
nodes: `{program} — 列出有权限的节点
|
|
48
44
|
|
|
49
45
|
用法:
|
|
50
|
-
{program} [
|
|
46
|
+
{program} [选项]
|
|
51
47
|
|
|
52
48
|
选项:
|
|
53
|
-
--env-file PATH 使用指定的管理配置文件。
|
|
54
49
|
-h, --help 显示帮助后退出。
|
|
55
50
|
-V, --version 显示 CLI 包版本后退出。
|
|
56
51
|
|
|
57
52
|
示例:
|
|
58
53
|
{program}
|
|
59
|
-
{program} --env-file ~/.marctl/production.env
|
|
60
54
|
|
|
61
55
|
输出:
|
|
62
56
|
以制表符分隔的节点 ID、名称、状态、平台列。
|
|
@@ -66,14 +60,13 @@ ${configurationHelp}`,
|
|
|
66
60
|
ssh: `{program} — 打开终端或执行远程命令
|
|
67
61
|
|
|
68
62
|
用法:
|
|
69
|
-
{program} [
|
|
63
|
+
{program} [选项] <节点> [command [arg...]]
|
|
70
64
|
|
|
71
65
|
参数:
|
|
72
66
|
<节点> 有权限的节点 ID,或精确、唯一的节点名称。
|
|
73
67
|
[command ...] 交给远端 Shell 的命令;省略时打开交互 Shell。
|
|
74
68
|
|
|
75
69
|
选项(放在节点前):
|
|
76
|
-
--env-file PATH 使用指定的管理配置文件。
|
|
77
70
|
-h, --help 显示帮助后退出。
|
|
78
71
|
-V, --version 显示 CLI 包版本后退出。
|
|
79
72
|
|
|
@@ -94,7 +87,7 @@ ${configurationHelp}`,
|
|
|
94
87
|
scp: `{program} — 复制普通文件
|
|
95
88
|
|
|
96
89
|
用法:
|
|
97
|
-
{program} [
|
|
90
|
+
{program} [选项] <源> <目标>
|
|
98
91
|
|
|
99
92
|
参数:
|
|
100
93
|
<源> 本机文件或有权限的节点文件端点。
|
|
@@ -106,7 +99,6 @@ ${configurationHelp}`,
|
|
|
106
99
|
选项:
|
|
107
100
|
-p 保留源文件权限及访问、修改时间。
|
|
108
101
|
-q 不显示进度(默认已不显示)。
|
|
109
|
-
--env-file PATH 使用指定的管理配置文件。
|
|
110
102
|
-h, --help 显示帮助后退出,须放在第一个文件端点前。
|
|
111
103
|
-V, --version 显示 CLI 包版本后退出,须放在第一个文件端点前。
|
|
112
104
|
-- 后续参数按字面路径处理。
|
|
@@ -151,20 +143,9 @@ ${configurationHelp}`,
|
|
|
151
143
|
disconnected: '离线',
|
|
152
144
|
errors: Object.freeze({
|
|
153
145
|
userContextUnavailable: '此 Node 会话没有有效的 marctl 用户授权。',
|
|
154
|
-
configServerUrlMissing: '未配置 MAR_CTL_SERVER_URL。',
|
|
155
|
-
configKeyMissing: '未配置 MAR_CTL_KEY。',
|
|
156
|
-
configKeyInvalid: 'MAR_CTL_KEY 包含无效字符。',
|
|
157
146
|
serverUrlInvalid: 'MAR_CTL_SERVER_URL 无效。',
|
|
158
147
|
serverUrlTransport: 'MAR_CTL_SERVER_URL 必须使用 HTTPS 或回环 HTTP。',
|
|
159
148
|
serverUrlOrigin: 'MAR_CTL_SERVER_URL 必须是不含凭据或查询参数的 Origin。',
|
|
160
|
-
envFileMissing: '指定的环境文件不存在。',
|
|
161
|
-
envFileRead: '无法读取环境文件。',
|
|
162
|
-
envFileSymlink: '环境文件不能是符号链接。请使用 chmod 600。',
|
|
163
|
-
envFileType: '环境文件必须是普通文件。请使用 chmod 600。',
|
|
164
|
-
envFilePermissions: '环境文件必须归当前用户所有,权限必须为 0600 或 0400。请使用 chmod 600。',
|
|
165
|
-
envFileEntry: '环境文件条目无效。',
|
|
166
|
-
envFileUnsupported: '环境变量不受支持。',
|
|
167
|
-
envFileQuoted: '带引号的环境变量值无效。',
|
|
168
149
|
fetchUnavailable: '当前 Node.js 运行时没有 Fetch。',
|
|
169
150
|
serverInvalidNodes: 'Server 返回的节点列表无效。',
|
|
170
151
|
serverInvalidManifest: 'Server 返回的节点清单无效。',
|
|
@@ -203,7 +184,6 @@ ${configurationHelp}`,
|
|
|
203
184
|
terminalSessionFailed: '终端会话失败。',
|
|
204
185
|
terminalDirectUnsafeFallback: 'OPEN 后无法安全取消直连终端,已跳过 relay 回退。',
|
|
205
186
|
invalidArguments: '命令参数无效。',
|
|
206
|
-
envFileValueRequired: '--env-file 必须提供值。',
|
|
207
187
|
sshOptionsUnsupported: '不支持 SSH 选项,请使用 marssh <节点>。',
|
|
208
188
|
scpOptionsUnsupported: 'Node Terminal 传输不支持此 SCP 选项。',
|
|
209
189
|
terminalBinaryUnavailable: '二进制复制需要支持 pipe 终端的 Server 与 Node。',
|
package/src/terminal.mjs
CHANGED
|
@@ -583,7 +583,9 @@ function runAttachedTerminal({
|
|
|
583
583
|
const onResize = () => {
|
|
584
584
|
try {
|
|
585
585
|
const { rows, cols } = getDimensions(stdout);
|
|
586
|
-
send({ type: 'resize', terminalId, rows, cols })
|
|
586
|
+
Promise.resolve(send({ type: 'resize', terminalId, rows, cols })).catch(() =>
|
|
587
|
+
fail('errors.terminalResizeFailed')
|
|
588
|
+
);
|
|
587
589
|
} catch {
|
|
588
590
|
fail('errors.terminalResizeFailed');
|
|
589
591
|
}
|
|
@@ -686,7 +688,7 @@ function rawToString(raw) {
|
|
|
686
688
|
function sendCloseFrame(socket, terminalId) {
|
|
687
689
|
if (!terminalId || !isSocketOpen(socket)) return;
|
|
688
690
|
try {
|
|
689
|
-
socket.send(JSON.stringify({ type: 'close', terminalId }));
|
|
691
|
+
Promise.resolve(socket.send(JSON.stringify({ type: 'close', terminalId }))).catch(() => {});
|
|
690
692
|
} catch {
|
|
691
693
|
// The DELETE request below remains the authoritative cleanup path.
|
|
692
694
|
}
|