@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/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
- export { ExecStdioBase64, ExecStdioText, Forwarder, GiB, HostGroupsAPI, MiB, NonRootUser, SecretExistsError, SecretsAPI, SlicerAPIError, SlicerClient, VM, VMFileSystem, VMsAPI, parseAddressMapping, resolveTransport };
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