@slicervm/sdk 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/index.cjs +314 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +204 -1
- package/dist/index.d.ts +204 -1
- package/dist/index.js +308 -3
- package/dist/index.js.map +1 -1
- package/dist/shell.cjs +152 -0
- package/dist/shell.cjs.map +1 -0
- package/dist/shell.d.cts +67 -0
- package/dist/shell.d.ts +67 -0
- package/dist/shell.js +143 -0
- package/dist/shell.js.map +1 -0
- package/package.json +11 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'os';
|
|
|
5
5
|
import path2 from 'path';
|
|
6
6
|
import net, { createServer } from 'net';
|
|
7
7
|
import fs from 'fs';
|
|
8
|
-
import { createWebSocketStream, WebSocket } from 'ws';
|
|
8
|
+
import { createWebSocketStream, WebSocket as WebSocket$1 } from 'ws';
|
|
9
9
|
|
|
10
10
|
// src/types.ts
|
|
11
11
|
var ExecStdioText = "text";
|
|
@@ -526,7 +526,7 @@ function openWebSocket(init, mapping) {
|
|
|
526
526
|
if (init.transport.kind === "socket") {
|
|
527
527
|
opts.agent = unixAgent(init.transport.socketPath);
|
|
528
528
|
}
|
|
529
|
-
return new WebSocket(url, opts);
|
|
529
|
+
return new WebSocket$1(url, opts);
|
|
530
530
|
}
|
|
531
531
|
function wsURLForVM(transport, hostname) {
|
|
532
532
|
if (transport.kind === "socket") {
|
|
@@ -734,6 +734,7 @@ var VM = class {
|
|
|
734
734
|
createdAt;
|
|
735
735
|
arch;
|
|
736
736
|
fs;
|
|
737
|
+
bg;
|
|
737
738
|
transport;
|
|
738
739
|
constructor(transport, init) {
|
|
739
740
|
this.transport = transport;
|
|
@@ -743,6 +744,7 @@ var VM = class {
|
|
|
743
744
|
if (init.createdAt !== void 0) this.createdAt = init.createdAt;
|
|
744
745
|
if (init.arch !== void 0) this.arch = init.arch;
|
|
745
746
|
this.fs = new VMFileSystem(transport, this.hostname);
|
|
747
|
+
this.bg = new VMBg(transport, this.hostname);
|
|
746
748
|
}
|
|
747
749
|
// --- lifecycle --------------------------------------------------------
|
|
748
750
|
async delete() {
|
|
@@ -944,6 +946,169 @@ function sleep(ms) {
|
|
|
944
946
|
function errMsg(e) {
|
|
945
947
|
return e instanceof Error ? e.message : String(e);
|
|
946
948
|
}
|
|
949
|
+
var VMBg = class {
|
|
950
|
+
constructor(transport, hostname) {
|
|
951
|
+
this.transport = transport;
|
|
952
|
+
this.hostname = hostname;
|
|
953
|
+
}
|
|
954
|
+
transport;
|
|
955
|
+
hostname;
|
|
956
|
+
/**
|
|
957
|
+
* Launch a long-running process. `command` + `args` is the deterministic
|
|
958
|
+
* exec form (no shell). Set `shell: '/bin/bash'` (or similar) to opt in
|
|
959
|
+
* to shell semantics — `$VAR` expansion, globs, `&&`/`||`, etc.
|
|
960
|
+
*/
|
|
961
|
+
async exec(req) {
|
|
962
|
+
if (!req.command) throw new Error("vm.bg.exec: command is required");
|
|
963
|
+
const q = new URLSearchParams();
|
|
964
|
+
q.set("background", "true");
|
|
965
|
+
q.set("cmd", req.command);
|
|
966
|
+
for (const a of req.args ?? []) q.append("args", a);
|
|
967
|
+
for (const e of req.env ?? []) q.append("env", e);
|
|
968
|
+
if (req.uid !== void 0) q.set("uid", String(req.uid));
|
|
969
|
+
if (req.gid !== void 0) q.set("gid", String(req.gid));
|
|
970
|
+
if (req.shell) q.set("shell", req.shell);
|
|
971
|
+
if (req.cwd) q.set("cwd", req.cwd);
|
|
972
|
+
if (req.ringBytes !== void 0 && req.ringBytes > 0) {
|
|
973
|
+
q.set("ring_bytes", String(req.ringBytes));
|
|
974
|
+
}
|
|
975
|
+
q.set("stdio", "base64");
|
|
976
|
+
const path3 = `/vm/${encodeURIComponent(this.hostname)}/exec?${q.toString()}`;
|
|
977
|
+
const wire = await this.transport.request("POST", path3);
|
|
978
|
+
return bgExecResponseFromWire(wire);
|
|
979
|
+
}
|
|
980
|
+
/** All background execs the agent currently tracks (running + exited-not-reaped). */
|
|
981
|
+
async list() {
|
|
982
|
+
const wire = await this.transport.request(
|
|
983
|
+
"GET",
|
|
984
|
+
`/vm/${encodeURIComponent(this.hostname)}/exec`
|
|
985
|
+
);
|
|
986
|
+
return (wire ?? []).map(bgExecInfoFromWire);
|
|
987
|
+
}
|
|
988
|
+
/** Latest status snapshot for one bg exec. Throws 404 if reaped or never existed. */
|
|
989
|
+
async info(execId) {
|
|
990
|
+
const wire = await this.transport.request(
|
|
991
|
+
"GET",
|
|
992
|
+
`/vm/${encodeURIComponent(this.hostname)}/exec/${encodeURIComponent(execId)}`
|
|
993
|
+
);
|
|
994
|
+
return bgExecInfoFromWire(wire);
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* NDJSON log stream. Yields one frame per log line — `started`, `stdout`,
|
|
998
|
+
* `stderr`, `exit`, plus optional `gap` frames if the ring evicted history
|
|
999
|
+
* before the requested cursor. Frames carry `data` base64-encoded; the SDK
|
|
1000
|
+
* also populates `dataBytes` / `stdoutBytes` / `stderrBytes` Buffers.
|
|
1001
|
+
*
|
|
1002
|
+
* `follow: false` (default) replays from the cursor and ends when the ring
|
|
1003
|
+
* is drained. `follow: true` keeps the stream open until the child exits or
|
|
1004
|
+
* the caller breaks out.
|
|
1005
|
+
*/
|
|
1006
|
+
async *logs(execId, opts = {}) {
|
|
1007
|
+
const q = new URLSearchParams();
|
|
1008
|
+
if (opts.follow) q.set("follow", "true");
|
|
1009
|
+
if (opts.fromId !== void 0 && opts.fromId > 0) q.set("from_id", String(opts.fromId));
|
|
1010
|
+
const path3 = `/vm/${encodeURIComponent(this.hostname)}/exec/${encodeURIComponent(execId)}/logs` + (q.toString() ? `?${q.toString()}` : "");
|
|
1011
|
+
for await (const raw of this.transport.requestNDJSON("GET", path3)) {
|
|
1012
|
+
const frame = normalizeExecFrame(raw);
|
|
1013
|
+
if (frame.encoding === "base64") {
|
|
1014
|
+
if (frame.data) frame.dataBytes = Buffer.from(frame.data, "base64");
|
|
1015
|
+
if (frame.stdout) frame.stdoutBytes = Buffer.from(frame.stdout, "base64");
|
|
1016
|
+
if (frame.stderr) frame.stderrBytes = Buffer.from(frame.stderr, "base64");
|
|
1017
|
+
}
|
|
1018
|
+
yield frame;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Signal a running bg exec. Default: SIGTERM with a 5 s grace period before
|
|
1023
|
+
* the agent escalates to SIGKILL. No-op (running=false) if the child has
|
|
1024
|
+
* already exited.
|
|
1025
|
+
*/
|
|
1026
|
+
async kill(execId, opts = {}) {
|
|
1027
|
+
const body = {};
|
|
1028
|
+
if (opts.signal) body.signal = opts.signal;
|
|
1029
|
+
if (opts.graceMs !== void 0) body.grace_ms = opts.graceMs;
|
|
1030
|
+
const wire = await this.transport.request(
|
|
1031
|
+
"POST",
|
|
1032
|
+
`/vm/${encodeURIComponent(this.hostname)}/exec/${encodeURIComponent(execId)}/kill`,
|
|
1033
|
+
body
|
|
1034
|
+
);
|
|
1035
|
+
return bgKillResponseFromWire(wire);
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Long-poll until the child exits or `timeoutSec` elapses. Returns
|
|
1039
|
+
* `timedOut: true` if the deadline hit. Server default for `timeoutSec=0`
|
|
1040
|
+
* is 30 s.
|
|
1041
|
+
*/
|
|
1042
|
+
async wait(execId, timeoutSec = 0) {
|
|
1043
|
+
const q = new URLSearchParams();
|
|
1044
|
+
if (timeoutSec > 0) q.set("timeout", String(timeoutSec));
|
|
1045
|
+
const path3 = `/vm/${encodeURIComponent(this.hostname)}/exec/${encodeURIComponent(execId)}/wait-exit` + (q.toString() ? `?${q.toString()}` : "");
|
|
1046
|
+
const wire = await this.transport.request("GET", path3);
|
|
1047
|
+
return bgWaitExitFromWire(wire);
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Reap a bg exec's ring buffer + registry entry. Does NOT kill a running
|
|
1051
|
+
* process — pair with `kill()` for "stop and clean up". After remove,
|
|
1052
|
+
* info/logs/kill/wait return 410 Gone.
|
|
1053
|
+
*/
|
|
1054
|
+
async remove(execId) {
|
|
1055
|
+
const wire = await this.transport.request(
|
|
1056
|
+
"DELETE",
|
|
1057
|
+
`/vm/${encodeURIComponent(this.hostname)}/exec/${encodeURIComponent(execId)}`
|
|
1058
|
+
);
|
|
1059
|
+
return bgDeleteFromWire(wire);
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
function bgExecResponseFromWire(w) {
|
|
1063
|
+
return {
|
|
1064
|
+
execId: w.exec_id,
|
|
1065
|
+
pid: w.pid,
|
|
1066
|
+
startedAt: w.started_at,
|
|
1067
|
+
ringBytes: w.ring_bytes
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function bgExecInfoFromWire(w) {
|
|
1071
|
+
const out = {
|
|
1072
|
+
execId: w.exec_id,
|
|
1073
|
+
pid: w.pid,
|
|
1074
|
+
command: w.command,
|
|
1075
|
+
startedAt: w.started_at,
|
|
1076
|
+
running: w.running,
|
|
1077
|
+
bytesWritten: w.bytes_written,
|
|
1078
|
+
bytesDropped: w.bytes_dropped,
|
|
1079
|
+
nextId: w.next_id,
|
|
1080
|
+
ringBytes: w.ring_bytes
|
|
1081
|
+
};
|
|
1082
|
+
if (w.args !== void 0) out.args = w.args;
|
|
1083
|
+
if (w.cwd !== void 0) out.cwd = w.cwd;
|
|
1084
|
+
if (w.uid !== void 0) out.uid = w.uid;
|
|
1085
|
+
if (w.exit_code !== void 0) out.exitCode = w.exit_code;
|
|
1086
|
+
if (w.signal !== void 0) out.signal = w.signal;
|
|
1087
|
+
if (w.ended_at !== void 0) out.endedAt = w.ended_at;
|
|
1088
|
+
return out;
|
|
1089
|
+
}
|
|
1090
|
+
function bgKillResponseFromWire(w) {
|
|
1091
|
+
return {
|
|
1092
|
+
execId: w.exec_id,
|
|
1093
|
+
pid: w.pid,
|
|
1094
|
+
running: w.running,
|
|
1095
|
+
signalSent: w.signal_sent
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function bgWaitExitFromWire(w) {
|
|
1099
|
+
const out = {
|
|
1100
|
+
execId: w.exec_id,
|
|
1101
|
+
running: w.running,
|
|
1102
|
+
timedOut: w.timed_out
|
|
1103
|
+
};
|
|
1104
|
+
if (w.exit_code !== void 0) out.exitCode = w.exit_code;
|
|
1105
|
+
if (w.signal !== void 0) out.signal = w.signal;
|
|
1106
|
+
if (w.ended_at !== void 0) out.endedAt = w.ended_at;
|
|
1107
|
+
return out;
|
|
1108
|
+
}
|
|
1109
|
+
function bgDeleteFromWire(w) {
|
|
1110
|
+
return { execId: w.exec_id, reaped: w.reaped };
|
|
1111
|
+
}
|
|
947
1112
|
|
|
948
1113
|
// src/namespaces.ts
|
|
949
1114
|
var HostGroupsAPI = class {
|
|
@@ -1113,6 +1278,146 @@ var SlicerClient = class _SlicerClient {
|
|
|
1113
1278
|
}
|
|
1114
1279
|
};
|
|
1115
1280
|
|
|
1116
|
-
|
|
1281
|
+
// src/shell.ts
|
|
1282
|
+
var FRAME_TYPE_DATA = 1;
|
|
1283
|
+
var FRAME_TYPE_WINDOW_SIZE = 2;
|
|
1284
|
+
var FRAME_TYPE_SHUTDOWN = 3;
|
|
1285
|
+
var FRAME_TYPE_HEARTBEAT = 4;
|
|
1286
|
+
var FRAME_TYPE_SESSION_CLOSE = 5;
|
|
1287
|
+
var HEADER_SIZE = 5;
|
|
1288
|
+
function encodeFrame(frameType, payload) {
|
|
1289
|
+
const payloadLen = payload ? payload.byteLength : 0;
|
|
1290
|
+
const buf = new Uint8Array(HEADER_SIZE + payloadLen);
|
|
1291
|
+
const view = new DataView(buf.buffer);
|
|
1292
|
+
view.setUint8(0, frameType);
|
|
1293
|
+
view.setUint32(1, payloadLen, false);
|
|
1294
|
+
if (payload) buf.set(payload, HEADER_SIZE);
|
|
1295
|
+
return buf;
|
|
1296
|
+
}
|
|
1297
|
+
function parseFrame(data) {
|
|
1298
|
+
if (data.byteLength < HEADER_SIZE) return null;
|
|
1299
|
+
const view = new DataView(data);
|
|
1300
|
+
const frameType = view.getUint8(0);
|
|
1301
|
+
const payloadLen = view.getUint32(1, false);
|
|
1302
|
+
if (data.byteLength < HEADER_SIZE + payloadLen) return null;
|
|
1303
|
+
const payload = new Uint8Array(data, HEADER_SIZE, payloadLen);
|
|
1304
|
+
return { frameType, payload };
|
|
1305
|
+
}
|
|
1306
|
+
var encoder = new TextEncoder();
|
|
1307
|
+
var decoder = new TextDecoder();
|
|
1308
|
+
var SlicerShellSession = class {
|
|
1309
|
+
constructor(terminal, options) {
|
|
1310
|
+
this.terminal = terminal;
|
|
1311
|
+
this.options = options;
|
|
1312
|
+
}
|
|
1313
|
+
terminal;
|
|
1314
|
+
options;
|
|
1315
|
+
ws = null;
|
|
1316
|
+
heartbeatTimer = null;
|
|
1317
|
+
dataDisposable = null;
|
|
1318
|
+
/** True when the WebSocket is open and relaying. */
|
|
1319
|
+
get connected() {
|
|
1320
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
1321
|
+
}
|
|
1322
|
+
/** Open the WebSocket and begin relaying. */
|
|
1323
|
+
connect() {
|
|
1324
|
+
if (this.ws) return;
|
|
1325
|
+
this.options.onStateChange?.("connecting");
|
|
1326
|
+
const ws = new WebSocket(this.options.url);
|
|
1327
|
+
ws.binaryType = "arraybuffer";
|
|
1328
|
+
this.ws = ws;
|
|
1329
|
+
ws.onopen = () => {
|
|
1330
|
+
this.options.onStateChange?.("connected");
|
|
1331
|
+
this.terminal.reset();
|
|
1332
|
+
this.sendResize(this.terminal.cols, this.terminal.rows);
|
|
1333
|
+
this.startHeartbeat();
|
|
1334
|
+
};
|
|
1335
|
+
ws.onmessage = (ev) => {
|
|
1336
|
+
if (!(ev.data instanceof ArrayBuffer)) return;
|
|
1337
|
+
const frame = parseFrame(ev.data);
|
|
1338
|
+
if (!frame) return;
|
|
1339
|
+
switch (frame.frameType) {
|
|
1340
|
+
case FRAME_TYPE_DATA:
|
|
1341
|
+
this.terminal.write(decoder.decode(frame.payload));
|
|
1342
|
+
break;
|
|
1343
|
+
case FRAME_TYPE_SHUTDOWN:
|
|
1344
|
+
case FRAME_TYPE_SESSION_CLOSE:
|
|
1345
|
+
this.teardown();
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
ws.onclose = () => {
|
|
1350
|
+
this.teardown();
|
|
1351
|
+
};
|
|
1352
|
+
ws.onerror = () => {
|
|
1353
|
+
this.options.onError?.("WebSocket error");
|
|
1354
|
+
this.teardown();
|
|
1355
|
+
};
|
|
1356
|
+
this.dataDisposable = this.terminal.onData((data) => {
|
|
1357
|
+
if (!this.connected) return;
|
|
1358
|
+
const payload = encoder.encode(data);
|
|
1359
|
+
this.ws.send(encodeFrame(FRAME_TYPE_DATA, payload));
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
/** Send a graceful shutdown frame and close. */
|
|
1363
|
+
disconnect() {
|
|
1364
|
+
if (this.ws) {
|
|
1365
|
+
try {
|
|
1366
|
+
this.ws.send(encodeFrame(FRAME_TYPE_SHUTDOWN));
|
|
1367
|
+
} catch {
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
this.teardown();
|
|
1371
|
+
}
|
|
1372
|
+
/** Send a window resize. Call this from FitAddon's onResize or a ResizeObserver. */
|
|
1373
|
+
resize(cols, rows) {
|
|
1374
|
+
if (!this.connected) return;
|
|
1375
|
+
this.sendResize(cols, rows);
|
|
1376
|
+
}
|
|
1377
|
+
// --- internals -------------------------------------------------------------
|
|
1378
|
+
sendResize(cols, rows) {
|
|
1379
|
+
const payload = new Uint8Array(8);
|
|
1380
|
+
const view = new DataView(payload.buffer);
|
|
1381
|
+
view.setUint32(0, cols, false);
|
|
1382
|
+
view.setUint32(4, rows, false);
|
|
1383
|
+
this.ws.send(encodeFrame(FRAME_TYPE_WINDOW_SIZE, payload));
|
|
1384
|
+
}
|
|
1385
|
+
startHeartbeat() {
|
|
1386
|
+
this.stopHeartbeat();
|
|
1387
|
+
const intervalMs = this.options.heartbeatIntervalMs ?? 3e4;
|
|
1388
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1389
|
+
if (!this.connected) return;
|
|
1390
|
+
this.ws.send(encodeFrame(FRAME_TYPE_HEARTBEAT));
|
|
1391
|
+
}, intervalMs);
|
|
1392
|
+
}
|
|
1393
|
+
stopHeartbeat() {
|
|
1394
|
+
if (this.heartbeatTimer !== null) {
|
|
1395
|
+
clearInterval(this.heartbeatTimer);
|
|
1396
|
+
this.heartbeatTimer = null;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
teardown() {
|
|
1400
|
+
this.stopHeartbeat();
|
|
1401
|
+
if (this.dataDisposable) {
|
|
1402
|
+
this.dataDisposable.dispose();
|
|
1403
|
+
this.dataDisposable = null;
|
|
1404
|
+
}
|
|
1405
|
+
if (this.ws) {
|
|
1406
|
+
const ws = this.ws;
|
|
1407
|
+
this.ws = null;
|
|
1408
|
+
ws.onopen = null;
|
|
1409
|
+
ws.onmessage = null;
|
|
1410
|
+
ws.onclose = null;
|
|
1411
|
+
ws.onerror = null;
|
|
1412
|
+
try {
|
|
1413
|
+
ws.close();
|
|
1414
|
+
} catch {
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
this.options.onStateChange?.("disconnected");
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1421
|
+
export { ExecStdioBase64, ExecStdioText, FRAME_TYPE_DATA, FRAME_TYPE_HEARTBEAT, FRAME_TYPE_SESSION_CLOSE, FRAME_TYPE_SHUTDOWN, FRAME_TYPE_WINDOW_SIZE, Forwarder, GiB, HostGroupsAPI, MiB, NonRootUser, SecretExistsError, SecretsAPI, SlicerAPIError, SlicerClient, SlicerShellSession, VM, VMBg, VMFileSystem, VMsAPI, encodeFrame, parseAddressMapping, parseFrame, resolveTransport };
|
|
1117
1422
|
//# sourceMappingURL=index.js.map
|
|
1118
1423
|
//# sourceMappingURL=index.js.map
|