@kosa8/sdk 0.1.0 → 0.3.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/README.md +82 -4
- package/dist/index.cjs +748 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +546 -1
- package/dist/index.d.ts +546 -1
- package/dist/index.js +737 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,6 +15,8 @@ var Kosa8Error = class extends Error {
|
|
|
15
15
|
path;
|
|
16
16
|
/** The decoded response body, when there was one. */
|
|
17
17
|
body;
|
|
18
|
+
/** kosa8 Cloud's stable error code (payment_required, quota_exceeded, ...). */
|
|
19
|
+
code;
|
|
18
20
|
constructor(message, options = {}) {
|
|
19
21
|
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
20
22
|
this.name = new.target.name;
|
|
@@ -24,10 +26,13 @@ var Kosa8Error = class extends Error {
|
|
|
24
26
|
this.method = options.method;
|
|
25
27
|
this.path = options.path;
|
|
26
28
|
this.body = options.body;
|
|
29
|
+
this.code = options.code;
|
|
27
30
|
}
|
|
28
31
|
};
|
|
29
32
|
var Kosa8ConnectionError = class extends Kosa8Error {
|
|
30
33
|
};
|
|
34
|
+
var Kosa8TimeoutError = class extends Kosa8ConnectionError {
|
|
35
|
+
};
|
|
31
36
|
var Kosa8ApiError = class extends Kosa8Error {
|
|
32
37
|
};
|
|
33
38
|
var Kosa8BadRequestError = class extends Kosa8ApiError {
|
|
@@ -58,6 +63,10 @@ var Kosa8StreamError = class extends Kosa8Error {
|
|
|
58
63
|
this.stderr = out.stderr;
|
|
59
64
|
}
|
|
60
65
|
};
|
|
66
|
+
var Kosa8PaymentRequiredError = class extends Kosa8ApiError {
|
|
67
|
+
};
|
|
68
|
+
var Kosa8RateLimitError = class extends Kosa8ApiError {
|
|
69
|
+
};
|
|
61
70
|
var Kosa8CheckpointError = class extends Kosa8Error {
|
|
62
71
|
};
|
|
63
72
|
function isRefusal(v) {
|
|
@@ -91,8 +100,12 @@ function errorFromResponse(status, body, ctx = {}) {
|
|
|
91
100
|
return new Kosa8PolicyError(message, opts);
|
|
92
101
|
case 404:
|
|
93
102
|
return new Kosa8NotFoundError(message, opts);
|
|
103
|
+
case 402:
|
|
104
|
+
return new Kosa8PaymentRequiredError(message, opts);
|
|
94
105
|
case 409:
|
|
95
106
|
return new Kosa8ConflictError(message, opts);
|
|
107
|
+
case 429:
|
|
108
|
+
return new Kosa8RateLimitError(message, opts);
|
|
96
109
|
case 501:
|
|
97
110
|
return new Kosa8UnsupportedError(message, opts);
|
|
98
111
|
default:
|
|
@@ -270,16 +283,136 @@ var CheckpointBook = class {
|
|
|
270
283
|
}
|
|
271
284
|
};
|
|
272
285
|
|
|
286
|
+
// src/tar.ts
|
|
287
|
+
import { promises as fsp } from "fs";
|
|
288
|
+
import * as path2 from "path";
|
|
289
|
+
var BLOCK = 512;
|
|
290
|
+
function octal(buf, off, len, n) {
|
|
291
|
+
buf.write(n.toString(8).padStart(len - 1, "0") + "\0", off, len, "ascii");
|
|
292
|
+
}
|
|
293
|
+
function header(name, type, size, mode, mtime, link = "") {
|
|
294
|
+
const h = Buffer.alloc(BLOCK);
|
|
295
|
+
h.write(name, 0, 100, "utf8");
|
|
296
|
+
octal(h, 100, 8, mode & 4095);
|
|
297
|
+
octal(h, 108, 8, 0);
|
|
298
|
+
octal(h, 116, 8, 0);
|
|
299
|
+
octal(h, 124, 12, size);
|
|
300
|
+
octal(h, 136, 12, mtime);
|
|
301
|
+
h.fill(" ", 148, 156);
|
|
302
|
+
h.write(type, 156, 1, "ascii");
|
|
303
|
+
h.write(link, 157, 100, "utf8");
|
|
304
|
+
h.write("ustar\0", 257, 6, "ascii");
|
|
305
|
+
h.write("00", 263, 2, "ascii");
|
|
306
|
+
let sum = 0;
|
|
307
|
+
for (const b of h) sum += b;
|
|
308
|
+
h.write(sum.toString(8).padStart(6, "0") + "\0 ", 148, 8, "ascii");
|
|
309
|
+
return h;
|
|
310
|
+
}
|
|
311
|
+
function paxRecord(key, value) {
|
|
312
|
+
const body = ` ${key}=${value}
|
|
313
|
+
`;
|
|
314
|
+
let n = Buffer.byteLength(body);
|
|
315
|
+
let len = n + String(n).length;
|
|
316
|
+
if (String(len).length !== String(n).length) len = n + String(len).length;
|
|
317
|
+
return `${len}${body}`;
|
|
318
|
+
}
|
|
319
|
+
function pad(n) {
|
|
320
|
+
const r = n % BLOCK;
|
|
321
|
+
return Buffer.alloc(r === 0 ? 0 : BLOCK - r);
|
|
322
|
+
}
|
|
323
|
+
function entry(out, name, type, mode, mtime, data, link = "") {
|
|
324
|
+
let pax = "";
|
|
325
|
+
if (Buffer.byteLength(name) > 100) pax += paxRecord("path", name);
|
|
326
|
+
if (Buffer.byteLength(link) > 100) pax += paxRecord("linkpath", link);
|
|
327
|
+
if (pax !== "") {
|
|
328
|
+
const p = Buffer.from(pax, "utf8");
|
|
329
|
+
out.push(header("PaxHeader", "x", p.length, 420, mtime), p, pad(p.length));
|
|
330
|
+
}
|
|
331
|
+
const size = data?.length ?? 0;
|
|
332
|
+
out.push(header(name.slice(0, 100), type, size, mode, mtime, link.slice(0, 100)));
|
|
333
|
+
if (data) out.push(data, pad(size));
|
|
334
|
+
}
|
|
335
|
+
async function readIgnore(dir) {
|
|
336
|
+
let text;
|
|
337
|
+
try {
|
|
338
|
+
text = await fsp.readFile(path2.join(dir, ".dockerignore"), "utf8");
|
|
339
|
+
} catch {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
const out = [];
|
|
343
|
+
for (const line of text.split(/\r?\n/)) {
|
|
344
|
+
const l = line.trim();
|
|
345
|
+
if (l === "" || line.startsWith("#")) continue;
|
|
346
|
+
out.push(path2.posix.normalize(l.replace(/\\/g, "/")).replace(/\/+$/, "") || ".");
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
async function tarContext(dir, dockerfile = "Dockerfile") {
|
|
351
|
+
try {
|
|
352
|
+
await fsp.access(path2.join(dir, dockerfile));
|
|
353
|
+
} catch {
|
|
354
|
+
throw new Kosa8Error(`no ${dockerfile} in ${dir}`);
|
|
355
|
+
}
|
|
356
|
+
const ignore = await readIgnore(dir);
|
|
357
|
+
const skip = (rel) => ignore.some((p) => rel === p || rel.startsWith(p + "/"));
|
|
358
|
+
const out = [];
|
|
359
|
+
const walk = async (rel) => {
|
|
360
|
+
const ents = await fsp.readdir(path2.join(dir, rel), { withFileTypes: true });
|
|
361
|
+
ents.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
362
|
+
const subdirs = [];
|
|
363
|
+
for (const e of ents) {
|
|
364
|
+
const r = rel === "" ? e.name : `${rel}/${e.name}`;
|
|
365
|
+
if (skip(r)) continue;
|
|
366
|
+
const full = path2.join(dir, r);
|
|
367
|
+
const st = await fsp.lstat(full);
|
|
368
|
+
const mtime = Math.floor(st.mtimeMs / 1e3);
|
|
369
|
+
if (st.isSymbolicLink()) {
|
|
370
|
+
entry(out, r, "2", st.mode, mtime, void 0, await fsp.readlink(full));
|
|
371
|
+
} else if (st.isDirectory()) {
|
|
372
|
+
entry(out, r + "/", "5", st.mode, mtime);
|
|
373
|
+
subdirs.push(r);
|
|
374
|
+
} else if (st.isFile()) {
|
|
375
|
+
entry(out, r, "0", st.mode, mtime, await fsp.readFile(full));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
for (const s of subdirs) await walk(s);
|
|
379
|
+
};
|
|
380
|
+
await walk("");
|
|
381
|
+
out.push(Buffer.alloc(BLOCK * 2));
|
|
382
|
+
return Buffer.concat(out);
|
|
383
|
+
}
|
|
384
|
+
|
|
273
385
|
// src/transport.ts
|
|
386
|
+
import * as fs2 from "fs";
|
|
274
387
|
import * as http from "http";
|
|
275
388
|
import * as https from "https";
|
|
276
389
|
import * as os2 from "os";
|
|
277
|
-
import * as
|
|
390
|
+
import * as path3 from "path";
|
|
278
391
|
var APPROVAL_HEADER = "X-Kosa8-Approval";
|
|
279
392
|
var ACTOR_HEADER = "X-Kosa8-Actor";
|
|
280
393
|
var AGENT_TOKEN_HEADER = "X-Kosa8-Agent-Token";
|
|
281
394
|
function defaultSocketPath() {
|
|
282
|
-
return
|
|
395
|
+
return path3.join(os2.homedir(), ".kosa8", "run", "kosa8.sock");
|
|
396
|
+
}
|
|
397
|
+
function defaultWorkerFile() {
|
|
398
|
+
return path3.join(os2.homedir(), ".kosa8", "worker.json");
|
|
399
|
+
}
|
|
400
|
+
function loadSavedWorker(file) {
|
|
401
|
+
let w;
|
|
402
|
+
try {
|
|
403
|
+
w = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
404
|
+
} catch {
|
|
405
|
+
return void 0;
|
|
406
|
+
}
|
|
407
|
+
if (!w || typeof w !== "object") return void 0;
|
|
408
|
+
const o = w;
|
|
409
|
+
const host = typeof o.host === "string" ? o.host.trim() : "";
|
|
410
|
+
if (host === "") return void 0;
|
|
411
|
+
return {
|
|
412
|
+
host,
|
|
413
|
+
token: typeof o.token === "string" && o.token !== "" ? o.token : void 0,
|
|
414
|
+
insecure: o.insecure === true
|
|
415
|
+
};
|
|
283
416
|
}
|
|
284
417
|
function isLoopback(hostname) {
|
|
285
418
|
const h = hostname.replace(/^\[|\]$/g, "");
|
|
@@ -288,11 +421,20 @@ function isLoopback(hostname) {
|
|
|
288
421
|
function resolveEndpoint(opts) {
|
|
289
422
|
const env = opts.env ?? process.env;
|
|
290
423
|
const fromEnv = (env.KOSA8_HOST ?? "").trim();
|
|
291
|
-
const raw = (opts.host ?? fromEnv).trim()
|
|
292
|
-
|
|
293
|
-
|
|
424
|
+
const raw = (opts.host ?? fromEnv).trim();
|
|
425
|
+
const envInsecure = opts.insecure === true || env.KOSA8_INSECURE === "1";
|
|
426
|
+
if (raw !== "") {
|
|
427
|
+
const token = opts.token ?? (opts.host === void 0 && fromEnv !== "" ? env.KOSA8_TOKEN : void 0);
|
|
428
|
+
return parseHost(raw, envInsecure, token);
|
|
429
|
+
}
|
|
430
|
+
if (opts.socketPath === void 0 && opts.workerFile !== null) {
|
|
431
|
+
const saved = loadSavedWorker(opts.workerFile ?? defaultWorkerFile());
|
|
432
|
+
if (saved) return parseHost(saved.host, envInsecure || saved.insecure, opts.token ?? saved.token);
|
|
294
433
|
}
|
|
295
|
-
|
|
434
|
+
return { kind: "unix", socketPath: opts.socketPath ?? defaultSocketPath() };
|
|
435
|
+
}
|
|
436
|
+
function parseHost(raw, insecure, token) {
|
|
437
|
+
let h = raw.replace(/\/+$/, "");
|
|
296
438
|
const i = h.indexOf("://");
|
|
297
439
|
if (i > 0) h = h.slice(0, i).toLowerCase() + h.slice(i);
|
|
298
440
|
if (h.startsWith("unix://")) {
|
|
@@ -308,13 +450,11 @@ function resolveEndpoint(opts) {
|
|
|
308
450
|
h = "http://" + h;
|
|
309
451
|
}
|
|
310
452
|
const url = new URL(h);
|
|
311
|
-
const insecure = opts.insecure === true || env.KOSA8_INSECURE === "1";
|
|
312
453
|
if (url.protocol === "http:" && !insecure && !isLoopback(url.hostname)) {
|
|
313
454
|
throw new Kosa8Error(
|
|
314
455
|
`refusing to talk to ${url.host} over plain http: the token and every request would cross the network unencrypted. Use https://${url.host} (a worker behind a TLS-terminating tunnel or proxy), or set insecure: true / KOSA8_INSECURE=1 if this link is already private and trusted`
|
|
315
456
|
);
|
|
316
457
|
}
|
|
317
|
-
const token = opts.token ?? (opts.host === void 0 && fromEnv !== "" ? env.KOSA8_TOKEN : void 0);
|
|
318
458
|
return { kind: "tcp", url, token: token || void 0 };
|
|
319
459
|
}
|
|
320
460
|
var Transport = class {
|
|
@@ -406,7 +546,7 @@ var Transport = class {
|
|
|
406
546
|
}
|
|
407
547
|
reject(
|
|
408
548
|
new Kosa8ConnectionError(
|
|
409
|
-
`kosa8 daemon unreachable at ${this.target}: ${err.message}` + (err.code === "ENOENT" || err.code === "ECONNREFUSED" ? " (is kosa8 installed and its daemon running? start it with `kosa8 daemon start`)" : ""),
|
|
549
|
+
`kosa8 daemon unreachable at ${this.target}: ${err.message}` + (err.code === "ENOENT" || err.code === "ECONNREFUSED" ? this.endpoint.kind === "unix" && process.platform === "win32" ? " (on Windows the engine runs in WSL2: run `kosa8 wsl setup`, or set KOSA8_HOST)" : " (is kosa8 installed and its daemon running? start it with `kosa8 daemon start`)" : ""),
|
|
410
550
|
{ method, path: p, cause: err }
|
|
411
551
|
)
|
|
412
552
|
);
|
|
@@ -519,6 +659,12 @@ function envList(env) {
|
|
|
519
659
|
return Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
520
660
|
}
|
|
521
661
|
var enc = encodeURIComponent;
|
|
662
|
+
function str(v) {
|
|
663
|
+
return typeof v === "string" ? v : "";
|
|
664
|
+
}
|
|
665
|
+
function num(v) {
|
|
666
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
667
|
+
}
|
|
522
668
|
var Kosa8Client = class {
|
|
523
669
|
transport;
|
|
524
670
|
checkpoints;
|
|
@@ -563,6 +709,11 @@ var Kosa8Client = class {
|
|
|
563
709
|
if (options.capture) body.capture = options.capture;
|
|
564
710
|
if (options.labels) body.labels = options.labels;
|
|
565
711
|
if (options.broker) body.broker = options.broker;
|
|
712
|
+
if (options.autoStopMin) body.auto_stop_min = options.autoStopMin;
|
|
713
|
+
if (options.autoDeleteMin) body.auto_delete_min = options.autoDeleteMin;
|
|
714
|
+
if (options.ttlMin) body.ttl_min = options.ttlMin;
|
|
715
|
+
if (options.ephemeral) body.ephemeral = true;
|
|
716
|
+
if (options.template) body.template = options.template;
|
|
566
717
|
return this.transport.json("POST", "/kosa8/v1/sandboxes", {
|
|
567
718
|
body,
|
|
568
719
|
approval: options.approval,
|
|
@@ -859,6 +1010,315 @@ var Kosa8Client = class {
|
|
|
859
1010
|
}
|
|
860
1011
|
return { sandbox: restored.sandbox, checkpoint: target, dropped, restore_ms: restored.restore_ms };
|
|
861
1012
|
}
|
|
1013
|
+
// --- toolbox: code -------------------------------------------------------------
|
|
1014
|
+
/**
|
|
1015
|
+
* POST /kosa8/v1/sandboxes/{id}/code: run a Python, JavaScript or
|
|
1016
|
+
* TypeScript snippet. It starts in `/workspace`. Charts a Python snippet
|
|
1017
|
+
* draws with matplotlib come back in `artifacts` as PNG bytes.
|
|
1018
|
+
*/
|
|
1019
|
+
async runCode(id, code, opts = {}) {
|
|
1020
|
+
const body = { language: opts.language ?? "python", code };
|
|
1021
|
+
if (opts.image) body.image = opts.image;
|
|
1022
|
+
if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
|
|
1023
|
+
if (opts.timeoutS) body.timeout_s = opts.timeoutS;
|
|
1024
|
+
const d = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/code`, {
|
|
1025
|
+
body,
|
|
1026
|
+
approval: opts.approval,
|
|
1027
|
+
signal: opts.signal
|
|
1028
|
+
});
|
|
1029
|
+
const exitCode = num(d.exit);
|
|
1030
|
+
const timedOut = d.timed_out === true;
|
|
1031
|
+
const arts = Array.isArray(d.artifacts) ? d.artifacts : [];
|
|
1032
|
+
return {
|
|
1033
|
+
ok: exitCode === 0 && !timedOut,
|
|
1034
|
+
exitCode,
|
|
1035
|
+
stdout: str(d.stdout),
|
|
1036
|
+
stderr: str(d.stderr),
|
|
1037
|
+
language: str(d.language),
|
|
1038
|
+
image: str(d.image),
|
|
1039
|
+
artifacts: arts.map((a) => ({ type: str(a.type), name: str(a.name), data: Buffer.from(str(a.data), "base64") })),
|
|
1040
|
+
durationMs: num(d.duration_ms),
|
|
1041
|
+
timedOut,
|
|
1042
|
+
truncated: d.truncated === true
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
// --- toolbox: files ------------------------------------------------------------
|
|
1046
|
+
/** GET …/files: a folder in the workspace (default `/workspace`), or one entry. */
|
|
1047
|
+
async listFiles(id, path4 = "/workspace", opts = {}) {
|
|
1048
|
+
const d = await this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/files`, {
|
|
1049
|
+
...opts,
|
|
1050
|
+
query: { path: path4 }
|
|
1051
|
+
});
|
|
1052
|
+
return d?.files ?? [];
|
|
1053
|
+
}
|
|
1054
|
+
/** GET …/files/stat: one entry, not following a symlink. */
|
|
1055
|
+
statFile(id, path4, opts = {}) {
|
|
1056
|
+
return this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/files/stat`, { ...opts, query: { path: path4 } });
|
|
1057
|
+
}
|
|
1058
|
+
/** GET …/files/content: a file's bytes. A body cut short is a Kosa8StreamError. */
|
|
1059
|
+
async readFile(id, path4, opts = {}) {
|
|
1060
|
+
const p = `/kosa8/v1/sandboxes/${enc(id)}/files/content`;
|
|
1061
|
+
const res = await this.transport.raw("GET", p, { query: { path: path4 }, signal: opts.signal, approval: opts.approval });
|
|
1062
|
+
let data;
|
|
1063
|
+
try {
|
|
1064
|
+
data = await readAll(res.stream, opts.signal);
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
if (opts.signal?.aborted) throw err;
|
|
1067
|
+
throw new Kosa8StreamError(
|
|
1068
|
+
`${path4}: read cut short: ${err instanceof Error ? err.message : String(err)}`,
|
|
1069
|
+
{ stdout: "", stderr: "" },
|
|
1070
|
+
{ method: "GET", path: p, cause: err }
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
if (res.status >= 400) {
|
|
1074
|
+
const text = data.toString("utf8");
|
|
1075
|
+
let body = text;
|
|
1076
|
+
try {
|
|
1077
|
+
body = JSON.parse(text);
|
|
1078
|
+
} catch {
|
|
1079
|
+
}
|
|
1080
|
+
throw errorFromResponse(res.status, body, { method: "GET", path: p });
|
|
1081
|
+
}
|
|
1082
|
+
const want = res.headers["content-length"];
|
|
1083
|
+
if (want !== void 0 && Number(want) !== data.length) {
|
|
1084
|
+
throw new Kosa8StreamError(`${path4}: got ${data.length} of ${want} bytes`, { stdout: "", stderr: "" }, { method: "GET", path: p });
|
|
1085
|
+
}
|
|
1086
|
+
return data;
|
|
1087
|
+
}
|
|
1088
|
+
/** A file's contents as text (UTF-8 by default). */
|
|
1089
|
+
async readText(id, path4, opts = {}) {
|
|
1090
|
+
return (await this.readFile(id, path4, opts)).toString(opts.encoding ?? "utf8");
|
|
1091
|
+
}
|
|
1092
|
+
/** PUT …/files/content: replace a file, making its folders. Answers the new entry. */
|
|
1093
|
+
writeFile(id, path4, data, opts = {}) {
|
|
1094
|
+
return this.transport.json("PUT", `/kosa8/v1/sandboxes/${enc(id)}/files/content`, {
|
|
1095
|
+
query: { path: path4, mode: opts.mode },
|
|
1096
|
+
rawBody: data,
|
|
1097
|
+
approval: opts.approval,
|
|
1098
|
+
signal: opts.signal
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
/** DELETE …/files: a file, or with `recursive` a folder and everything in it. */
|
|
1102
|
+
deleteFile(id, path4, opts = {}) {
|
|
1103
|
+
return this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(id)}/files`, {
|
|
1104
|
+
query: { path: path4, recursive: opts.recursive },
|
|
1105
|
+
approval: opts.approval,
|
|
1106
|
+
signal: opts.signal
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
/** POST …/files/mkdir: a folder, with its parents. */
|
|
1110
|
+
makeDir(id, path4, opts = {}) {
|
|
1111
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/files/mkdir`, { ...opts, body: { path: path4 } });
|
|
1112
|
+
}
|
|
1113
|
+
/** POST …/files/move: rename or move a file or folder. Answers the entry at `to`. */
|
|
1114
|
+
moveFile(id, from, to, opts = {}) {
|
|
1115
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/files/move`, { ...opts, body: { from, to } });
|
|
1116
|
+
}
|
|
1117
|
+
// --- toolbox: git ----------------------------------------------------------------
|
|
1118
|
+
/**
|
|
1119
|
+
* POST …/git: one git operation in the workspace. A git failure resolves
|
|
1120
|
+
* with `ok: false`; only a refusal (bad op, no sandbox, policy) throws.
|
|
1121
|
+
*/
|
|
1122
|
+
async git(id, op, opts = {}) {
|
|
1123
|
+
const { approval, signal, authorName, authorEmail, ...fields } = opts;
|
|
1124
|
+
const body = { op };
|
|
1125
|
+
for (const [k, v] of Object.entries(fields)) if (v !== void 0) body[k] = v;
|
|
1126
|
+
if (authorName !== void 0) body.author_name = authorName;
|
|
1127
|
+
if (authorEmail !== void 0) body.author_email = authorEmail;
|
|
1128
|
+
const d = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/git`, {
|
|
1129
|
+
body,
|
|
1130
|
+
approval,
|
|
1131
|
+
signal
|
|
1132
|
+
});
|
|
1133
|
+
const out = {
|
|
1134
|
+
op: str(d.op) || op,
|
|
1135
|
+
ok: d.ok === true,
|
|
1136
|
+
exitCode: num(d.exit),
|
|
1137
|
+
stdout: str(d.stdout),
|
|
1138
|
+
stderr: str(d.stderr),
|
|
1139
|
+
path: str(d.path)
|
|
1140
|
+
};
|
|
1141
|
+
if (d.status) out.status = d.status;
|
|
1142
|
+
if (d.log) out.log = d.log;
|
|
1143
|
+
if (d.branches) out.branches = d.branches;
|
|
1144
|
+
return out;
|
|
1145
|
+
}
|
|
1146
|
+
/** Clone `url` into the workspace (into `path`, else a folder named after the repo). */
|
|
1147
|
+
gitClone(id, url, opts = {}) {
|
|
1148
|
+
return this.git(id, "clone", { ...opts, url });
|
|
1149
|
+
}
|
|
1150
|
+
/** `git status` of the repository at `path`, parsed into `status`. */
|
|
1151
|
+
gitStatus(id, path4, opts = {}) {
|
|
1152
|
+
return this.git(id, "status", { ...opts, path: path4 });
|
|
1153
|
+
}
|
|
1154
|
+
/** Stage everything (unless `addAll: false`) and commit. A failed add is returned as is. */
|
|
1155
|
+
async gitCommit(id, path4, message, opts = {}) {
|
|
1156
|
+
const { addAll = true, ...rest } = opts;
|
|
1157
|
+
if (addAll) {
|
|
1158
|
+
const r = await this.git(id, "add", { path: path4, image: rest.image, approval: rest.approval, signal: rest.signal });
|
|
1159
|
+
if (!r.ok) return r;
|
|
1160
|
+
}
|
|
1161
|
+
return this.git(id, "commit", { ...rest, path: path4, message });
|
|
1162
|
+
}
|
|
1163
|
+
// --- toolbox: shell sessions -------------------------------------------------------
|
|
1164
|
+
/** POST …/sessions: a shell that keeps `cd` and `export` between commands. */
|
|
1165
|
+
createSession(id, name, opts = {}) {
|
|
1166
|
+
const body = {};
|
|
1167
|
+
if (name) body.name = name;
|
|
1168
|
+
if (opts.image) body.image = opts.image;
|
|
1169
|
+
if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
|
|
1170
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/sessions`, {
|
|
1171
|
+
body,
|
|
1172
|
+
approval: opts.approval,
|
|
1173
|
+
signal: opts.signal
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
/** GET …/sessions */
|
|
1177
|
+
async listSessions(id, opts = {}) {
|
|
1178
|
+
return await this.transport.json("GET", `/kosa8/v1/sandboxes/${enc(id)}/sessions`, opts) ?? [];
|
|
1179
|
+
}
|
|
1180
|
+
/** POST …/sessions/{name}/exec: run one command line in the session. */
|
|
1181
|
+
async sessionExec(id, session, command, opts = {}) {
|
|
1182
|
+
const body = { command };
|
|
1183
|
+
if (opts.timeoutS) body.timeout_s = opts.timeoutS;
|
|
1184
|
+
const d = await this.transport.json(
|
|
1185
|
+
"POST",
|
|
1186
|
+
`/kosa8/v1/sandboxes/${enc(id)}/sessions/${enc(session)}/exec`,
|
|
1187
|
+
{ body, approval: opts.approval, signal: opts.signal }
|
|
1188
|
+
);
|
|
1189
|
+
const exitCode = num(d.exit);
|
|
1190
|
+
const timedOut = d.timed_out === true;
|
|
1191
|
+
return {
|
|
1192
|
+
ok: exitCode === 0 && !timedOut,
|
|
1193
|
+
exitCode,
|
|
1194
|
+
stdout: str(d.stdout),
|
|
1195
|
+
stderr: str(d.stderr),
|
|
1196
|
+
cwd: str(d.cwd),
|
|
1197
|
+
durationMs: num(d.duration_ms),
|
|
1198
|
+
timedOut,
|
|
1199
|
+
truncated: d.truncated === true
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
/** Run a command in a session (default `main`), making the session on first use. */
|
|
1203
|
+
async shell(id, command, opts = {}) {
|
|
1204
|
+
const { session = "main", ...rest } = opts;
|
|
1205
|
+
try {
|
|
1206
|
+
return await this.sessionExec(id, session, command, rest);
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
if (!(err instanceof Kosa8NotFoundError) || !err.message.includes("no such session")) throw err;
|
|
1209
|
+
}
|
|
1210
|
+
await this.createSession(id, session, { approval: rest.approval, signal: rest.signal });
|
|
1211
|
+
return this.sessionExec(id, session, command, rest);
|
|
1212
|
+
}
|
|
1213
|
+
/** DELETE …/sessions/{name} */
|
|
1214
|
+
deleteSession(id, session, opts = {}) {
|
|
1215
|
+
return this.transport.json("DELETE", `/kosa8/v1/sandboxes/${enc(id)}/sessions/${enc(session)}`, opts);
|
|
1216
|
+
}
|
|
1217
|
+
// --- toolbox: terminals ----------------------------------------------------------
|
|
1218
|
+
/**
|
|
1219
|
+
* POST …/pty: a container with a terminal; resolves with its id. Connect
|
|
1220
|
+
* to `/kosa8/v1/sandboxes/{id}/containers/{cid}/tty` with any WebSocket
|
|
1221
|
+
* client: binary messages are the terminal, text messages JSON control.
|
|
1222
|
+
*/
|
|
1223
|
+
async startPty(id, opts = {}) {
|
|
1224
|
+
const body = {};
|
|
1225
|
+
if (opts.image) body.image = opts.image;
|
|
1226
|
+
if (opts.command && opts.command.length > 0) body.command = opts.command;
|
|
1227
|
+
if (opts.cols && opts.rows) {
|
|
1228
|
+
body.cols = opts.cols;
|
|
1229
|
+
body.rows = opts.rows;
|
|
1230
|
+
}
|
|
1231
|
+
if (opts.env && Object.keys(opts.env).length > 0) body.env = opts.env;
|
|
1232
|
+
const out = await this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/pty`, {
|
|
1233
|
+
body,
|
|
1234
|
+
approval: opts.approval,
|
|
1235
|
+
signal: opts.signal
|
|
1236
|
+
});
|
|
1237
|
+
return out.id;
|
|
1238
|
+
}
|
|
1239
|
+
/** POST …/containers/{cid}/resize: set a terminal's size. */
|
|
1240
|
+
resizePty(id, container, cols, rows, opts = {}) {
|
|
1241
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/containers/${enc(container)}/resize`, {
|
|
1242
|
+
...opts,
|
|
1243
|
+
body: { cols, rows }
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
// --- toolbox: previews -----------------------------------------------------------
|
|
1247
|
+
/** POST …/previews: a signed link, which expires, to a web server in the sandbox. */
|
|
1248
|
+
createPreview(id, port, opts = {}) {
|
|
1249
|
+
const body = { port };
|
|
1250
|
+
if (opts.session) body.session = opts.session;
|
|
1251
|
+
if (opts.container) body.container = opts.container;
|
|
1252
|
+
if (opts.expiresInS) body.expires_in_s = opts.expiresInS;
|
|
1253
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/previews`, {
|
|
1254
|
+
body,
|
|
1255
|
+
approval: opts.approval,
|
|
1256
|
+
signal: opts.signal
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
/** POST /kosa8/v1/previews/rotate: end every preview link at once. */
|
|
1260
|
+
revokePreviews(opts = {}) {
|
|
1261
|
+
return this.transport.json("POST", "/kosa8/v1/previews/rotate", opts);
|
|
1262
|
+
}
|
|
1263
|
+
// --- toolbox: lifecycle ----------------------------------------------------------
|
|
1264
|
+
/**
|
|
1265
|
+
* POST …/lifecycle: change auto-stop, auto-delete, TTL or ephemeral.
|
|
1266
|
+
* Options left out keep their value; 0 (or `false`) turns one off.
|
|
1267
|
+
*/
|
|
1268
|
+
setLifecycle(id, opts = {}) {
|
|
1269
|
+
const body = {};
|
|
1270
|
+
if (opts.autoStopMin !== void 0) body.auto_stop_min = opts.autoStopMin;
|
|
1271
|
+
if (opts.autoDeleteMin !== void 0) body.auto_delete_min = opts.autoDeleteMin;
|
|
1272
|
+
if (opts.ttlMin !== void 0) body.ttl_min = opts.ttlMin;
|
|
1273
|
+
if (opts.ephemeral !== void 0) body.ephemeral = opts.ephemeral;
|
|
1274
|
+
return this.transport.json("POST", `/kosa8/v1/sandboxes/${enc(id)}/lifecycle`, {
|
|
1275
|
+
body,
|
|
1276
|
+
approval: opts.approval,
|
|
1277
|
+
signal: opts.signal
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
// --- toolbox: templates ----------------------------------------------------------
|
|
1281
|
+
/** GET /kosa8/v1/templates */
|
|
1282
|
+
async listTemplates(opts = {}) {
|
|
1283
|
+
return await this.transport.json("GET", "/kosa8/v1/templates", opts) ?? [];
|
|
1284
|
+
}
|
|
1285
|
+
/** DELETE /kosa8/v1/templates/{name}. Sandboxes made from it keep their copy. */
|
|
1286
|
+
deleteTemplate(name, opts = {}) {
|
|
1287
|
+
return this.transport.json("DELETE", `/kosa8/v1/templates/${enc(name)}`, opts);
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* POST /kosa8/v1/templates: build `contextDir`'s Dockerfile in a throwaway
|
|
1291
|
+
* sandbox and keep the image as a template, for `createSandbox({ template })`.
|
|
1292
|
+
* The context is packed honouring `.dockerignore`. Resolves with the final
|
|
1293
|
+
* frame (`{ done, ok, image }`); a failed build throws Kosa8StreamError.
|
|
1294
|
+
*/
|
|
1295
|
+
async buildTemplate(name, contextDir = ".", opts = {}) {
|
|
1296
|
+
const dockerfile = opts.dockerfile ?? "Dockerfile";
|
|
1297
|
+
const q = new URLSearchParams();
|
|
1298
|
+
q.append("name", name);
|
|
1299
|
+
q.append("dockerfile", dockerfile);
|
|
1300
|
+
for (const [k, v] of Object.entries(opts.buildArgs ?? {})) q.append("buildarg", `${k}=${v}`);
|
|
1301
|
+
const egress = egressString(opts.egress);
|
|
1302
|
+
if (egress) q.append("egress", egress);
|
|
1303
|
+
if (opts.replace) q.append("replace", "1");
|
|
1304
|
+
const ctx = await tarContext(contextDir, dockerfile);
|
|
1305
|
+
let out = "";
|
|
1306
|
+
for await (const f of this.transport.frames("POST", `/kosa8/v1/templates?${q.toString()}`, {
|
|
1307
|
+
rawBody: ctx,
|
|
1308
|
+
approval: opts.approval,
|
|
1309
|
+
signal: opts.signal
|
|
1310
|
+
})) {
|
|
1311
|
+
if (f.done) {
|
|
1312
|
+
if (f.error) throw new Kosa8StreamError(f.error, { stdout: out, stderr: "" }, { daemonMessage: f.error });
|
|
1313
|
+
return f;
|
|
1314
|
+
}
|
|
1315
|
+
if (f.data) {
|
|
1316
|
+
out += f.data.toString("utf8");
|
|
1317
|
+
opts.onOutput?.(f.data, f.stream ?? "stdout");
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
throw new Kosa8StreamError("template build stream ended without a final frame", { stdout: out, stderr: "" });
|
|
1321
|
+
}
|
|
862
1322
|
// --- helpers -------------------------------------------------------------------
|
|
863
1323
|
/** A handle bound to one sandbox. */
|
|
864
1324
|
sandbox(id) {
|
|
@@ -958,12 +1418,274 @@ var SandboxHandle = class {
|
|
|
958
1418
|
rewind(opts) {
|
|
959
1419
|
return this.client.rewind(this.id, opts);
|
|
960
1420
|
}
|
|
1421
|
+
runCode(code, opts) {
|
|
1422
|
+
return this.client.runCode(this.id, code, opts);
|
|
1423
|
+
}
|
|
1424
|
+
listFiles(path4, opts) {
|
|
1425
|
+
return this.client.listFiles(this.id, path4, opts);
|
|
1426
|
+
}
|
|
1427
|
+
statFile(path4, opts) {
|
|
1428
|
+
return this.client.statFile(this.id, path4, opts);
|
|
1429
|
+
}
|
|
1430
|
+
readFile(path4, opts) {
|
|
1431
|
+
return this.client.readFile(this.id, path4, opts);
|
|
1432
|
+
}
|
|
1433
|
+
readText(path4, opts) {
|
|
1434
|
+
return this.client.readText(this.id, path4, opts);
|
|
1435
|
+
}
|
|
1436
|
+
writeFile(path4, data, opts) {
|
|
1437
|
+
return this.client.writeFile(this.id, path4, data, opts);
|
|
1438
|
+
}
|
|
1439
|
+
deleteFile(path4, opts) {
|
|
1440
|
+
return this.client.deleteFile(this.id, path4, opts);
|
|
1441
|
+
}
|
|
1442
|
+
makeDir(path4, opts) {
|
|
1443
|
+
return this.client.makeDir(this.id, path4, opts);
|
|
1444
|
+
}
|
|
1445
|
+
moveFile(from, to, opts) {
|
|
1446
|
+
return this.client.moveFile(this.id, from, to, opts);
|
|
1447
|
+
}
|
|
1448
|
+
git(op, opts) {
|
|
1449
|
+
return this.client.git(this.id, op, opts);
|
|
1450
|
+
}
|
|
1451
|
+
gitClone(url, opts) {
|
|
1452
|
+
return this.client.gitClone(this.id, url, opts);
|
|
1453
|
+
}
|
|
1454
|
+
gitStatus(path4, opts) {
|
|
1455
|
+
return this.client.gitStatus(this.id, path4, opts);
|
|
1456
|
+
}
|
|
1457
|
+
gitCommit(path4, message, opts) {
|
|
1458
|
+
return this.client.gitCommit(this.id, path4, message, opts);
|
|
1459
|
+
}
|
|
1460
|
+
createSession(name, opts) {
|
|
1461
|
+
return this.client.createSession(this.id, name, opts);
|
|
1462
|
+
}
|
|
1463
|
+
sessions(opts) {
|
|
1464
|
+
return this.client.listSessions(this.id, opts);
|
|
1465
|
+
}
|
|
1466
|
+
sessionExec(session, command, opts) {
|
|
1467
|
+
return this.client.sessionExec(this.id, session, command, opts);
|
|
1468
|
+
}
|
|
1469
|
+
shell(command, opts) {
|
|
1470
|
+
return this.client.shell(this.id, command, opts);
|
|
1471
|
+
}
|
|
1472
|
+
deleteSession(session, opts) {
|
|
1473
|
+
return this.client.deleteSession(this.id, session, opts);
|
|
1474
|
+
}
|
|
1475
|
+
startPty(opts) {
|
|
1476
|
+
return this.client.startPty(this.id, opts);
|
|
1477
|
+
}
|
|
1478
|
+
resizePty(container, cols, rows, opts) {
|
|
1479
|
+
return this.client.resizePty(this.id, container, cols, rows, opts);
|
|
1480
|
+
}
|
|
1481
|
+
createPreview(port, opts) {
|
|
1482
|
+
return this.client.createPreview(this.id, port, opts);
|
|
1483
|
+
}
|
|
1484
|
+
setLifecycle(opts) {
|
|
1485
|
+
return this.client.setLifecycle(this.id, opts);
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
// src/cloud.ts
|
|
1490
|
+
var DEFAULT_CLOUD_URL = "https://api.kosa8.cloud";
|
|
1491
|
+
var CLOUD_KEY_PREFIX = "k8c_live_";
|
|
1492
|
+
function trimSlash(s) {
|
|
1493
|
+
return s.replace(/\/+$/, "");
|
|
1494
|
+
}
|
|
1495
|
+
function checkBase(url) {
|
|
1496
|
+
let u;
|
|
1497
|
+
try {
|
|
1498
|
+
u = new URL(url);
|
|
1499
|
+
} catch {
|
|
1500
|
+
throw new Kosa8Error(`kosa8 Cloud address is not a URL: ${url}`);
|
|
1501
|
+
}
|
|
1502
|
+
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
|
|
1503
|
+
if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
|
|
1504
|
+
throw new Kosa8Error(`kosa8 Cloud address must use https: ${url}`);
|
|
1505
|
+
}
|
|
1506
|
+
return trimSlash(u.toString());
|
|
1507
|
+
}
|
|
1508
|
+
var Kosa8Cloud = class {
|
|
1509
|
+
baseUrl;
|
|
1510
|
+
sandboxes;
|
|
1511
|
+
functions;
|
|
1512
|
+
apiKey;
|
|
1513
|
+
fetchImpl;
|
|
1514
|
+
timeoutMs;
|
|
1515
|
+
constructor(options = {}) {
|
|
1516
|
+
const env = options.env ?? (typeof process !== "undefined" ? process.env : {});
|
|
1517
|
+
const key = (options.apiKey ?? env.KOSA8_API_KEY ?? "").trim();
|
|
1518
|
+
if (!key) throw new Kosa8Error("no kosa8 Cloud API key: pass apiKey or set KOSA8_API_KEY");
|
|
1519
|
+
if (!key.startsWith(CLOUD_KEY_PREFIX)) throw new Kosa8Error(`a kosa8 Cloud API key starts with ${CLOUD_KEY_PREFIX}`);
|
|
1520
|
+
this.apiKey = key;
|
|
1521
|
+
this.baseUrl = checkBase(options.baseUrl ?? env.KOSA8_CLOUD_URL ?? DEFAULT_CLOUD_URL);
|
|
1522
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
1523
|
+
if (typeof f !== "function") throw new Kosa8Error("no fetch available; pass options.fetch");
|
|
1524
|
+
this.fetchImpl = f;
|
|
1525
|
+
this.timeoutMs = options.timeoutMs ?? 12e4;
|
|
1526
|
+
this.sandboxes = new CloudSandboxes(this);
|
|
1527
|
+
this.functions = new CloudFunctions(this);
|
|
1528
|
+
}
|
|
1529
|
+
/** The org, the key in use and, for a member's key, the member. */
|
|
1530
|
+
me() {
|
|
1531
|
+
return this.request("GET", "/v1/me");
|
|
1532
|
+
}
|
|
1533
|
+
/** This month's usage, bill estimate and whether work is paused for payment. */
|
|
1534
|
+
usage() {
|
|
1535
|
+
return this.request("GET", "/v1/usage");
|
|
1536
|
+
}
|
|
1537
|
+
/** @internal */
|
|
1538
|
+
async request(method, path4, body) {
|
|
1539
|
+
const ctl = new AbortController();
|
|
1540
|
+
const timer = setTimeout(() => ctl.abort(), this.timeoutMs);
|
|
1541
|
+
let resp;
|
|
1542
|
+
try {
|
|
1543
|
+
resp = await this.fetchImpl(`${this.baseUrl}${path4}`, {
|
|
1544
|
+
method,
|
|
1545
|
+
headers: {
|
|
1546
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1547
|
+
Accept: "application/json",
|
|
1548
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
1549
|
+
},
|
|
1550
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
1551
|
+
signal: ctl.signal
|
|
1552
|
+
});
|
|
1553
|
+
} catch (e) {
|
|
1554
|
+
if (ctl.signal.aborted) {
|
|
1555
|
+
throw new Kosa8TimeoutError(`kosa8 Cloud did not answer within ${this.timeoutMs} ms`, { method, path: path4, cause: e });
|
|
1556
|
+
}
|
|
1557
|
+
throw new Kosa8ConnectionError(`could not reach kosa8 Cloud at ${this.baseUrl}: ${String(e?.message ?? e)}`, { method, path: path4, cause: e });
|
|
1558
|
+
} finally {
|
|
1559
|
+
clearTimeout(timer);
|
|
1560
|
+
}
|
|
1561
|
+
const text = await resp.text();
|
|
1562
|
+
let data = text;
|
|
1563
|
+
try {
|
|
1564
|
+
data = text === "" ? null : JSON.parse(text);
|
|
1565
|
+
} catch {
|
|
1566
|
+
}
|
|
1567
|
+
if (!resp.ok) throw cloudError(resp.status, data, { method, path: path4 });
|
|
1568
|
+
return data;
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
function cloudError(status, body, ctx) {
|
|
1572
|
+
let code;
|
|
1573
|
+
let msgBody = body;
|
|
1574
|
+
if (typeof body === "object" && body !== null) {
|
|
1575
|
+
const b = body;
|
|
1576
|
+
if (typeof b.error === "string") code = b.error;
|
|
1577
|
+
msgBody = { error: typeof b.message === "string" && b.message ? b.message : b.error };
|
|
1578
|
+
}
|
|
1579
|
+
return errorFromResponse(status, msgBody, { ...ctx, code });
|
|
1580
|
+
}
|
|
1581
|
+
var CloudSandboxes = class {
|
|
1582
|
+
constructor(cloud) {
|
|
1583
|
+
this.cloud = cloud;
|
|
1584
|
+
}
|
|
1585
|
+
/** Start a sandbox; resolves once it is running. */
|
|
1586
|
+
async create(opts = {}) {
|
|
1587
|
+
const body = {};
|
|
1588
|
+
if (opts.template !== void 0) body.template = opts.template;
|
|
1589
|
+
if (opts.region !== void 0) body.region = opts.region;
|
|
1590
|
+
if (opts.sleepAfterS !== void 0) body.sleep_after_s = opts.sleepAfterS;
|
|
1591
|
+
const r = await this.cloud.request("POST", "/v1/sandboxes", body);
|
|
1592
|
+
return new CloudSandbox(this.cloud, r.sandbox);
|
|
1593
|
+
}
|
|
1594
|
+
/** The org's sandboxes; with all, deleted ones too. */
|
|
1595
|
+
async list(opts = {}) {
|
|
1596
|
+
const r = await this.cloud.request("GET", `/v1/sandboxes${opts.all ? "?all=1" : ""}`);
|
|
1597
|
+
return r.sandboxes.map((s) => new CloudSandbox(this.cloud, s));
|
|
1598
|
+
}
|
|
1599
|
+
async get(id) {
|
|
1600
|
+
const r = await this.cloud.request("GET", `/v1/sandboxes/${encodeURIComponent(id)}`);
|
|
1601
|
+
return new CloudSandbox(this.cloud, r.sandbox);
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
var CloudSandbox = class {
|
|
1605
|
+
constructor(cloud, info) {
|
|
1606
|
+
this.cloud = cloud;
|
|
1607
|
+
this.info = info;
|
|
1608
|
+
}
|
|
1609
|
+
get id() {
|
|
1610
|
+
return this.info.id;
|
|
1611
|
+
}
|
|
1612
|
+
path(suffix = "") {
|
|
1613
|
+
return `/v1/sandboxes/${encodeURIComponent(this.info.id)}${suffix}`;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Run a command and wait for it. A string runs through `sh -c`. A
|
|
1617
|
+
* sleeping sandbox wakes for it.
|
|
1618
|
+
*/
|
|
1619
|
+
async exec(cmd, opts = {}) {
|
|
1620
|
+
const argv = typeof cmd === "string" ? ["sh", "-c", cmd] : cmd;
|
|
1621
|
+
const body = { cmd: argv };
|
|
1622
|
+
if (opts.image !== void 0) body.image = opts.image;
|
|
1623
|
+
if (opts.env) body.env = Object.entries(opts.env).map(([k, v]) => `${k}=${v}`);
|
|
1624
|
+
const r = await this.cloud.request(
|
|
1625
|
+
"POST",
|
|
1626
|
+
this.path("/exec"),
|
|
1627
|
+
body
|
|
1628
|
+
);
|
|
1629
|
+
const out = { exitCode: r.exit, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
1630
|
+
if (typeof r.woke_ms === "number") out.wokeMs = r.woke_ms;
|
|
1631
|
+
return out;
|
|
1632
|
+
}
|
|
1633
|
+
/** Snapshot it and free its VM; it then costs only snapshot storage. */
|
|
1634
|
+
async sleep() {
|
|
1635
|
+
this.info = (await this.cloud.request("POST", this.path("/sleep"))).sandbox;
|
|
1636
|
+
return this;
|
|
1637
|
+
}
|
|
1638
|
+
/** Restore a sleeping sandbox, files and all. */
|
|
1639
|
+
async wake() {
|
|
1640
|
+
this.info = (await this.cloud.request("POST", this.path("/wake"))).sandbox;
|
|
1641
|
+
return this;
|
|
1642
|
+
}
|
|
1643
|
+
async refresh() {
|
|
1644
|
+
this.info = (await this.cloud.request("GET", this.path())).sandbox;
|
|
1645
|
+
return this;
|
|
1646
|
+
}
|
|
1647
|
+
async delete() {
|
|
1648
|
+
await this.cloud.request("DELETE", this.path());
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
var CloudFunctions = class {
|
|
1652
|
+
constructor(cloud) {
|
|
1653
|
+
this.cloud = cloud;
|
|
1654
|
+
}
|
|
1655
|
+
/** Deploy (create or replace) a function; every deploy bumps its version. */
|
|
1656
|
+
deploy(name, spec) {
|
|
1657
|
+
return this.cloud.request("PUT", `/v1/functions/${encodeURIComponent(name)}`, spec);
|
|
1658
|
+
}
|
|
1659
|
+
async list() {
|
|
1660
|
+
return (await this.cloud.request("GET", "/v1/functions")).functions;
|
|
1661
|
+
}
|
|
1662
|
+
get(name) {
|
|
1663
|
+
return this.cloud.request("GET", `/v1/functions/${encodeURIComponent(name)}`);
|
|
1664
|
+
}
|
|
1665
|
+
async delete(name) {
|
|
1666
|
+
await this.cloud.request("DELETE", `/v1/functions/${encodeURIComponent(name)}`);
|
|
1667
|
+
}
|
|
1668
|
+
/** Call it once. A handler that raises resolves with ok: false, not a throw. */
|
|
1669
|
+
invoke(name, event = null) {
|
|
1670
|
+
return this.cloud.request("POST", `/v1/functions/${encodeURIComponent(name)}/invoke`, { event });
|
|
1671
|
+
}
|
|
1672
|
+
/** One call per event, results in input order. */
|
|
1673
|
+
map(name, events, opts = {}) {
|
|
1674
|
+
const body = { events };
|
|
1675
|
+
if (opts.concurrency !== void 0) body.concurrency = opts.concurrency;
|
|
1676
|
+
return this.cloud.request("POST", `/v1/functions/${encodeURIComponent(name)}/map`, body);
|
|
1677
|
+
}
|
|
961
1678
|
};
|
|
962
1679
|
export {
|
|
963
1680
|
ACTOR_HEADER,
|
|
964
1681
|
AGENT_TOKEN_HEADER,
|
|
965
1682
|
APPROVAL_HEADER,
|
|
1683
|
+
CLOUD_KEY_PREFIX,
|
|
966
1684
|
CheckpointBook,
|
|
1685
|
+
CloudFunctions,
|
|
1686
|
+
CloudSandbox,
|
|
1687
|
+
CloudSandboxes,
|
|
1688
|
+
DEFAULT_CLOUD_URL,
|
|
967
1689
|
FileCheckpointStore,
|
|
968
1690
|
Kosa8ApiError,
|
|
969
1691
|
Kosa8ApprovalRequiredError,
|
|
@@ -971,20 +1693,26 @@ export {
|
|
|
971
1693
|
Kosa8BadRequestError,
|
|
972
1694
|
Kosa8CheckpointError,
|
|
973
1695
|
Kosa8Client,
|
|
1696
|
+
Kosa8Cloud,
|
|
974
1697
|
Kosa8ConflictError,
|
|
975
1698
|
Kosa8ConnectionError,
|
|
976
1699
|
Kosa8Error,
|
|
977
1700
|
Kosa8NotFoundError,
|
|
1701
|
+
Kosa8PaymentRequiredError,
|
|
978
1702
|
Kosa8PolicyError,
|
|
1703
|
+
Kosa8RateLimitError,
|
|
979
1704
|
Kosa8StreamError,
|
|
1705
|
+
Kosa8TimeoutError,
|
|
980
1706
|
Kosa8UnsupportedError,
|
|
981
1707
|
MAX_CHECKPOINTS_PER_SANDBOX,
|
|
982
1708
|
MemoryCheckpointStore,
|
|
983
1709
|
SandboxHandle,
|
|
984
1710
|
Transport,
|
|
985
1711
|
defaultSocketPath,
|
|
1712
|
+
defaultWorkerFile,
|
|
986
1713
|
errorFromResponse,
|
|
987
1714
|
resolveEndpoint,
|
|
1715
|
+
tarContext,
|
|
988
1716
|
validateSandboxName
|
|
989
1717
|
};
|
|
990
1718
|
//# sourceMappingURL=index.js.map
|