@promigence/sdk 0.1.0-beta.12 → 0.1.0-beta.14
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/caller.js +10 -0
- package/dist/cjs/caller.js +10 -0
- package/dist/cjs/client.d.ts +16 -6
- package/dist/cjs/client.js +1180 -1129
- package/dist/cjs/core.d.ts +1 -1
- package/dist/cjs/core.js +1 -1
- package/dist/cjs/dispatcher.d.ts +2 -0
- package/dist/cjs/dispatcher.js +5 -0
- package/dist/cjs/http2.d.ts +11 -0
- package/dist/cjs/http2.js +335 -71
- package/dist/cjs/keyed.d.ts +6 -0
- package/dist/cjs/keyed.js +59 -0
- package/dist/cjs/proxy.js +11 -0
- package/dist/cjs/sandbox.d.ts +3 -1
- package/dist/cjs/sandbox.js +33 -20
- package/dist/cjs/trace.d.ts +2 -0
- package/dist/cjs/trace.js +17 -0
- package/dist/cjs/transport-worker.js +83 -14
- package/dist/client.d.ts +16 -6
- package/dist/client.js +1179 -1127
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/dispatcher.d.ts +2 -0
- package/dist/dispatcher.js +5 -0
- package/dist/http2.d.ts +11 -0
- package/dist/http2.js +335 -72
- package/dist/keyed.d.ts +6 -0
- package/dist/keyed.js +55 -0
- package/dist/proxy.js +11 -0
- package/dist/sandbox.d.ts +3 -1
- package/dist/sandbox.js +33 -20
- package/dist/trace.d.ts +2 -0
- package/dist/trace.js +16 -0
- package/dist/transport-worker.js +83 -14
- package/package.json +1 -1
package/dist/keyed.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is `method path` a create that takes an `Idempotency-Key`? `path` is relative to `/v1` and may
|
|
3
|
+
* carry a query string. The list is the API reference's (generated from the operations that
|
|
4
|
+
* document the header), so a create the API keys is never sent unkeyed by this client.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isKeyedCreate(method: string, path: string): boolean;
|
package/dist/keyed.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { KEYED_CREATES } from "./keyed.gen.js";
|
|
2
|
+
const KEYED_PATTERNS = KEYED_CREATES.map(([m, p]) => [m, p.split("/").filter(Boolean)]);
|
|
3
|
+
const KEYED_METHODS = new Set(KEYED_CREATES.map(([m]) => m));
|
|
4
|
+
let keyedLastMethod = "";
|
|
5
|
+
let keyedLastPath = "";
|
|
6
|
+
let keyedLastAnswer = false;
|
|
7
|
+
let keyedReads = 0;
|
|
8
|
+
export function isKeyedCreate(method, path) {
|
|
9
|
+
if (!KEYED_METHODS.has(method))
|
|
10
|
+
return false;
|
|
11
|
+
if (path === keyedLastPath && method === keyedLastMethod)
|
|
12
|
+
return keyedLastAnswer;
|
|
13
|
+
const answer = keyedCreateOf(method, path);
|
|
14
|
+
keyedLastMethod = method;
|
|
15
|
+
keyedLastPath = path;
|
|
16
|
+
keyedLastAnswer = answer;
|
|
17
|
+
return answer;
|
|
18
|
+
}
|
|
19
|
+
function keyedCreateOf(method, path) {
|
|
20
|
+
keyedReads++;
|
|
21
|
+
let end = path.indexOf("?");
|
|
22
|
+
if (end < 0)
|
|
23
|
+
end = path.length;
|
|
24
|
+
for (let p = 0; p < KEYED_PATTERNS.length; p++) {
|
|
25
|
+
const entry = KEYED_PATTERNS[p];
|
|
26
|
+
if (entry[0] === method && segmentsMatch(path, end, entry[1]))
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
function segmentsMatch(path, end, pat) {
|
|
32
|
+
let i = 0;
|
|
33
|
+
let k = 0;
|
|
34
|
+
for (;;) {
|
|
35
|
+
while (i < end && path.charCodeAt(i) === 0x2f)
|
|
36
|
+
i++;
|
|
37
|
+
if (i >= end)
|
|
38
|
+
return k === pat.length;
|
|
39
|
+
if (k >= pat.length)
|
|
40
|
+
return false;
|
|
41
|
+
let j = i;
|
|
42
|
+
while (j < end && path.charCodeAt(j) !== 0x2f)
|
|
43
|
+
j++;
|
|
44
|
+
const s = pat[k];
|
|
45
|
+
if (s.charCodeAt(0) !== 0x7b && (j - i !== s.length || !path.startsWith(s, i)))
|
|
46
|
+
return false;
|
|
47
|
+
k++;
|
|
48
|
+
i = j;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export const __keyedInternals = {
|
|
52
|
+
reads() {
|
|
53
|
+
return keyedReads;
|
|
54
|
+
},
|
|
55
|
+
};
|
package/dist/proxy.js
CHANGED
|
@@ -57,12 +57,23 @@ export function noProxy(host, port, env = process.env) {
|
|
|
57
57
|
}
|
|
58
58
|
return false;
|
|
59
59
|
}
|
|
60
|
+
let proxyLastInputs;
|
|
61
|
+
let proxyLast;
|
|
60
62
|
export function proxyFor(url, env = process.env) {
|
|
61
63
|
const https = envOf(env, "HTTPS_PROXY");
|
|
62
64
|
const http = envOf(env, "HTTP_PROXY");
|
|
63
65
|
const all = envOf(env, "ALL_PROXY");
|
|
64
66
|
if (https === undefined && http === undefined && all === undefined)
|
|
65
67
|
return undefined;
|
|
68
|
+
const inputs = `${url}\n${https ?? "\0"}\n${http ?? "\0"}\n${all ?? "\0"}\n${envOf(env, "NO_PROXY") ?? "\0"}`;
|
|
69
|
+
if (inputs === proxyLastInputs)
|
|
70
|
+
return proxyLast;
|
|
71
|
+
const found = proxyForOf(url, env, https, http, all);
|
|
72
|
+
proxyLastInputs = inputs;
|
|
73
|
+
proxyLast = found;
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
function proxyForOf(url, env, https, http, all) {
|
|
66
77
|
let u;
|
|
67
78
|
try {
|
|
68
79
|
u = new URL(url);
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -396,7 +396,9 @@ export declare class Sandbox {
|
|
|
396
396
|
}>;
|
|
397
397
|
/**
|
|
398
398
|
* `forkHandles`, plus what `create` needs to end a run it was interrupted starting: the key the
|
|
399
|
-
* create is sent under, a signal for it and its waits, and the start options as sent
|
|
399
|
+
* create is sent under, a signal for it and its waits, and the start options as sent (`onStart`
|
|
400
|
+
* is handed the very options sent, the signal included); `client` and `count` stand in for the
|
|
401
|
+
* options' own.
|
|
400
402
|
*/
|
|
401
403
|
private static forkHandlesWith;
|
|
402
404
|
/** Attach to a running sandbox (`GET /sandboxes/:id`). */
|
package/dist/sandbox.js
CHANGED
|
@@ -168,16 +168,19 @@ function onSigint() {
|
|
|
168
168
|
return;
|
|
169
169
|
void Promise.allSettled(cleanups.map((f) => f())).finally(() => { process.kill(process.pid, "SIGINT"); });
|
|
170
170
|
}
|
|
171
|
+
function createAborted() {
|
|
172
|
+
return new TimeoutError("Sandbox.create was aborted by the caller's signal; anything it had started was ended", "Nothing to fix: the caller aborted it.");
|
|
173
|
+
}
|
|
171
174
|
function holdSigint(cleanup) {
|
|
172
175
|
if (typeof process === "undefined" || typeof process.listenerCount !== "function")
|
|
173
|
-
return
|
|
174
|
-
if (process.listenerCount("SIGINT") - (sigintHeld ? 1 : 0) > 0)
|
|
175
|
-
return () => { };
|
|
176
|
-
creatingNow.add(cleanup);
|
|
176
|
+
return undefined;
|
|
177
177
|
if (!sigintHeld) {
|
|
178
|
+
if (process.listenerCount("SIGINT") > 0)
|
|
179
|
+
return undefined;
|
|
178
180
|
process.once("SIGINT", onSigint);
|
|
179
181
|
sigintHeld = true;
|
|
180
182
|
}
|
|
183
|
+
creatingNow.add(cleanup);
|
|
181
184
|
return () => {
|
|
182
185
|
creatingNow.delete(cleanup);
|
|
183
186
|
if (creatingNow.size === 0 && sigintHeld) {
|
|
@@ -455,7 +458,7 @@ export class Sandbox {
|
|
|
455
458
|
...(opts.apiKey === undefined ? {} : { apiKey: opts.apiKey }),
|
|
456
459
|
...(opts.apiUrl === undefined ? {} : { apiUrl: opts.apiUrl }),
|
|
457
460
|
});
|
|
458
|
-
const aborted =
|
|
461
|
+
const aborted = createAborted;
|
|
459
462
|
if (opts.signal?.aborted)
|
|
460
463
|
throw aborted();
|
|
461
464
|
if (opts.image !== undefined) {
|
|
@@ -476,24 +479,31 @@ export class Sandbox {
|
|
|
476
479
|
else if (snapshotIdOrAlias === undefined || snapshotIdOrAlias === "base") {
|
|
477
480
|
snapshotIdOrAlias = DEFAULT_SNAPSHOT;
|
|
478
481
|
}
|
|
479
|
-
const stop = new AbortController();
|
|
480
482
|
const key = newIdempotencyKey();
|
|
481
483
|
let sent;
|
|
482
484
|
let runId;
|
|
483
485
|
let ending;
|
|
484
486
|
const end = () => (ending ??= endInterruptedCreate(client, runId, sent));
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
opts.signal
|
|
487
|
+
let stop;
|
|
488
|
+
const release = holdSigint(() => { stop?.abort(); return end(); });
|
|
489
|
+
const callerSignal = opts.signal;
|
|
490
|
+
const onAbort = callerSignal === undefined ? undefined : () => { stop?.abort(); };
|
|
491
|
+
if (release !== undefined || callerSignal !== undefined)
|
|
492
|
+
stop = new AbortController();
|
|
493
|
+
if (onAbort !== undefined)
|
|
494
|
+
callerSignal.addEventListener("abort", onAbort, { once: true });
|
|
488
495
|
try {
|
|
489
496
|
let handles;
|
|
490
497
|
try {
|
|
491
|
-
|
|
492
|
-
idempotencyKey: key,
|
|
493
|
-
}
|
|
498
|
+
const extra = {
|
|
499
|
+
idempotencyKey: key, onStart: (s) => { sent = s; }, client, count: 1,
|
|
500
|
+
};
|
|
501
|
+
if (stop !== undefined)
|
|
502
|
+
extra.signal = stop.signal;
|
|
503
|
+
({ runId, handles } = await Sandbox.forkHandlesWith(snapshotIdOrAlias, opts, extra));
|
|
494
504
|
}
|
|
495
505
|
catch (e) {
|
|
496
|
-
if (stop
|
|
506
|
+
if (stop?.signal.aborted) {
|
|
497
507
|
await end();
|
|
498
508
|
throw aborted();
|
|
499
509
|
}
|
|
@@ -506,7 +516,7 @@ export class Sandbox {
|
|
|
506
516
|
return await first;
|
|
507
517
|
}
|
|
508
518
|
catch (e) {
|
|
509
|
-
if (stop
|
|
519
|
+
if (stop?.signal.aborted) {
|
|
510
520
|
await end();
|
|
511
521
|
throw aborted();
|
|
512
522
|
}
|
|
@@ -518,8 +528,9 @@ export class Sandbox {
|
|
|
518
528
|
}
|
|
519
529
|
}
|
|
520
530
|
finally {
|
|
521
|
-
release();
|
|
522
|
-
|
|
531
|
+
release?.();
|
|
532
|
+
if (onAbort !== undefined)
|
|
533
|
+
callerSignal.removeEventListener("abort", onAbort);
|
|
523
534
|
}
|
|
524
535
|
}
|
|
525
536
|
static async fork(snapshotIdOrAlias, opts = {}) {
|
|
@@ -530,11 +541,11 @@ export class Sandbox {
|
|
|
530
541
|
return Sandbox.forkHandlesWith(snapshotIdOrAlias, opts, {});
|
|
531
542
|
}
|
|
532
543
|
static async forkHandlesWith(snapshotIdOrAlias, opts, extra) {
|
|
533
|
-
const client = opts.client ?? new PromigenceClient({
|
|
544
|
+
const client = extra.client ?? opts.client ?? new PromigenceClient({
|
|
534
545
|
...(opts.apiKey === undefined ? {} : { apiKey: opts.apiKey }),
|
|
535
546
|
...(opts.apiUrl === undefined ? {} : { apiUrl: opts.apiUrl }),
|
|
536
547
|
});
|
|
537
|
-
const count = opts.count ?? 1;
|
|
548
|
+
const count = extra.count ?? opts.count ?? 1;
|
|
538
549
|
const spendCap = spendCapFromEnv(opts.spendCap);
|
|
539
550
|
const start = { snapshot: snapshotIdOrAlias, count };
|
|
540
551
|
if (spendCap !== undefined)
|
|
@@ -555,8 +566,10 @@ export class Sandbox {
|
|
|
555
566
|
start.optimizeFor = opts.optimizeFor;
|
|
556
567
|
if (extra.idempotencyKey !== undefined)
|
|
557
568
|
start.idempotencyKey = extra.idempotencyKey;
|
|
558
|
-
extra.
|
|
559
|
-
|
|
569
|
+
if (extra.signal !== undefined)
|
|
570
|
+
start.signal = extra.signal;
|
|
571
|
+
extra.onStart?.(start);
|
|
572
|
+
const run = await client.runs.start(start);
|
|
560
573
|
const envs = opts.envs;
|
|
561
574
|
const mk = (ref) => {
|
|
562
575
|
const init = { sandboxId: ref.sandbox_id, runId: run.run_id, snapshot: snapshotIdOrAlias, client };
|
package/dist/trace.d.ts
CHANGED
|
@@ -31,3 +31,5 @@ export declare function mark(s: TraceStamp | undefined, phase: string): void;
|
|
|
31
31
|
export declare function traceEvent(name: string): void;
|
|
32
32
|
/** A process event that happened at `t` (`performance.now()` terms), recorded now: one another thread reported. */
|
|
33
33
|
export declare function traceEventAt(name: string, t: number): void;
|
|
34
|
+
/** `fn` runs once at exit, before the trace is written (tracing on only). */
|
|
35
|
+
export declare function onTraceFlush(fn: () => void): void;
|
package/dist/trace.js
CHANGED
|
@@ -60,7 +60,23 @@ function modulePath() {
|
|
|
60
60
|
const m = /\((.*):\d+:\d+\)\s*$/.exec(frame) ?? /at (.*):\d+:\d+\s*$/.exec(frame);
|
|
61
61
|
return m?.[1] ?? "";
|
|
62
62
|
}
|
|
63
|
+
const beforeFlush = [];
|
|
64
|
+
export function onTraceFlush(fn) {
|
|
65
|
+
if (!TRACE_ON)
|
|
66
|
+
return;
|
|
67
|
+
beforeFlush.push(fn);
|
|
68
|
+
if (!hooked) {
|
|
69
|
+
hooked = true;
|
|
70
|
+
process.once("exit", flush);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
63
73
|
function flush() {
|
|
74
|
+
for (const fn of beforeFlush.splice(0)) {
|
|
75
|
+
try {
|
|
76
|
+
fn();
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
64
80
|
const head = {
|
|
65
81
|
trace: "promigence-sdk",
|
|
66
82
|
version: VERSION,
|
package/dist/transport-worker.js
CHANGED
|
@@ -34,6 +34,11 @@ function start(init) {
|
|
|
34
34
|
}
|
|
35
35
|
catch { }
|
|
36
36
|
};
|
|
37
|
+
const ev = (n) => {
|
|
38
|
+
if (init.trace === true)
|
|
39
|
+
post({ t: "ev", n, hr: hrNow() });
|
|
40
|
+
};
|
|
41
|
+
ev("boot");
|
|
37
42
|
const live = () => sessions.filter((e) => !e.retired && !e.session.closed && !e.session.destroyed);
|
|
38
43
|
const end = (reason) => {
|
|
39
44
|
if (closed)
|
|
@@ -94,14 +99,14 @@ function start(init) {
|
|
|
94
99
|
catch {
|
|
95
100
|
context = undefined;
|
|
96
101
|
}
|
|
97
|
-
const connect = (address) => new Promise((resolve) => {
|
|
102
|
+
const connect = (address, k) => new Promise((resolve) => {
|
|
98
103
|
let done = false;
|
|
99
104
|
const finish = (e) => { if (!done) {
|
|
100
105
|
done = true;
|
|
101
106
|
clearTimeout(timer);
|
|
102
107
|
resolve(e);
|
|
103
108
|
} };
|
|
104
|
-
const timer = setTimeout(() => { try {
|
|
109
|
+
const timer = setTimeout(() => { ev(`timeout${k}`); try {
|
|
105
110
|
socket.destroy();
|
|
106
111
|
}
|
|
107
112
|
catch { } finish(null); }, init.connectTimeoutMs);
|
|
@@ -113,8 +118,11 @@ function start(init) {
|
|
|
113
118
|
checkServerIdentity: guardCertChain,
|
|
114
119
|
...(context === undefined ? {} : { secureContext: context }),
|
|
115
120
|
});
|
|
116
|
-
|
|
121
|
+
if (init.trace === true)
|
|
122
|
+
socket.once("connect", () => { ev(`tcp${k}`); });
|
|
123
|
+
socket.once("error", (e) => { ev(`fail${k} ${e?.code ?? "error"}`); finish(null); });
|
|
117
124
|
socket.once("secureConnect", () => {
|
|
125
|
+
ev(`tls${k}`);
|
|
118
126
|
if (socket.alpnProtocol !== "h2") {
|
|
119
127
|
socket.destroy();
|
|
120
128
|
finish(null);
|
|
@@ -167,14 +175,66 @@ function start(init) {
|
|
|
167
175
|
resolve(v4);
|
|
168
176
|
return;
|
|
169
177
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
const at = init.lookupAtMs.length > 0 ? init.lookupAtMs : [0];
|
|
179
|
+
const resolvers = [];
|
|
180
|
+
const timers = [];
|
|
181
|
+
let done = false;
|
|
182
|
+
let asked = 0;
|
|
183
|
+
let failed = 0;
|
|
184
|
+
const finish = (addrs) => {
|
|
185
|
+
if (done)
|
|
186
|
+
return;
|
|
187
|
+
done = true;
|
|
188
|
+
for (const t of timers)
|
|
189
|
+
clearTimeout(t);
|
|
190
|
+
for (const r of resolvers) {
|
|
191
|
+
try {
|
|
192
|
+
r.cancel();
|
|
193
|
+
}
|
|
194
|
+
catch { }
|
|
195
|
+
}
|
|
196
|
+
resolve(addrs);
|
|
197
|
+
};
|
|
198
|
+
const gaveUp = () => {
|
|
199
|
+
if (++failed < asked)
|
|
200
|
+
return;
|
|
201
|
+
if (asked < at.length)
|
|
202
|
+
ask();
|
|
203
|
+
else
|
|
204
|
+
finish([]);
|
|
205
|
+
};
|
|
206
|
+
const ask = () => {
|
|
207
|
+
if (done || asked >= at.length)
|
|
208
|
+
return;
|
|
209
|
+
const k = asked++;
|
|
210
|
+
try {
|
|
211
|
+
const r = new dns.Resolver({ timeout: init.lookupTimeoutMs, tries: init.lookupTries });
|
|
212
|
+
const list = Array.isArray(init.servers) && init.servers.length > 0 ? init.servers : r.getServers();
|
|
213
|
+
if (list.length > 0)
|
|
214
|
+
r.setServers(list.map((_, i) => list[(i + k) % list.length]));
|
|
215
|
+
resolvers.push(r);
|
|
216
|
+
ev(`lookup${k + 1}`);
|
|
217
|
+
r.resolve4(name, (err, addrs) => {
|
|
218
|
+
if (done)
|
|
219
|
+
return;
|
|
220
|
+
if (!err && Array.isArray(addrs) && addrs.length > 0) {
|
|
221
|
+
ev(`addrs${k + 1} ${addrs.length}`);
|
|
222
|
+
finish(addrs);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
ev(`nolookup${k + 1} ${err?.code ?? "empty"}`);
|
|
226
|
+
gaveUp();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
gaveUp();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
for (const ms of at) {
|
|
234
|
+
if (ms <= 0)
|
|
235
|
+
ask();
|
|
236
|
+
else
|
|
237
|
+
timers.push(setTimeout(ask, ms));
|
|
178
238
|
}
|
|
179
239
|
});
|
|
180
240
|
const limitOf = (e) => {
|
|
@@ -196,7 +256,7 @@ function start(init) {
|
|
|
196
256
|
}
|
|
197
257
|
const n = Math.max(1, init.sessions);
|
|
198
258
|
pendingConnects = n;
|
|
199
|
-
const tries = Array.from({ length: n }, (_, i) => connect(addrs[i % addrs.length]).then((e) => {
|
|
259
|
+
const tries = Array.from({ length: n }, (_, i) => connect(addrs[i % addrs.length], i).then((e) => {
|
|
200
260
|
pendingConnects--;
|
|
201
261
|
if (closed) {
|
|
202
262
|
if (e)
|
|
@@ -334,7 +394,7 @@ function start(init) {
|
|
|
334
394
|
return;
|
|
335
395
|
}
|
|
336
396
|
entry.active++;
|
|
337
|
-
|
|
397
|
+
Atomics.store(init.acks, req.mark, req.id);
|
|
338
398
|
const sent = init.trace !== true ? 0 : granted > 0 ? granted : hrNow();
|
|
339
399
|
let head = 0;
|
|
340
400
|
let status = 0;
|
|
@@ -443,11 +503,20 @@ function start(init) {
|
|
|
443
503
|
else
|
|
444
504
|
armIdle(init.idleMs);
|
|
445
505
|
};
|
|
506
|
+
let wedged = false;
|
|
446
507
|
port.on("message", (m) => {
|
|
447
|
-
if (closed)
|
|
508
|
+
if (closed || wedged)
|
|
509
|
+
return;
|
|
510
|
+
if (m?.t === "wedge-for-test") {
|
|
511
|
+
wedged = true;
|
|
448
512
|
return;
|
|
513
|
+
}
|
|
449
514
|
if (m?.t === "req")
|
|
450
515
|
void run(m);
|
|
516
|
+
else if (m?.t === "reqs") {
|
|
517
|
+
for (const r of m.list)
|
|
518
|
+
void run(r);
|
|
519
|
+
}
|
|
451
520
|
else if (m?.t === "drain")
|
|
452
521
|
drain("asked");
|
|
453
522
|
else if (m?.t === "cancel" && typeof m.id === "number") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promigence/sdk",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.14",
|
|
4
4
|
"description": "The fastest sandboxes for AI agents and code. The Promigence TypeScript SDK: snapshot an environment once, fork it as often as you need, run commands in every copy. A simple Sandbox class and the full PromigenceClient, with zero runtime dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://www.promigence.ai",
|