@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/client.js CHANGED
@@ -8,11 +8,11 @@ import { keychainReadCached, keychainStore, storeLabel } from "./credstore.js";
8
8
  import { CALLER_HEADER, resolveCaller, userAgentWithCaller } from "./caller.js";
9
9
  import { connectPty } from "./pty.js";
10
10
  import { connectSshTunnel } from "./ssh.js";
11
- import { H2Response, Http2Transport, http2Allowed, warmTls } from "./http2.js";
11
+ import { H2Response, http2Allowed, sharedTransport, warmTls } from "./http2.js";
12
12
  import { CONTEXT_TAR_PREFIX } from "./context.js";
13
13
  import { proxyFetch, proxyFor, transportEnv } from "./proxy.js";
14
14
  import { mark, traceBegin, traceEvent } from "./trace.js";
15
- import { KEYED_CREATES } from "./keyed.gen.js";
15
+ import { isKeyedCreate } from "./keyed.js";
16
16
  import { customDispatcher, longRequestDispatcher } from "./dispatcher.js";
17
17
  import { compromisedKeyCause, guardedFetchDispatcher } from "./ca-guard.js";
18
18
  import { hintArg } from "./hint.js";
@@ -262,15 +262,34 @@ export function keyOrigin(env = process.env) {
262
262
  return stored.store === "macos-keychain" ? "macOS Keychain" : stored.store === "secret-service" ? "Secret Service keyring" : "credentials file";
263
263
  return undefined;
264
264
  }
265
+ function nonEmpty(v) {
266
+ return v ? v : undefined;
267
+ }
268
+ function withV1(u) {
269
+ const t = u.replace(/\/+$/, "");
270
+ return /^https?:\/\/[^/?#]+$/i.test(t) ? `${t}/v1` : t;
271
+ }
272
+ let credsLastInputs;
273
+ let credsLast;
265
274
  function resolveCredentials(opts, env = process.env) {
266
- const set = (v) => (v ? v : undefined);
267
- const trim = (u) => {
268
- const t = u.replace(/\/+$/, "");
269
- return /^https?:\/\/[^/?#]+$/i.test(t) ? `${t}/v1` : t;
270
- };
275
+ const set = nonEmpty;
276
+ const trim = withV1;
271
277
  const optKey = opts.apiKey?.trim();
272
278
  const envKey = optKey === undefined ? set(env["PROMIGENCE_API_KEY"]?.trim()) : undefined;
273
279
  const envUrl = optKey !== undefined && opts.apiUrl !== undefined ? undefined : set(env["PROMIGENCE_API_URL"]);
280
+ const inputs = optKey === undefined && envKey === undefined
281
+ ? undefined
282
+ : `${optKey ?? "\0"}\n${envKey ?? "\0"}\n${envUrl ?? "\0"}\n${opts.apiUrl ?? "\0"}`;
283
+ if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined)
284
+ return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl };
285
+ const worked = resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl);
286
+ if (inputs !== undefined && worked.refusal === undefined) {
287
+ credsLastInputs = inputs;
288
+ credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl };
289
+ }
290
+ return worked;
291
+ }
292
+ function resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl) {
274
293
  const given = optKey !== undefined || envKey !== undefined;
275
294
  const { creds, store } = given
276
295
  ? { creds: {}, store: undefined }
@@ -671,28 +690,7 @@ export function errorFor(code, message, fix, extra = {}) {
671
690
  }));
672
691
  }
673
692
  }
674
- const KEYED_PATTERNS = KEYED_CREATES.map(([m, p]) => [m, p.split("/").filter(Boolean)]);
675
- const KEYED_METHODS = new Set(KEYED_CREATES.map(([m]) => m));
676
- export function isKeyedCreate(method, path) {
677
- if (!KEYED_METHODS.has(method))
678
- return false;
679
- const segs = (path.split("?")[0] ?? "").split("/").filter(Boolean);
680
- for (const [m, pat] of KEYED_PATTERNS) {
681
- if (m !== method || pat.length !== segs.length)
682
- continue;
683
- let all = true;
684
- for (let i = 0; i < pat.length; i++) {
685
- const s = pat[i];
686
- if (s.startsWith("{") ? (segs[i] ?? "").length === 0 : s !== segs[i]) {
687
- all = false;
688
- break;
689
- }
690
- }
691
- if (all)
692
- return true;
693
- }
694
- return false;
695
- }
693
+ export { isKeyedCreate };
696
694
  function commandTimeoutMs(v) {
697
695
  if (v === undefined)
698
696
  return undefined;
@@ -773,6 +771,12 @@ export function serverAppMs(header) {
773
771
  function headerOf(res, name) {
774
772
  return res instanceof H2Response ? res.header(name) : res.headers.get(name);
775
773
  }
774
+ function ownFetchDispatcherFor(timeoutMs) {
775
+ const guarded = customDispatcher() ? undefined : guardedFetchDispatcher();
776
+ if (guarded !== undefined)
777
+ return longRequestDispatcher(timeoutMs, guarded) ?? guarded;
778
+ return longRequestDispatcher(timeoutMs);
779
+ }
776
780
  export class PromigenceClient {
777
781
  apiUrl;
778
782
  #apiKey;
@@ -804,7 +808,7 @@ export class PromigenceClient {
804
808
  this.fetchImpl = opts.fetch ?? (proxy ? proxyFetch(proxy) : globalThis.fetch);
805
809
  this.ownFetch = opts.fetch === undefined && proxy === undefined;
806
810
  this.h2 = env !== undefined && this.apiUrl.startsWith("https:") && http2Allowed(opts.http2, env)
807
- ? new Http2Transport(this.fetchImpl)
811
+ ? sharedTransport(this.fetchImpl)
808
812
  : undefined;
809
813
  this.requestTimeoutMs = opts.requestTimeoutMs ?? 30_000;
810
814
  this.maxRetries = opts.maxRetries ?? 3;
@@ -903,11 +907,18 @@ export class PromigenceClient {
903
907
  return guardedFetchDispatcher();
904
908
  }
905
909
  dispatcherFor(timeoutMs) {
906
- const guarded = this.guardDispatcher();
907
- if (guarded !== undefined)
908
- return longRequestDispatcher(timeoutMs, guarded) ?? guarded;
910
+ if (this.ownFetch)
911
+ return ownFetchDispatcherFor(timeoutMs);
909
912
  return longRequestDispatcher(timeoutMs);
910
913
  }
914
+ async againKeyed(attempt, signal) {
915
+ if (attempt >= this.maxRetries || signal?.aborted)
916
+ return false;
917
+ this.stats.retries++;
918
+ const backoff = Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt);
919
+ await sleep(backoff * (0.5 + Math.random() * 0.5));
920
+ return true;
921
+ }
911
922
  async request(opts) {
912
923
  const tr = traceBegin(opts.method, opts.path);
913
924
  const url = `${this.apiUrl}${opts.path}`;
@@ -934,16 +945,8 @@ export class PromigenceClient {
934
945
  if (idempotencyKey)
935
946
  headers["idempotency-key"] = idempotencyKey;
936
947
  const replayable = opts.method === "GET" || opts.method === "DELETE" || (keyedCreate && Boolean(idempotencyKey));
937
- const again = async (attempt) => {
938
- if (!keyedCreate || !idempotencyKey || attempt >= this.maxRetries || opts.signal?.aborted)
939
- return false;
940
- this.stats.retries++;
941
- const backoff = Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt);
942
- await sleep(backoff * (0.5 + Math.random() * 0.5));
943
- return true;
944
- };
945
948
  const timeoutMs = opts.timeoutMs ?? this.requestTimeoutMs;
946
- const dispatcher = this.dispatcherFor(timeoutMs);
949
+ let dispatcher;
947
950
  let resentDelete = false;
948
951
  for (let attempt = 0;; attempt++) {
949
952
  let res;
@@ -956,8 +959,7 @@ export class PromigenceClient {
956
959
  init.signal = opts.signal;
957
960
  if (opts.body !== undefined)
958
961
  init.body = JSON.stringify(opts.body);
959
- if (dispatcher !== undefined)
960
- init.dispatcher = dispatcher;
962
+ init.dispatcherFor = ownFetchDispatcherFor;
961
963
  if (tr !== undefined) {
962
964
  init.trace = tr;
963
965
  mark(tr, "init");
@@ -973,7 +975,7 @@ export class PromigenceClient {
973
975
  headers,
974
976
  signal: signals.length === 1 ? signals[0] : AbortSignal.any(signals),
975
977
  ...(opts.body === undefined ? {} : { body: JSON.stringify(opts.body) }),
976
- ...(dispatcher === undefined ? {} : { dispatcher }),
978
+ ...((dispatcher ??= this.dispatcherFor(timeoutMs)) === undefined ? {} : { dispatcher }),
977
979
  };
978
980
  if (tr !== undefined)
979
981
  mark(tr, "init");
@@ -991,7 +993,7 @@ export class PromigenceClient {
991
993
  }
992
994
  catch (err) {
993
995
  const e = err;
994
- if (await again(attempt))
996
+ if (keyedCreate && idempotencyKey && await this.againKeyed(attempt, opts.signal))
995
997
  continue;
996
998
  const cutOff = e?.cause?.code === "UND_ERR_HEADERS_TIMEOUT" || e?.cause?.code === "UND_ERR_BODY_TIMEOUT";
997
999
  const timedOut = e?.name === "TimeoutError" || e?.name === "AbortError" || cutOff;
@@ -1302,316 +1304,332 @@ export class PromigenceClient {
1302
1304
  ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
1303
1305
  });
1304
1306
  }
1305
- templates = {
1306
- list: async (opts = {}) => {
1307
- const r = await this.request({
1307
+ group_templates() {
1308
+ return {
1309
+ list: async (opts = {}) => {
1310
+ const r = await this.request({
1311
+ method: "GET",
1312
+ path: "/templates",
1313
+ anonymous: true,
1314
+ ...(opts.signal ? { signal: opts.signal } : {}),
1315
+ });
1316
+ return r?.templates ?? [];
1317
+ },
1318
+ get: (name, opts = {}) => this.request({
1308
1319
  method: "GET",
1309
- path: "/templates",
1320
+ path: `/templates/${seg(name)}`,
1310
1321
  anonymous: true,
1311
1322
  ...(opts.signal ? { signal: opts.signal } : {}),
1312
- });
1313
- return r?.templates ?? [];
1314
- },
1315
- get: (name, opts = {}) => this.request({
1316
- method: "GET",
1317
- path: `/templates/${seg(name)}`,
1318
- anonymous: true,
1319
- ...(opts.signal ? { signal: opts.signal } : {}),
1320
- }),
1321
- };
1323
+ }),
1324
+ };
1325
+ }
1322
1326
  me(opts = {}) {
1323
1327
  return this.request({ method: "GET", path: "/me", ...(opts.signal ? { signal: opts.signal } : {}) });
1324
1328
  }
1325
1329
  outcomes(opts = {}) {
1326
1330
  return this.request({ method: "GET", path: "/me/outcomes?period=month", ...(opts.signal ? { signal: opts.signal } : {}) });
1327
1331
  }
1328
- billing = {
1329
- usage: () => this.request({ method: "GET", path: "/billing" }),
1330
- ledger: async (limit = 100) => (await this.billing.ledgerPage({ limit })).items,
1331
- ledgerPage: async (opts = {}) => {
1332
- const limit = opts.limit ?? 100;
1333
- const r = await this.request({ method: "GET", path: `/me/ledger?${pageQuery(limit, 1000, opts.cursor)}` });
1334
- return pageOf(r, "entries", limit);
1335
- },
1336
- historyPage: async (opts = {}) => {
1337
- const limit = opts.limit ?? 50;
1338
- const q = new URLSearchParams(pageQuery(limit, 100, opts.cursor));
1339
- if (opts.kind !== undefined && opts.kind !== "all")
1340
- q.set("kind", opts.kind);
1341
- const r = await this.request({ method: "GET", path: `/billing/history?${q.toString()}` });
1342
- return pageOf(r, "entries", limit);
1343
- },
1344
- checkout: () => this.request({ method: "POST", path: "/billing/checkout", body: {} }),
1345
- portal: () => this.request({ method: "POST", path: "/billing/portal", body: {} }),
1346
- referrals: () => this.request({ method: "GET", path: "/referrals" }),
1347
- applyReferral: (code) => this.request({ method: "POST", path: "/referrals/apply", body: { code } }),
1348
- };
1349
- keys = {
1350
- list: async (opts = {}) => {
1351
- const r = await this.request({ method: "GET", path: `/keys${opts.all ? "?all=1" : ""}` });
1352
- return r?.keys ?? [];
1353
- },
1354
- create: (opts = {}) => {
1355
- const body = { ...expiryFields(opts) };
1356
- if (opts.name !== undefined)
1357
- body["name"] = opts.name.slice(0, 64);
1358
- return this.request({ method: "POST", path: "/keys", body });
1359
- },
1360
- revoke: (keyId) => {
1361
- if (!keyId?.trim())
1362
- throw new InvalidArgumentError("keys.revoke needs a key id", "Pass a key_id from keys.list().");
1363
- return this.request({ method: "DELETE", path: `/keys/${encodeURIComponent(keyId)}` });
1364
- },
1365
- };
1366
- orgs = {
1367
- list: async () => {
1368
- const r = await this.request({ method: "GET", path: "/orgs" });
1369
- return r?.orgs ?? [];
1370
- },
1371
- setDefault: (org) => this.request({ method: "POST", path: `/orgs/${orgSeg(org)}/default`, body: {} }),
1372
- createKey: (org, opts = {}) => {
1373
- const body = { ...expiryFields(opts) };
1374
- if (opts.name !== undefined)
1375
- body["name"] = opts.name.slice(0, 64);
1376
- if (opts.project !== undefined)
1377
- body["project"] = opts.project;
1378
- return this.request({ method: "POST", path: `/orgs/${orgSeg(org)}/keys`, body });
1379
- },
1380
- members: async (org) => {
1381
- const r = await this.request({ method: "GET", path: `/orgs/${orgSeg(org)}/members` });
1382
- return r?.members ?? [];
1383
- },
1384
- invites: async (org) => {
1385
- const r = await this.request({ method: "GET", path: `/orgs/${orgSeg(org)}/invites` });
1386
- return r?.invites ?? [];
1387
- },
1388
- leave: (org) => this.request({ method: "DELETE", path: `/orgs/${orgSeg(org)}/members/me` }),
1389
- projects: {
1390
- list: async (org) => {
1391
- const r = await this.request({ method: "GET", path: `/orgs/${orgSeg(org)}/projects` });
1392
- return r?.projects ?? [];
1332
+ group_billing() {
1333
+ return {
1334
+ usage: () => this.request({ method: "GET", path: "/billing" }),
1335
+ ledger: async (limit = 100) => (await this.billing.ledgerPage({ limit })).items,
1336
+ ledgerPage: async (opts = {}) => {
1337
+ const limit = opts.limit ?? 100;
1338
+ const r = await this.request({ method: "GET", path: `/me/ledger?${pageQuery(limit, 1000, opts.cursor)}` });
1339
+ return pageOf(r, "entries", limit);
1340
+ },
1341
+ historyPage: async (opts = {}) => {
1342
+ const limit = opts.limit ?? 50;
1343
+ const q = new URLSearchParams(pageQuery(limit, 100, opts.cursor));
1344
+ if (opts.kind !== undefined && opts.kind !== "all")
1345
+ q.set("kind", opts.kind);
1346
+ const r = await this.request({ method: "GET", path: `/billing/history?${q.toString()}` });
1347
+ return pageOf(r, "entries", limit);
1348
+ },
1349
+ checkout: () => this.request({ method: "POST", path: "/billing/checkout", body: {} }),
1350
+ portal: () => this.request({ method: "POST", path: "/billing/portal", body: {} }),
1351
+ referrals: () => this.request({ method: "GET", path: "/referrals" }),
1352
+ applyReferral: (code) => this.request({ method: "POST", path: "/referrals/apply", body: { code } }),
1353
+ };
1354
+ }
1355
+ group_keys() {
1356
+ return {
1357
+ list: async (opts = {}) => {
1358
+ const r = await this.request({ method: "GET", path: `/keys${opts.all ? "?all=1" : ""}` });
1359
+ return r?.keys ?? [];
1360
+ },
1361
+ create: (opts = {}) => {
1362
+ const body = { ...expiryFields(opts) };
1363
+ if (opts.name !== undefined)
1364
+ body["name"] = opts.name.slice(0, 64);
1365
+ return this.request({ method: "POST", path: "/keys", body });
1366
+ },
1367
+ revoke: (keyId) => {
1368
+ if (!keyId?.trim())
1369
+ throw new InvalidArgumentError("keys.revoke needs a key id", "Pass a key_id from keys.list().");
1370
+ return this.request({ method: "DELETE", path: `/keys/${encodeURIComponent(keyId)}` });
1371
+ },
1372
+ };
1373
+ }
1374
+ group_orgs() {
1375
+ return {
1376
+ list: async () => {
1377
+ const r = await this.request({ method: "GET", path: "/orgs" });
1378
+ return r?.orgs ?? [];
1379
+ },
1380
+ setDefault: (org) => this.request({ method: "POST", path: `/orgs/${orgSeg(org)}/default`, body: {} }),
1381
+ createKey: (org, opts = {}) => {
1382
+ const body = { ...expiryFields(opts) };
1383
+ if (opts.name !== undefined)
1384
+ body["name"] = opts.name.slice(0, 64);
1385
+ if (opts.project !== undefined)
1386
+ body["project"] = opts.project;
1387
+ return this.request({ method: "POST", path: `/orgs/${orgSeg(org)}/keys`, body });
1393
1388
  },
1394
- create: (org, name) => this.request({ method: "POST", path: `/orgs/${orgSeg(org)}/projects`, body: { name } }),
1395
- rename: (org, project, name) => this.request({
1396
- method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: { name },
1389
+ members: async (org) => {
1390
+ const r = await this.request({ method: "GET", path: `/orgs/${orgSeg(org)}/members` });
1391
+ return r?.members ?? [];
1392
+ },
1393
+ invites: async (org) => {
1394
+ const r = await this.request({ method: "GET", path: `/orgs/${orgSeg(org)}/invites` });
1395
+ return r?.invites ?? [];
1396
+ },
1397
+ leave: (org) => this.request({ method: "DELETE", path: `/orgs/${orgSeg(org)}/members/me` }),
1398
+ projects: {
1399
+ list: async (org) => {
1400
+ const r = await this.request({ method: "GET", path: `/orgs/${orgSeg(org)}/projects` });
1401
+ return r?.projects ?? [];
1402
+ },
1403
+ create: (org, name) => this.request({ method: "POST", path: `/orgs/${orgSeg(org)}/projects`, body: { name } }),
1404
+ rename: (org, project, name) => this.request({
1405
+ method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: { name },
1406
+ }),
1407
+ },
1408
+ };
1409
+ }
1410
+ group_credits() {
1411
+ return {
1412
+ offer: () => this.request({ method: "GET", path: "/credits/offer" }),
1413
+ };
1414
+ }
1415
+ group_github() {
1416
+ return {
1417
+ status: () => this.request({ method: "GET", path: "/integrations/github" }),
1418
+ connect: () => this.request({ method: "POST", path: "/integrations/github/connect", body: {} }),
1419
+ ticket: () => this.request({ method: "POST", path: "/integrations/github/requests", body: {} }),
1420
+ request: (requestId) => this.request({
1421
+ method: "GET",
1422
+ path: `/integrations/github/requests/${encodeURIComponent(requestId)}`,
1397
1423
  }),
1398
- },
1399
- };
1400
- credits = {
1401
- offer: () => this.request({ method: "GET", path: "/credits/offer" }),
1402
- };
1403
- github = {
1404
- status: () => this.request({ method: "GET", path: "/integrations/github" }),
1405
- connect: () => this.request({ method: "POST", path: "/integrations/github/connect", body: {} }),
1406
- ticket: () => this.request({ method: "POST", path: "/integrations/github/requests", body: {} }),
1407
- request: (requestId) => this.request({
1408
- method: "GET",
1409
- path: `/integrations/github/requests/${encodeURIComponent(requestId)}`,
1410
- }),
1411
- disconnect: (installationId) => this.request({
1412
- method: "DELETE",
1413
- path: `/integrations/github/installations/${encodeURIComponent(String(installationId))}`,
1414
- }),
1415
- repos: (opts = {}) => {
1416
- const q = new URLSearchParams();
1417
- if (opts.page !== undefined)
1418
- q.set("page", String(Math.max(1, Math.floor(opts.page))));
1419
- if (opts.perPage !== undefined)
1420
- q.set("per_page", String(Math.min(100, Math.max(1, Math.floor(opts.perPage)))));
1421
- const qs = q.toString();
1422
- return this.request({ method: "GET", path: `/integrations/github/repos${qs ? `?${qs}` : ""}` });
1423
- },
1424
- untrack: (trackId) => this.request({ method: "DELETE", path: `/integrations/github/tracks/${encodeURIComponent(trackId)}` }),
1425
- };
1426
- gitlab = {
1427
- status: () => this.request({ method: "GET", path: "/integrations/gitlab" }),
1428
- connect: () => this.request({ method: "POST", path: "/integrations/gitlab/connect", body: {} }),
1429
- ticket: () => this.request({ method: "POST", path: "/integrations/gitlab/requests", body: {} }),
1430
- request: (requestId) => this.request({
1431
- method: "GET",
1432
- path: `/integrations/gitlab/requests/${encodeURIComponent(requestId)}`,
1433
- }),
1434
- disconnect: (connectionId) => this.request({
1435
- method: "DELETE",
1436
- path: `/integrations/gitlab/connections/${encodeURIComponent(connectionId)}`,
1437
- }),
1438
- repos: (opts = {}) => {
1439
- const q = new URLSearchParams();
1440
- if (opts.page !== undefined)
1441
- q.set("page", String(Math.max(1, Math.floor(opts.page))));
1442
- if (opts.perPage !== undefined)
1443
- q.set("per_page", String(Math.min(100, Math.max(1, Math.floor(opts.perPage)))));
1444
- if (opts.connectionId !== undefined)
1445
- q.set("connection_id", opts.connectionId);
1446
- const qs = q.toString();
1447
- return this.request({ method: "GET", path: `/integrations/gitlab/repos${qs ? `?${qs}` : ""}` });
1448
- },
1449
- untrack: (trackId) => this.request({ method: "DELETE", path: `/integrations/gitlab/tracks/${encodeURIComponent(trackId)}` }),
1450
- };
1451
- runs = {
1452
- start: (opts) => {
1453
- if (!opts.snapshot)
1454
- throw new InvalidArgumentError("runs.start needs a snapshot", "Pass { snapshot: 'snap_… | alias | alias@commit7' }.");
1455
- if (!Number.isInteger(opts.count) || opts.count < 1)
1456
- throw new InvalidArgumentError("runs.start needs count >= 1", "Pass { count: 1 } or more.");
1457
- const body = { snapshot: opts.snapshot, count: opts.count };
1458
- if (opts.timeoutMs !== undefined)
1459
- body["timeout_ms"] = opts.timeoutMs;
1460
- if (opts.spendCap !== undefined)
1461
- body["spend_cap"] = checkSpendCap(opts.spendCap);
1462
- if (opts.allowPartial !== undefined)
1463
- body["allow_partial"] = opts.allowPartial;
1464
- if (opts.verifyEach !== undefined)
1465
- body["verify_each"] = opts.verifyEach;
1466
- if (opts.network !== undefined)
1467
- body["network"] = opts.network;
1468
- if (opts.optimizeFor !== undefined)
1469
- body["optimize_for"] = opts.optimizeFor;
1470
- if (opts.secrets?.length)
1471
- body["secrets"] = opts.secrets;
1472
- if (opts.labels && Object.keys(opts.labels).length)
1473
- body["labels"] = opts.labels;
1474
- if (opts.exec) {
1475
- const e = {};
1476
- if (opts.exec.argv)
1477
- e["argv"] = opts.exec.argv;
1478
- if (opts.exec.sh)
1479
- e["sh"] = opts.exec.sh;
1480
- if (opts.exec.cwd)
1481
- e["cwd"] = opts.exec.cwd;
1482
- if (opts.exec.env)
1483
- e["env"] = opts.exec.env;
1484
- const t = commandTimeoutMs(opts.exec.timeoutMs);
1485
- if (t !== undefined)
1486
- e["timeout_ms"] = t;
1487
- if (opts.exec.maxOutputBytes !== undefined)
1488
- e["max_output_bytes"] = opts.exec.maxOutputBytes;
1489
- e["kill_on_exit"] = opts.exec.killOnExit ?? true;
1490
- body["exec"] = e;
1491
- }
1492
- if (opts.grade) {
1493
- if (!opts.grade.cmd)
1494
- throw new InvalidArgumentError("runs.start grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
1495
- const g = { cmd: opts.grade.cmd };
1496
- if (opts.grade.holdoutCmd)
1497
- g["holdout_cmd"] = opts.grade.holdoutCmd;
1498
- if (opts.grade.timeoutMs !== undefined)
1499
- g["timeout_ms"] = opts.grade.timeoutMs;
1500
- if (opts.grade.score !== undefined)
1501
- g["score"] = opts.grade.score;
1502
- body["grade"] = g;
1503
- }
1504
- if (opts.gradeOutput !== undefined)
1505
- body["grade_output"] = opts.gradeOutput;
1506
- const waitMs = opts.waitMs !== undefined && opts.waitMs > 0 ? Math.min(opts.waitMs, WAIT_MS_CAP) : 0;
1507
- const req = { method: "POST", path: `/runs${waitQuery(waitMs)}`, body };
1508
- if (waitMs > 0)
1509
- req.timeoutMs = waitMs + this.requestTimeoutMs;
1510
- if (opts.idempotencyKey)
1511
- req.idempotencyKey = opts.idempotencyKey;
1512
- if (opts.signal)
1513
- req.signal = opts.signal;
1514
- return this.request(req);
1515
- },
1516
- get: (runId, opts = {}) => this.request({
1517
- method: "GET",
1518
- path: `/runs/${encodeURIComponent(runId)}${waitQuery(opts.waitMs)}`,
1519
- ...this.readExtras(opts),
1520
- }),
1521
- list: async (limit = 20) => (await this.runs.listPage({ limit })).items,
1522
- listPage: async (opts = {}) => {
1523
- const limit = opts.limit ?? 20;
1524
- const r = await this.request({ method: "GET", path: `/runs?${pageQuery(limit, 500, opts.cursor)}` });
1525
- return pageOf(r, "runs", limit);
1526
- },
1527
- status: (runId, opts = {}) => this.runs.get(runId, opts),
1528
- sandboxes: async (runId, opts = {}) => {
1529
- const r = await this.runs.sandboxesWithCapacity(runId, opts);
1530
- return r.sandboxes;
1531
- },
1532
- sandboxesWithCapacity: async (runId, opts = {}) => {
1533
- const r = await this.request({
1424
+ disconnect: (installationId) => this.request({
1425
+ method: "DELETE",
1426
+ path: `/integrations/github/installations/${encodeURIComponent(String(installationId))}`,
1427
+ }),
1428
+ repos: (opts = {}) => {
1429
+ const q = new URLSearchParams();
1430
+ if (opts.page !== undefined)
1431
+ q.set("page", String(Math.max(1, Math.floor(opts.page))));
1432
+ if (opts.perPage !== undefined)
1433
+ q.set("per_page", String(Math.min(100, Math.max(1, Math.floor(opts.perPage)))));
1434
+ const qs = q.toString();
1435
+ return this.request({ method: "GET", path: `/integrations/github/repos${qs ? `?${qs}` : ""}` });
1436
+ },
1437
+ untrack: (trackId) => this.request({ method: "DELETE", path: `/integrations/github/tracks/${encodeURIComponent(trackId)}` }),
1438
+ };
1439
+ }
1440
+ group_gitlab() {
1441
+ return {
1442
+ status: () => this.request({ method: "GET", path: "/integrations/gitlab" }),
1443
+ connect: () => this.request({ method: "POST", path: "/integrations/gitlab/connect", body: {} }),
1444
+ ticket: () => this.request({ method: "POST", path: "/integrations/gitlab/requests", body: {} }),
1445
+ request: (requestId) => this.request({
1446
+ method: "GET",
1447
+ path: `/integrations/gitlab/requests/${encodeURIComponent(requestId)}`,
1448
+ }),
1449
+ disconnect: (connectionId) => this.request({
1450
+ method: "DELETE",
1451
+ path: `/integrations/gitlab/connections/${encodeURIComponent(connectionId)}`,
1452
+ }),
1453
+ repos: (opts = {}) => {
1454
+ const q = new URLSearchParams();
1455
+ if (opts.page !== undefined)
1456
+ q.set("page", String(Math.max(1, Math.floor(opts.page))));
1457
+ if (opts.perPage !== undefined)
1458
+ q.set("per_page", String(Math.min(100, Math.max(1, Math.floor(opts.perPage)))));
1459
+ if (opts.connectionId !== undefined)
1460
+ q.set("connection_id", opts.connectionId);
1461
+ const qs = q.toString();
1462
+ return this.request({ method: "GET", path: `/integrations/gitlab/repos${qs ? `?${qs}` : ""}` });
1463
+ },
1464
+ untrack: (trackId) => this.request({ method: "DELETE", path: `/integrations/gitlab/tracks/${encodeURIComponent(trackId)}` }),
1465
+ };
1466
+ }
1467
+ group_runs() {
1468
+ return {
1469
+ start: (opts) => {
1470
+ if (!opts.snapshot)
1471
+ throw new InvalidArgumentError("runs.start needs a snapshot", "Pass { snapshot: 'snap_… | alias | alias@commit7' }.");
1472
+ if (!Number.isInteger(opts.count) || opts.count < 1)
1473
+ throw new InvalidArgumentError("runs.start needs count >= 1", "Pass { count: 1 } or more.");
1474
+ const body = { snapshot: opts.snapshot, count: opts.count };
1475
+ if (opts.timeoutMs !== undefined)
1476
+ body["timeout_ms"] = opts.timeoutMs;
1477
+ if (opts.spendCap !== undefined)
1478
+ body["spend_cap"] = checkSpendCap(opts.spendCap);
1479
+ if (opts.allowPartial !== undefined)
1480
+ body["allow_partial"] = opts.allowPartial;
1481
+ if (opts.verifyEach !== undefined)
1482
+ body["verify_each"] = opts.verifyEach;
1483
+ if (opts.network !== undefined)
1484
+ body["network"] = opts.network;
1485
+ if (opts.optimizeFor !== undefined)
1486
+ body["optimize_for"] = opts.optimizeFor;
1487
+ if (opts.secrets?.length)
1488
+ body["secrets"] = opts.secrets;
1489
+ if (opts.labels && Object.keys(opts.labels).length)
1490
+ body["labels"] = opts.labels;
1491
+ if (opts.exec) {
1492
+ const e = {};
1493
+ if (opts.exec.argv)
1494
+ e["argv"] = opts.exec.argv;
1495
+ if (opts.exec.sh)
1496
+ e["sh"] = opts.exec.sh;
1497
+ if (opts.exec.cwd)
1498
+ e["cwd"] = opts.exec.cwd;
1499
+ if (opts.exec.env)
1500
+ e["env"] = opts.exec.env;
1501
+ const t = commandTimeoutMs(opts.exec.timeoutMs);
1502
+ if (t !== undefined)
1503
+ e["timeout_ms"] = t;
1504
+ if (opts.exec.maxOutputBytes !== undefined)
1505
+ e["max_output_bytes"] = opts.exec.maxOutputBytes;
1506
+ e["kill_on_exit"] = opts.exec.killOnExit ?? true;
1507
+ body["exec"] = e;
1508
+ }
1509
+ if (opts.grade) {
1510
+ if (!opts.grade.cmd)
1511
+ throw new InvalidArgumentError("runs.start grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
1512
+ const g = { cmd: opts.grade.cmd };
1513
+ if (opts.grade.holdoutCmd)
1514
+ g["holdout_cmd"] = opts.grade.holdoutCmd;
1515
+ if (opts.grade.timeoutMs !== undefined)
1516
+ g["timeout_ms"] = opts.grade.timeoutMs;
1517
+ if (opts.grade.score !== undefined)
1518
+ g["score"] = opts.grade.score;
1519
+ body["grade"] = g;
1520
+ }
1521
+ if (opts.gradeOutput !== undefined)
1522
+ body["grade_output"] = opts.gradeOutput;
1523
+ const waitMs = opts.waitMs !== undefined && opts.waitMs > 0 ? Math.min(opts.waitMs, WAIT_MS_CAP) : 0;
1524
+ const req = { method: "POST", path: `/runs${waitQuery(waitMs)}`, body };
1525
+ if (waitMs > 0)
1526
+ req.timeoutMs = waitMs + this.requestTimeoutMs;
1527
+ if (opts.idempotencyKey)
1528
+ req.idempotencyKey = opts.idempotencyKey;
1529
+ if (opts.signal)
1530
+ req.signal = opts.signal;
1531
+ return this.request(req);
1532
+ },
1533
+ get: (runId, opts = {}) => this.request({
1534
1534
  method: "GET",
1535
- path: `/runs/${encodeURIComponent(runId)}/sandboxes${waitQuery(opts.waitMs)}`,
1535
+ path: `/runs/${encodeURIComponent(runId)}${waitQuery(opts.waitMs)}`,
1536
1536
  ...this.readExtras(opts),
1537
- });
1538
- if (Array.isArray(r))
1539
- return { sandboxes: r };
1540
- const sandboxes = r?.sandboxes ?? [];
1541
- return r?.capacity ? { sandboxes, capacity: r.capacity } : { sandboxes };
1542
- },
1543
- report: (runId) => this.request({ method: "GET", path: `/runs/${encodeURIComponent(runId)}/report` }),
1544
- cancel: (runId) => this.request({ method: "POST", path: `/runs/${encodeURIComponent(runId)}/cancel`, body: {} }),
1545
- kill: (runId) => this.request({ method: "DELETE", path: `/runs/${encodeURIComponent(runId)}` }),
1546
- quote: (opts) => {
1547
- if (!opts.snapshot)
1548
- throw new InvalidArgumentError("runs.quote needs a snapshot", "Pass { snapshot: 'snap_… | alias | alias@commit7' }.");
1549
- if (!Number.isInteger(opts.count) || opts.count < 1)
1550
- throw new InvalidArgumentError("runs.quote needs count >= 1", "Pass { count: 1 } or more.");
1551
- const body = { snapshot: opts.snapshot, count: opts.count };
1552
- if (opts.timeoutMs !== undefined)
1553
- body["timeout_ms"] = opts.timeoutMs;
1554
- if (opts.tier !== undefined)
1555
- body["tier"] = opts.tier;
1556
- if (opts.spendCap !== undefined)
1557
- body["spend_cap"] = checkSpendCap(opts.spendCap);
1558
- return this.request({ method: "POST", path: "/runs/quote", body });
1559
- },
1560
- replay: (runId, sandboxIds, opts = {}) => this.request({
1561
- method: "POST",
1562
- path: `/runs/${encodeURIComponent(runId)}/replay`,
1563
- body: {
1564
- ...(sandboxIds?.length ? { sandbox_ids: sandboxIds } : {}),
1565
- ...(opts.env && Object.keys(opts.env).length ? { env: opts.env } : {}),
1537
+ }),
1538
+ list: async (limit = 20) => (await this.runs.listPage({ limit })).items,
1539
+ listPage: async (opts = {}) => {
1540
+ const limit = opts.limit ?? 20;
1541
+ const r = await this.request({ method: "GET", path: `/runs?${pageQuery(limit, 500, opts.cursor)}` });
1542
+ return pageOf(r, "runs", limit);
1566
1543
  },
1567
- }),
1568
- captures: (runId) => this.request({
1569
- method: "GET",
1570
- path: `/runs/${encodeURIComponent(runId)}/captures`,
1571
- }),
1572
- open: (runId, opts = {}) => {
1573
- const body = {};
1574
- if (opts.sandboxId !== undefined)
1575
- body["sandbox_id"] = opts.sandboxId;
1576
- if (opts.spendCap !== undefined)
1577
- body["spend_cap"] = opts.spendCap;
1578
- if (opts.timeoutMs !== undefined)
1579
- body["timeout_ms"] = opts.timeoutMs;
1580
- return this.request({
1544
+ status: (runId, opts = {}) => this.runs.get(runId, opts),
1545
+ sandboxes: async (runId, opts = {}) => {
1546
+ const r = await this.runs.sandboxesWithCapacity(runId, opts);
1547
+ return r.sandboxes;
1548
+ },
1549
+ sandboxesWithCapacity: async (runId, opts = {}) => {
1550
+ const r = await this.request({
1551
+ method: "GET",
1552
+ path: `/runs/${encodeURIComponent(runId)}/sandboxes${waitQuery(opts.waitMs)}`,
1553
+ ...this.readExtras(opts),
1554
+ });
1555
+ if (Array.isArray(r))
1556
+ return { sandboxes: r };
1557
+ const sandboxes = r?.sandboxes ?? [];
1558
+ return r?.capacity ? { sandboxes, capacity: r.capacity } : { sandboxes };
1559
+ },
1560
+ report: (runId) => this.request({ method: "GET", path: `/runs/${encodeURIComponent(runId)}/report` }),
1561
+ cancel: (runId) => this.request({ method: "POST", path: `/runs/${encodeURIComponent(runId)}/cancel`, body: {} }),
1562
+ kill: (runId) => this.request({ method: "DELETE", path: `/runs/${encodeURIComponent(runId)}` }),
1563
+ quote: (opts) => {
1564
+ if (!opts.snapshot)
1565
+ throw new InvalidArgumentError("runs.quote needs a snapshot", "Pass { snapshot: 'snap_… | alias | alias@commit7' }.");
1566
+ if (!Number.isInteger(opts.count) || opts.count < 1)
1567
+ throw new InvalidArgumentError("runs.quote needs count >= 1", "Pass { count: 1 } or more.");
1568
+ const body = { snapshot: opts.snapshot, count: opts.count };
1569
+ if (opts.timeoutMs !== undefined)
1570
+ body["timeout_ms"] = opts.timeoutMs;
1571
+ if (opts.tier !== undefined)
1572
+ body["tier"] = opts.tier;
1573
+ if (opts.spendCap !== undefined)
1574
+ body["spend_cap"] = checkSpendCap(opts.spendCap);
1575
+ return this.request({ method: "POST", path: "/runs/quote", body });
1576
+ },
1577
+ replay: (runId, sandboxIds, opts = {}) => this.request({
1581
1578
  method: "POST",
1582
- path: `/runs/${encodeURIComponent(runId)}/open`,
1583
- body,
1584
- });
1585
- },
1586
- waitReady: async (runId, opts = {}) => {
1587
- const seen = new Set();
1588
- const progress = opts.onReady !== undefined || opts.onCapacity !== undefined;
1589
- const r = await this.waitLoop(opts, progress ? PROGRESS_POLL_MS : LONG_POLL_MS, [], async (read) => {
1590
- const { sandboxes, capacity } = await this.runs.sandboxesWithCapacity(runId, read);
1591
- if (capacity)
1592
- opts.onCapacity?.(capacity);
1593
- return sandboxes;
1594
- }, (sandboxes) => {
1595
- for (const s of sandboxes) {
1596
- if (!isWaiting(s.state) && !seen.has(s.sandbox_id)) {
1597
- seen.add(s.sandbox_id);
1598
- opts.onReady?.(s);
1579
+ path: `/runs/${encodeURIComponent(runId)}/replay`,
1580
+ body: {
1581
+ ...(sandboxIds?.length ? { sandbox_ids: sandboxIds } : {}),
1582
+ ...(opts.env && Object.keys(opts.env).length ? { env: opts.env } : {}),
1583
+ },
1584
+ }),
1585
+ captures: (runId) => this.request({
1586
+ method: "GET",
1587
+ path: `/runs/${encodeURIComponent(runId)}/captures`,
1588
+ }),
1589
+ open: (runId, opts = {}) => {
1590
+ const body = {};
1591
+ if (opts.sandboxId !== undefined)
1592
+ body["sandbox_id"] = opts.sandboxId;
1593
+ if (opts.spendCap !== undefined)
1594
+ body["spend_cap"] = opts.spendCap;
1595
+ if (opts.timeoutMs !== undefined)
1596
+ body["timeout_ms"] = opts.timeoutMs;
1597
+ return this.request({
1598
+ method: "POST",
1599
+ path: `/runs/${encodeURIComponent(runId)}/open`,
1600
+ body,
1601
+ });
1602
+ },
1603
+ waitReady: async (runId, opts = {}) => {
1604
+ const seen = new Set();
1605
+ const progress = opts.onReady !== undefined || opts.onCapacity !== undefined;
1606
+ const r = await this.waitLoop(opts, progress ? PROGRESS_POLL_MS : LONG_POLL_MS, [], async (read) => {
1607
+ const { sandboxes, capacity } = await this.runs.sandboxesWithCapacity(runId, read);
1608
+ if (capacity)
1609
+ opts.onCapacity?.(capacity);
1610
+ return sandboxes;
1611
+ }, (sandboxes) => {
1612
+ for (const s of sandboxes) {
1613
+ if (!isWaiting(s.state) && !seen.has(s.sandbox_id)) {
1614
+ seen.add(s.sandbox_id);
1615
+ opts.onReady?.(s);
1616
+ }
1599
1617
  }
1600
- }
1601
- return sandboxes.length > 0 && sandboxes.every((s) => !isWaiting(s.state));
1602
- });
1603
- return { sandboxes: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
1604
- },
1605
- waitDone: async (runId, opts = {}) => {
1606
- const r = await this.waitLoop(opts, LONG_POLL_MS, { run_id: runId, state: "running" }, (read) => this.runs.get(runId, read), (run) => {
1607
- const terminal = run.state === "completed" || run.state === "cap_killed" || run.state === "aborted";
1608
- const allDone = Array.isArray(run.sandboxes) && run.sandboxes.length > 0 &&
1609
- run.sandboxes.every((s) => s.state === "ended" || s.command_outcome != null);
1610
- return terminal || allDone;
1611
- });
1612
- return { run: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
1613
- },
1614
- };
1618
+ return sandboxes.length > 0 && sandboxes.every((s) => !isWaiting(s.state));
1619
+ });
1620
+ return { sandboxes: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
1621
+ },
1622
+ waitDone: async (runId, opts = {}) => {
1623
+ const r = await this.waitLoop(opts, LONG_POLL_MS, { run_id: runId, state: "running" }, (read) => this.runs.get(runId, read), (run) => {
1624
+ const terminal = run.state === "completed" || run.state === "cap_killed" || run.state === "aborted";
1625
+ const allDone = Array.isArray(run.sandboxes) && run.sandboxes.length > 0 &&
1626
+ run.sandboxes.every((s) => s.state === "ended" || s.command_outcome != null);
1627
+ return terminal || allDone;
1628
+ });
1629
+ return { run: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
1630
+ },
1631
+ };
1632
+ }
1615
1633
  async waitLoop(opts, maxWaitMs, empty, read, done) {
1616
1634
  const bound = opts.waitTimeoutMs ?? WAIT_TIMEOUT_MS;
1617
1635
  const started = Date.now();
@@ -1665,720 +1683,722 @@ export class PromigenceClient {
1665
1683
  await sleep(Math.min(opts.pollMs ?? pollInterval(i), Math.max(1, bound - elapsed)));
1666
1684
  }
1667
1685
  }
1668
- sandboxes = {
1669
- exec: (sandboxId, opts) => {
1670
- if (!opts.argv?.length && !opts.sh) {
1671
- throw new InvalidArgumentError("exec needs argv or sh", "Pass { argv: ['bun','typecheck'] } or { sh: 'bun typecheck' }.");
1672
- }
1673
- const body = {};
1674
- if (opts.argv?.length)
1675
- body["argv"] = opts.argv;
1676
- if (opts.sh)
1677
- body["sh"] = opts.sh;
1678
- if (opts.cwd)
1679
- body["cwd"] = opts.cwd;
1680
- if (opts.env)
1681
- body["env"] = opts.env;
1682
- if (opts.user)
1683
- body["user"] = opts.user;
1684
- if (opts.secrets?.length)
1685
- body["secrets"] = opts.secrets;
1686
- if (opts.terminal)
1687
- body["terminal"] = true;
1688
- if (opts.killOnExit !== undefined)
1689
- body["kill_on_exit"] = opts.killOnExit;
1690
- const timeoutMs = commandTimeoutMs(opts.timeoutMs) ?? 15 * 60_000;
1691
- body["timeout_ms"] = timeoutMs;
1692
- body["max_output_bytes"] = opts.maxOutputBytes ?? MAX_OUTPUT_BYTES_DEFAULT;
1693
- if (opts.encoding === "base64")
1694
- body["encoding"] = "base64";
1695
- const req = {
1696
- method: "POST",
1697
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs`,
1698
- body,
1699
- timeoutMs: timeoutMs + 30_000,
1700
- };
1701
- if (opts.idempotencyKey)
1702
- req.idempotencyKey = opts.idempotencyKey;
1703
- if (opts.signal)
1704
- req.signal = opts.signal;
1705
- return this.request(req);
1706
- },
1707
- execStream: async (sandboxId, opts, on = {}) => {
1708
- if (!opts.argv?.length && !opts.sh) {
1709
- throw new InvalidArgumentError("exec needs argv or sh", "Pass { argv: ['bun','typecheck'] } or { sh: 'bun typecheck' }.");
1710
- }
1711
- if (opts.idempotencyKey) {
1712
- throw new InvalidArgumentError("a streamed exec cannot take an idempotencyKey (its output cannot be replayed)", "Use sandboxes.exec (buffered) or sandboxes.execBackground with the key, or stream without one.");
1713
- }
1714
- const body = {};
1715
- if (opts.argv?.length)
1716
- body["argv"] = opts.argv;
1717
- if (opts.sh)
1718
- body["sh"] = opts.sh;
1719
- if (opts.cwd)
1720
- body["cwd"] = opts.cwd;
1721
- if (opts.env)
1722
- body["env"] = opts.env;
1723
- if (opts.user)
1724
- body["user"] = opts.user;
1725
- if (opts.secrets?.length)
1726
- body["secrets"] = opts.secrets;
1727
- if (opts.terminal)
1728
- body["terminal"] = true;
1729
- if (opts.killOnExit !== undefined)
1730
- body["kill_on_exit"] = opts.killOnExit;
1731
- body["timeout_ms"] = commandTimeoutMs(opts.timeoutMs) ?? 15 * 60_000;
1732
- body["max_output_bytes"] = opts.maxOutputBytes ?? MAX_OUTPUT_BYTES_DEFAULT;
1733
- const url = `${this.apiUrl}/sandboxes/${encodeURIComponent(sandboxId)}/execs`;
1734
- const idle = new AbortController();
1735
- let silent = false;
1736
- let reader;
1737
- let timer;
1738
- const arm = () => {
1739
- clearTimeout(timer);
1740
- timer = setTimeout(() => {
1741
- silent = true;
1742
- idle.abort();
1743
- reader?.cancel().catch(() => { });
1744
- }, STREAM_IDLE_MS);
1745
- };
1746
- let stream;
1747
- const bearer = `Bearer ${this.requireKey()}`;
1748
- const dispatcher = this.guardDispatcher();
1749
- for (let attempt = 0; stream === undefined; attempt++) {
1750
- let r;
1751
- this.stats.requests++;
1752
- arm();
1753
- try {
1754
- r = await this.fetchImpl(url, {
1755
- method: "POST",
1756
- headers: {
1757
- accept: "text/event-stream",
1758
- "content-type": "application/json",
1759
- "user-agent": this.userAgent,
1760
- authorization: bearer,
1761
- ...(this.caller ? { [CALLER_HEADER]: this.caller } : {}),
1762
- },
1763
- body: JSON.stringify(body),
1764
- signal: opts.signal ? AbortSignal.any([opts.signal, idle.signal]) : idle.signal,
1765
- ...(dispatcher === undefined ? {} : { dispatcher }),
1766
- });
1767
- }
1768
- catch (err) {
1769
- clearTimeout(timer);
1770
- const e = err;
1771
- const why = silent ? `no answer in ${STREAM_IDLE_MS / 1000} s` : transportDetail(e);
1772
- throw new UnreachableError(`cannot reach ${url}: ${why}`);
1773
- }
1774
- if (r.ok && r.body) {
1775
- stream = r.body;
1776
- break;
1686
+ group_sandboxes() {
1687
+ return {
1688
+ exec: (sandboxId, opts) => {
1689
+ if (!opts.argv?.length && !opts.sh) {
1690
+ throw new InvalidArgumentError("exec needs argv or sh", "Pass { argv: ['bun','typecheck'] } or { sh: 'bun typecheck' }.");
1777
1691
  }
1778
- clearTimeout(timer);
1779
- const text = await r.text().catch(() => "");
1780
- let payload = {};
1781
- try {
1782
- payload = text ? JSON.parse(text) : {};
1692
+ const body = {};
1693
+ if (opts.argv?.length)
1694
+ body["argv"] = opts.argv;
1695
+ if (opts.sh)
1696
+ body["sh"] = opts.sh;
1697
+ if (opts.cwd)
1698
+ body["cwd"] = opts.cwd;
1699
+ if (opts.env)
1700
+ body["env"] = opts.env;
1701
+ if (opts.user)
1702
+ body["user"] = opts.user;
1703
+ if (opts.secrets?.length)
1704
+ body["secrets"] = opts.secrets;
1705
+ if (opts.terminal)
1706
+ body["terminal"] = true;
1707
+ if (opts.killOnExit !== undefined)
1708
+ body["kill_on_exit"] = opts.killOnExit;
1709
+ const timeoutMs = commandTimeoutMs(opts.timeoutMs) ?? 15 * 60_000;
1710
+ body["timeout_ms"] = timeoutMs;
1711
+ body["max_output_bytes"] = opts.maxOutputBytes ?? MAX_OUTPUT_BYTES_DEFAULT;
1712
+ if (opts.encoding === "base64")
1713
+ body["encoding"] = "base64";
1714
+ const req = {
1715
+ method: "POST",
1716
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs`,
1717
+ body,
1718
+ timeoutMs: timeoutMs + 30_000,
1719
+ };
1720
+ if (opts.idempotencyKey)
1721
+ req.idempotencyKey = opts.idempotencyKey;
1722
+ if (opts.signal)
1723
+ req.signal = opts.signal;
1724
+ return this.request(req);
1725
+ },
1726
+ execStream: async (sandboxId, opts, on = {}) => {
1727
+ if (!opts.argv?.length && !opts.sh) {
1728
+ throw new InvalidArgumentError("exec needs argv or sh", "Pass { argv: ['bun','typecheck'] } or { sh: 'bun typecheck' }.");
1783
1729
  }
1784
- catch { }
1785
- const retryAfterMs = parseRetryAfter(r.headers.get("retry-after"));
1786
- const wait = this.refusalBackoff(r.status, retryAfterMs, payload.error?.code, attempt, false, opts.signal);
1787
- if (wait !== undefined) {
1788
- this.stats.retries++;
1789
- await sleep(wait);
1790
- continue;
1730
+ if (opts.idempotencyKey) {
1731
+ throw new InvalidArgumentError("a streamed exec cannot take an idempotencyKey (its output cannot be replayed)", "Use sandboxes.exec (buffered) or sandboxes.execBackground with the key, or stream without one.");
1791
1732
  }
1792
- const code = payload.error?.code ?? codeForStatus(r.status);
1793
- throw errorFor(code, payload.error?.message ?? `HTTP ${r.status} from POST /sandboxes/${sandboxId}/execs`, payload.error?.fix ?? "See the API response for details.", {
1794
- status: r.status,
1795
- ...bodyExtra(payload),
1796
- ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
1797
- });
1798
- }
1799
- const out = [];
1800
- const errOut = [];
1801
- let exit;
1802
- let failure;
1803
- const dec = new TextDecoder();
1804
- let buf = "";
1805
- const lost = (how) => new UnreachableError(`the exec stream for sandbox ${sandboxId} ${how}`, "The connection dropped before the command finished; its result is unknown. " +
1806
- `Read it by id (\`promigence exec output <exec_id> --sandbox ${hintArg(sandboxId)}\`) or re-run.`);
1807
- reader = stream.getReader();
1808
- try {
1809
- for (;;) {
1733
+ const body = {};
1734
+ if (opts.argv?.length)
1735
+ body["argv"] = opts.argv;
1736
+ if (opts.sh)
1737
+ body["sh"] = opts.sh;
1738
+ if (opts.cwd)
1739
+ body["cwd"] = opts.cwd;
1740
+ if (opts.env)
1741
+ body["env"] = opts.env;
1742
+ if (opts.user)
1743
+ body["user"] = opts.user;
1744
+ if (opts.secrets?.length)
1745
+ body["secrets"] = opts.secrets;
1746
+ if (opts.terminal)
1747
+ body["terminal"] = true;
1748
+ if (opts.killOnExit !== undefined)
1749
+ body["kill_on_exit"] = opts.killOnExit;
1750
+ body["timeout_ms"] = commandTimeoutMs(opts.timeoutMs) ?? 15 * 60_000;
1751
+ body["max_output_bytes"] = opts.maxOutputBytes ?? MAX_OUTPUT_BYTES_DEFAULT;
1752
+ const url = `${this.apiUrl}/sandboxes/${encodeURIComponent(sandboxId)}/execs`;
1753
+ const idle = new AbortController();
1754
+ let silent = false;
1755
+ let reader;
1756
+ let timer;
1757
+ const arm = () => {
1758
+ clearTimeout(timer);
1759
+ timer = setTimeout(() => {
1760
+ silent = true;
1761
+ idle.abort();
1762
+ reader?.cancel().catch(() => { });
1763
+ }, STREAM_IDLE_MS);
1764
+ };
1765
+ let stream;
1766
+ const bearer = `Bearer ${this.requireKey()}`;
1767
+ const dispatcher = this.guardDispatcher();
1768
+ for (let attempt = 0; stream === undefined; attempt++) {
1769
+ let r;
1770
+ this.stats.requests++;
1810
1771
  arm();
1811
- let chunk;
1812
1772
  try {
1813
- chunk = await reader.read();
1773
+ r = await this.fetchImpl(url, {
1774
+ method: "POST",
1775
+ headers: {
1776
+ accept: "text/event-stream",
1777
+ "content-type": "application/json",
1778
+ "user-agent": this.userAgent,
1779
+ authorization: bearer,
1780
+ ...(this.caller ? { [CALLER_HEADER]: this.caller } : {}),
1781
+ },
1782
+ body: JSON.stringify(body),
1783
+ signal: opts.signal ? AbortSignal.any([opts.signal, idle.signal]) : idle.signal,
1784
+ ...(dispatcher === undefined ? {} : { dispatcher }),
1785
+ });
1814
1786
  }
1815
1787
  catch (err) {
1816
- if (opts.signal?.aborted)
1817
- throw err;
1818
- if (silent)
1819
- throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
1788
+ clearTimeout(timer);
1820
1789
  const e = err;
1821
- throw lost(`was cut off (${e?.cause?.code ?? e?.message ?? "connection lost"})`);
1790
+ const why = silent ? `no answer in ${STREAM_IDLE_MS / 1000} s` : transportDetail(e);
1791
+ throw new UnreachableError(`cannot reach ${url}: ${why}`);
1822
1792
  }
1823
- if (chunk.done)
1793
+ if (r.ok && r.body) {
1794
+ stream = r.body;
1824
1795
  break;
1825
- const value = chunk.value;
1826
- buf += dec.decode(value, { stream: true });
1827
- let sep;
1828
- while ((sep = buf.indexOf("\n\n")) >= 0) {
1829
- const block = buf.slice(0, sep);
1830
- buf = buf.slice(sep + 2);
1831
- let event = "";
1832
- let data = "";
1833
- for (const line of block.split("\n")) {
1834
- if (line.startsWith("event:"))
1835
- event = line.slice(6).trim();
1836
- else if (line.startsWith("data:"))
1837
- data = line.slice(5).replace(/^ /, "");
1838
- }
1839
- if (!event)
1840
- continue;
1841
- if (event === "heartbeat") {
1842
- on.heartbeat?.();
1843
- continue;
1844
- }
1845
- if (event === "reconnect") {
1846
- reader.cancel().catch(() => { });
1847
- throw new ServiceRestartError(`a service restart stopped the command on sandbox ${sandboxId} while its output was streaming`, "Not billed: the time the command ran is credited back. The sandbox kept running; the command may have done part of its work before it stopped: run it again if that is safe.");
1848
- }
1849
- let parsed = {};
1796
+ }
1797
+ clearTimeout(timer);
1798
+ const text = await r.text().catch(() => "");
1799
+ let payload = {};
1800
+ try {
1801
+ payload = text ? JSON.parse(text) : {};
1802
+ }
1803
+ catch { }
1804
+ const retryAfterMs = parseRetryAfter(r.headers.get("retry-after"));
1805
+ const wait = this.refusalBackoff(r.status, retryAfterMs, payload.error?.code, attempt, false, opts.signal);
1806
+ if (wait !== undefined) {
1807
+ this.stats.retries++;
1808
+ await sleep(wait);
1809
+ continue;
1810
+ }
1811
+ const code = payload.error?.code ?? codeForStatus(r.status);
1812
+ throw errorFor(code, payload.error?.message ?? `HTTP ${r.status} from POST /sandboxes/${sandboxId}/execs`, payload.error?.fix ?? "See the API response for details.", {
1813
+ status: r.status,
1814
+ ...bodyExtra(payload),
1815
+ ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
1816
+ });
1817
+ }
1818
+ const out = [];
1819
+ const errOut = [];
1820
+ let exit;
1821
+ let failure;
1822
+ const dec = new TextDecoder();
1823
+ let buf = "";
1824
+ const lost = (how) => new UnreachableError(`the exec stream for sandbox ${sandboxId} ${how}`, "The connection dropped before the command finished; its result is unknown. " +
1825
+ `Read it by id (\`promigence exec output <exec_id> --sandbox ${hintArg(sandboxId)}\`) or re-run.`);
1826
+ reader = stream.getReader();
1827
+ try {
1828
+ for (;;) {
1829
+ arm();
1830
+ let chunk;
1850
1831
  try {
1851
- parsed = JSON.parse(data);
1832
+ chunk = await reader.read();
1852
1833
  }
1853
- catch { }
1854
- if (event === "stdout" || event === "stderr") {
1855
- const bytes = b64ToBytes(String(parsed["data"] ?? ""));
1856
- if (event === "stdout") {
1857
- out.push(bytes);
1858
- on.stdout?.(bytes);
1834
+ catch (err) {
1835
+ if (opts.signal?.aborted)
1836
+ throw err;
1837
+ if (silent)
1838
+ throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
1839
+ const e = err;
1840
+ throw lost(`was cut off (${e?.cause?.code ?? e?.message ?? "connection lost"})`);
1841
+ }
1842
+ if (chunk.done)
1843
+ break;
1844
+ const value = chunk.value;
1845
+ buf += dec.decode(value, { stream: true });
1846
+ let sep;
1847
+ while ((sep = buf.indexOf("\n\n")) >= 0) {
1848
+ const block = buf.slice(0, sep);
1849
+ buf = buf.slice(sep + 2);
1850
+ let event = "";
1851
+ let data = "";
1852
+ for (const line of block.split("\n")) {
1853
+ if (line.startsWith("event:"))
1854
+ event = line.slice(6).trim();
1855
+ else if (line.startsWith("data:"))
1856
+ data = line.slice(5).replace(/^ /, "");
1859
1857
  }
1860
- else {
1861
- errOut.push(bytes);
1862
- on.stderr?.(bytes);
1858
+ if (!event)
1859
+ continue;
1860
+ if (event === "heartbeat") {
1861
+ on.heartbeat?.();
1862
+ continue;
1863
+ }
1864
+ if (event === "reconnect") {
1865
+ reader.cancel().catch(() => { });
1866
+ throw new ServiceRestartError(`a service restart stopped the command on sandbox ${sandboxId} while its output was streaming`, "Not billed: the time the command ran is credited back. The sandbox kept running; the command may have done part of its work before it stopped: run it again if that is safe.");
1867
+ }
1868
+ let parsed = {};
1869
+ try {
1870
+ parsed = JSON.parse(data);
1871
+ }
1872
+ catch { }
1873
+ if (event === "stdout" || event === "stderr") {
1874
+ const bytes = b64ToBytes(String(parsed["data"] ?? ""));
1875
+ if (event === "stdout") {
1876
+ out.push(bytes);
1877
+ on.stdout?.(bytes);
1878
+ }
1879
+ else {
1880
+ errOut.push(bytes);
1881
+ on.stderr?.(bytes);
1882
+ }
1883
+ }
1884
+ else if (event === "exit") {
1885
+ exit = { ...(exit ?? {}), ...parsed };
1886
+ }
1887
+ else if (event === "error") {
1888
+ failure = parsed;
1863
1889
  }
1864
- }
1865
- else if (event === "exit") {
1866
- exit = { ...(exit ?? {}), ...parsed };
1867
- }
1868
- else if (event === "error") {
1869
- failure = parsed;
1870
1890
  }
1871
1891
  }
1872
1892
  }
1873
- }
1874
- finally {
1875
- clearTimeout(timer);
1876
- }
1877
- if (failure) {
1878
- throw errorFor(failure.code ?? "internal", failure.message ?? "the exec stream failed", failure.fix ?? "Re-run; if it repeats, include the exec_id.", { status: 200, ...bodyExtra({ error: failure }) });
1879
- }
1880
- if (!exit && silent)
1881
- throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
1882
- if (!exit) {
1883
- throw lost("ended without an exit event");
1884
- }
1885
- const rawCode = exit["exit_code"];
1886
- const signal = exit["signal"];
1887
- const exitCode = typeof rawCode === "number" && Number.isFinite(rawCode)
1888
- ? rawCode
1889
- : typeof signal === "number" && signal > 0 ? 128 + signal : null;
1890
- const join = (parts) => {
1891
- const n = parts.reduce((a, p) => a + p.length, 0);
1892
- const all = new Uint8Array(n);
1893
- let at = 0;
1894
- for (const p of parts) {
1895
- all.set(p, at);
1896
- at += p.length;
1893
+ finally {
1894
+ clearTimeout(timer);
1897
1895
  }
1898
- return new TextDecoder().decode(all);
1899
- };
1900
- return {
1901
- exec_id: String(exit["exec_id"] ?? ""),
1902
- sandbox_id: sandboxId,
1903
- exit_code: exitCode,
1904
- stdout: join(out),
1905
- stderr: join(errOut),
1906
- truncated: Boolean(exit["truncated"] ?? false),
1907
- ...(exit["timed_out"] === true ? { timed_out: true } : {}),
1908
- ...(exit["oom_killed"] === true ? { oom_killed: true } : {}),
1909
- ...(typeof exit["reason"] === "string" ? { reason: exit["reason"] } : {}),
1910
- ...(typeof signal === "number" ? { signal } : {}),
1911
- };
1912
- },
1913
- execBackground: (sandboxId, opts) => {
1914
- if (!opts.argv?.length && !opts.sh) {
1915
- throw new InvalidArgumentError("exec needs argv or sh", "Pass { argv: ['npm','run','dev'] } or { sh: 'npm run dev' }.");
1916
- }
1917
- const body = { background: true };
1918
- if (opts.argv?.length)
1919
- body["argv"] = opts.argv;
1920
- if (opts.sh)
1921
- body["sh"] = opts.sh;
1922
- if (opts.cwd)
1923
- body["cwd"] = opts.cwd;
1924
- if (opts.env)
1925
- body["env"] = opts.env;
1926
- if (opts.user)
1927
- body["user"] = opts.user;
1928
- if (opts.secrets?.length)
1929
- body["secrets"] = opts.secrets;
1930
- if (opts.stdin)
1931
- body["stdin"] = true;
1932
- if (opts.follow)
1933
- body["follow"] = true;
1934
- const timeoutMs = commandTimeoutMs(opts.timeoutMs);
1935
- if (timeoutMs !== undefined)
1936
- body["timeout_ms"] = timeoutMs;
1937
- if (opts.maxOutputBytes !== undefined)
1938
- body["max_output_bytes"] = opts.maxOutputBytes;
1939
- return this.request({
1940
- method: "POST",
1941
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs`,
1942
- body,
1943
- ...(opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
1944
- });
1945
- },
1946
- execs: {
1947
- list: async (sandboxId) => {
1948
- const r = await this.request({
1949
- method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs`,
1950
- });
1951
- return r.execs ?? [];
1952
- },
1953
- get: (sandboxId, execId, opts = {}) => {
1954
- const wait = opts.waitMs !== undefined && opts.waitMs > 0 ? Math.min(Math.floor(opts.waitMs), WAIT_MS_CAP) : 0;
1955
- return this.request({
1956
- method: "GET",
1957
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}${wait ? `?wait_ms=${wait}` : ""}`,
1958
- ...(wait ? { timeoutMs: wait + this.requestTimeoutMs } : {}),
1959
- ...(opts.signal ? { signal: opts.signal } : {}),
1960
- });
1961
- },
1962
- read: async (sandboxId, execId, stream, opts = {}) => {
1963
- const q = [`stream=${stream}`, `offset=${Math.max(0, Math.floor(opts.offset ?? 0))}`];
1964
- if (opts.len !== undefined)
1965
- q.push(`len=${Math.max(0, Math.floor(opts.len))}`);
1966
- const wait = opts.waitMs !== undefined && opts.waitMs > 0 ? Math.min(Math.floor(opts.waitMs), WAIT_MS_CAP) : 0;
1967
- if (wait)
1968
- q.push(`wait_ms=${wait}`);
1969
- if (wait && opts.untilOutput)
1970
- q.push("until=output");
1971
- const r = await this.request({
1972
- method: "GET",
1973
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}?${q.join("&")}`,
1974
- ...(wait ? { timeoutMs: wait + this.requestTimeoutMs } : {}),
1975
- ...(opts.signal ? { signal: opts.signal } : {}),
1976
- });
1977
- return { data: b64ToBytes(r.data ?? ""), offset: r.offset ?? opts.offset ?? 0, eof: Boolean(r.eof), skipped: r.skipped ?? 0, record: r };
1978
- },
1979
- stdin: (sandboxId, execId, opts = {}) => {
1980
- if (opts.data === undefined && !opts.eof) {
1981
- throw new InvalidArgumentError("execs.stdin needs data, eof, or both", "Pass { data: 'y\\n' } to write, { eof: true } to close, or both to do each in one call.");
1896
+ if (failure) {
1897
+ throw errorFor(failure.code ?? "internal", failure.message ?? "the exec stream failed", failure.fix ?? "Re-run; if it repeats, include the exec_id.", { status: 200, ...bodyExtra({ error: failure }) });
1982
1898
  }
1983
- const bytes = typeof opts.data === "string" ? new TextEncoder().encode(opts.data) : opts.data;
1984
- return this.request({
1985
- method: "POST",
1986
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}/stdin`,
1987
- body: {
1988
- ...(bytes === undefined ? {} : { data: bytesToB64(bytes) }),
1989
- ...(opts.eof ? { eof: true } : {}),
1990
- },
1991
- });
1992
- },
1993
- signal: (sandboxId, execId, signal = "SIGTERM") => this.request({
1994
- method: "POST",
1995
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}/signal`,
1996
- body: { signal },
1997
- }),
1998
- forget: (sandboxId, execId) => this.request({
1999
- method: "DELETE",
2000
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}`,
2001
- }),
2002
- },
2003
- ports: {
2004
- expose: (sandboxId, port) => {
2005
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
2006
- throw new InvalidArgumentError(`port must be an integer 1..65535, got ${port}`, "Pass the port your process listens on inside the sandbox, e.g. 3000.");
1899
+ if (!exit && silent)
1900
+ throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
1901
+ if (!exit) {
1902
+ throw lost("ended without an exit event");
2007
1903
  }
2008
- return withPreviewsFix(this.request({
2009
- method: "POST", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`, body: { port },
2010
- }), sandboxId, port);
2011
- },
2012
- list: async (sandboxId) => {
2013
- const r = await withPreviewsFix(this.request({
2014
- method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`,
2015
- }), sandboxId);
2016
- return r.ports ?? [];
1904
+ const rawCode = exit["exit_code"];
1905
+ const signal = exit["signal"];
1906
+ const exitCode = typeof rawCode === "number" && Number.isFinite(rawCode)
1907
+ ? rawCode
1908
+ : typeof signal === "number" && signal > 0 ? 128 + signal : null;
1909
+ const join = (parts) => {
1910
+ const n = parts.reduce((a, p) => a + p.length, 0);
1911
+ const all = new Uint8Array(n);
1912
+ let at = 0;
1913
+ for (const p of parts) {
1914
+ all.set(p, at);
1915
+ at += p.length;
1916
+ }
1917
+ return new TextDecoder().decode(all);
1918
+ };
1919
+ return {
1920
+ exec_id: String(exit["exec_id"] ?? ""),
1921
+ sandbox_id: sandboxId,
1922
+ exit_code: exitCode,
1923
+ stdout: join(out),
1924
+ stderr: join(errOut),
1925
+ truncated: Boolean(exit["truncated"] ?? false),
1926
+ ...(exit["timed_out"] === true ? { timed_out: true } : {}),
1927
+ ...(exit["oom_killed"] === true ? { oom_killed: true } : {}),
1928
+ ...(typeof exit["reason"] === "string" ? { reason: exit["reason"] } : {}),
1929
+ ...(typeof signal === "number" ? { signal } : {}),
1930
+ };
2017
1931
  },
2018
- revoke: (sandboxId, tokenOrPort) => this.request({
2019
- method: "DELETE",
2020
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${encodeURIComponent(String(tokenOrPort))}`,
2021
- }),
2022
- },
2023
- ssh: {
2024
- open: (sandboxId, opts) => {
2025
- if (!opts.publicKey?.trim()) {
2026
- throw new InvalidArgumentError("publicKey is required", "Pass the contents of an ssh-ed25519 .pub file, or use `promigence sandbox ssh <id>`, which makes one.");
1932
+ execBackground: (sandboxId, opts) => {
1933
+ if (!opts.argv?.length && !opts.sh) {
1934
+ throw new InvalidArgumentError("exec needs argv or sh", "Pass { argv: ['npm','run','dev'] } or { sh: 'npm run dev' }.");
2027
1935
  }
1936
+ const body = { background: true };
1937
+ if (opts.argv?.length)
1938
+ body["argv"] = opts.argv;
1939
+ if (opts.sh)
1940
+ body["sh"] = opts.sh;
1941
+ if (opts.cwd)
1942
+ body["cwd"] = opts.cwd;
1943
+ if (opts.env)
1944
+ body["env"] = opts.env;
1945
+ if (opts.user)
1946
+ body["user"] = opts.user;
1947
+ if (opts.secrets?.length)
1948
+ body["secrets"] = opts.secrets;
1949
+ if (opts.stdin)
1950
+ body["stdin"] = true;
1951
+ if (opts.follow)
1952
+ body["follow"] = true;
1953
+ const timeoutMs = commandTimeoutMs(opts.timeoutMs);
1954
+ if (timeoutMs !== undefined)
1955
+ body["timeout_ms"] = timeoutMs;
1956
+ if (opts.maxOutputBytes !== undefined)
1957
+ body["max_output_bytes"] = opts.maxOutputBytes;
2028
1958
  return this.request({
2029
1959
  method: "POST",
2030
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/ssh`,
2031
- body: {
2032
- public_key: opts.publicKey.trim(),
2033
- ...(opts.ttlS === undefined ? {} : { ttl_s: Math.floor(opts.ttlS) }),
2034
- ...(opts.idleTimeoutS === undefined
2035
- ? {}
2036
- : { idle_timeout_s: Math.floor(opts.idleTimeoutS) }),
2037
- },
1960
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs`,
1961
+ body,
1962
+ ...(opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
2038
1963
  });
2039
1964
  },
2040
- list: (sandboxId) => this.request({
2041
- method: "GET",
2042
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/ssh`,
2043
- }),
2044
- revoke: (sandboxId, keyIdOrFingerprint) => this.request({
2045
- method: "DELETE",
2046
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/ssh/${encodeURIComponent(keyIdOrFingerprint)}`,
2047
- }),
2048
- connect: (sandboxId, opts) => connectSshTunnel({
2049
- url: `${this.apiUrl}/sandboxes/${encodeURIComponent(sandboxId)}/ssh/connect`,
2050
- token: opts.token ?? this.requireKey(),
2051
- userAgent: this.userAgent,
2052
- ...(this.caller ? { caller: this.caller } : {}),
2053
- onData: opts.onData,
2054
- onClose: opts.onClose,
2055
- ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
2056
- refuse: (status, text) => {
2057
- let payload = {};
2058
- try {
2059
- payload = text ? JSON.parse(text) : {};
2060
- }
2061
- catch { }
2062
- const code = payload.error?.code ?? (status === 0 ? "unreachable" : codeForStatus(status));
2063
- return errorFor(code, payload.error?.message ?? `HTTP ${status} from the SSH tunnel`, payload.error?.fix ?? "See the API response for details.", { status, ...bodyExtra(payload) });
1965
+ execs: {
1966
+ list: async (sandboxId) => {
1967
+ const r = await this.request({
1968
+ method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs`,
1969
+ });
1970
+ return r.execs ?? [];
2064
1971
  },
2065
- }),
2066
- },
2067
- pty: (sandboxId, opts = {}) => {
2068
- const q = [];
2069
- if (opts.cols !== undefined)
2070
- q.push(`cols=${Math.floor(opts.cols)}`);
2071
- if (opts.rows !== undefined)
2072
- q.push(`rows=${Math.floor(opts.rows)}`);
2073
- const path = `/sandboxes/${encodeURIComponent(sandboxId)}/pty${q.length ? `?${q.join("&")}` : ""}`;
2074
- const open = () => connectPty({
2075
- url: `${this.apiUrl}${path}`,
2076
- apiKey: this.requireKey(),
2077
- userAgent: this.userAgent,
2078
- ...(this.caller ? { caller: this.caller } : {}),
2079
- ...(opts.cmd ? { command: opts.cmd } : {}),
2080
- ...(opts.secrets?.length ? { secrets: opts.secrets } : {}),
2081
- ...(opts.onData ? { onData: opts.onData } : {}),
2082
- ...(opts.onExit ? { onExit: opts.onExit } : {}),
2083
- ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
2084
- ...(opts.resume === undefined ? {} : { resume: opts.resume }),
2085
- ...(opts.reconnect === undefined ? {} : { reconnect: opts.reconnect }),
2086
- ...(opts.onReconnected ? { onReconnected: opts.onReconnected } : {}),
2087
- refuse: (status, text, retryAfter) => {
2088
- let payload = {};
2089
- try {
2090
- payload = text ? JSON.parse(text) : {};
2091
- }
2092
- catch { }
2093
- const code = payload.error?.code ?? (status === 0 ? "unreachable" : codeForStatus(status));
2094
- const retryAfterMs = parseRetryAfter(retryAfter ?? null);
2095
- return errorFor(code, payload.error?.message ?? `HTTP ${status} from GET ${path}`, payload.error?.fix ?? "See the API response for details.", {
2096
- status,
2097
- ...bodyExtra(payload),
2098
- ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
1972
+ get: (sandboxId, execId, opts = {}) => {
1973
+ const wait = opts.waitMs !== undefined && opts.waitMs > 0 ? Math.min(Math.floor(opts.waitMs), WAIT_MS_CAP) : 0;
1974
+ return this.request({
1975
+ method: "GET",
1976
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}${wait ? `?wait_ms=${wait}` : ""}`,
1977
+ ...(wait ? { timeoutMs: wait + this.requestTimeoutMs } : {}),
1978
+ ...(opts.signal ? { signal: opts.signal } : {}),
2099
1979
  });
2100
1980
  },
2101
- });
2102
- return (async () => {
2103
- for (let attempt = 0;; attempt++) {
2104
- try {
2105
- return await open();
2106
- }
2107
- catch (err) {
2108
- const e = err instanceof PromigenceError ? err : undefined;
2109
- const wait = e ? this.refusalBackoff(e.status, e.retryAfterMs, e.code, attempt, false) : undefined;
2110
- if (wait === undefined)
2111
- throw err;
2112
- this.stats.retries++;
2113
- await sleep(wait);
1981
+ read: async (sandboxId, execId, stream, opts = {}) => {
1982
+ const q = [`stream=${stream}`, `offset=${Math.max(0, Math.floor(opts.offset ?? 0))}`];
1983
+ if (opts.len !== undefined)
1984
+ q.push(`len=${Math.max(0, Math.floor(opts.len))}`);
1985
+ const wait = opts.waitMs !== undefined && opts.waitMs > 0 ? Math.min(Math.floor(opts.waitMs), WAIT_MS_CAP) : 0;
1986
+ if (wait)
1987
+ q.push(`wait_ms=${wait}`);
1988
+ if (wait && opts.untilOutput)
1989
+ q.push("until=output");
1990
+ const r = await this.request({
1991
+ method: "GET",
1992
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}?${q.join("&")}`,
1993
+ ...(wait ? { timeoutMs: wait + this.requestTimeoutMs } : {}),
1994
+ ...(opts.signal ? { signal: opts.signal } : {}),
1995
+ });
1996
+ return { data: b64ToBytes(r.data ?? ""), offset: r.offset ?? opts.offset ?? 0, eof: Boolean(r.eof), skipped: r.skipped ?? 0, record: r };
1997
+ },
1998
+ stdin: (sandboxId, execId, opts = {}) => {
1999
+ if (opts.data === undefined && !opts.eof) {
2000
+ throw new InvalidArgumentError("execs.stdin needs data, eof, or both", "Pass { data: 'y\\n' } to write, { eof: true } to close, or both to do each in one call.");
2114
2001
  }
2115
- }
2116
- })();
2117
- },
2118
- get: (sandboxId) => {
2119
- checkSandboxId(sandboxId, "sandboxes.get");
2120
- return this.request({ method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}` });
2121
- },
2122
- pause: (sandboxId) => this.request({
2123
- method: "POST",
2124
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/pause`,
2125
- body: {},
2126
- timeoutMs: 10 * 60_000,
2127
- }),
2128
- resume: (sandboxId, opts = {}) => this.request({
2129
- method: "POST",
2130
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/resume`,
2131
- body: {
2132
- ...(opts.optimizeFor === undefined ? {} : { optimize_for: opts.optimizeFor }),
2133
- ...(opts.spendCap === undefined ? {} : { spend_cap: checkSpendCap(opts.spendCap) }),
2002
+ const bytes = typeof opts.data === "string" ? new TextEncoder().encode(opts.data) : opts.data;
2003
+ return this.request({
2004
+ method: "POST",
2005
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}/stdin`,
2006
+ body: {
2007
+ ...(bytes === undefined ? {} : { data: bytesToB64(bytes) }),
2008
+ ...(opts.eof ? { eof: true } : {}),
2009
+ },
2010
+ });
2011
+ },
2012
+ signal: (sandboxId, execId, signal = "SIGTERM") => this.request({
2013
+ method: "POST",
2014
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}/signal`,
2015
+ body: { signal },
2016
+ }),
2017
+ forget: (sandboxId, execId) => this.request({
2018
+ method: "DELETE",
2019
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/${encodeURIComponent(execId)}`,
2020
+ }),
2134
2021
  },
2135
- }),
2136
- snapshot: (sandboxId, opts = {}) => this.request({
2137
- method: "POST",
2138
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/snapshot`,
2139
- body: {
2140
- ...(opts.alias === undefined ? {} : { alias: opts.alias }),
2141
- ...(opts.force === undefined ? {} : { force: opts.force }),
2142
- ...(opts.moveAlias === undefined ? {} : { move_alias: opts.moveAlias }),
2022
+ ports: {
2023
+ expose: (sandboxId, port) => {
2024
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
2025
+ throw new InvalidArgumentError(`port must be an integer 1..65535, got ${port}`, "Pass the port your process listens on inside the sandbox, e.g. 3000.");
2026
+ }
2027
+ return withPreviewsFix(this.request({
2028
+ method: "POST", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`, body: { port },
2029
+ }), sandboxId, port);
2030
+ },
2031
+ list: async (sandboxId) => {
2032
+ const r = await withPreviewsFix(this.request({
2033
+ method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`,
2034
+ }), sandboxId);
2035
+ return r.ports ?? [];
2036
+ },
2037
+ revoke: (sandboxId, tokenOrPort) => this.request({
2038
+ method: "DELETE",
2039
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${encodeURIComponent(String(tokenOrPort))}`,
2040
+ }),
2143
2041
  },
2144
- timeoutMs: 10 * 60_000,
2145
- }),
2146
- list: async (opts = 50) => (await this.sandboxes.listPage(opts)).items,
2147
- listPage: async (opts = 50) => {
2148
- const o = typeof opts === "number" ? { limit: opts } : opts;
2149
- const q = new URLSearchParams({ limit: String(Math.max(1, Math.trunc(o.limit ?? 50))) });
2150
- const pairs = Object.entries(o.labels ?? {});
2151
- if (pairs.length)
2152
- q.set("labels", pairs.map(([k, v]) => `${k}=${v}`).join(","));
2153
- if (o.state?.length)
2154
- q.set("state", o.state.join(","));
2155
- if (o.cursor)
2156
- q.set("cursor", o.cursor);
2157
- const r = await this.request({
2158
- method: "GET",
2159
- path: `/sandboxes?${q.toString()}`,
2160
- });
2161
- return { items: r.sandboxes ?? [], nextCursor: r.next_cursor ?? null, limit: r.limit ?? (o.limit ?? 50) };
2162
- },
2163
- probe: (sandboxId, opts = {}) => this.request({
2164
- method: "POST",
2165
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/probe`,
2166
- body: opts.paths?.length ? { paths: opts.paths } : {},
2167
- }),
2168
- kill: (sandboxId) => this.request({ method: "DELETE", path: `/sandboxes/${encodeURIComponent(sandboxId)}` }),
2169
- network: (sandboxId) => this.request({ method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}/network` }),
2170
- setNetworkNone: (sandboxId) => this.request({
2171
- method: "POST",
2172
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/network`,
2173
- body: { network: "none" },
2174
- }),
2175
- patch: (sandboxId, opts) => {
2176
- const body = {};
2177
- if (opts.timeoutMs !== undefined) {
2178
- if (!Number.isInteger(opts.timeoutMs) || opts.timeoutMs <= 0) {
2179
- throw new InvalidArgumentError("sandboxes.patch needs a positive timeoutMs", "Pass { timeoutMs: 1_800_000 }.");
2180
- }
2181
- body["timeout_ms"] = opts.timeoutMs;
2182
- }
2183
- if (opts.autoPauseS !== undefined) {
2184
- if (!Number.isInteger(opts.autoPauseS) || opts.autoPauseS < 0) {
2185
- throw new InvalidArgumentError("sandboxes.patch needs a non-negative whole number of seconds for autoPauseS", "Pass { autoPauseS: 900 } to pause it after 15 idle minutes, or { autoPauseS: 0 } to turn that off.");
2186
- }
2187
- body["auto_pause_s"] = opts.autoPauseS;
2188
- }
2189
- if (opts.autoPauseMode !== undefined) {
2190
- if (opts.autoPauseMode !== "requests" && opts.autoPauseMode !== "activity") {
2191
- throw new InvalidArgumentError(`sandboxes.patch: autoPauseMode must be "requests" or "activity", got ${JSON.stringify(opts.autoPauseMode)}`, 'Pass { autoPauseMode: "activity" } for an agent that runs inside the sandbox.');
2192
- }
2193
- body["auto_pause_mode"] = opts.autoPauseMode;
2194
- }
2195
- if (Object.keys(body).length === 0) {
2196
- throw new InvalidArgumentError("sandboxes.patch was given nothing to change", "Pass { timeoutMs } to extend it, { autoPauseS } to set its idle rule, or both.");
2197
- }
2198
- return this.request({
2199
- method: "PATCH",
2200
- path: `/sandboxes/${encodeURIComponent(sandboxId)}`,
2201
- body,
2202
- });
2203
- },
2204
- files: {
2205
- list: (sandboxId, path = "/", opts = {}) => {
2206
- const q = [`path=${encodeURIComponent(path)}`];
2207
- if (opts.recursive)
2208
- q.push("recursive=1");
2209
- if (opts.depth !== undefined)
2210
- q.push(`depth=${Math.max(0, Math.floor(opts.depth))}`);
2211
- if (opts.limit !== undefined)
2212
- q.push(`limit=${Math.max(1, Math.floor(opts.limit))}`);
2213
- if (opts.hidden === false)
2214
- q.push("hidden=0");
2215
- return this.request({
2042
+ ssh: {
2043
+ open: (sandboxId, opts) => {
2044
+ if (!opts.publicKey?.trim()) {
2045
+ throw new InvalidArgumentError("publicKey is required", "Pass the contents of an ssh-ed25519 .pub file, or use `promigence sandbox ssh <id>`, which makes one.");
2046
+ }
2047
+ return this.request({
2048
+ method: "POST",
2049
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/ssh`,
2050
+ body: {
2051
+ public_key: opts.publicKey.trim(),
2052
+ ...(opts.ttlS === undefined ? {} : { ttl_s: Math.floor(opts.ttlS) }),
2053
+ ...(opts.idleTimeoutS === undefined
2054
+ ? {}
2055
+ : { idle_timeout_s: Math.floor(opts.idleTimeoutS) }),
2056
+ },
2057
+ });
2058
+ },
2059
+ list: (sandboxId) => this.request({
2216
2060
  method: "GET",
2217
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/list?${q.join("&")}`,
2218
- });
2061
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/ssh`,
2062
+ }),
2063
+ revoke: (sandboxId, keyIdOrFingerprint) => this.request({
2064
+ method: "DELETE",
2065
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/ssh/${encodeURIComponent(keyIdOrFingerprint)}`,
2066
+ }),
2067
+ connect: (sandboxId, opts) => connectSshTunnel({
2068
+ url: `${this.apiUrl}/sandboxes/${encodeURIComponent(sandboxId)}/ssh/connect`,
2069
+ token: opts.token ?? this.requireKey(),
2070
+ userAgent: this.userAgent,
2071
+ ...(this.caller ? { caller: this.caller } : {}),
2072
+ onData: opts.onData,
2073
+ onClose: opts.onClose,
2074
+ ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
2075
+ refuse: (status, text) => {
2076
+ let payload = {};
2077
+ try {
2078
+ payload = text ? JSON.parse(text) : {};
2079
+ }
2080
+ catch { }
2081
+ const code = payload.error?.code ?? (status === 0 ? "unreachable" : codeForStatus(status));
2082
+ return errorFor(code, payload.error?.message ?? `HTTP ${status} from the SSH tunnel`, payload.error?.fix ?? "See the API response for details.", { status, ...bodyExtra(payload) });
2083
+ },
2084
+ }),
2219
2085
  },
2220
- search: (sandboxId, path, opts = {}) => {
2221
- if (!opts.pattern && !opts.contains) {
2222
- throw new InvalidArgumentError("files.search needs `pattern`, `contains`, or both", "Pass { pattern: '*.py' } for a name glob, { contains: 'TODO' } for text inside files.");
2223
- }
2224
- const q = [`path=${encodeURIComponent(path)}`];
2225
- if (opts.pattern)
2226
- q.push(`pattern=${encodeURIComponent(opts.pattern)}`);
2227
- if (opts.contains)
2228
- q.push(`contains=${encodeURIComponent(opts.contains)}`);
2229
- if (opts.ignoreCase)
2230
- q.push("ignore_case=1");
2231
- if (opts.limit !== undefined)
2232
- q.push(`limit=${Math.max(1, Math.floor(opts.limit))}`);
2233
- if (opts.hidden === false)
2234
- q.push("hidden=0");
2235
- return this.request({
2236
- method: "GET",
2237
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/search?${q.join("&")}`,
2086
+ pty: (sandboxId, opts = {}) => {
2087
+ const q = [];
2088
+ if (opts.cols !== undefined)
2089
+ q.push(`cols=${Math.floor(opts.cols)}`);
2090
+ if (opts.rows !== undefined)
2091
+ q.push(`rows=${Math.floor(opts.rows)}`);
2092
+ const path = `/sandboxes/${encodeURIComponent(sandboxId)}/pty${q.length ? `?${q.join("&")}` : ""}`;
2093
+ const open = () => connectPty({
2094
+ url: `${this.apiUrl}${path}`,
2095
+ apiKey: this.requireKey(),
2096
+ userAgent: this.userAgent,
2097
+ ...(this.caller ? { caller: this.caller } : {}),
2098
+ ...(opts.cmd ? { command: opts.cmd } : {}),
2099
+ ...(opts.secrets?.length ? { secrets: opts.secrets } : {}),
2100
+ ...(opts.onData ? { onData: opts.onData } : {}),
2101
+ ...(opts.onExit ? { onExit: opts.onExit } : {}),
2102
+ ...(opts.timeoutMs === undefined ? {} : { timeoutMs: opts.timeoutMs }),
2103
+ ...(opts.resume === undefined ? {} : { resume: opts.resume }),
2104
+ ...(opts.reconnect === undefined ? {} : { reconnect: opts.reconnect }),
2105
+ ...(opts.onReconnected ? { onReconnected: opts.onReconnected } : {}),
2106
+ refuse: (status, text, retryAfter) => {
2107
+ let payload = {};
2108
+ try {
2109
+ payload = text ? JSON.parse(text) : {};
2110
+ }
2111
+ catch { }
2112
+ const code = payload.error?.code ?? (status === 0 ? "unreachable" : codeForStatus(status));
2113
+ const retryAfterMs = parseRetryAfter(retryAfter ?? null);
2114
+ return errorFor(code, payload.error?.message ?? `HTTP ${status} from GET ${path}`, payload.error?.fix ?? "See the API response for details.", {
2115
+ status,
2116
+ ...bodyExtra(payload),
2117
+ ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
2118
+ });
2119
+ },
2238
2120
  });
2121
+ return (async () => {
2122
+ for (let attempt = 0;; attempt++) {
2123
+ try {
2124
+ return await open();
2125
+ }
2126
+ catch (err) {
2127
+ const e = err instanceof PromigenceError ? err : undefined;
2128
+ const wait = e ? this.refusalBackoff(e.status, e.retryAfterMs, e.code, attempt, false) : undefined;
2129
+ if (wait === undefined)
2130
+ throw err;
2131
+ this.stats.retries++;
2132
+ await sleep(wait);
2133
+ }
2134
+ }
2135
+ })();
2239
2136
  },
2240
- mkdir: (sandboxId, path, opts = {}) => this.request({
2137
+ get: (sandboxId) => {
2138
+ checkSandboxId(sandboxId, "sandboxes.get");
2139
+ return this.request({ method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}` });
2140
+ },
2141
+ pause: (sandboxId) => this.request({
2241
2142
  method: "POST",
2242
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/mkdir`,
2243
- body: {
2244
- path,
2245
- ...(opts.mode === undefined ? {} : { mode: modeParam(opts.mode) }),
2246
- ...(opts.parents === undefined ? {} : { parents: opts.parents }),
2247
- },
2143
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/pause`,
2144
+ body: {},
2145
+ timeoutMs: 10 * 60_000,
2248
2146
  }),
2249
- move: (sandboxId, from, to, opts = {}) => this.request({
2147
+ resume: (sandboxId, opts = {}) => this.request({
2250
2148
  method: "POST",
2251
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/move`,
2252
- body: { from, to, ...(opts.overwrite === undefined ? {} : { overwrite: opts.overwrite }) },
2149
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/resume`,
2150
+ body: {
2151
+ ...(opts.optimizeFor === undefined ? {} : { optimize_for: opts.optimizeFor }),
2152
+ ...(opts.spendCap === undefined ? {} : { spend_cap: checkSpendCap(opts.spendCap) }),
2153
+ },
2253
2154
  }),
2254
- copy: (sandboxId, from, to, opts = {}) => this.request({
2155
+ snapshot: (sandboxId, opts = {}) => this.request({
2255
2156
  method: "POST",
2256
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/copy`,
2157
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/snapshot`,
2257
2158
  body: {
2258
- from, to,
2259
- ...(opts.recursive === undefined ? {} : { recursive: opts.recursive }),
2260
- ...(opts.overwrite === undefined ? {} : { overwrite: opts.overwrite }),
2159
+ ...(opts.alias === undefined ? {} : { alias: opts.alias }),
2160
+ ...(opts.force === undefined ? {} : { force: opts.force }),
2161
+ ...(opts.moveAlias === undefined ? {} : { move_alias: opts.moveAlias }),
2261
2162
  },
2163
+ timeoutMs: 10 * 60_000,
2164
+ }),
2165
+ list: async (opts = 50) => (await this.sandboxes.listPage(opts)).items,
2166
+ listPage: async (opts = 50) => {
2167
+ const o = typeof opts === "number" ? { limit: opts } : opts;
2168
+ const q = new URLSearchParams({ limit: String(Math.max(1, Math.trunc(o.limit ?? 50))) });
2169
+ const pairs = Object.entries(o.labels ?? {});
2170
+ if (pairs.length)
2171
+ q.set("labels", pairs.map(([k, v]) => `${k}=${v}`).join(","));
2172
+ if (o.state?.length)
2173
+ q.set("state", o.state.join(","));
2174
+ if (o.cursor)
2175
+ q.set("cursor", o.cursor);
2176
+ const r = await this.request({
2177
+ method: "GET",
2178
+ path: `/sandboxes?${q.toString()}`,
2179
+ });
2180
+ return { items: r.sandboxes ?? [], nextCursor: r.next_cursor ?? null, limit: r.limit ?? (o.limit ?? 50) };
2181
+ },
2182
+ probe: (sandboxId, opts = {}) => this.request({
2183
+ method: "POST",
2184
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/probe`,
2185
+ body: opts.paths?.length ? { paths: opts.paths } : {},
2186
+ }),
2187
+ kill: (sandboxId) => this.request({ method: "DELETE", path: `/sandboxes/${encodeURIComponent(sandboxId)}` }),
2188
+ network: (sandboxId) => this.request({ method: "GET", path: `/sandboxes/${encodeURIComponent(sandboxId)}/network` }),
2189
+ setNetworkNone: (sandboxId) => this.request({
2190
+ method: "POST",
2191
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/network`,
2192
+ body: { network: "none" },
2262
2193
  }),
2263
- chmod: (sandboxId, path, opts) => {
2264
- if (opts.mode === undefined && opts.uid === undefined && opts.gid === undefined) {
2265
- throw new InvalidArgumentError("files.chmod needs one of mode, uid or gid", "Pass { mode: 0o755 }, { uid: 1000 }, { gid: 1000 }, or any combination.");
2194
+ patch: (sandboxId, opts) => {
2195
+ const body = {};
2196
+ if (opts.timeoutMs !== undefined) {
2197
+ if (!Number.isInteger(opts.timeoutMs) || opts.timeoutMs <= 0) {
2198
+ throw new InvalidArgumentError("sandboxes.patch needs a positive timeoutMs", "Pass { timeoutMs: 1_800_000 }.");
2199
+ }
2200
+ body["timeout_ms"] = opts.timeoutMs;
2201
+ }
2202
+ if (opts.autoPauseS !== undefined) {
2203
+ if (!Number.isInteger(opts.autoPauseS) || opts.autoPauseS < 0) {
2204
+ throw new InvalidArgumentError("sandboxes.patch needs a non-negative whole number of seconds for autoPauseS", "Pass { autoPauseS: 900 } to pause it after 15 idle minutes, or { autoPauseS: 0 } to turn that off.");
2205
+ }
2206
+ body["auto_pause_s"] = opts.autoPauseS;
2207
+ }
2208
+ if (opts.autoPauseMode !== undefined) {
2209
+ if (opts.autoPauseMode !== "requests" && opts.autoPauseMode !== "activity") {
2210
+ throw new InvalidArgumentError(`sandboxes.patch: autoPauseMode must be "requests" or "activity", got ${JSON.stringify(opts.autoPauseMode)}`, 'Pass { autoPauseMode: "activity" } for an agent that runs inside the sandbox.');
2211
+ }
2212
+ body["auto_pause_mode"] = opts.autoPauseMode;
2213
+ }
2214
+ if (Object.keys(body).length === 0) {
2215
+ throw new InvalidArgumentError("sandboxes.patch was given nothing to change", "Pass { timeoutMs } to extend it, { autoPauseS } to set its idle rule, or both.");
2266
2216
  }
2267
2217
  return this.request({
2218
+ method: "PATCH",
2219
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}`,
2220
+ body,
2221
+ });
2222
+ },
2223
+ files: {
2224
+ list: (sandboxId, path = "/", opts = {}) => {
2225
+ const q = [`path=${encodeURIComponent(path)}`];
2226
+ if (opts.recursive)
2227
+ q.push("recursive=1");
2228
+ if (opts.depth !== undefined)
2229
+ q.push(`depth=${Math.max(0, Math.floor(opts.depth))}`);
2230
+ if (opts.limit !== undefined)
2231
+ q.push(`limit=${Math.max(1, Math.floor(opts.limit))}`);
2232
+ if (opts.hidden === false)
2233
+ q.push("hidden=0");
2234
+ return this.request({
2235
+ method: "GET",
2236
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/list?${q.join("&")}`,
2237
+ });
2238
+ },
2239
+ search: (sandboxId, path, opts = {}) => {
2240
+ if (!opts.pattern && !opts.contains) {
2241
+ throw new InvalidArgumentError("files.search needs `pattern`, `contains`, or both", "Pass { pattern: '*.py' } for a name glob, { contains: 'TODO' } for text inside files.");
2242
+ }
2243
+ const q = [`path=${encodeURIComponent(path)}`];
2244
+ if (opts.pattern)
2245
+ q.push(`pattern=${encodeURIComponent(opts.pattern)}`);
2246
+ if (opts.contains)
2247
+ q.push(`contains=${encodeURIComponent(opts.contains)}`);
2248
+ if (opts.ignoreCase)
2249
+ q.push("ignore_case=1");
2250
+ if (opts.limit !== undefined)
2251
+ q.push(`limit=${Math.max(1, Math.floor(opts.limit))}`);
2252
+ if (opts.hidden === false)
2253
+ q.push("hidden=0");
2254
+ return this.request({
2255
+ method: "GET",
2256
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/search?${q.join("&")}`,
2257
+ });
2258
+ },
2259
+ mkdir: (sandboxId, path, opts = {}) => this.request({
2268
2260
  method: "POST",
2269
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/chmod`,
2261
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/mkdir`,
2270
2262
  body: {
2271
2263
  path,
2272
2264
  ...(opts.mode === undefined ? {} : { mode: modeParam(opts.mode) }),
2273
- ...(opts.uid === undefined ? {} : { uid: opts.uid }),
2274
- ...(opts.gid === undefined ? {} : { gid: opts.gid }),
2265
+ ...(opts.parents === undefined ? {} : { parents: opts.parents }),
2266
+ },
2267
+ }),
2268
+ move: (sandboxId, from, to, opts = {}) => this.request({
2269
+ method: "POST",
2270
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/move`,
2271
+ body: { from, to, ...(opts.overwrite === undefined ? {} : { overwrite: opts.overwrite }) },
2272
+ }),
2273
+ copy: (sandboxId, from, to, opts = {}) => this.request({
2274
+ method: "POST",
2275
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/copy`,
2276
+ body: {
2277
+ from, to,
2275
2278
  ...(opts.recursive === undefined ? {} : { recursive: opts.recursive }),
2279
+ ...(opts.overwrite === undefined ? {} : { overwrite: opts.overwrite }),
2276
2280
  },
2277
- });
2278
- },
2279
- archive: (sandboxId, path, opts = {}) => this.request({
2280
- method: "POST",
2281
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/archive`,
2282
- body: {
2283
- path,
2284
- ...(opts.out === undefined ? {} : { out: opts.out }),
2285
- ...(opts.exclude?.length ? { exclude: opts.exclude } : {}),
2286
- },
2287
- }),
2288
- extract: (sandboxId, archive, into, opts = {}) => this.request({
2289
- method: "POST",
2290
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/extract`,
2291
- body: { archive, into, ...(opts.strip === undefined ? {} : { strip: Math.max(0, Math.floor(opts.strip)) }) },
2292
- }),
2293
- stat: (sandboxId, path) => this.request({
2294
- method: "GET",
2295
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/stat?path=${encodeURIComponent(path)}`,
2296
- }),
2297
- get: (sandboxId, path, opts = {}) => this.bytes("GET", `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}`, undefined, opts),
2298
- download: (sandboxId, path, onChunk, opts = {}) => this.stream(`/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}`, onChunk, opts),
2299
- text: async (sandboxId, path, opts = {}) => new TextDecoder().decode(await this.sandboxes.files.get(sandboxId, path, opts)),
2300
- put: async (sandboxId, path, data, opts = {}) => {
2301
- const body = typeof data === "string" ? new TextEncoder().encode(data) : data;
2302
- return this.sandboxes.files.putFrom(sandboxId, path, {
2303
- size: body.length,
2304
- read: (offset, len) => body.subarray(offset, offset + len),
2305
- }, opts);
2306
- },
2307
- putFrom: async (sandboxId, path, source, opts = {}) => {
2308
- const mode = opts.mode === undefined ? "" : `&mode=${opts.mode.toString(8)}`;
2309
- const base = `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}${mode}`;
2310
- let sent = 0;
2311
- let chunks = 0;
2312
- let last;
2313
- do {
2314
- const slice = await source.read(sent, Math.min(FILE_PUT_CHUNK_BYTES, source.size - sent));
2315
- if (slice.length === 0 && sent < source.size) {
2316
- throw new InvalidArgumentError(`the source of ${path} ended at ${sent} of ${source.size} bytes`, "The file shrank while it was being sent; send it again once nothing is writing it.");
2281
+ }),
2282
+ chmod: (sandboxId, path, opts) => {
2283
+ if (opts.mode === undefined && opts.uid === undefined && opts.gid === undefined) {
2284
+ throw new InvalidArgumentError("files.chmod needs one of mode, uid or gid", "Pass { mode: 0o755 }, { uid: 1000 }, { gid: 1000 }, or any combination.");
2317
2285
  }
2318
- const raw = await this.bytes("PUT", sent === 0 ? base : `${base}&offset=${sent}`, slice, opts);
2319
- last = JSON.parse(new TextDecoder().decode(raw));
2320
- sent += slice.length;
2321
- chunks += 1;
2322
- } while (sent < source.size);
2323
- return { ...last, bytes: source.size, chunks };
2286
+ return this.request({
2287
+ method: "POST",
2288
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/chmod`,
2289
+ body: {
2290
+ path,
2291
+ ...(opts.mode === undefined ? {} : { mode: modeParam(opts.mode) }),
2292
+ ...(opts.uid === undefined ? {} : { uid: opts.uid }),
2293
+ ...(opts.gid === undefined ? {} : { gid: opts.gid }),
2294
+ ...(opts.recursive === undefined ? {} : { recursive: opts.recursive }),
2295
+ },
2296
+ });
2297
+ },
2298
+ archive: (sandboxId, path, opts = {}) => this.request({
2299
+ method: "POST",
2300
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/archive`,
2301
+ body: {
2302
+ path,
2303
+ ...(opts.out === undefined ? {} : { out: opts.out }),
2304
+ ...(opts.exclude?.length ? { exclude: opts.exclude } : {}),
2305
+ },
2306
+ }),
2307
+ extract: (sandboxId, archive, into, opts = {}) => this.request({
2308
+ method: "POST",
2309
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/extract`,
2310
+ body: { archive, into, ...(opts.strip === undefined ? {} : { strip: Math.max(0, Math.floor(opts.strip)) }) },
2311
+ }),
2312
+ stat: (sandboxId, path) => this.request({
2313
+ method: "GET",
2314
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/stat?path=${encodeURIComponent(path)}`,
2315
+ }),
2316
+ get: (sandboxId, path, opts = {}) => this.bytes("GET", `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}`, undefined, opts),
2317
+ download: (sandboxId, path, onChunk, opts = {}) => this.stream(`/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}`, onChunk, opts),
2318
+ text: async (sandboxId, path, opts = {}) => new TextDecoder().decode(await this.sandboxes.files.get(sandboxId, path, opts)),
2319
+ put: async (sandboxId, path, data, opts = {}) => {
2320
+ const body = typeof data === "string" ? new TextEncoder().encode(data) : data;
2321
+ return this.sandboxes.files.putFrom(sandboxId, path, {
2322
+ size: body.length,
2323
+ read: (offset, len) => body.subarray(offset, offset + len),
2324
+ }, opts);
2325
+ },
2326
+ putFrom: async (sandboxId, path, source, opts = {}) => {
2327
+ const mode = opts.mode === undefined ? "" : `&mode=${opts.mode.toString(8)}`;
2328
+ const base = `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}${mode}`;
2329
+ let sent = 0;
2330
+ let chunks = 0;
2331
+ let last;
2332
+ do {
2333
+ const slice = await source.read(sent, Math.min(FILE_PUT_CHUNK_BYTES, source.size - sent));
2334
+ if (slice.length === 0 && sent < source.size) {
2335
+ throw new InvalidArgumentError(`the source of ${path} ended at ${sent} of ${source.size} bytes`, "The file shrank while it was being sent; send it again once nothing is writing it.");
2336
+ }
2337
+ const raw = await this.bytes("PUT", sent === 0 ? base : `${base}&offset=${sent}`, slice, opts);
2338
+ last = JSON.parse(new TextDecoder().decode(raw));
2339
+ sent += slice.length;
2340
+ chunks += 1;
2341
+ } while (sent < source.size);
2342
+ return { ...last, bytes: source.size, chunks };
2343
+ },
2344
+ delete: (sandboxId, path, opts = {}) => this.request({
2345
+ method: "DELETE",
2346
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}`
2347
+ + (opts.recursive ? "&recursive=1" : ""),
2348
+ }),
2324
2349
  },
2325
- delete: (sandboxId, path, opts = {}) => this.request({
2326
- method: "DELETE",
2327
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}`
2328
- + (opts.recursive ? "&recursive=1" : ""),
2329
- }),
2330
- },
2331
- output: async (execId, opts = {}) => {
2332
- if (!execId)
2333
- throw new InvalidArgumentError("exec output needs an exec_id", "Pass the `exec_id` the exec envelope printed.");
2334
- let runId = opts.runId;
2335
- const fromSandbox = async (sid) => {
2336
- const rec = await this.request({
2337
- method: "GET", path: `/sandboxes/${encodeURIComponent(sid)}`,
2338
- });
2339
- const hit = (rec?.execs ?? []).find((e) => e.exec_id === execId);
2340
- if (hit && rec?.run_id)
2341
- runId ??= rec.run_id;
2342
- return hit;
2343
- };
2344
- let found;
2345
- let sandboxId = opts.sandboxId;
2346
- let routeMissing = false;
2347
- try {
2348
- found = await this.request({ method: "GET", path: `/execs/${encodeURIComponent(execId)}` });
2349
- }
2350
- catch (err) {
2351
- if (!(err instanceof NotFoundError))
2352
- throw err;
2353
- found = undefined;
2354
- routeMissing = true;
2355
- }
2356
- if (found && opts.runId && found.run_id && found.run_id !== opts.runId)
2357
- found = undefined;
2358
- let otherSandbox = false;
2359
- if (found && opts.sandboxId && found.sandbox_id && found.sandbox_id !== opts.sandboxId) {
2360
- found = undefined;
2361
- otherSandbox = true;
2362
- }
2363
- if (found)
2364
- runId ??= found.run_id;
2365
- if (!found && (routeMissing || otherSandbox) && opts.sandboxId) {
2350
+ output: async (execId, opts = {}) => {
2351
+ if (!execId)
2352
+ throw new InvalidArgumentError("exec output needs an exec_id", "Pass the `exec_id` the exec envelope printed.");
2353
+ let runId = opts.runId;
2354
+ const fromSandbox = async (sid) => {
2355
+ const rec = await this.request({
2356
+ method: "GET", path: `/sandboxes/${encodeURIComponent(sid)}`,
2357
+ });
2358
+ const hit = (rec?.execs ?? []).find((e) => e.exec_id === execId);
2359
+ if (hit && rec?.run_id)
2360
+ runId ??= rec.run_id;
2361
+ return hit;
2362
+ };
2363
+ let found;
2364
+ let sandboxId = opts.sandboxId;
2365
+ let routeMissing = false;
2366
2366
  try {
2367
- found = await fromSandbox(opts.sandboxId);
2367
+ found = await this.request({ method: "GET", path: `/execs/${encodeURIComponent(execId)}` });
2368
2368
  }
2369
2369
  catch (err) {
2370
2370
  if (!(err instanceof NotFoundError))
2371
2371
  throw err;
2372
2372
  found = undefined;
2373
+ routeMissing = true;
2373
2374
  }
2374
- }
2375
- sandboxId ??= found?.sandbox_id;
2376
- if (!found) {
2377
- throw new NotFoundError(`exec ${execId} not found${sandboxId ? ` in sandbox ${sandboxId}` : opts.runId ? ` in run ${opts.runId}` : ""}`, "Check the `exec_id` from the exec envelope, and that the sandbox it ran in still exists (`promigence run report <run_id>`).");
2378
- }
2379
- return sliceOutput(found, sandboxId, runId, opts.range);
2380
- },
2381
- };
2375
+ if (found && opts.runId && found.run_id && found.run_id !== opts.runId)
2376
+ found = undefined;
2377
+ let otherSandbox = false;
2378
+ if (found && opts.sandboxId && found.sandbox_id && found.sandbox_id !== opts.sandboxId) {
2379
+ found = undefined;
2380
+ otherSandbox = true;
2381
+ }
2382
+ if (found)
2383
+ runId ??= found.run_id;
2384
+ if (!found && (routeMissing || otherSandbox) && opts.sandboxId) {
2385
+ try {
2386
+ found = await fromSandbox(opts.sandboxId);
2387
+ }
2388
+ catch (err) {
2389
+ if (!(err instanceof NotFoundError))
2390
+ throw err;
2391
+ found = undefined;
2392
+ }
2393
+ }
2394
+ sandboxId ??= found?.sandbox_id;
2395
+ if (!found) {
2396
+ throw new NotFoundError(`exec ${execId} not found${sandboxId ? ` in sandbox ${sandboxId}` : opts.runId ? ` in run ${opts.runId}` : ""}`, "Check the `exec_id` from the exec envelope, and that the sandbox it ran in still exists (`promigence run report <run_id>`).");
2397
+ }
2398
+ return sliceOutput(found, sandboxId, runId, opts.range);
2399
+ },
2400
+ };
2401
+ }
2382
2402
  sandbox(id) {
2383
2403
  return {
2384
2404
  id,
@@ -2398,142 +2418,174 @@ export class PromigenceClient {
2398
2418
  probe: (opts = {}) => this.sandboxes.probe(id, opts),
2399
2419
  };
2400
2420
  }
2401
- secrets = {
2402
- set: (name, value, opts = {}) => {
2403
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name ?? "")) {
2404
- throw new InvalidArgumentError(`secrets.set: ${JSON.stringify(name ?? null)} is not usable as an environment variable`, "Use letters, digits and underscores, not starting with a digit: OPENAI_API_KEY.");
2405
- }
2406
- if (!value) {
2407
- throw new InvalidArgumentError(`secrets.set: ${name} has an empty value`, `To remove it, call secrets.delete(${JSON.stringify(name)}).`);
2408
- }
2409
- const body = { name, value };
2410
- if (opts.hosts !== undefined)
2411
- body.hosts = opts.hosts;
2412
- if (opts.headers !== undefined)
2413
- body.headers = opts.headers;
2414
- if (opts.raw !== undefined)
2415
- body.raw = opts.raw;
2416
- if (opts.scanResponses !== undefined)
2417
- body.scan_responses = opts.scanResponses;
2418
- return this.request({ method: "POST", path: "/secrets", body });
2419
- },
2420
- list: () => this.request({ method: "GET", path: "/secrets" }),
2421
- delete: (name) => this.request({
2422
- method: "DELETE",
2423
- path: `/secrets/${encodeURIComponent(name)}`,
2424
- }),
2425
- };
2426
- images = {
2427
- resolve: (image, opts = {}) => {
2428
- if (!image?.trim()) {
2429
- throw new InvalidArgumentError("images.resolve needs an image reference", "Pass a name like 'python:3.12' or 'ghcr.io/org/app:v1'.");
2430
- }
2431
- const body = { image: image.trim() };
2432
- if (opts.registrySecret)
2433
- body["registry_secret"] = opts.registrySecret;
2434
- return this.request({ method: "POST", path: "/images/resolve", body });
2435
- },
2436
- };
2437
- captures = {
2438
- delete: (snapshotId) => this.request({
2439
- method: "DELETE",
2440
- path: `/captures/${encodeURIComponent(snapshotId)}`,
2441
- }),
2442
- };
2443
- snapshots = {
2444
- create: (opts) => {
2445
- const body = snapshotCreateBody(opts);
2446
- return this.request({
2447
- method: "POST", path: "/snapshots", body,
2448
- ...(opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
2449
- });
2450
- },
2451
- status: (buildId, opts = {}) => this.request({
2452
- method: "GET",
2453
- path: `/snapshots/builds/${encodeURIComponent(buildId)}${waitQuery(opts.waitMs)}`,
2454
- ...this.readExtras(opts),
2455
- }),
2456
- buildLog: async (buildId, tail = 200) => {
2457
- const r = await this.request({
2458
- method: "GET",
2459
- path: `/snapshots/builds/${encodeURIComponent(buildId)}/build-log?tail=${Math.max(1, Math.floor(tail))}`,
2460
- });
2461
- return r?.log ?? [];
2462
- },
2463
- builds: async (limit = 20) => (await this.snapshots.buildsPage({ limit })).items,
2464
- buildsPage: async (opts = {}) => {
2465
- const limit = opts.limit ?? 20;
2466
- const r = await this.request({ method: "GET", path: `/snapshots/builds?${pageQuery(limit, 500, opts.cursor)}` });
2467
- return pageOf(r, "builds", limit);
2468
- },
2469
- get: (idOrAlias) => this.request({ method: "GET", path: `/snapshots/${seg(idOrAlias)}` }),
2470
- list: async (opts = {}) => (await this.snapshots.listPage(opts)).items,
2471
- listPage: async (opts = {}) => {
2472
- const q = new URLSearchParams();
2473
- if (opts.limit !== undefined)
2474
- q.set("limit", String(Math.max(1, Math.trunc(opts.limit))));
2475
- if (opts.cursor)
2476
- q.set("cursor", opts.cursor);
2477
- const qs = q.toString();
2478
- const r = await this.request({
2479
- method: "GET", path: `/snapshots${qs ? `?${qs}` : ""}`, ...(this.authenticated ? {} : { anonymous: true }),
2480
- });
2481
- if (Array.isArray(r))
2482
- return { items: r, nextCursor: null, limit: opts.limit ?? r.length };
2483
- return { items: r?.snapshots ?? [], nextCursor: r?.next_cursor ?? null, limit: r?.limit ?? (opts.limit ?? 0) };
2484
- },
2485
- public: async (limit = 20) => {
2486
- const r = await this.request({
2421
+ group_secrets() {
2422
+ return {
2423
+ set: (name, value, opts = {}) => {
2424
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name ?? "")) {
2425
+ throw new InvalidArgumentError(`secrets.set: ${JSON.stringify(name ?? null)} is not usable as an environment variable`, "Use letters, digits and underscores, not starting with a digit: OPENAI_API_KEY.");
2426
+ }
2427
+ if (!value) {
2428
+ throw new InvalidArgumentError(`secrets.set: ${name} has an empty value`, `To remove it, call secrets.delete(${JSON.stringify(name)}).`);
2429
+ }
2430
+ const body = { name, value };
2431
+ if (opts.hosts !== undefined)
2432
+ body.hosts = opts.hosts;
2433
+ if (opts.headers !== undefined)
2434
+ body.headers = opts.headers;
2435
+ if (opts.raw !== undefined)
2436
+ body.raw = opts.raw;
2437
+ if (opts.scanResponses !== undefined)
2438
+ body.scan_responses = opts.scanResponses;
2439
+ return this.request({ method: "POST", path: "/secrets", body });
2440
+ },
2441
+ list: () => this.request({ method: "GET", path: "/secrets" }),
2442
+ delete: (name) => this.request({
2443
+ method: "DELETE",
2444
+ path: `/secrets/${encodeURIComponent(name)}`,
2445
+ }),
2446
+ };
2447
+ }
2448
+ group_images() {
2449
+ return {
2450
+ resolve: (image, opts = {}) => {
2451
+ if (!image?.trim()) {
2452
+ throw new InvalidArgumentError("images.resolve needs an image reference", "Pass a name like 'python:3.12' or 'ghcr.io/org/app:v1'.");
2453
+ }
2454
+ const body = { image: image.trim() };
2455
+ if (opts.registrySecret)
2456
+ body["registry_secret"] = opts.registrySecret;
2457
+ return this.request({ method: "POST", path: "/images/resolve", body });
2458
+ },
2459
+ };
2460
+ }
2461
+ group_captures() {
2462
+ return {
2463
+ delete: (snapshotId) => this.request({
2464
+ method: "DELETE",
2465
+ path: `/captures/${encodeURIComponent(snapshotId)}`,
2466
+ }),
2467
+ };
2468
+ }
2469
+ group_snapshots() {
2470
+ return {
2471
+ create: (opts) => {
2472
+ const body = snapshotCreateBody(opts);
2473
+ return this.request({
2474
+ method: "POST", path: "/snapshots", body,
2475
+ ...(opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
2476
+ });
2477
+ },
2478
+ status: (buildId, opts = {}) => this.request({
2487
2479
  method: "GET",
2488
- path: `/snapshots?limit=${Math.max(1, Math.floor(limit))}`,
2489
- anonymous: true,
2490
- });
2491
- const all = Array.isArray(r) ? r : (r?.snapshots ?? []);
2492
- return all.filter((s) => s.visibility === undefined || s.visibility === "public").slice(0, limit);
2493
- },
2494
- delete: (idOrAlias) => {
2495
- if (!idOrAlias)
2496
- throw new InvalidArgumentError("snapshots.delete needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
2497
- return this.request({ method: "DELETE", path: `/snapshots/${seg(idOrAlias)}` });
2498
- },
2499
- alias: (idOrAlias, alias) => this.request({
2500
- method: "PUT",
2501
- path: `/snapshots/${seg(idOrAlias)}/alias`,
2502
- body: { alias },
2503
- }),
2504
- waitBuild: async (buildId, opts = {}) => {
2505
- const r = await this.waitLoop(opts, BUILD_POLL_MS, { build_id: buildId, state: "running" }, (read) => this.snapshots.status(buildId, read), (b) => b.state !== "running" && b.state !== "queued");
2506
- return { build: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
2507
- },
2508
- verify: (id, opts = {}) => {
2509
- if (!id)
2510
- throw new InvalidArgumentError("snapshots.verify needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
2511
- const body = {};
2512
- if (opts.timeoutMs !== undefined)
2513
- body["timeout_ms"] = opts.timeoutMs;
2514
- if (opts.spendCap !== undefined)
2515
- body["spend_cap"] = opts.spendCap;
2516
- return this.request({
2517
- method: "POST",
2518
- path: `/snapshots/${encodeURIComponent(id)}/verify`,
2519
- body,
2520
- });
2521
- },
2522
- manifest: (idOrAlias) => this.request({ method: "GET", path: `/snapshots/${seg(idOrAlias)}/manifest` }),
2523
- bundle: async (idOrAlias, opts = {}) => {
2524
- const [record, m] = await Promise.all([
2525
- this.snapshots.get(idOrAlias).catch(() => undefined),
2526
- this.snapshots.manifest(idOrAlias),
2527
- ]);
2528
- if (!m?.manifest || typeof m.manifest !== "object") {
2529
- throw new NotFoundError(`snapshot ${idOrAlias} has no manifest to export`, "Only a verified snapshot has one: check 'promigence snapshot inspect' first.");
2530
- }
2531
- const alias = opts.alias ?? record?.alias ?? undefined;
2532
- const bundle = buildBundle(m.manifest, { ...(alias ? { alias } : {}) });
2533
- return { bundle, served_by: "client", ...(record ? { record } : {}) };
2534
- },
2535
- };
2480
+ path: `/snapshots/builds/${encodeURIComponent(buildId)}${waitQuery(opts.waitMs)}`,
2481
+ ...this.readExtras(opts),
2482
+ }),
2483
+ buildLog: async (buildId, tail = 200) => {
2484
+ const r = await this.request({
2485
+ method: "GET",
2486
+ path: `/snapshots/builds/${encodeURIComponent(buildId)}/build-log?tail=${Math.max(1, Math.floor(tail))}`,
2487
+ });
2488
+ return r?.log ?? [];
2489
+ },
2490
+ builds: async (limit = 20) => (await this.snapshots.buildsPage({ limit })).items,
2491
+ buildsPage: async (opts = {}) => {
2492
+ const limit = opts.limit ?? 20;
2493
+ const r = await this.request({ method: "GET", path: `/snapshots/builds?${pageQuery(limit, 500, opts.cursor)}` });
2494
+ return pageOf(r, "builds", limit);
2495
+ },
2496
+ get: (idOrAlias) => this.request({ method: "GET", path: `/snapshots/${seg(idOrAlias)}` }),
2497
+ list: async (opts = {}) => (await this.snapshots.listPage(opts)).items,
2498
+ listPage: async (opts = {}) => {
2499
+ const q = new URLSearchParams();
2500
+ if (opts.limit !== undefined)
2501
+ q.set("limit", String(Math.max(1, Math.trunc(opts.limit))));
2502
+ if (opts.cursor)
2503
+ q.set("cursor", opts.cursor);
2504
+ const qs = q.toString();
2505
+ const r = await this.request({
2506
+ method: "GET", path: `/snapshots${qs ? `?${qs}` : ""}`, ...(this.authenticated ? {} : { anonymous: true }),
2507
+ });
2508
+ if (Array.isArray(r))
2509
+ return { items: r, nextCursor: null, limit: opts.limit ?? r.length };
2510
+ return { items: r?.snapshots ?? [], nextCursor: r?.next_cursor ?? null, limit: r?.limit ?? (opts.limit ?? 0) };
2511
+ },
2512
+ public: async (limit = 20) => {
2513
+ const r = await this.request({
2514
+ method: "GET",
2515
+ path: `/snapshots?limit=${Math.max(1, Math.floor(limit))}`,
2516
+ anonymous: true,
2517
+ });
2518
+ const all = Array.isArray(r) ? r : (r?.snapshots ?? []);
2519
+ return all.filter((s) => s.visibility === undefined || s.visibility === "public").slice(0, limit);
2520
+ },
2521
+ delete: (idOrAlias) => {
2522
+ if (!idOrAlias)
2523
+ throw new InvalidArgumentError("snapshots.delete needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
2524
+ return this.request({ method: "DELETE", path: `/snapshots/${seg(idOrAlias)}` });
2525
+ },
2526
+ alias: (idOrAlias, alias) => this.request({
2527
+ method: "PUT",
2528
+ path: `/snapshots/${seg(idOrAlias)}/alias`,
2529
+ body: { alias },
2530
+ }),
2531
+ waitBuild: async (buildId, opts = {}) => {
2532
+ const r = await this.waitLoop(opts, BUILD_POLL_MS, { build_id: buildId, state: "running" }, (read) => this.snapshots.status(buildId, read), (b) => b.state !== "running" && b.state !== "queued");
2533
+ return { build: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
2534
+ },
2535
+ verify: (id, opts = {}) => {
2536
+ if (!id)
2537
+ throw new InvalidArgumentError("snapshots.verify needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
2538
+ const body = {};
2539
+ if (opts.timeoutMs !== undefined)
2540
+ body["timeout_ms"] = opts.timeoutMs;
2541
+ if (opts.spendCap !== undefined)
2542
+ body["spend_cap"] = opts.spendCap;
2543
+ return this.request({
2544
+ method: "POST",
2545
+ path: `/snapshots/${encodeURIComponent(id)}/verify`,
2546
+ body,
2547
+ });
2548
+ },
2549
+ manifest: (idOrAlias) => this.request({ method: "GET", path: `/snapshots/${seg(idOrAlias)}/manifest` }),
2550
+ bundle: async (idOrAlias, opts = {}) => {
2551
+ const [record, m] = await Promise.all([
2552
+ this.snapshots.get(idOrAlias).catch(() => undefined),
2553
+ this.snapshots.manifest(idOrAlias),
2554
+ ]);
2555
+ if (!m?.manifest || typeof m.manifest !== "object") {
2556
+ throw new NotFoundError(`snapshot ${idOrAlias} has no manifest to export`, "Only a verified snapshot has one: check 'promigence snapshot inspect' first.");
2557
+ }
2558
+ const alias = opts.alias ?? record?.alias ?? undefined;
2559
+ const bundle = buildBundle(m.manifest, { ...(alias ? { alias } : {}) });
2560
+ return { bundle, served_by: "client", ...(record ? { record } : {}) };
2561
+ },
2562
+ };
2563
+ }
2564
+ }
2565
+ const API_GROUPS = ["templates", "billing", "keys", "orgs", "credits", "github", "gitlab", "runs", "sandboxes", "secrets", "images", "captures", "snapshots"];
2566
+ function installLazyGroups(proto) {
2567
+ for (const name of API_GROUPS) {
2568
+ const build = `group_${name}`;
2569
+ const own = (self, value) => {
2570
+ Object.defineProperty(self, name, { value, writable: true, enumerable: true, configurable: true });
2571
+ };
2572
+ Object.defineProperty(proto, name, {
2573
+ configurable: true,
2574
+ enumerable: false,
2575
+ get() {
2576
+ const value = this[build].call(this);
2577
+ own(this, value);
2578
+ return value;
2579
+ },
2580
+ set(value) {
2581
+ own(this, value);
2582
+ },
2583
+ });
2584
+ }
2536
2585
  }
2586
+ installLazyGroups(PromigenceClient.prototype);
2587
+ const lazyGroupTypesMatch = [true, true, true, true, true, true, true, true, true, true, true, true, true];
2588
+ void lazyGroupTypesMatch;
2537
2589
  const POLL_LADDER_MS = [10, 25, 25, 25, 25, 25, 25, 25, 25, 50, 50, 50, 100, 100, 100, 200];
2538
2590
  function pollInterval(iteration) {
2539
2591
  const step = POLL_LADDER_MS[iteration];