@slicervm/sdk 0.1.0

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 ADDED
@@ -0,0 +1,743 @@
1
+ import http from 'http';
2
+ import https from 'https';
3
+ import { URL } from 'url';
4
+ import os from 'os';
5
+ import path from 'path';
6
+
7
+ // src/types.ts
8
+ var ExecStdioText = "text";
9
+ var ExecStdioBase64 = "base64";
10
+ var SecretExistsError = class extends Error {
11
+ constructor(name) {
12
+ super(`secret already exists: ${name}`);
13
+ this.name = "SecretExistsError";
14
+ }
15
+ };
16
+ var SlicerAPIError = class extends Error {
17
+ status;
18
+ method;
19
+ path;
20
+ body;
21
+ constructor(method, path2, status, body) {
22
+ super(`slicer ${method} ${path2} failed: ${status} ${body}`);
23
+ this.name = "SlicerAPIError";
24
+ this.method = method;
25
+ this.path = path2;
26
+ this.status = status;
27
+ this.body = body;
28
+ }
29
+ };
30
+ var MiB = (n) => n * 1024 * 1024;
31
+ var GiB = (n) => n * 1024 * 1024 * 1024;
32
+ var NonRootUser = 4294967295;
33
+ function resolveTransport(baseURL) {
34
+ const trimmed = baseURL.trim();
35
+ if (!trimmed) throw new Error("Slicer baseURL is required");
36
+ let candidate = trimmed;
37
+ if (candidate.startsWith("unix://")) candidate = candidate.slice("unix://".length);
38
+ if (candidate.startsWith("~/")) candidate = path.join(os.homedir(), candidate.slice(2));
39
+ const socketLike = candidate.startsWith("/") || candidate.startsWith("./") || candidate.startsWith("../") || candidate.endsWith(".sock");
40
+ if (socketLike) return { kind: "socket", socketPath: candidate };
41
+ return { kind: "net", url: new URL(trimmed) };
42
+ }
43
+ var TransportClient = class {
44
+ transport;
45
+ token;
46
+ userAgent;
47
+ constructor(opts) {
48
+ this.transport = resolveTransport(opts.baseURL);
49
+ this.token = opts.token;
50
+ this.userAgent = opts.userAgent ?? "slicer-sdk-ts/0.1.0";
51
+ }
52
+ agent() {
53
+ return this.transport.kind === "net" && this.transport.url.protocol === "https:" ? https : http;
54
+ }
55
+ buildRequestOptions(method, reqPath, extraHeaders = {}) {
56
+ const headers = {
57
+ "User-Agent": this.userAgent,
58
+ ...extraHeaders
59
+ };
60
+ if (this.token) headers["Authorization"] = `Bearer ${this.token}`;
61
+ if (this.transport.kind === "socket") {
62
+ return {
63
+ socketPath: this.transport.socketPath,
64
+ method,
65
+ path: reqPath,
66
+ headers,
67
+ setHost: true
68
+ };
69
+ }
70
+ const u = this.transport.url;
71
+ return {
72
+ protocol: u.protocol,
73
+ hostname: u.hostname,
74
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
75
+ method,
76
+ path: reqPath,
77
+ headers
78
+ };
79
+ }
80
+ /** Buffered JSON request. Rejects on non-2xx via SlicerAPIError. */
81
+ request(method, reqPath, body) {
82
+ return new Promise((resolve, reject) => {
83
+ const payload = body === void 0 ? void 0 : Buffer.from(JSON.stringify(body));
84
+ const headers = {};
85
+ if (payload) {
86
+ headers["Content-Type"] = "application/json";
87
+ headers["Content-Length"] = String(payload.length);
88
+ }
89
+ const req = this.agent().request(
90
+ this.buildRequestOptions(method, reqPath, headers),
91
+ (res) => {
92
+ const chunks = [];
93
+ res.on("data", (c) => chunks.push(c));
94
+ res.on("end", () => {
95
+ const raw = Buffer.concat(chunks).toString("utf8");
96
+ const status = res.statusCode ?? 0;
97
+ if (status < 200 || status >= 300) {
98
+ reject(new SlicerAPIError(method, reqPath, status, raw));
99
+ return;
100
+ }
101
+ if (!raw) {
102
+ resolve(void 0);
103
+ return;
104
+ }
105
+ try {
106
+ resolve(JSON.parse(raw));
107
+ } catch {
108
+ resolve(raw);
109
+ }
110
+ });
111
+ res.on("error", reject);
112
+ }
113
+ );
114
+ req.on("error", reject);
115
+ if (payload) req.write(payload);
116
+ req.end();
117
+ });
118
+ }
119
+ /** Raw-bytes request (for binary cp endpoints). */
120
+ requestRaw(method, reqPath, body, contentType = "application/octet-stream") {
121
+ return new Promise((resolve, reject) => {
122
+ const headers = {};
123
+ if (body) {
124
+ headers["Content-Type"] = contentType;
125
+ headers["Content-Length"] = String(body.length);
126
+ }
127
+ const req = this.agent().request(
128
+ this.buildRequestOptions(method, reqPath, headers),
129
+ (res) => {
130
+ const chunks = [];
131
+ res.on("data", (c) => chunks.push(c));
132
+ res.on("end", () => {
133
+ const out = Buffer.concat(chunks);
134
+ const status = res.statusCode ?? 0;
135
+ if (status < 200 || status >= 300) {
136
+ reject(new SlicerAPIError(method, reqPath, status, out.toString("utf8")));
137
+ return;
138
+ }
139
+ resolve(out);
140
+ });
141
+ res.on("error", reject);
142
+ }
143
+ );
144
+ req.on("error", reject);
145
+ if (body) req.write(body);
146
+ req.end();
147
+ });
148
+ }
149
+ /** Streaming request producing a Node Readable of the response body. */
150
+ requestStreamRaw(method, reqPath, body, contentType = "application/octet-stream") {
151
+ return new Promise((resolve, reject) => {
152
+ const headers = {};
153
+ if (body instanceof Buffer) {
154
+ headers["Content-Type"] = contentType;
155
+ headers["Content-Length"] = String(body.length);
156
+ } else if (body) {
157
+ headers["Content-Type"] = contentType;
158
+ headers["Transfer-Encoding"] = "chunked";
159
+ }
160
+ const req = this.agent().request(
161
+ this.buildRequestOptions(method, reqPath, headers),
162
+ (res) => {
163
+ const status = res.statusCode ?? 0;
164
+ if (status < 200 || status >= 300) {
165
+ const chunks = [];
166
+ res.on("data", (c) => chunks.push(c));
167
+ res.on(
168
+ "end",
169
+ () => reject(
170
+ new SlicerAPIError(method, reqPath, status, Buffer.concat(chunks).toString("utf8"))
171
+ )
172
+ );
173
+ return;
174
+ }
175
+ resolve(res);
176
+ }
177
+ );
178
+ req.on("error", reject);
179
+ if (body instanceof Buffer) {
180
+ req.write(body);
181
+ req.end();
182
+ } else if (body) {
183
+ body.pipe(req);
184
+ } else {
185
+ req.end();
186
+ }
187
+ });
188
+ }
189
+ /** Yields decoded JSON frames from an NDJSON response (one JSON object per line). */
190
+ async *requestNDJSON(method, reqPath, body) {
191
+ const res = await this.requestStreamRaw(method, reqPath, body);
192
+ res.setEncoding("utf8");
193
+ let buffer = "";
194
+ for await (const chunk of res) {
195
+ buffer += chunk;
196
+ let nl;
197
+ while ((nl = buffer.indexOf("\n")) >= 0) {
198
+ const line = buffer.slice(0, nl).trim();
199
+ buffer = buffer.slice(nl + 1);
200
+ if (!line) continue;
201
+ try {
202
+ yield JSON.parse(line);
203
+ } catch {
204
+ }
205
+ }
206
+ }
207
+ const trailing = buffer.trim();
208
+ if (trailing) {
209
+ try {
210
+ yield JSON.parse(trailing);
211
+ } catch {
212
+ }
213
+ }
214
+ }
215
+ };
216
+
217
+ // src/wire.ts
218
+ function hostGroupFromWire(w) {
219
+ return {
220
+ name: w.name ?? "",
221
+ count: w.count ?? 0,
222
+ ramBytes: w.ram_bytes ?? 0,
223
+ cpus: w.cpus ?? 0,
224
+ arch: w.arch ?? "",
225
+ ...w.gpu_count !== void 0 && { gpuCount: w.gpu_count }
226
+ };
227
+ }
228
+ function vmFromWire(w) {
229
+ return {
230
+ hostname: w.hostname,
231
+ ip: w.ip,
232
+ createdAt: w.created_at,
233
+ ...w.hostgroup !== void 0 && { hostGroup: w.hostgroup },
234
+ ...w.ram_bytes !== void 0 && { ramBytes: w.ram_bytes },
235
+ ...w.cpus !== void 0 && { cpus: w.cpus },
236
+ ...w.arch !== void 0 && { arch: w.arch },
237
+ ...w.tags !== void 0 && { tags: w.tags },
238
+ ...w.status !== void 0 && { status: w.status },
239
+ ...w.persistent !== void 0 && { persistent: w.persistent }
240
+ };
241
+ }
242
+ function createVMReqToWire(r) {
243
+ const o = {};
244
+ if (r.ramBytes !== void 0) o.ram_bytes = r.ramBytes;
245
+ if (r.cpus !== void 0) o.cpus = r.cpus;
246
+ if (r.gpuCount !== void 0) o.gpu_count = r.gpuCount;
247
+ if (r.persistent !== void 0) o.persistent = r.persistent;
248
+ if (r.diskImage !== void 0) o.disk_image = r.diskImage;
249
+ if (r.importUser !== void 0) o.import_user = r.importUser;
250
+ if (r.sshKeys !== void 0) o.ssh_keys = r.sshKeys;
251
+ if (r.userdata !== void 0) o.userdata = r.userdata;
252
+ if (r.ip !== void 0) o.ip = r.ip;
253
+ if (r.tags !== void 0) o.tags = r.tags;
254
+ if (r.secrets !== void 0) o.secrets = r.secrets;
255
+ return o;
256
+ }
257
+ function createVMResFromWire(w) {
258
+ return {
259
+ hostname: w.hostname,
260
+ ip: w.ip,
261
+ createdAt: w.created_at,
262
+ ...w.hostgroup !== void 0 && { hostGroup: w.hostgroup },
263
+ ...w.arch !== void 0 && { arch: w.arch }
264
+ };
265
+ }
266
+ function agentHealthFromWire(w) {
267
+ return {
268
+ ...w.hostname !== void 0 && { hostname: w.hostname },
269
+ ...w.agent_uptime !== void 0 && { agentUptime: w.agent_uptime },
270
+ ...w.agent_version !== void 0 && { agentVersion: w.agent_version },
271
+ ...w.system_uptime !== void 0 && { systemUptime: w.system_uptime },
272
+ ...w.userdata_ran !== void 0 && { userdataRan: w.userdata_ran }
273
+ };
274
+ }
275
+ function fsEntryFromWire(w) {
276
+ return { name: w.name, type: w.type, size: w.size, mtime: w.mtime, mode: w.mode };
277
+ }
278
+ function vmStatFromWire(w) {
279
+ return {
280
+ hostname: w.hostname,
281
+ ip: w.ip,
282
+ createdAt: w.created_at,
283
+ ...w.snapshot !== void 0 && { snapshot: w.snapshot },
284
+ ...w.error !== void 0 && { error: w.error }
285
+ };
286
+ }
287
+ function secretFromWire(w) {
288
+ return {
289
+ name: w.name,
290
+ size: w.size,
291
+ permissions: w.permissions,
292
+ ...w.uid !== void 0 && { uid: w.uid },
293
+ ...w.gid !== void 0 && { gid: w.gid },
294
+ ...w.modified_at !== void 0 && { modifiedAt: w.modified_at }
295
+ };
296
+ }
297
+
298
+ // src/vm.ts
299
+ var VMFileSystem = class {
300
+ constructor(transport, hostname) {
301
+ this.transport = transport;
302
+ this.hostname = hostname;
303
+ }
304
+ transport;
305
+ hostname;
306
+ async readDir(path2) {
307
+ const q = new URLSearchParams({ path: path2 });
308
+ const wire = await this.transport.request(
309
+ "GET",
310
+ `/vm/${encodeURIComponent(this.hostname)}/fs/readdir?${q.toString()}`
311
+ );
312
+ return (wire ?? []).map(fsEntryFromWire);
313
+ }
314
+ async stat(path2) {
315
+ const q = new URLSearchParams({ path: path2 });
316
+ try {
317
+ const wire = await this.transport.request(
318
+ "GET",
319
+ `/vm/${encodeURIComponent(this.hostname)}/fs/stat?${q.toString()}`
320
+ );
321
+ return fsEntryFromWire(wire);
322
+ } catch (err) {
323
+ if (err instanceof SlicerAPIError && err.status === 404) return null;
324
+ throw err;
325
+ }
326
+ }
327
+ async exists(path2) {
328
+ return await this.stat(path2) !== null;
329
+ }
330
+ async mkdir(req) {
331
+ await this.transport.request(
332
+ "POST",
333
+ `/vm/${encodeURIComponent(this.hostname)}/fs/mkdir`,
334
+ {
335
+ path: req.path,
336
+ ...req.recursive !== void 0 && { recursive: req.recursive },
337
+ ...req.mode !== void 0 && { mode: req.mode }
338
+ }
339
+ );
340
+ }
341
+ async remove(path2, recursive = false) {
342
+ const q = new URLSearchParams({ path: path2, recursive: String(recursive) });
343
+ await this.transport.request(
344
+ "DELETE",
345
+ `/vm/${encodeURIComponent(this.hostname)}/fs/remove?${q.toString()}`
346
+ );
347
+ }
348
+ async readFile(path2) {
349
+ const q = new URLSearchParams({ path: path2, mode: "binary" });
350
+ return this.transport.requestRaw(
351
+ "GET",
352
+ `/vm/${encodeURIComponent(this.hostname)}/cp?${q.toString()}`
353
+ );
354
+ }
355
+ async writeFile(path2, content, opts = {}) {
356
+ const q = new URLSearchParams({ path: path2, mode: "binary" });
357
+ if (opts.uid !== void 0) q.set("uid", String(opts.uid));
358
+ if (opts.gid !== void 0) q.set("gid", String(opts.gid));
359
+ if (opts.permissions) q.set("permissions", opts.permissions);
360
+ const body = typeof content === "string" ? Buffer.from(content) : content;
361
+ await this.transport.requestRaw(
362
+ "POST",
363
+ `/vm/${encodeURIComponent(this.hostname)}/cp?${q.toString()}`,
364
+ body
365
+ );
366
+ }
367
+ /** Upload a tar archive, expanded into the VM at `path`. */
368
+ async tarTo(path2, tar) {
369
+ const q = new URLSearchParams({ path: path2, mode: "tar" });
370
+ if (tar instanceof Buffer) {
371
+ await this.transport.requestRaw(
372
+ "POST",
373
+ `/vm/${encodeURIComponent(this.hostname)}/cp?${q.toString()}`,
374
+ tar,
375
+ "application/x-tar"
376
+ );
377
+ return;
378
+ }
379
+ const res = await this.transport.requestStreamRaw(
380
+ "POST",
381
+ `/vm/${encodeURIComponent(this.hostname)}/cp?${q.toString()}`,
382
+ tar,
383
+ "application/x-tar"
384
+ );
385
+ for await (const _ of res) void _;
386
+ }
387
+ /** Download `path` from the VM as a tar archive. */
388
+ async tarFrom(path2) {
389
+ const q = new URLSearchParams({ path: path2, mode: "tar" });
390
+ return this.transport.requestRaw(
391
+ "GET",
392
+ `/vm/${encodeURIComponent(this.hostname)}/cp?${q.toString()}`
393
+ );
394
+ }
395
+ };
396
+ var VM = class {
397
+ hostname;
398
+ hostGroup;
399
+ ip;
400
+ createdAt;
401
+ arch;
402
+ fs;
403
+ transport;
404
+ constructor(transport, init) {
405
+ this.transport = transport;
406
+ this.hostname = init.hostname;
407
+ this.hostGroup = init.hostGroup;
408
+ if (init.ip !== void 0) this.ip = init.ip;
409
+ if (init.createdAt !== void 0) this.createdAt = init.createdAt;
410
+ if (init.arch !== void 0) this.arch = init.arch;
411
+ this.fs = new VMFileSystem(transport, this.hostname);
412
+ }
413
+ // --- lifecycle --------------------------------------------------------
414
+ async delete() {
415
+ await this.transport.request(
416
+ "DELETE",
417
+ `/hostgroup/${encodeURIComponent(this.hostGroup)}/nodes/${encodeURIComponent(
418
+ this.hostname
419
+ )}`
420
+ );
421
+ }
422
+ // --- health / logs ----------------------------------------------------
423
+ async health() {
424
+ const wire = await this.transport.request(
425
+ "GET",
426
+ `/vm/${encodeURIComponent(this.hostname)}/health`
427
+ );
428
+ return agentHealthFromWire(wire);
429
+ }
430
+ async logs() {
431
+ return this.transport.request("GET", `/vm/${encodeURIComponent(this.hostname)}/logs`);
432
+ }
433
+ async waitForAgent(opts = {}) {
434
+ const timeoutMs = opts.timeoutMs ?? 12e4;
435
+ const intervalMs = opts.intervalMs ?? 500;
436
+ const deadline = Date.now() + timeoutMs;
437
+ let lastErr;
438
+ while (Date.now() < deadline) {
439
+ try {
440
+ return await this.health();
441
+ } catch (err) {
442
+ lastErr = err;
443
+ await sleep(intervalMs);
444
+ }
445
+ }
446
+ throw new Error(
447
+ `agent for ${this.hostname} did not become ready within ${timeoutMs}ms: ${errMsg(lastErr)}`
448
+ );
449
+ }
450
+ async waitForUserdata(opts = {}) {
451
+ const timeoutMs = opts.timeoutMs ?? 12e4;
452
+ const intervalMs = opts.intervalMs ?? 500;
453
+ const deadline = Date.now() + timeoutMs;
454
+ let last;
455
+ let lastErr;
456
+ while (Date.now() < deadline) {
457
+ try {
458
+ last = await this.health();
459
+ if (last.userdataRan) return last;
460
+ } catch (err) {
461
+ lastErr = err;
462
+ }
463
+ await sleep(intervalMs);
464
+ }
465
+ throw new Error(
466
+ `userdata for ${this.hostname} did not complete within ${timeoutMs}ms${lastErr ? `: ${errMsg(lastErr)}` : ""}`
467
+ );
468
+ }
469
+ // --- power ------------------------------------------------------------
470
+ async shutdown(req = {}) {
471
+ await this.transport.request(
472
+ "POST",
473
+ `/vm/${encodeURIComponent(this.hostname)}/shutdown`,
474
+ req
475
+ );
476
+ }
477
+ async pause() {
478
+ await this.transport.request("POST", `/vm/${encodeURIComponent(this.hostname)}/pause`);
479
+ }
480
+ async resume() {
481
+ await this.transport.request("POST", `/vm/${encodeURIComponent(this.hostname)}/resume`);
482
+ }
483
+ async relaunch() {
484
+ await this.transport.request("POST", `/vm/${encodeURIComponent(this.hostname)}/relaunch`);
485
+ }
486
+ /** Mac-only on current daemons. Throws `SlicerAPIError 404` on Linux. */
487
+ async suspend() {
488
+ await this.transport.request("POST", `/vm/${encodeURIComponent(this.hostname)}/suspend`);
489
+ }
490
+ /** Mac-only on current daemons. Throws `SlicerAPIError 404` on Linux. */
491
+ async restore() {
492
+ await this.transport.request("POST", `/vm/${encodeURIComponent(this.hostname)}/restore`);
493
+ }
494
+ // --- exec -------------------------------------------------------------
495
+ /**
496
+ * Streaming exec — yields NDJSON frames (`started`, `stdout`, `stderr`, `exit`).
497
+ * When `req.stdio === 'base64'`, each frame's `data`/`stdout`/`stderr` string
498
+ * fields are preserved as-is (base64-encoded) and the SDK populates decoded
499
+ * `dataBytes`/`stdoutBytes`/`stderrBytes` Buffers alongside for convenience.
500
+ */
501
+ async *exec(req) {
502
+ const { path: path2, body } = buildExecPath(this.hostname, req, false);
503
+ for await (const frame of this.transport.requestNDJSON("POST", path2, body)) {
504
+ if (frame.encoding === "base64") {
505
+ if (frame.data) frame.dataBytes = Buffer.from(frame.data, "base64");
506
+ if (frame.stdout) frame.stdoutBytes = Buffer.from(frame.stdout, "base64");
507
+ if (frame.stderr) frame.stderrBytes = Buffer.from(frame.stderr, "base64");
508
+ }
509
+ yield frame;
510
+ }
511
+ }
512
+ async execBuffered(req) {
513
+ if (req.stdin !== void 0) {
514
+ throw new Error("stdin is not supported by execBuffered; use exec() instead");
515
+ }
516
+ const { path: path2, body } = buildExecPath(this.hostname, req, true);
517
+ const raw = await this.transport.requestRaw("POST", path2, body);
518
+ const text = raw.toString("utf8");
519
+ const parsed = text ? JSON.parse(text) : {};
520
+ const common = {
521
+ exitCode: parsed.exit_code ?? 0,
522
+ ...parsed.pid !== void 0 && { pid: parsed.pid },
523
+ ...parsed.started_at !== void 0 && { startedAt: parsed.started_at },
524
+ ...parsed.ended_at !== void 0 && { endedAt: parsed.ended_at },
525
+ ...parsed.signal !== void 0 && { signal: parsed.signal },
526
+ ...parsed.error !== void 0 && { error: parsed.error }
527
+ };
528
+ if (req.stdio === "base64" || parsed.encoding === "base64") {
529
+ return {
530
+ stdout: Buffer.from(parsed.stdout ?? "", "base64"),
531
+ stderr: Buffer.from(parsed.stderr ?? "", "base64"),
532
+ encoding: "base64",
533
+ ...common
534
+ };
535
+ }
536
+ return {
537
+ stdout: parsed.stdout ?? "",
538
+ stderr: parsed.stderr ?? "",
539
+ ...parsed.encoding !== void 0 && { encoding: parsed.encoding },
540
+ ...common
541
+ };
542
+ }
543
+ };
544
+ function buildExecPath(hostname, req, buffered) {
545
+ const q = new URLSearchParams();
546
+ if (req.command) q.set("cmd", req.command);
547
+ for (const a of req.args ?? []) q.append("args", a);
548
+ for (const e of req.env ?? []) q.append("env", e);
549
+ if (req.uid !== void 0) q.set("uid", String(req.uid));
550
+ if (req.gid !== void 0) q.set("gid", String(req.gid));
551
+ if (req.cwd) q.set("cwd", req.cwd);
552
+ if (req.shell) q.set("shell", req.shell);
553
+ if (req.permissions) q.set("permissions", req.permissions);
554
+ if (req.stdio) q.set("stdio", req.stdio);
555
+ if (buffered) q.set("buffered", "true");
556
+ let body;
557
+ if (req.stdin !== void 0) {
558
+ q.set("stdin", "true");
559
+ body = typeof req.stdin === "string" ? Buffer.from(req.stdin) : req.stdin;
560
+ }
561
+ return {
562
+ path: `/vm/${encodeURIComponent(hostname)}/exec?${q.toString()}`,
563
+ body
564
+ };
565
+ }
566
+ function sleep(ms) {
567
+ return new Promise((r) => setTimeout(r, ms));
568
+ }
569
+ function errMsg(e) {
570
+ return e instanceof Error ? e.message : String(e);
571
+ }
572
+
573
+ // src/namespaces.ts
574
+ var HostGroupsAPI = class {
575
+ constructor(transport) {
576
+ this.transport = transport;
577
+ }
578
+ transport;
579
+ async list() {
580
+ const wire = await this.transport.request("GET", "/hostgroup");
581
+ return (wire ?? []).map(hostGroupFromWire);
582
+ }
583
+ /** Convenience lookup (no single-group endpoint exists on the daemon). */
584
+ async find(name) {
585
+ return (await this.list()).find((g) => g.name === name);
586
+ }
587
+ async listVMs(name, opts = {}) {
588
+ const q = buildListQuery(opts);
589
+ const wire = await this.transport.request(
590
+ "GET",
591
+ `/hostgroup/${encodeURIComponent(name)}/nodes${q}`
592
+ );
593
+ return (wire ?? []).map(vmFromWire);
594
+ }
595
+ };
596
+ var VMsAPI = class {
597
+ constructor(transport) {
598
+ this.transport = transport;
599
+ }
600
+ transport;
601
+ async create(hostGroup, req = {}, opts = {}) {
602
+ const qs = new URLSearchParams();
603
+ if (opts.wait) qs.set("wait", opts.wait);
604
+ if (opts.waitTimeoutSec !== void 0) qs.set("timeout", `${opts.waitTimeoutSec}s`);
605
+ const query = qs.toString() ? `?${qs.toString()}` : "";
606
+ const wire = await this.transport.request(
607
+ "POST",
608
+ `/hostgroup/${encodeURIComponent(hostGroup)}/nodes${query}`,
609
+ createVMReqToWire(req)
610
+ );
611
+ const res = createVMResFromWire(wire);
612
+ return new VM(this.transport, {
613
+ hostname: res.hostname,
614
+ hostGroup: res.hostGroup ?? hostGroup,
615
+ ...res.ip !== void 0 && { ip: res.ip },
616
+ ...res.createdAt !== void 0 && { createdAt: res.createdAt },
617
+ ...res.arch !== void 0 && { arch: res.arch }
618
+ });
619
+ }
620
+ /**
621
+ * Build a VM handle for an existing VM given its hostgroup + hostname. No
622
+ * request is issued — use `health()` or `getInfo()` to verify reachability.
623
+ */
624
+ attach(hostGroup, hostname) {
625
+ return new VM(this.transport, { hostname, hostGroup });
626
+ }
627
+ /** Look up a VM by hostname across all host groups. Returns `undefined` if not found. */
628
+ async get(hostname) {
629
+ const all = await this.list();
630
+ const found = all.find((v) => v.hostname === hostname);
631
+ if (!found) return void 0;
632
+ return new VM(this.transport, {
633
+ hostname: found.hostname,
634
+ hostGroup: found.hostGroup ?? "",
635
+ ...found.ip !== void 0 && { ip: found.ip },
636
+ ...found.createdAt !== void 0 && { createdAt: found.createdAt },
637
+ ...found.arch !== void 0 && { arch: found.arch }
638
+ });
639
+ }
640
+ /** Return raw VM metadata across all host groups. */
641
+ async list(opts = {}) {
642
+ const q = buildListQuery(opts);
643
+ const wire = await this.transport.request("GET", `/nodes${q}`);
644
+ return (wire ?? []).map(vmFromWire);
645
+ }
646
+ async stats() {
647
+ const raw = await this.transport.request("GET", "/nodes/stats");
648
+ return (raw ?? []).map(vmStatFromWire);
649
+ }
650
+ /**
651
+ * Raw response accessor — bypasses the VM handle. Useful when you only
652
+ * want the create metadata without a handle.
653
+ */
654
+ async createRaw(hostGroup, req = {}, opts = {}) {
655
+ const qs = new URLSearchParams();
656
+ if (opts.wait) qs.set("wait", opts.wait);
657
+ if (opts.waitTimeoutSec !== void 0) qs.set("timeout", `${opts.waitTimeoutSec}s`);
658
+ const query = qs.toString() ? `?${qs.toString()}` : "";
659
+ const wire = await this.transport.request(
660
+ "POST",
661
+ `/hostgroup/${encodeURIComponent(hostGroup)}/nodes${query}`,
662
+ createVMReqToWire(req)
663
+ );
664
+ return createVMResFromWire(wire);
665
+ }
666
+ };
667
+ var SecretsAPI = class {
668
+ constructor(transport) {
669
+ this.transport = transport;
670
+ }
671
+ transport;
672
+ async list() {
673
+ const wire = await this.transport.request("GET", "/secrets");
674
+ return (wire ?? []).map(secretFromWire);
675
+ }
676
+ async create(req) {
677
+ const body = {
678
+ name: req.name,
679
+ data: Buffer.from(req.data).toString("base64"),
680
+ ...req.permissions !== void 0 && { permissions: req.permissions },
681
+ ...req.uid !== void 0 && { uid: req.uid },
682
+ ...req.gid !== void 0 && { gid: req.gid }
683
+ };
684
+ try {
685
+ await this.transport.request("POST", "/secrets", body);
686
+ } catch (err) {
687
+ if (err instanceof SlicerAPIError && err.status === 409) {
688
+ throw new SecretExistsError(req.name);
689
+ }
690
+ throw err;
691
+ }
692
+ }
693
+ async patch(name, req) {
694
+ const body = {
695
+ data: Buffer.from(req.data).toString("base64"),
696
+ ...req.permissions !== void 0 && { permissions: req.permissions },
697
+ ...req.uid !== void 0 && { uid: req.uid },
698
+ ...req.gid !== void 0 && { gid: req.gid }
699
+ };
700
+ await this.transport.request("PATCH", `/secrets/${encodeURIComponent(name)}`, body);
701
+ }
702
+ async delete(name) {
703
+ await this.transport.request("DELETE", `/secrets/${encodeURIComponent(name)}`);
704
+ }
705
+ };
706
+ function buildListQuery(opts) {
707
+ const qs = new URLSearchParams();
708
+ if (opts.tag) qs.set("tag", opts.tag);
709
+ if (opts.tagPrefix) qs.set("tag_prefix", opts.tagPrefix);
710
+ const s = qs.toString();
711
+ return s ? `?${s}` : "";
712
+ }
713
+
714
+ // src/client.ts
715
+ var SlicerClient = class _SlicerClient {
716
+ transport;
717
+ hostGroups;
718
+ vms;
719
+ secrets;
720
+ constructor(opts) {
721
+ this.transport = new TransportClient(opts);
722
+ this.hostGroups = new HostGroupsAPI(this.transport);
723
+ this.vms = new VMsAPI(this.transport);
724
+ this.secrets = new SecretsAPI(this.transport);
725
+ }
726
+ static fromEnv(overrides = {}) {
727
+ const baseURL = overrides.baseURL ?? process.env.SLICER_URL;
728
+ if (!baseURL) throw new Error("SLICER_URL is required (or pass baseURL)");
729
+ const token = overrides.token ?? process.env.SLICER_TOKEN ?? void 0;
730
+ return new _SlicerClient({
731
+ baseURL,
732
+ ...token !== void 0 && { token },
733
+ ...overrides.userAgent !== void 0 && { userAgent: overrides.userAgent }
734
+ });
735
+ }
736
+ async getInfo() {
737
+ return this.transport.request("GET", "/info");
738
+ }
739
+ };
740
+
741
+ export { ExecStdioBase64, ExecStdioText, GiB, HostGroupsAPI, MiB, NonRootUser, SecretExistsError, SecretsAPI, SlicerAPIError, SlicerClient, VM, VMFileSystem, VMsAPI, resolveTransport };
742
+ //# sourceMappingURL=index.js.map
743
+ //# sourceMappingURL=index.js.map