@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 CHANGED
@@ -23,6 +23,13 @@ console.log(result.stdout);
23
23
  await vm.fs.writeFile('/tmp/hello.txt', 'hi');
24
24
  console.log((await vm.fs.readFile('/tmp/hello.txt')).toString());
25
25
 
26
+ // Background exec — detached, survives disconnect, logs into an agent-side
27
+ // ring buffer. Manage via vm.bg.list/info/logs/kill/wait/remove.
28
+ const bg = await vm.bg.exec({ command: 'npm', args: ['run', 'dev'], cwd: '/app' });
29
+ for await (const f of vm.bg.logs(bg.execId, { follow: true })) {
30
+ if (f.stdoutBytes) process.stdout.write(f.stdoutBytes);
31
+ }
32
+
26
33
  await vm.delete();
27
34
  ```
28
35
 
package/dist/index.cjs CHANGED
@@ -745,6 +745,7 @@ var VM = class {
745
745
  createdAt;
746
746
  arch;
747
747
  fs;
748
+ bg;
748
749
  transport;
749
750
  constructor(transport, init) {
750
751
  this.transport = transport;
@@ -754,6 +755,7 @@ var VM = class {
754
755
  if (init.createdAt !== void 0) this.createdAt = init.createdAt;
755
756
  if (init.arch !== void 0) this.arch = init.arch;
756
757
  this.fs = new VMFileSystem(transport, this.hostname);
758
+ this.bg = new VMBg(transport, this.hostname);
757
759
  }
758
760
  // --- lifecycle --------------------------------------------------------
759
761
  async delete() {
@@ -955,6 +957,169 @@ function sleep(ms) {
955
957
  function errMsg(e) {
956
958
  return e instanceof Error ? e.message : String(e);
957
959
  }
960
+ var VMBg = class {
961
+ constructor(transport, hostname) {
962
+ this.transport = transport;
963
+ this.hostname = hostname;
964
+ }
965
+ transport;
966
+ hostname;
967
+ /**
968
+ * Launch a long-running process. `command` + `args` is the deterministic
969
+ * exec form (no shell). Set `shell: '/bin/bash'` (or similar) to opt in
970
+ * to shell semantics — `$VAR` expansion, globs, `&&`/`||`, etc.
971
+ */
972
+ async exec(req) {
973
+ if (!req.command) throw new Error("vm.bg.exec: command is required");
974
+ const q = new URLSearchParams();
975
+ q.set("background", "true");
976
+ q.set("cmd", req.command);
977
+ for (const a of req.args ?? []) q.append("args", a);
978
+ for (const e of req.env ?? []) q.append("env", e);
979
+ if (req.uid !== void 0) q.set("uid", String(req.uid));
980
+ if (req.gid !== void 0) q.set("gid", String(req.gid));
981
+ if (req.shell) q.set("shell", req.shell);
982
+ if (req.cwd) q.set("cwd", req.cwd);
983
+ if (req.ringBytes !== void 0 && req.ringBytes > 0) {
984
+ q.set("ring_bytes", String(req.ringBytes));
985
+ }
986
+ q.set("stdio", "base64");
987
+ const path3 = `/vm/${encodeURIComponent(this.hostname)}/exec?${q.toString()}`;
988
+ const wire = await this.transport.request("POST", path3);
989
+ return bgExecResponseFromWire(wire);
990
+ }
991
+ /** All background execs the agent currently tracks (running + exited-not-reaped). */
992
+ async list() {
993
+ const wire = await this.transport.request(
994
+ "GET",
995
+ `/vm/${encodeURIComponent(this.hostname)}/exec`
996
+ );
997
+ return (wire ?? []).map(bgExecInfoFromWire);
998
+ }
999
+ /** Latest status snapshot for one bg exec. Throws 404 if reaped or never existed. */
1000
+ async info(execId) {
1001
+ const wire = await this.transport.request(
1002
+ "GET",
1003
+ `/vm/${encodeURIComponent(this.hostname)}/exec/${encodeURIComponent(execId)}`
1004
+ );
1005
+ return bgExecInfoFromWire(wire);
1006
+ }
1007
+ /**
1008
+ * NDJSON log stream. Yields one frame per log line — `started`, `stdout`,
1009
+ * `stderr`, `exit`, plus optional `gap` frames if the ring evicted history
1010
+ * before the requested cursor. Frames carry `data` base64-encoded; the SDK
1011
+ * also populates `dataBytes` / `stdoutBytes` / `stderrBytes` Buffers.
1012
+ *
1013
+ * `follow: false` (default) replays from the cursor and ends when the ring
1014
+ * is drained. `follow: true` keeps the stream open until the child exits or
1015
+ * the caller breaks out.
1016
+ */
1017
+ async *logs(execId, opts = {}) {
1018
+ const q = new URLSearchParams();
1019
+ if (opts.follow) q.set("follow", "true");
1020
+ if (opts.fromId !== void 0 && opts.fromId > 0) q.set("from_id", String(opts.fromId));
1021
+ const path3 = `/vm/${encodeURIComponent(this.hostname)}/exec/${encodeURIComponent(execId)}/logs` + (q.toString() ? `?${q.toString()}` : "");
1022
+ for await (const raw of this.transport.requestNDJSON("GET", path3)) {
1023
+ const frame = normalizeExecFrame(raw);
1024
+ if (frame.encoding === "base64") {
1025
+ if (frame.data) frame.dataBytes = Buffer.from(frame.data, "base64");
1026
+ if (frame.stdout) frame.stdoutBytes = Buffer.from(frame.stdout, "base64");
1027
+ if (frame.stderr) frame.stderrBytes = Buffer.from(frame.stderr, "base64");
1028
+ }
1029
+ yield frame;
1030
+ }
1031
+ }
1032
+ /**
1033
+ * Signal a running bg exec. Default: SIGTERM with a 5 s grace period before
1034
+ * the agent escalates to SIGKILL. No-op (running=false) if the child has
1035
+ * already exited.
1036
+ */
1037
+ async kill(execId, opts = {}) {
1038
+ const body = {};
1039
+ if (opts.signal) body.signal = opts.signal;
1040
+ if (opts.graceMs !== void 0) body.grace_ms = opts.graceMs;
1041
+ const wire = await this.transport.request(
1042
+ "POST",
1043
+ `/vm/${encodeURIComponent(this.hostname)}/exec/${encodeURIComponent(execId)}/kill`,
1044
+ body
1045
+ );
1046
+ return bgKillResponseFromWire(wire);
1047
+ }
1048
+ /**
1049
+ * Long-poll until the child exits or `timeoutSec` elapses. Returns
1050
+ * `timedOut: true` if the deadline hit. Server default for `timeoutSec=0`
1051
+ * is 30 s.
1052
+ */
1053
+ async wait(execId, timeoutSec = 0) {
1054
+ const q = new URLSearchParams();
1055
+ if (timeoutSec > 0) q.set("timeout", String(timeoutSec));
1056
+ const path3 = `/vm/${encodeURIComponent(this.hostname)}/exec/${encodeURIComponent(execId)}/wait-exit` + (q.toString() ? `?${q.toString()}` : "");
1057
+ const wire = await this.transport.request("GET", path3);
1058
+ return bgWaitExitFromWire(wire);
1059
+ }
1060
+ /**
1061
+ * Reap a bg exec's ring buffer + registry entry. Does NOT kill a running
1062
+ * process — pair with `kill()` for "stop and clean up". After remove,
1063
+ * info/logs/kill/wait return 410 Gone.
1064
+ */
1065
+ async remove(execId) {
1066
+ const wire = await this.transport.request(
1067
+ "DELETE",
1068
+ `/vm/${encodeURIComponent(this.hostname)}/exec/${encodeURIComponent(execId)}`
1069
+ );
1070
+ return bgDeleteFromWire(wire);
1071
+ }
1072
+ };
1073
+ function bgExecResponseFromWire(w) {
1074
+ return {
1075
+ execId: w.exec_id,
1076
+ pid: w.pid,
1077
+ startedAt: w.started_at,
1078
+ ringBytes: w.ring_bytes
1079
+ };
1080
+ }
1081
+ function bgExecInfoFromWire(w) {
1082
+ const out = {
1083
+ execId: w.exec_id,
1084
+ pid: w.pid,
1085
+ command: w.command,
1086
+ startedAt: w.started_at,
1087
+ running: w.running,
1088
+ bytesWritten: w.bytes_written,
1089
+ bytesDropped: w.bytes_dropped,
1090
+ nextId: w.next_id,
1091
+ ringBytes: w.ring_bytes
1092
+ };
1093
+ if (w.args !== void 0) out.args = w.args;
1094
+ if (w.cwd !== void 0) out.cwd = w.cwd;
1095
+ if (w.uid !== void 0) out.uid = w.uid;
1096
+ if (w.exit_code !== void 0) out.exitCode = w.exit_code;
1097
+ if (w.signal !== void 0) out.signal = w.signal;
1098
+ if (w.ended_at !== void 0) out.endedAt = w.ended_at;
1099
+ return out;
1100
+ }
1101
+ function bgKillResponseFromWire(w) {
1102
+ return {
1103
+ execId: w.exec_id,
1104
+ pid: w.pid,
1105
+ running: w.running,
1106
+ signalSent: w.signal_sent
1107
+ };
1108
+ }
1109
+ function bgWaitExitFromWire(w) {
1110
+ const out = {
1111
+ execId: w.exec_id,
1112
+ running: w.running,
1113
+ timedOut: w.timed_out
1114
+ };
1115
+ if (w.exit_code !== void 0) out.exitCode = w.exit_code;
1116
+ if (w.signal !== void 0) out.signal = w.signal;
1117
+ if (w.ended_at !== void 0) out.endedAt = w.ended_at;
1118
+ return out;
1119
+ }
1120
+ function bgDeleteFromWire(w) {
1121
+ return { execId: w.exec_id, reaped: w.reaped };
1122
+ }
958
1123
 
959
1124
  // src/namespaces.ts
960
1125
  var HostGroupsAPI = class {
@@ -1124,8 +1289,153 @@ var SlicerClient = class _SlicerClient {
1124
1289
  }
1125
1290
  };
1126
1291
 
1292
+ // src/shell.ts
1293
+ var FRAME_TYPE_DATA = 1;
1294
+ var FRAME_TYPE_WINDOW_SIZE = 2;
1295
+ var FRAME_TYPE_SHUTDOWN = 3;
1296
+ var FRAME_TYPE_HEARTBEAT = 4;
1297
+ var FRAME_TYPE_SESSION_CLOSE = 5;
1298
+ var HEADER_SIZE = 5;
1299
+ function encodeFrame(frameType, payload) {
1300
+ const payloadLen = payload ? payload.byteLength : 0;
1301
+ const buf = new Uint8Array(HEADER_SIZE + payloadLen);
1302
+ const view = new DataView(buf.buffer);
1303
+ view.setUint8(0, frameType);
1304
+ view.setUint32(1, payloadLen, false);
1305
+ if (payload) buf.set(payload, HEADER_SIZE);
1306
+ return buf;
1307
+ }
1308
+ function parseFrame(data) {
1309
+ if (data.byteLength < HEADER_SIZE) return null;
1310
+ const view = new DataView(data);
1311
+ const frameType = view.getUint8(0);
1312
+ const payloadLen = view.getUint32(1, false);
1313
+ if (data.byteLength < HEADER_SIZE + payloadLen) return null;
1314
+ const payload = new Uint8Array(data, HEADER_SIZE, payloadLen);
1315
+ return { frameType, payload };
1316
+ }
1317
+ var encoder = new TextEncoder();
1318
+ var decoder = new TextDecoder();
1319
+ var SlicerShellSession = class {
1320
+ constructor(terminal, options) {
1321
+ this.terminal = terminal;
1322
+ this.options = options;
1323
+ }
1324
+ terminal;
1325
+ options;
1326
+ ws = null;
1327
+ heartbeatTimer = null;
1328
+ dataDisposable = null;
1329
+ /** True when the WebSocket is open and relaying. */
1330
+ get connected() {
1331
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
1332
+ }
1333
+ /** Open the WebSocket and begin relaying. */
1334
+ connect() {
1335
+ if (this.ws) return;
1336
+ this.options.onStateChange?.("connecting");
1337
+ const ws = new WebSocket(this.options.url);
1338
+ ws.binaryType = "arraybuffer";
1339
+ this.ws = ws;
1340
+ ws.onopen = () => {
1341
+ this.options.onStateChange?.("connected");
1342
+ this.terminal.reset();
1343
+ this.sendResize(this.terminal.cols, this.terminal.rows);
1344
+ this.startHeartbeat();
1345
+ };
1346
+ ws.onmessage = (ev) => {
1347
+ if (!(ev.data instanceof ArrayBuffer)) return;
1348
+ const frame = parseFrame(ev.data);
1349
+ if (!frame) return;
1350
+ switch (frame.frameType) {
1351
+ case FRAME_TYPE_DATA:
1352
+ this.terminal.write(decoder.decode(frame.payload));
1353
+ break;
1354
+ case FRAME_TYPE_SHUTDOWN:
1355
+ case FRAME_TYPE_SESSION_CLOSE:
1356
+ this.teardown();
1357
+ break;
1358
+ }
1359
+ };
1360
+ ws.onclose = () => {
1361
+ this.teardown();
1362
+ };
1363
+ ws.onerror = () => {
1364
+ this.options.onError?.("WebSocket error");
1365
+ this.teardown();
1366
+ };
1367
+ this.dataDisposable = this.terminal.onData((data) => {
1368
+ if (!this.connected) return;
1369
+ const payload = encoder.encode(data);
1370
+ this.ws.send(encodeFrame(FRAME_TYPE_DATA, payload));
1371
+ });
1372
+ }
1373
+ /** Send a graceful shutdown frame and close. */
1374
+ disconnect() {
1375
+ if (this.ws) {
1376
+ try {
1377
+ this.ws.send(encodeFrame(FRAME_TYPE_SHUTDOWN));
1378
+ } catch {
1379
+ }
1380
+ }
1381
+ this.teardown();
1382
+ }
1383
+ /** Send a window resize. Call this from FitAddon's onResize or a ResizeObserver. */
1384
+ resize(cols, rows) {
1385
+ if (!this.connected) return;
1386
+ this.sendResize(cols, rows);
1387
+ }
1388
+ // --- internals -------------------------------------------------------------
1389
+ sendResize(cols, rows) {
1390
+ const payload = new Uint8Array(8);
1391
+ const view = new DataView(payload.buffer);
1392
+ view.setUint32(0, cols, false);
1393
+ view.setUint32(4, rows, false);
1394
+ this.ws.send(encodeFrame(FRAME_TYPE_WINDOW_SIZE, payload));
1395
+ }
1396
+ startHeartbeat() {
1397
+ this.stopHeartbeat();
1398
+ const intervalMs = this.options.heartbeatIntervalMs ?? 3e4;
1399
+ this.heartbeatTimer = setInterval(() => {
1400
+ if (!this.connected) return;
1401
+ this.ws.send(encodeFrame(FRAME_TYPE_HEARTBEAT));
1402
+ }, intervalMs);
1403
+ }
1404
+ stopHeartbeat() {
1405
+ if (this.heartbeatTimer !== null) {
1406
+ clearInterval(this.heartbeatTimer);
1407
+ this.heartbeatTimer = null;
1408
+ }
1409
+ }
1410
+ teardown() {
1411
+ this.stopHeartbeat();
1412
+ if (this.dataDisposable) {
1413
+ this.dataDisposable.dispose();
1414
+ this.dataDisposable = null;
1415
+ }
1416
+ if (this.ws) {
1417
+ const ws = this.ws;
1418
+ this.ws = null;
1419
+ ws.onopen = null;
1420
+ ws.onmessage = null;
1421
+ ws.onclose = null;
1422
+ ws.onerror = null;
1423
+ try {
1424
+ ws.close();
1425
+ } catch {
1426
+ }
1427
+ }
1428
+ this.options.onStateChange?.("disconnected");
1429
+ }
1430
+ };
1431
+
1127
1432
  exports.ExecStdioBase64 = ExecStdioBase64;
1128
1433
  exports.ExecStdioText = ExecStdioText;
1434
+ exports.FRAME_TYPE_DATA = FRAME_TYPE_DATA;
1435
+ exports.FRAME_TYPE_HEARTBEAT = FRAME_TYPE_HEARTBEAT;
1436
+ exports.FRAME_TYPE_SESSION_CLOSE = FRAME_TYPE_SESSION_CLOSE;
1437
+ exports.FRAME_TYPE_SHUTDOWN = FRAME_TYPE_SHUTDOWN;
1438
+ exports.FRAME_TYPE_WINDOW_SIZE = FRAME_TYPE_WINDOW_SIZE;
1129
1439
  exports.Forwarder = Forwarder;
1130
1440
  exports.GiB = GiB;
1131
1441
  exports.HostGroupsAPI = HostGroupsAPI;
@@ -1135,10 +1445,14 @@ exports.SecretExistsError = SecretExistsError;
1135
1445
  exports.SecretsAPI = SecretsAPI;
1136
1446
  exports.SlicerAPIError = SlicerAPIError;
1137
1447
  exports.SlicerClient = SlicerClient;
1448
+ exports.SlicerShellSession = SlicerShellSession;
1138
1449
  exports.VM = VM;
1450
+ exports.VMBg = VMBg;
1139
1451
  exports.VMFileSystem = VMFileSystem;
1140
1452
  exports.VMsAPI = VMsAPI;
1453
+ exports.encodeFrame = encodeFrame;
1141
1454
  exports.parseAddressMapping = parseAddressMapping;
1455
+ exports.parseFrame = parseFrame;
1142
1456
  exports.resolveTransport = resolveTransport;
1143
1457
  //# sourceMappingURL=index.cjs.map
1144
1458
  //# sourceMappingURL=index.cjs.map