@promigence/sdk 0.1.0-beta.14 → 0.1.0-beta.16

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
@@ -2,7 +2,7 @@ import { readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { inspect } from "node:util";
4
4
  import { dirname, join } from "node:path";
5
- import { ApiError, AuthError, EnvFailedError, PromigenceError, InvalidArgumentError, NotFoundError, NotReleasedError, NotVerifiedError, RateLimitError, ServiceRestartError, SpendCapError, TimeoutError, UnreachableError, DEFAULT_API_URL, ERRORS_DOCS_URL, FILE_PUT_CHUNK_BYTES, MAX_OUTPUT_BYTES_DEFAULT, VERSION, WAIT_TIMEOUT_MS, PreviewsUnavailableError, exitForCode, previewsOff, withPreviewsFix, } from "./core.js";
5
+ import { ApiError, AuthError, EnvFailedError, PromigenceError, InvalidArgumentError, NotFoundError, NotReleasedError, NotVerifiedError, RateLimitError, ServiceRestartError, SpendCapError, TimeoutError, UnreachableError, DEFAULT_API_URL, FILE_PUT_CHUNK_BYTES, MAX_OUTPUT_BYTES_DEFAULT, VERSION, WAIT_TIMEOUT_MS, PreviewsUnavailableError, SANDBOX_ENDED_FIX, SandboxEndedError, errorsDocsFor, exitForCode, previewsOff, sandboxEndedIn, withPreviewsFix, } from "./core.js";
6
6
  import { buildBundle } from "./bundle.js";
7
7
  import { keychainReadCached, keychainStore, storeLabel } from "./credstore.js";
8
8
  import { CALLER_HEADER, resolveCaller, userAgentWithCaller } from "./caller.js";
@@ -42,6 +42,26 @@ function bytesToB64(b) {
42
42
  function modeParam(mode) {
43
43
  return typeof mode === "number" ? mode.toString(8) : mode;
44
44
  }
45
+ function jsonBody(value) {
46
+ return { data: new TextEncoder().encode(JSON.stringify(value)), contentType: "application/json" };
47
+ }
48
+ function fileForm(fields, data) {
49
+ const boundary = `promigence-${globalThis.crypto.randomUUID().replace(/-/g, "")}`;
50
+ let head = "";
51
+ for (const [name, value] of fields) {
52
+ head += `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`;
53
+ }
54
+ head += `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="blob"\r\n`
55
+ + "Content-Type: application/octet-stream\r\n\r\n";
56
+ const enc = new TextEncoder();
57
+ const pre = enc.encode(head);
58
+ const post = enc.encode(`\r\n--${boundary}--\r\n`);
59
+ const out = new Uint8Array(pre.length + data.length + post.length);
60
+ out.set(pre, 0);
61
+ out.set(data, pre.length);
62
+ out.set(post, pre.length + data.length);
63
+ return { data: out, contentType: `multipart/form-data; boundary=${boundary}` };
64
+ }
45
65
  export function execOutputBytes(r, stream = "stdout") {
46
66
  const b64 = r[`${stream}_b64`];
47
67
  if (typeof b64 === "string")
@@ -71,6 +91,32 @@ function pageQuery(limit, max, cursor) {
71
91
  q.set("cursor", cursor);
72
92
  return q.toString();
73
93
  }
94
+ export const OUTPUT_RETENTION_MAX_DAYS = 30;
95
+ function projectUpdateBody(u) {
96
+ const body = {};
97
+ if (u.name !== undefined)
98
+ body["name"] = u.name;
99
+ if (u.outputRetentionDays !== undefined) {
100
+ const d = u.outputRetentionDays;
101
+ if (d !== null && (!Number.isInteger(d) || d < 1 || d > OUTPUT_RETENTION_MAX_DAYS)) {
102
+ throw new InvalidArgumentError(`outputRetentionDays must be a whole number from 1 to ${OUTPUT_RETENTION_MAX_DAYS}, or null, not ${String(d)}`, "Pass null for the organization's setting.");
103
+ }
104
+ body["output_retention_days"] = d;
105
+ }
106
+ if (u.failureCaptures !== undefined) {
107
+ if (u.failureCaptures !== null && typeof u.failureCaptures !== "boolean") {
108
+ throw new InvalidArgumentError("failureCaptures must be true, false or null", "Pass null for the organization's setting.");
109
+ }
110
+ if (Object.keys(body).length) {
111
+ throw new InvalidArgumentError("failureCaptures is set on its own", "Change it in a call of its own, without name or outputRetentionDays.");
112
+ }
113
+ body["failure_captures"] = u.failureCaptures;
114
+ }
115
+ if (!Object.keys(body).length) {
116
+ throw new InvalidArgumentError("projects.update needs something to change", "Pass name, outputRetentionDays or failureCaptures.");
117
+ }
118
+ return body;
119
+ }
74
120
  function expiryFields(o) {
75
121
  const body = {};
76
122
  if (o.expiresInDays !== undefined)
@@ -230,30 +276,6 @@ export function readCredentials(env = process.env) {
230
276
  function storedName(store, env) {
231
277
  return store === undefined || store === "file" ? `the credentials file (${credentialsPath(env)})` : storeLabel(store);
232
278
  }
233
- const COMPAT_REFUSED_HOSTS = /(^|\.)e2b(-staging)?\.(app|dev|pro)$/i;
234
- export function apiUrlFromCompatUrl(url) {
235
- const u = (url ?? "").trim();
236
- if (!u)
237
- return undefined;
238
- let host = "";
239
- try {
240
- host = new URL(u.includes("://") ? u : `https://${u}`).hostname;
241
- }
242
- catch {
243
- return undefined;
244
- }
245
- return COMPAT_REFUSED_HOSTS.test(host) ? undefined : u;
246
- }
247
- export function apiUrlFromCompatDomain(domain) {
248
- const d = (domain ?? "").trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
249
- if (!d || COMPAT_REFUSED_HOSTS.test(d))
250
- return undefined;
251
- return `https://api.${d}/v1`;
252
- }
253
- export function compatApiKey(key) {
254
- const k = key?.trim();
255
- return k && !k.startsWith("e2b_") ? k : undefined;
256
- }
257
279
  export function keyOrigin(env = process.env) {
258
280
  if (env["PROMIGENCE_API_KEY"]?.trim())
259
281
  return "PROMIGENCE_API_KEY";
@@ -280,12 +302,13 @@ function resolveCredentials(opts, env = process.env) {
280
302
  const inputs = optKey === undefined && envKey === undefined
281
303
  ? undefined
282
304
  : `${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 };
305
+ if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined) {
306
+ return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl, ...(credsLast.origin ? { origin: credsLast.origin } : {}) };
307
+ }
285
308
  const worked = resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl);
286
309
  if (inputs !== undefined && worked.refusal === undefined) {
287
310
  credsLastInputs = inputs;
288
- credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl };
311
+ credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl, ...(worked.origin ? { origin: worked.origin } : {}) };
289
312
  }
290
313
  return worked;
291
314
  }
@@ -319,8 +342,15 @@ function resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl) {
319
342
  if (refusal)
320
343
  return { apiKey: undefined, apiUrl, refusal };
321
344
  }
322
- return { apiKey: key, apiUrl };
345
+ const origin = !key ? undefined
346
+ : source === "opts" ? { source: "option" }
347
+ : source === "env" ? { source: "env" }
348
+ : source === "file" ? { source: "stored", ...(store === undefined ? {} : { store }) }
349
+ : { source: "fallback" };
350
+ return { apiKey: key, apiUrl, ...(origin ? { origin } : {}) };
323
351
  }
352
+ export const NO_KEY_MESSAGE = "no API key: none was given, PROMIGENCE_API_KEY is not set, and none is stored";
353
+ export const NO_KEY_FIX = "Pass { apiKey } or set PROMIGENCE_API_KEY. No key yet: `promigence login` signs you up or in and stores one.";
324
354
  function keyRefusal(key, from) {
325
355
  if (/^[\x21-\x7E]+$/.test(key))
326
356
  return undefined;
@@ -600,6 +630,8 @@ export function snapshotCreateBody(opts) {
600
630
  }
601
631
  if (o.alias)
602
632
  body["alias"] = o.alias;
633
+ if (o.alias && o.moveAlias)
634
+ body["move_alias"] = true;
603
635
  if (o.grade) {
604
636
  if (!o.grade.cmd)
605
637
  throw new InvalidArgumentError("snapshots.create grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
@@ -634,6 +666,9 @@ const REFUSED_503_CODES = new Set([
634
666
  "builder_unavailable", "builder_busy", "overloaded", "tier_unavailable", "registry_unavailable",
635
667
  "secrets_unavailable",
636
668
  ]);
669
+ const SDK_FIXES = {
670
+ cap_exceeded: "Raise `spendCap`, lower `timeoutMs` or `count`, or pass `allowPartial` to launch what fits.",
671
+ };
637
672
  function withoutCliHelp(fix) {
638
673
  const out = fix.replace(/\s*See `promigence [^`]*--help`\.?\s*$/, "").trim();
639
674
  if (out === fix.trim())
@@ -643,7 +678,7 @@ function withoutCliHelp(fix) {
643
678
  export function errorFor(code, message, fix, extra = {}) {
644
679
  const attach = (e) => {
645
680
  const w = e;
646
- w.docs = extra.docs ?? `${ERRORS_DOCS_URL}#${code}`;
681
+ w.docs = extra.docs !== undefined && e.code === code ? extra.docs : errorsDocsFor(e.code);
647
682
  if (extra.status !== undefined)
648
683
  w.status = extra.status;
649
684
  if (extra.requestId !== undefined)
@@ -659,6 +694,8 @@ export function errorFor(code, message, fix, extra = {}) {
659
694
  if ((code === "usage" || code === "invalid_argument" || code === "unsupported") && previewsOff(message)) {
660
695
  return attach(new PreviewsUnavailableError());
661
696
  }
697
+ if (!extra.cli && Object.prototype.hasOwnProperty.call(SDK_FIXES, code))
698
+ fix = SDK_FIXES[code];
662
699
  switch (code) {
663
700
  case "auth": return attach(new AuthError(message, fix));
664
701
  case "not_found": return attach(new NotFoundError(message, fix));
@@ -672,8 +709,13 @@ export function errorFor(code, message, fix, extra = {}) {
672
709
  case "not_verified": return attach(new NotVerifiedError(message, fix));
673
710
  case "timeout": return attach(new TimeoutError(message, fix));
674
711
  case "usage":
675
- case "invalid_argument":
712
+ case "invalid_argument": {
713
+ const ended = code === "usage" ? sandboxEndedIn(message) : undefined;
714
+ if (ended !== undefined) {
715
+ return attach(new SandboxEndedError(message, ended.sandboxId, ended.state, extra.cli ? fix : SANDBOX_ENDED_FIX));
716
+ }
676
717
  return attach(new InvalidArgumentError(message, extra.cli ? fix : withoutCliHelp(fix), code));
718
+ }
677
719
  case "not_released": return attach(new NotReleasedError(message));
678
720
  case "service_restart": return attach(new ServiceRestartError(message, fix));
679
721
  default:
@@ -744,6 +786,21 @@ const WAIT_MS_CAP = 30_000;
744
786
  export const LONG_POLL_MS = 5_000;
745
787
  const BUILD_POLL_MS = 10_000;
746
788
  const PROGRESS_POLL_MS = 200;
789
+ export const RUN_SETTLE_MS = 5_000;
790
+ export function runWorkDone(run, endedSince, now = Date.now()) {
791
+ if (run.state === "completed" || run.state === "cap_killed" || run.state === "aborted")
792
+ return true;
793
+ const sbs = Array.isArray(run.sandboxes) ? run.sandboxes : [];
794
+ const allDone = sbs.length > 0 && sbs.every((s) => s.state === "ended" || s.command_outcome != null);
795
+ if (!allDone) {
796
+ delete endedSince.at;
797
+ return false;
798
+ }
799
+ if (sbs.some((s) => s.state !== "ended"))
800
+ return true;
801
+ endedSince.at ??= now;
802
+ return now - endedSince.at >= RUN_SETTLE_MS;
803
+ }
747
804
  const WAITED_HEADER = "promigence-waited-ms";
748
805
  const seg = (s) => encodeURIComponent(s).replace(/%40/g, "@");
749
806
  function waitQuery(waitMs) {
@@ -781,6 +838,9 @@ export class PromigenceClient {
781
838
  apiUrl;
782
839
  #apiKey;
783
840
  #keyRefusal;
841
+ #keyOrigin;
842
+ #onKeyUse;
843
+ #keyUseSaid = false;
784
844
  #bearer;
785
845
  fetchImpl;
786
846
  ownFetch;
@@ -802,6 +862,8 @@ export class PromigenceClient {
802
862
  const resolved = resolveCredentials(opts);
803
863
  this.#apiKey = resolved.apiKey;
804
864
  this.#keyRefusal = resolved.refusal;
865
+ this.#keyOrigin = resolved.origin;
866
+ this.#onKeyUse = opts.onKeyUse;
805
867
  this.apiUrl = resolved.apiUrl;
806
868
  const env = opts.fetch === undefined ? transportEnv() : undefined;
807
869
  const proxy = env !== undefined ? proxyFor(this.apiUrl, env) : undefined;
@@ -867,11 +929,18 @@ export class PromigenceClient {
867
929
  catch {
868
930
  }
869
931
  }
932
+ get keyOrigin() {
933
+ return this.#keyOrigin;
934
+ }
870
935
  requireKey() {
871
936
  if (this.#keyRefusal)
872
937
  throw this.#keyRefusal;
873
938
  if (!this.#apiKey)
874
- throw new AuthError();
939
+ throw this.cliErrors ? new AuthError(NO_KEY_MESSAGE) : new AuthError(NO_KEY_MESSAGE, NO_KEY_FIX);
940
+ if (!this.#keyUseSaid && this.#onKeyUse !== undefined && this.#keyOrigin !== undefined) {
941
+ this.#keyUseSaid = true;
942
+ this.#onKeyUse(this.#keyOrigin);
943
+ }
875
944
  return this.#apiKey;
876
945
  }
877
946
  bearer() {
@@ -944,7 +1013,8 @@ export class PromigenceClient {
944
1013
  const idempotencyKey = opts.idempotencyKey ?? (keyedCreate ? newIdempotencyKey() : undefined);
945
1014
  if (idempotencyKey)
946
1015
  headers["idempotency-key"] = idempotencyKey;
947
- const replayable = opts.method === "GET" || opts.method === "DELETE" || (keyedCreate && Boolean(idempotencyKey));
1016
+ const replayable = opts.method === "GET" || opts.method === "DELETE" || opts.replayable === true
1017
+ || (keyedCreate && Boolean(idempotencyKey));
948
1018
  const timeoutMs = opts.timeoutMs ?? this.requestTimeoutMs;
949
1019
  let dispatcher;
950
1020
  let resentDelete = false;
@@ -1141,7 +1211,7 @@ export class PromigenceClient {
1141
1211
  }
1142
1212
  }
1143
1213
  }
1144
- async bytes(method, path, body, opts = {}) {
1214
+ async bytes(method, path, body, opts = {}, read = method === "GET") {
1145
1215
  const url = `${this.apiUrl}${path}`;
1146
1216
  const headers = {
1147
1217
  accept: "application/octet-stream, application/json",
@@ -1151,7 +1221,7 @@ export class PromigenceClient {
1151
1221
  if (this.caller)
1152
1222
  headers[CALLER_HEADER] = this.caller;
1153
1223
  if (body !== undefined)
1154
- headers["content-type"] = "application/octet-stream";
1224
+ headers["content-type"] = body.contentType;
1155
1225
  const signals = [AbortSignal.timeout(opts.timeoutMs ?? FILE_TIMEOUT_MS)];
1156
1226
  if (opts.signal)
1157
1227
  signals.push(opts.signal);
@@ -1165,7 +1235,7 @@ export class PromigenceClient {
1165
1235
  method,
1166
1236
  headers,
1167
1237
  signal,
1168
- ...(body === undefined ? {} : { body: body }),
1238
+ ...(body === undefined ? {} : { body: body.data }),
1169
1239
  ...(dispatcher === undefined ? {} : { dispatcher }),
1170
1240
  });
1171
1241
  }
@@ -1185,21 +1255,23 @@ export class PromigenceClient {
1185
1255
  }
1186
1256
  catch { }
1187
1257
  const retryAfterMs = parseRetryAfter(res.headers.get("retry-after"));
1188
- const wait = this.refusalBackoff(res.status, retryAfterMs, payload.error?.code, attempt, method === "GET", opts.signal);
1258
+ const wait = this.refusalBackoff(res.status, retryAfterMs, payload.error?.code, attempt, read, opts.signal);
1189
1259
  if (wait !== undefined) {
1190
1260
  this.stats.retries++;
1191
1261
  await sleep(wait);
1192
1262
  continue;
1193
1263
  }
1194
1264
  const code = payload.error?.code ?? codeForStatus(res.status);
1265
+ const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
1195
1266
  throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
1196
1267
  status: res.status,
1197
1268
  ...bodyExtra(payload),
1198
1269
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
1270
+ ...(rid ? { requestId: rid } : {}),
1199
1271
  });
1200
1272
  }
1201
1273
  }
1202
- async stream(path, onChunk, opts = {}) {
1274
+ async stream(method, path, onChunk, opts = {}, body) {
1203
1275
  const url = `${this.apiUrl}${path}`;
1204
1276
  const headers = {
1205
1277
  accept: "application/octet-stream, application/json",
@@ -1208,17 +1280,25 @@ export class PromigenceClient {
1208
1280
  };
1209
1281
  if (this.caller)
1210
1282
  headers[CALLER_HEADER] = this.caller;
1283
+ if (body !== undefined)
1284
+ headers["content-type"] = body.contentType;
1211
1285
  const signals = [AbortSignal.timeout(opts.timeoutMs ?? FILE_TIMEOUT_MS)];
1212
1286
  if (opts.signal)
1213
1287
  signals.push(opts.signal);
1214
1288
  const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
1215
- const timedOut = () => new TimeoutError(`GET ${path} timed out after ${opts.timeoutMs ?? FILE_TIMEOUT_MS} ms`, "Retry; for a large artefact raise timeoutMs.");
1289
+ const timedOut = () => new TimeoutError(`${method} ${path} timed out after ${opts.timeoutMs ?? FILE_TIMEOUT_MS} ms`, "Retry; for a large artefact raise timeoutMs.");
1216
1290
  const dispatcher = this.guardDispatcher();
1217
1291
  for (let attempt = 0;; attempt++) {
1218
1292
  let res;
1219
1293
  this.stats.requests++;
1220
1294
  try {
1221
- res = await this.fetchImpl(url, { method: "GET", headers, signal, ...(dispatcher === undefined ? {} : { dispatcher }) });
1295
+ res = await this.fetchImpl(url, {
1296
+ method,
1297
+ headers,
1298
+ signal,
1299
+ ...(body === undefined ? {} : { body: body.data }),
1300
+ ...(dispatcher === undefined ? {} : { dispatcher }),
1301
+ });
1222
1302
  }
1223
1303
  catch (err) {
1224
1304
  const e = err;
@@ -1267,10 +1347,12 @@ export class PromigenceClient {
1267
1347
  continue;
1268
1348
  }
1269
1349
  const code = payload.error?.code ?? codeForStatus(res.status);
1270
- throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from GET ${path}`, payload.error?.fix ?? "See the API response for details.", {
1350
+ const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
1351
+ throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
1271
1352
  status: res.status,
1272
1353
  ...bodyExtra(payload),
1273
1354
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
1355
+ ...(rid ? { requestId: rid } : {}),
1274
1356
  });
1275
1357
  }
1276
1358
  }
@@ -1346,6 +1428,29 @@ export class PromigenceClient {
1346
1428
  const r = await this.request({ method: "GET", path: `/billing/history?${q.toString()}` });
1347
1429
  return pageOf(r, "entries", limit);
1348
1430
  },
1431
+ holds: () => this.request({ method: "GET", path: "/billing/holds" }),
1432
+ buyCredits: (amountUsd) => this.request({ method: "POST", path: "/billing/credits", body: { amount_usd: amountUsd } }),
1433
+ settleCredits: (sessionId) => {
1434
+ if (!sessionId?.trim())
1435
+ throw new InvalidArgumentError("billing.settleCredits needs a session id", "Pass the session_id buyCredits() answered.");
1436
+ return this.request({ method: "POST", path: `/billing/credits/${encodeURIComponent(sessionId)}/settle`, body: {} });
1437
+ },
1438
+ autoReload: () => this.request({ method: "GET", path: "/billing/auto-reload" }),
1439
+ setAutoReload: (opts) => this.request({
1440
+ method: "PUT",
1441
+ path: "/billing/auto-reload",
1442
+ body: {
1443
+ on: opts.on,
1444
+ ...(opts.thresholdUsd === undefined ? {} : { threshold_usd: opts.thresholdUsd }),
1445
+ ...(opts.amountUsd === undefined ? {} : { amount_usd: opts.amountUsd }),
1446
+ ...(opts.authorizationVersion === undefined ? {} : { authorization_version: opts.authorizationVersion }),
1447
+ },
1448
+ }),
1449
+ repayDispute: (disputeId) => {
1450
+ if (!disputeId?.trim())
1451
+ throw new InvalidArgumentError("billing.repayDispute needs a dispute id", "Pass the hold's dispute_id from billing.usage().");
1452
+ return this.request({ method: "POST", path: `/billing/disputes/${encodeURIComponent(disputeId)}/repay`, body: {} });
1453
+ },
1349
1454
  checkout: () => this.request({ method: "POST", path: "/billing/checkout", body: {} }),
1350
1455
  portal: () => this.request({ method: "POST", path: "/billing/portal", body: {} }),
1351
1456
  referrals: () => this.request({ method: "GET", path: "/referrals" }),
@@ -1404,6 +1509,9 @@ export class PromigenceClient {
1404
1509
  rename: (org, project, name) => this.request({
1405
1510
  method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: { name },
1406
1511
  }),
1512
+ update: async (org, project, update) => this.request({
1513
+ method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: projectUpdateBody(update),
1514
+ }),
1407
1515
  },
1408
1516
  };
1409
1517
  }
@@ -1620,12 +1728,8 @@ export class PromigenceClient {
1620
1728
  return { sandboxes: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
1621
1729
  },
1622
1730
  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
- });
1731
+ const endedSince = {};
1732
+ const r = await this.waitLoop(opts, LONG_POLL_MS, { run_id: runId, state: "running" }, (read) => this.runs.get(runId, read), (run) => runWorkDone(run, endedSince));
1629
1733
  return { run: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
1630
1734
  },
1631
1735
  };
@@ -2034,10 +2138,19 @@ export class PromigenceClient {
2034
2138
  }), sandboxId);
2035
2139
  return r.ports ?? [];
2036
2140
  },
2037
- revoke: (sandboxId, tokenOrPort) => this.request({
2038
- method: "DELETE",
2039
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${encodeURIComponent(String(tokenOrPort))}`,
2040
- }),
2141
+ revoke: (sandboxId, tokenOrPort) => {
2142
+ const which = String(tokenOrPort).trim();
2143
+ return /^[0-9]+$/.test(which)
2144
+ ? this.request({
2145
+ method: "DELETE",
2146
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${which}`,
2147
+ })
2148
+ : this.request({
2149
+ method: "POST",
2150
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/revoke`,
2151
+ body: { token: which },
2152
+ });
2153
+ },
2041
2154
  },
2042
2155
  ssh: {
2043
2156
  open: (sandboxId, opts) => {
@@ -2152,6 +2265,7 @@ export class PromigenceClient {
2152
2265
  ...(opts.spendCap === undefined ? {} : { spend_cap: checkSpendCap(opts.spendCap) }),
2153
2266
  },
2154
2267
  }),
2268
+ resumePausedForCredits: () => this.request({ method: "POST", path: "/sandboxes/resume", body: { paused_for: "credits" } }),
2155
2269
  snapshot: (sandboxId, opts = {}) => this.request({
2156
2270
  method: "POST",
2157
2271
  path: `/sandboxes/${encodeURIComponent(sandboxId)}/snapshot`,
@@ -2222,38 +2336,42 @@ export class PromigenceClient {
2222
2336
  },
2223
2337
  files: {
2224
2338
  list: (sandboxId, path = "/", opts = {}) => {
2225
- const q = [`path=${encodeURIComponent(path)}`];
2339
+ const body = { path };
2226
2340
  if (opts.recursive)
2227
- q.push("recursive=1");
2341
+ body.recursive = true;
2228
2342
  if (opts.depth !== undefined)
2229
- q.push(`depth=${Math.max(0, Math.floor(opts.depth))}`);
2343
+ body.depth = Math.max(0, Math.floor(opts.depth));
2230
2344
  if (opts.limit !== undefined)
2231
- q.push(`limit=${Math.max(1, Math.floor(opts.limit))}`);
2345
+ body.limit = Math.max(1, Math.floor(opts.limit));
2232
2346
  if (opts.hidden === false)
2233
- q.push("hidden=0");
2347
+ body.hidden = false;
2234
2348
  return this.request({
2235
- method: "GET",
2236
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/list?${q.join("&")}`,
2349
+ method: "POST",
2350
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/list`,
2351
+ body,
2352
+ replayable: true,
2237
2353
  });
2238
2354
  },
2239
2355
  search: (sandboxId, path, opts = {}) => {
2240
2356
  if (!opts.pattern && !opts.contains) {
2241
2357
  throw new InvalidArgumentError("files.search needs `pattern`, `contains`, or both", "Pass { pattern: '*.py' } for a name glob, { contains: 'TODO' } for text inside files.");
2242
2358
  }
2243
- const q = [`path=${encodeURIComponent(path)}`];
2359
+ const body = { path };
2244
2360
  if (opts.pattern)
2245
- q.push(`pattern=${encodeURIComponent(opts.pattern)}`);
2361
+ body.pattern = opts.pattern;
2246
2362
  if (opts.contains)
2247
- q.push(`contains=${encodeURIComponent(opts.contains)}`);
2363
+ body.contains = opts.contains;
2248
2364
  if (opts.ignoreCase)
2249
- q.push("ignore_case=1");
2365
+ body.ignore_case = true;
2250
2366
  if (opts.limit !== undefined)
2251
- q.push(`limit=${Math.max(1, Math.floor(opts.limit))}`);
2367
+ body.limit = Math.max(1, Math.floor(opts.limit));
2252
2368
  if (opts.hidden === false)
2253
- q.push("hidden=0");
2369
+ body.hidden = false;
2254
2370
  return this.request({
2255
- method: "GET",
2256
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/search?${q.join("&")}`,
2371
+ method: "POST",
2372
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/search`,
2373
+ body,
2374
+ replayable: true,
2257
2375
  });
2258
2376
  },
2259
2377
  mkdir: (sandboxId, path, opts = {}) => this.request({
@@ -2310,11 +2428,13 @@ export class PromigenceClient {
2310
2428
  body: { archive, into, ...(opts.strip === undefined ? {} : { strip: Math.max(0, Math.floor(opts.strip)) }) },
2311
2429
  }),
2312
2430
  stat: (sandboxId, path) => this.request({
2313
- method: "GET",
2314
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/stat?path=${encodeURIComponent(path)}`,
2431
+ method: "POST",
2432
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files/stat`,
2433
+ body: { path },
2434
+ replayable: true,
2315
2435
  }),
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),
2436
+ get: (sandboxId, path, opts = {}) => this.bytes("POST", `/sandboxes/${encodeURIComponent(sandboxId)}/files/read`, jsonBody({ path }), opts, true),
2437
+ download: (sandboxId, path, onChunk, opts = {}) => this.stream("POST", `/sandboxes/${encodeURIComponent(sandboxId)}/files/read`, onChunk, opts, jsonBody({ path })),
2318
2438
  text: async (sandboxId, path, opts = {}) => new TextDecoder().decode(await this.sandboxes.files.get(sandboxId, path, opts)),
2319
2439
  put: async (sandboxId, path, data, opts = {}) => {
2320
2440
  const body = typeof data === "string" ? new TextEncoder().encode(data) : data;
@@ -2324,8 +2444,7 @@ export class PromigenceClient {
2324
2444
  }, opts);
2325
2445
  },
2326
2446
  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}`;
2447
+ const url = `/sandboxes/${encodeURIComponent(sandboxId)}/files`;
2329
2448
  let sent = 0;
2330
2449
  let chunks = 0;
2331
2450
  let last;
@@ -2334,7 +2453,12 @@ export class PromigenceClient {
2334
2453
  if (slice.length === 0 && sent < source.size) {
2335
2454
  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
2455
  }
2337
- const raw = await this.bytes("PUT", sent === 0 ? base : `${base}&offset=${sent}`, slice, opts);
2456
+ const fields = [["path", path]];
2457
+ if (opts.mode !== undefined)
2458
+ fields.push(["mode", opts.mode.toString(8)]);
2459
+ if (sent > 0)
2460
+ fields.push(["offset", String(sent)]);
2461
+ const raw = await this.bytes("PUT", url, fileForm(fields, slice), opts);
2338
2462
  last = JSON.parse(new TextDecoder().decode(raw));
2339
2463
  sent += slice.length;
2340
2464
  chunks += 1;
@@ -2343,8 +2467,8 @@ export class PromigenceClient {
2343
2467
  },
2344
2468
  delete: (sandboxId, path, opts = {}) => this.request({
2345
2469
  method: "DELETE",
2346
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/files?path=${encodeURIComponent(path)}`
2347
- + (opts.recursive ? "&recursive=1" : ""),
2470
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/files`,
2471
+ body: { path, ...(opts.recursive ? { recursive: true } : {}) },
2348
2472
  }),
2349
2473
  },
2350
2474
  output: async (execId, opts = {}) => {
@@ -2523,10 +2647,10 @@ export class PromigenceClient {
2523
2647
  throw new InvalidArgumentError("snapshots.delete needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
2524
2648
  return this.request({ method: "DELETE", path: `/snapshots/${seg(idOrAlias)}` });
2525
2649
  },
2526
- alias: (idOrAlias, alias) => this.request({
2650
+ alias: (idOrAlias, alias, opts = {}) => this.request({
2527
2651
  method: "PUT",
2528
2652
  path: `/snapshots/${seg(idOrAlias)}/alias`,
2529
- body: { alias },
2653
+ body: { alias, ...(alias !== null && opts.moveAlias ? { move_alias: true } : {}) },
2530
2654
  }),
2531
2655
  waitBuild: async (buildId, opts = {}) => {
2532
2656
  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");
@@ -18,6 +18,7 @@ export declare class CommandTimeoutError extends CommandExitError {
18
18
  readonly code: string;
19
19
  readonly fix: string;
20
20
  readonly guide: string;
21
+ readonly docs: string;
21
22
  constructor(result: CommandResult, timeoutMs: number | undefined);
22
23
  }
23
24
  /**
@@ -1,4 +1,4 @@
1
- import { CommandExitError } from "./core.js";
1
+ import { CommandExitError, errorsDocsFor } from "./core.js";
2
2
  import { hintArg } from "./hint.js";
3
3
  export function secondsText(ms) {
4
4
  const s = ms / 1000;
@@ -10,9 +10,11 @@ export class CommandTimeoutError extends CommandExitError {
10
10
  code;
11
11
  fix;
12
12
  guide;
13
+ docs;
13
14
  constructor(result, timeoutMs) {
14
15
  super(result);
15
16
  this.code = "task_timeout";
17
+ this.docs = errorsDocsFor(this.code);
16
18
  this.timeoutMs = timeoutMs;
17
19
  this.message = timeoutMs === undefined
18
20
  ? `command timed out (killed at its timeout, exit ${result.exitCode})`
package/dist/context.d.ts CHANGED
@@ -15,9 +15,11 @@
15
15
  * - Empty directories and permission bits travel as they are on disk; a set-user-ID or
16
16
  * set-group-ID bit is cleared, with a note.
17
17
  * - Files that look like credentials (`.env`, private keys, cloud credentials, ...) are refused,
18
- * by name, unless `allowSecrets` says otherwise. The check goes by file name: it is
19
- * best-effort and cannot see what a file holds, so look over the list (`listContext`) before
20
- * you send a directory.
18
+ * by name, unless `allowSecretFiles` names them (each one) or `allowSecrets` says otherwise. The
19
+ * check goes by file name and reads only two kinds of file, to clear the plain false
20
+ * positives: a `.pem` holding certificates or public keys and no private key, and an `.npmrc`
21
+ * with no token or password in it. It is best-effort, so look over the list (`listContext`)
22
+ * before you send a directory.
21
23
  * - Symbolic links travel as links, never as the file they point at.
22
24
  */
23
25
  /** The prefix a packed directory travels under in `dockerfile.context`. */
@@ -65,9 +67,17 @@ export declare function dockerignoreMatcher(text: string): {
65
67
  /**
66
68
  * Does this path look like a credential (`.env`, a private key, cloud or registry credentials, a
67
69
  * token file, ...)? By name only: best-effort, it cannot see what a file holds. Templates such as
68
- * `.env.example` are not.
70
+ * `.env.example` are not. [`secretLikeFile`] also reads the few files whose contents can clear them.
69
71
  */
70
72
  export declare function secretLike(path: string): boolean;
73
+ /** The most of a file [`secretLikeFile`] reads; a larger one is judged by its name alone. */
74
+ export declare const CONTENT_CHECK_MAX_BYTES: number;
75
+ /**
76
+ * [`secretLike`] for a file on disk at `abs` (`rel` is its path in the upload): a name that looks
77
+ * like a credential, unless what the file holds clears it ([`clearedByContent`]). A link, a file
78
+ * over [`CONTENT_CHECK_MAX_BYTES`] and a file that cannot be read keep the name's verdict.
79
+ */
80
+ export declare function secretLikeFile(rel: string, abs: string): boolean;
71
81
  /** What a build context is packed from and sent with. */
72
82
  export interface ContextOptions {
73
83
  /**
@@ -84,6 +94,11 @@ export interface ContextOptions {
84
94
  * nothing is sent.
85
95
  */
86
96
  allowSecrets?: boolean;
97
+ /**
98
+ * Send these files that look like credentials, and only these: paths inside the context,
99
+ * `/`-separated, as `listContext` names them. Every other one is still refused.
100
+ */
101
+ allowSecretFiles?: readonly string[];
87
102
  /** Called with each note: what was left out by default, or changed on the way. */
88
103
  onNote?: (note: string) => void;
89
104
  }