@promigence/sdk 0.1.0-beta.15 → 0.1.0-beta.17

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PromigenceClient = exports.LONG_POLL_MS = exports.isKeyedCreate = exports.REPO_CREDENTIALS_FIX = exports.CONTEXT_GIT_RE = exports.IMAGE_DIGEST_RE = exports.MAX_SOURCES = exports.NOTICE_HEADER = exports.OUTPUT_RETENTION_MAX_DAYS = exports.isWaiting = exports.FILE_TIMEOUT_MS = void 0;
3
+ exports.PromigenceClient = exports.RUN_SETTLE_MS = exports.LONG_POLL_MS = exports.isKeyedCreate = exports.REPO_CREDENTIALS_FIX = exports.CONTEXT_GIT_RE = exports.IMAGE_DIGEST_RE = exports.NO_KEY_FIX = exports.NO_KEY_MESSAGE = exports.MAX_SOURCES = exports.NOTICE_HEADER = exports.OUTPUT_RETENTION_MAX_DAYS = exports.isWaiting = exports.FILE_TIMEOUT_MS = void 0;
4
4
  exports.execOutputBytes = execOutputBytes;
5
5
  exports.configDir = configDir;
6
6
  exports.credentialsPath = credentialsPath;
@@ -10,9 +10,6 @@ exports.storedCredentials = storedCredentials;
10
10
  exports.storedCredentialsText = storedCredentialsText;
11
11
  exports.readCredentialsWithStore = readCredentialsWithStore;
12
12
  exports.readCredentials = readCredentials;
13
- exports.apiUrlFromCompatUrl = apiUrlFromCompatUrl;
14
- exports.apiUrlFromCompatDomain = apiUrlFromCompatDomain;
15
- exports.compatApiKey = compatApiKey;
16
13
  exports.keyOrigin = keyOrigin;
17
14
  exports.imageRefProblem = imageRefProblem;
18
15
  exports.checkImageRef = checkImageRef;
@@ -27,6 +24,7 @@ exports.errorFor = errorFor;
27
24
  exports.checkSandboxId = checkSandboxId;
28
25
  exports.checkSpendCap = checkSpendCap;
29
26
  exports.newIdempotencyKey = newIdempotencyKey;
27
+ exports.runWorkDone = runWorkDone;
30
28
  exports.serverAppMs = serverAppMs;
31
29
  exports.toCommandResult = toCommandResult;
32
30
  const node_fs_1 = require("node:fs");
@@ -48,6 +46,7 @@ Object.defineProperty(exports, "isKeyedCreate", { enumerable: true, get: functio
48
46
  const dispatcher_js_1 = require("./dispatcher.js");
49
47
  const ca_guard_js_1 = require("./ca-guard.js");
50
48
  const hint_js_1 = require("./hint.js");
49
+ const command_errors_js_1 = require("./command-errors.js");
51
50
  const github_js_1 = require("./github.js");
52
51
  const gitlab_js_1 = require("./gitlab.js");
53
52
  exports.FILE_TIMEOUT_MS = 15 * 60_000;
@@ -126,27 +125,46 @@ function pageQuery(limit, max, cursor) {
126
125
  }
127
126
  exports.OUTPUT_RETENTION_MAX_DAYS = 30;
128
127
  function projectUpdateBody(u) {
128
+ return settingsBody(u, "projects.update", "the organization's");
129
+ }
130
+ function orgUpdateBody(u) {
131
+ return settingsBody(u, "orgs.update", "the default");
132
+ }
133
+ function settingsBody(u, what, fallback) {
129
134
  const body = {};
130
135
  if (u.name !== undefined)
131
136
  body["name"] = u.name;
132
137
  if (u.outputRetentionDays !== undefined) {
133
138
  const d = u.outputRetentionDays;
134
139
  if (d !== null && (!Number.isInteger(d) || d < 1 || d > exports.OUTPUT_RETENTION_MAX_DAYS)) {
135
- throw new core_js_1.InvalidArgumentError(`outputRetentionDays must be a whole number from 1 to ${exports.OUTPUT_RETENTION_MAX_DAYS}, or null, not ${String(d)}`, "Pass null for the organization's setting.");
140
+ throw new core_js_1.InvalidArgumentError(`outputRetentionDays must be a whole number from 1 to ${exports.OUTPUT_RETENTION_MAX_DAYS}, or null, not ${String(d)}`, `Pass null for ${fallback} setting.`);
136
141
  }
137
142
  body["output_retention_days"] = d;
138
143
  }
139
144
  if (u.failureCaptures !== undefined) {
140
145
  if (u.failureCaptures !== null && typeof u.failureCaptures !== "boolean") {
141
- throw new core_js_1.InvalidArgumentError("failureCaptures must be true, false or null", "Pass null for the organization's setting.");
146
+ throw new core_js_1.InvalidArgumentError("failureCaptures must be true, false or null", `Pass null for ${fallback} setting.`);
142
147
  }
143
148
  if (Object.keys(body).length) {
144
149
  throw new core_js_1.InvalidArgumentError("failureCaptures is set on its own", "Change it in a call of its own, without name or outputRetentionDays.");
145
150
  }
146
151
  body["failure_captures"] = u.failureCaptures;
147
152
  }
153
+ if (u.previewLinkLength !== undefined) {
154
+ const l = u.previewLinkLength;
155
+ if (l === "custom") {
156
+ throw new core_js_1.InvalidArgumentError("custom domains for preview links are coming soon", `Pass "short", "long" or null (${fallback}) for now.`);
157
+ }
158
+ if (l !== null && l !== "short" && l !== "long") {
159
+ throw new core_js_1.InvalidArgumentError(`previewLinkLength must be "short", "long" or null, not ${JSON.stringify(l)}`, `Pass null for ${fallback} setting.`);
160
+ }
161
+ if (Object.keys(body).length) {
162
+ throw new core_js_1.InvalidArgumentError("previewLinkLength is set on its own", "Change it in a call of its own, without the other settings.");
163
+ }
164
+ body["preview_link_length"] = l;
165
+ }
148
166
  if (!Object.keys(body).length) {
149
- throw new core_js_1.InvalidArgumentError("projects.update needs something to change", "Pass name, outputRetentionDays or failureCaptures.");
167
+ throw new core_js_1.InvalidArgumentError(`${what} needs something to change`, "Pass name, outputRetentionDays, failureCaptures or previewLinkLength.");
150
168
  }
151
169
  return body;
152
170
  }
@@ -158,6 +176,14 @@ function expiryFields(o) {
158
176
  body["expires_at"] = o.expiresAt;
159
177
  return body;
160
178
  }
179
+ function checkPreviewLength(length) {
180
+ if (length === undefined || length === "short" || length === "long")
181
+ return;
182
+ if (length === "custom") {
183
+ throw new core_js_1.InvalidArgumentError("custom domains for preview links are coming soon", "Pass length: \"short\" (the default) or \"long\" for now.");
184
+ }
185
+ throw new core_js_1.InvalidArgumentError(`length must be "short" or "long", got ${JSON.stringify(length)}`, "Omit it for the length your project or organization chose.");
186
+ }
161
187
  exports.NOTICE_HEADER = "promigence-notice";
162
188
  exports.MAX_SOURCES = 8;
163
189
  function configDir(env = process.env) {
@@ -309,30 +335,6 @@ function readCredentials(env = process.env) {
309
335
  function storedName(store, env) {
310
336
  return store === undefined || store === "file" ? `the credentials file (${credentialsPath(env)})` : (0, credstore_js_1.storeLabel)(store);
311
337
  }
312
- const COMPAT_REFUSED_HOSTS = /(^|\.)e2b(-staging)?\.(app|dev|pro)$/i;
313
- function apiUrlFromCompatUrl(url) {
314
- const u = (url ?? "").trim();
315
- if (!u)
316
- return undefined;
317
- let host = "";
318
- try {
319
- host = new URL(u.includes("://") ? u : `https://${u}`).hostname;
320
- }
321
- catch {
322
- return undefined;
323
- }
324
- return COMPAT_REFUSED_HOSTS.test(host) ? undefined : u;
325
- }
326
- function apiUrlFromCompatDomain(domain) {
327
- const d = (domain ?? "").trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
328
- if (!d || COMPAT_REFUSED_HOSTS.test(d))
329
- return undefined;
330
- return `https://api.${d}/v1`;
331
- }
332
- function compatApiKey(key) {
333
- const k = key?.trim();
334
- return k && !k.startsWith("e2b_") ? k : undefined;
335
- }
336
338
  function keyOrigin(env = process.env) {
337
339
  if (env["PROMIGENCE_API_KEY"]?.trim())
338
340
  return "PROMIGENCE_API_KEY";
@@ -359,12 +361,13 @@ function resolveCredentials(opts, env = process.env) {
359
361
  const inputs = optKey === undefined && envKey === undefined
360
362
  ? undefined
361
363
  : `${optKey ?? "\0"}\n${envKey ?? "\0"}\n${envUrl ?? "\0"}\n${opts.apiUrl ?? "\0"}`;
362
- if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined)
363
- return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl };
364
+ if (inputs !== undefined && inputs === credsLastInputs && credsLast !== undefined) {
365
+ return { apiKey: credsLast.apiKey, apiUrl: credsLast.apiUrl, ...(credsLast.origin ? { origin: credsLast.origin } : {}) };
366
+ }
364
367
  const worked = resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl);
365
368
  if (inputs !== undefined && worked.refusal === undefined) {
366
369
  credsLastInputs = inputs;
367
- credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl };
370
+ credsLast = { apiKey: worked.apiKey, apiUrl: worked.apiUrl, ...(worked.origin ? { origin: worked.origin } : {}) };
368
371
  }
369
372
  return worked;
370
373
  }
@@ -398,8 +401,15 @@ function resolveCredentialsFrom(opts, env, set, trim, optKey, envKey, envUrl) {
398
401
  if (refusal)
399
402
  return { apiKey: undefined, apiUrl, refusal };
400
403
  }
401
- return { apiKey: key, apiUrl };
404
+ const origin = !key ? undefined
405
+ : source === "opts" ? { source: "option" }
406
+ : source === "env" ? { source: "env" }
407
+ : source === "file" ? { source: "stored", ...(store === undefined ? {} : { store }) }
408
+ : { source: "fallback" };
409
+ return { apiKey: key, apiUrl, ...(origin ? { origin } : {}) };
402
410
  }
411
+ exports.NO_KEY_MESSAGE = "no API key: none was given, PROMIGENCE_API_KEY is not set, and none is stored";
412
+ exports.NO_KEY_FIX = "Pass { apiKey } or set PROMIGENCE_API_KEY. No key yet: `promigence login` signs you up or in and stores one.";
403
413
  function keyRefusal(key, from) {
404
414
  if (/^[\x21-\x7E]+$/.test(key))
405
415
  return undefined;
@@ -679,6 +689,8 @@ function snapshotCreateBody(opts) {
679
689
  }
680
690
  if (o.alias)
681
691
  body["alias"] = o.alias;
692
+ if (o.alias && o.moveAlias)
693
+ body["move_alias"] = true;
682
694
  if (o.grade) {
683
695
  if (!o.grade.cmd)
684
696
  throw new core_js_1.InvalidArgumentError("snapshots.create grade needs a cmd", "Pass { grade: { cmd: 'pytest -q tests/' } }.");
@@ -713,6 +725,9 @@ const REFUSED_503_CODES = new Set([
713
725
  "builder_unavailable", "builder_busy", "overloaded", "tier_unavailable", "registry_unavailable",
714
726
  "secrets_unavailable",
715
727
  ]);
728
+ const SDK_FIXES = {
729
+ cap_exceeded: "Raise `spendCap`, lower `timeoutMs` or `count`, or pass `allowPartial` to launch what fits.",
730
+ };
716
731
  function withoutCliHelp(fix) {
717
732
  const out = fix.replace(/\s*See `promigence [^`]*--help`\.?\s*$/, "").trim();
718
733
  if (out === fix.trim())
@@ -722,7 +737,7 @@ function withoutCliHelp(fix) {
722
737
  function errorFor(code, message, fix, extra = {}) {
723
738
  const attach = (e) => {
724
739
  const w = e;
725
- w.docs = extra.docs ?? `${core_js_1.ERRORS_DOCS_URL}#${code}`;
740
+ w.docs = extra.docs !== undefined && e.code === code ? extra.docs : (0, core_js_1.errorsDocsFor)(e.code);
726
741
  if (extra.status !== undefined)
727
742
  w.status = extra.status;
728
743
  if (extra.requestId !== undefined)
@@ -738,6 +753,8 @@ function errorFor(code, message, fix, extra = {}) {
738
753
  if ((code === "usage" || code === "invalid_argument" || code === "unsupported") && (0, core_js_1.previewsOff)(message)) {
739
754
  return attach(new core_js_1.PreviewsUnavailableError());
740
755
  }
756
+ if (!extra.cli && Object.prototype.hasOwnProperty.call(SDK_FIXES, code))
757
+ fix = SDK_FIXES[code];
741
758
  switch (code) {
742
759
  case "auth": return attach(new core_js_1.AuthError(message, fix));
743
760
  case "not_found": return attach(new core_js_1.NotFoundError(message, fix));
@@ -751,8 +768,13 @@ function errorFor(code, message, fix, extra = {}) {
751
768
  case "not_verified": return attach(new core_js_1.NotVerifiedError(message, fix));
752
769
  case "timeout": return attach(new core_js_1.TimeoutError(message, fix));
753
770
  case "usage":
754
- case "invalid_argument":
771
+ case "invalid_argument": {
772
+ const ended = code === "usage" ? (0, core_js_1.sandboxEndedIn)(message) : undefined;
773
+ if (ended !== undefined) {
774
+ return attach(new core_js_1.SandboxEndedError(message, ended.sandboxId, ended.state, extra.cli ? fix : core_js_1.SANDBOX_ENDED_FIX));
775
+ }
755
776
  return attach(new core_js_1.InvalidArgumentError(message, extra.cli ? fix : withoutCliHelp(fix), code));
777
+ }
756
778
  case "not_released": return attach(new core_js_1.NotReleasedError(message));
757
779
  case "service_restart": return attach(new core_js_1.ServiceRestartError(message, fix));
758
780
  default:
@@ -804,6 +826,76 @@ function parseRetryAfter(header) {
804
826
  const MAX_RETRY_SLEEP_MS = 10_000;
805
827
  const STREAM_IDLE_MS = 90_000;
806
828
  const sleep = (ms) => new Promise((r) => { setTimeout(r, ms); });
829
+ const EXEC_KEY_HEADER = "promigence-exec-key";
830
+ const EXEC_HOLD_DEFAULT_S = 120;
831
+ const REATTACH_SLACK_MS = 30_000;
832
+ const REATTACH_BACKOFF_MS = 250;
833
+ const REATTACH_BACKOFF_MAX_MS = 2_000;
834
+ const REATTACH_RETRY_AFTER_MAX_MS = 5_000;
835
+ const REATTACH_TAKEN_MS = 10_000;
836
+ const EXEC_CANCEL_TIMEOUT_MS = 3_000;
837
+ const REATTACH_AGAIN = new Set([429, 502, 503, 504]);
838
+ function newExecKey() {
839
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
840
+ let s = "";
841
+ for (const b of bytes)
842
+ s += b.toString(16).padStart(2, "0");
843
+ return s;
844
+ }
845
+ const UNSENT_CODES = /^(?:ECONNREFUSED|ENOTFOUND|EAI_[A-Z]+|ENETUNREACH|EHOSTUNREACH|UND_ERR_CONNECT_TIMEOUT|ERR_TLS_[A-Z_]+|ERR_SSL_[A-Z_]+|CERT_[A-Z_]+|UNABLE_TO_[A-Z_]+|DEPTH_ZERO_SELF_SIGNED_CERT|SELF_SIGNED_CERT_IN_CHAIN|HOSTNAME_MISMATCH|EPROTO)$/;
846
+ function provedUnsent(err) {
847
+ if ((0, ca_guard_js_1.compromisedKeyCause)(err) !== undefined)
848
+ return true;
849
+ const top = err;
850
+ if (top?.name === "TypeError" && /header/i.test(top.message ?? ""))
851
+ return true;
852
+ const unsent = (x) => {
853
+ const code = x?.code;
854
+ return typeof code === "string" && UNSENT_CODES.test(code);
855
+ };
856
+ let c = err;
857
+ for (let depth = 0; c !== null && typeof c === "object" && depth < 6; depth++) {
858
+ if (unsent(c))
859
+ return true;
860
+ const each = c.errors;
861
+ if (Array.isArray(each) && each.length > 0 && each.every(unsent))
862
+ return true;
863
+ c = c.cause;
864
+ }
865
+ return false;
866
+ }
867
+ class HeldDrop extends Error {
868
+ key;
869
+ constructor(key, why) {
870
+ super(why);
871
+ this.key = key;
872
+ this.name = "HeldDrop";
873
+ }
874
+ }
875
+ function sleepUnless(ms, signal) {
876
+ if (signal === undefined)
877
+ return sleep(ms);
878
+ return new Promise((resolve) => {
879
+ if (signal.aborted) {
880
+ resolve();
881
+ return;
882
+ }
883
+ const done = () => { clearTimeout(t); signal.removeEventListener("abort", done); resolve(); };
884
+ const t = setTimeout(done, ms);
885
+ signal.addEventListener("abort", done, { once: true });
886
+ });
887
+ }
888
+ function reattachWait(attempt, retryAfterMs) {
889
+ if (retryAfterMs !== undefined)
890
+ return Math.min(retryAfterMs, REATTACH_RETRY_AFTER_MAX_MS);
891
+ return Math.min(REATTACH_BACKOFF_MAX_MS, REATTACH_BACKOFF_MS * 2 ** Math.min(attempt, 8)) * (0.5 + Math.random() * 0.5);
892
+ }
893
+ function stoppedUnattended(answer) {
894
+ return answer?.reason === "not_reattached";
895
+ }
896
+ function abortedByCaller(method, path) {
897
+ return new core_js_1.TimeoutError(`request ${method} ${path} was aborted by the caller's signal`, "Nothing to fix: the caller aborted it.");
898
+ }
807
899
  const POLLING_TICK_MS = 5;
808
900
  const PARKED_TICK_MS = 50;
809
901
  async function keepingLoopAwake(everyMs, fn) {
@@ -822,6 +914,21 @@ const WAIT_MS_CAP = 30_000;
822
914
  exports.LONG_POLL_MS = 5_000;
823
915
  const BUILD_POLL_MS = 10_000;
824
916
  const PROGRESS_POLL_MS = 200;
917
+ exports.RUN_SETTLE_MS = 5_000;
918
+ function runWorkDone(run, endedSince, now = Date.now()) {
919
+ if (run.state === "completed" || run.state === "cap_killed" || run.state === "aborted")
920
+ return true;
921
+ const sbs = Array.isArray(run.sandboxes) ? run.sandboxes : [];
922
+ const allDone = sbs.length > 0 && sbs.every((s) => s.state === "ended" || s.command_outcome != null);
923
+ if (!allDone) {
924
+ delete endedSince.at;
925
+ return false;
926
+ }
927
+ if (sbs.some((s) => s.state !== "ended"))
928
+ return true;
929
+ endedSince.at ??= now;
930
+ return now - endedSince.at >= exports.RUN_SETTLE_MS;
931
+ }
825
932
  const WAITED_HEADER = "promigence-waited-ms";
826
933
  const seg = (s) => encodeURIComponent(s).replace(/%40/g, "@");
827
934
  function waitQuery(waitMs) {
@@ -859,6 +966,9 @@ class PromigenceClient {
859
966
  apiUrl;
860
967
  #apiKey;
861
968
  #keyRefusal;
969
+ #keyOrigin;
970
+ #onKeyUse;
971
+ #keyUseSaid = false;
862
972
  #bearer;
863
973
  fetchImpl;
864
974
  ownFetch;
@@ -880,6 +990,8 @@ class PromigenceClient {
880
990
  const resolved = resolveCredentials(opts);
881
991
  this.#apiKey = resolved.apiKey;
882
992
  this.#keyRefusal = resolved.refusal;
993
+ this.#keyOrigin = resolved.origin;
994
+ this.#onKeyUse = opts.onKeyUse;
883
995
  this.apiUrl = resolved.apiUrl;
884
996
  const env = opts.fetch === undefined ? (0, proxy_js_1.transportEnv)() : undefined;
885
997
  const proxy = env !== undefined ? (0, proxy_js_1.proxyFor)(this.apiUrl, env) : undefined;
@@ -945,11 +1057,18 @@ class PromigenceClient {
945
1057
  catch {
946
1058
  }
947
1059
  }
1060
+ get keyOrigin() {
1061
+ return this.#keyOrigin;
1062
+ }
948
1063
  requireKey() {
949
1064
  if (this.#keyRefusal)
950
1065
  throw this.#keyRefusal;
951
1066
  if (!this.#apiKey)
952
- throw new core_js_1.AuthError();
1067
+ throw this.cliErrors ? new core_js_1.AuthError(exports.NO_KEY_MESSAGE) : new core_js_1.AuthError(exports.NO_KEY_MESSAGE, exports.NO_KEY_FIX);
1068
+ if (!this.#keyUseSaid && this.#onKeyUse !== undefined && this.#keyOrigin !== undefined) {
1069
+ this.#keyUseSaid = true;
1070
+ this.#onKeyUse(this.#keyOrigin);
1071
+ }
953
1072
  return this.#apiKey;
954
1073
  }
955
1074
  bearer() {
@@ -997,6 +1116,82 @@ class PromigenceClient {
997
1116
  await sleep(backoff * (0.5 + Math.random() * 0.5));
998
1117
  return true;
999
1118
  }
1119
+ async heldExec(sandboxId, req) {
1120
+ const signal = req.signal;
1121
+ const abortedBefore = signal?.aborted === true;
1122
+ let key;
1123
+ try {
1124
+ let res;
1125
+ try {
1126
+ res = await this.request({ ...req, held: { onKey: (k) => { key = k; } } });
1127
+ }
1128
+ catch (err) {
1129
+ if (!(err instanceof HeldDrop))
1130
+ throw err;
1131
+ res = await this.reattachBuffered(sandboxId, err.key, req);
1132
+ }
1133
+ if (stoppedUnattended(res)) {
1134
+ throw new core_js_1.ExecLostError(`the command on sandbox ${sandboxId} was stopped: its connection to the service dropped and no client came back for it in time`, { sandboxId }, "not_reattached");
1135
+ }
1136
+ return res;
1137
+ }
1138
+ catch (err) {
1139
+ if (key !== undefined && signal?.aborted && !abortedBefore)
1140
+ await this.cancelHeldExec(sandboxId, key);
1141
+ throw err;
1142
+ }
1143
+ }
1144
+ async reattachBuffered(sandboxId, key, req) {
1145
+ const budgetMs = EXEC_HOLD_DEFAULT_S * 1000 + REATTACH_SLACK_MS;
1146
+ let droppedAt = Date.now();
1147
+ for (let attempt = 0;; attempt++) {
1148
+ if (req.signal?.aborted)
1149
+ throw abortedByCaller(req.method, req.path);
1150
+ this.stats.retries++;
1151
+ const sentAt = Date.now();
1152
+ try {
1153
+ return await this.request({
1154
+ method: "POST",
1155
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/attach`,
1156
+ body: { key },
1157
+ once: true,
1158
+ ...(req.timeoutMs === undefined ? {} : { timeoutMs: req.timeoutMs }),
1159
+ ...(req.signal === undefined ? {} : { signal: req.signal }),
1160
+ });
1161
+ }
1162
+ catch (err) {
1163
+ if (req.signal?.aborted)
1164
+ throw abortedByCaller(req.method, req.path);
1165
+ const status = err instanceof core_js_1.PromigenceError ? err.status : undefined;
1166
+ if (status === 404) {
1167
+ throw new core_js_1.ExecLostError(`the connection to the service dropped before the command on sandbox ${sandboxId} answered, and it could not be picked up again: nothing is kept for it (it ended, it never started, or this service cannot keep it)`, { sandboxId }, "gone");
1168
+ }
1169
+ const lostConnection = status === undefined && (err instanceof core_js_1.UnreachableError || err instanceof core_js_1.TimeoutError);
1170
+ if (!lostConnection && !(status !== undefined && REATTACH_AGAIN.has(status)))
1171
+ throw err;
1172
+ if (Date.now() - sentAt >= REATTACH_TAKEN_MS)
1173
+ droppedAt = Date.now();
1174
+ const left = budgetMs - (Date.now() - droppedAt);
1175
+ if (left <= 0) {
1176
+ throw new core_js_1.ExecLostError(`the connection to the service dropped while the command on sandbox ${sandboxId} was running, and the service could not be reached again in time: the command is stopped ${EXEC_HOLD_DEFAULT_S} s after the drop`, { sandboxId }, "unreachable");
1177
+ }
1178
+ await sleepUnless(Math.min(left, reattachWait(attempt, err instanceof core_js_1.PromigenceError ? err.retryAfterMs : undefined)), req.signal);
1179
+ }
1180
+ }
1181
+ }
1182
+ async cancelHeldExec(sandboxId, key) {
1183
+ try {
1184
+ await this.request({
1185
+ method: "POST",
1186
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/execs/cancel`,
1187
+ body: { key },
1188
+ timeoutMs: EXEC_CANCEL_TIMEOUT_MS,
1189
+ once: true,
1190
+ });
1191
+ }
1192
+ catch {
1193
+ }
1194
+ }
1000
1195
  async request(opts) {
1001
1196
  const tr = (0, trace_js_1.traceBegin)(opts.method, opts.path);
1002
1197
  const url = `${this.apiUrl}${opts.path}`;
@@ -1027,9 +1222,16 @@ class PromigenceClient {
1027
1222
  const timeoutMs = opts.timeoutMs ?? this.requestTimeoutMs;
1028
1223
  let dispatcher;
1029
1224
  let resentDelete = false;
1225
+ const maxRetries = opts.once === true ? 0 : this.maxRetries;
1226
+ let execKey;
1030
1227
  for (let attempt = 0;; attempt++) {
1031
1228
  let res;
1032
1229
  this.stats.requests++;
1230
+ if (opts.held !== undefined) {
1231
+ execKey = newExecKey();
1232
+ headers[EXEC_KEY_HEADER] = execKey;
1233
+ opts.held.onKey(execKey);
1234
+ }
1033
1235
  const sentAt = performance.now();
1034
1236
  try {
1035
1237
  if (this.h2) {
@@ -1072,11 +1274,14 @@ class PromigenceClient {
1072
1274
  }
1073
1275
  catch (err) {
1074
1276
  const e = err;
1277
+ if (execKey !== undefined && !opts.signal?.aborted && !provedUnsent(err)) {
1278
+ throw new HeldDrop(execKey, transportDetail(e));
1279
+ }
1075
1280
  if (keyedCreate && idempotencyKey && await this.againKeyed(attempt, opts.signal))
1076
1281
  continue;
1077
1282
  const cutOff = e?.cause?.code === "UND_ERR_HEADERS_TIMEOUT" || e?.cause?.code === "UND_ERR_BODY_TIMEOUT";
1078
1283
  const timedOut = e?.name === "TimeoutError" || e?.name === "AbortError" || cutOff;
1079
- if (!timedOut && replayable && attempt < this.maxRetries && !opts.signal?.aborted) {
1284
+ if (!timedOut && replayable && attempt < maxRetries && !opts.signal?.aborted) {
1080
1285
  this.stats.retries++;
1081
1286
  if (opts.method === "DELETE")
1082
1287
  resentDelete = true;
@@ -1084,9 +1289,8 @@ class PromigenceClient {
1084
1289
  await sleep(backoff * (0.5 + Math.random() * 0.5));
1085
1290
  continue;
1086
1291
  }
1087
- if (opts.signal?.aborted) {
1088
- throw new core_js_1.TimeoutError(`request ${opts.method} ${opts.path} was aborted by the caller's signal`, "Nothing to fix: the caller aborted it.");
1089
- }
1292
+ if (opts.signal?.aborted)
1293
+ throw abortedByCaller(opts.method, opts.path);
1090
1294
  if (e?.name === "TimeoutError" || e?.name === "AbortError") {
1091
1295
  throw new core_js_1.TimeoutError(`request ${opts.method} ${opts.path} timed out after ${timeoutMs} ms`, "Retry. To allow longer: promigence --request-timeout 120s <command>, " +
1092
1296
  "PROMIGENCE_REQUEST_TIMEOUT=120s, or requestTimeoutMs in the SDK. A --wait is bounded " +
@@ -1103,6 +1307,11 @@ class PromigenceClient {
1103
1307
  const gateway = (res.status === 502 || res.status === 504) && replayable;
1104
1308
  let retryable = (res.status === 429 || res.status === 503 || gateway) && (retryAfterMs ?? 0) <= MAX_RETRY_SLEEP_MS;
1105
1309
  let body;
1310
+ if (execKey !== undefined && (res.status === 502 || res.status === 504)) {
1311
+ body = await res.text().catch(() => "");
1312
+ if (errorCodeOf(body) === undefined)
1313
+ throw new HeldDrop(execKey, `HTTP ${res.status}`);
1314
+ }
1106
1315
  if (retryable && res.status === 503 && !replayable && retryAfterMs === undefined) {
1107
1316
  body = await res.text().catch(() => "");
1108
1317
  retryable = REFUSED_503_CODES.has(errorCodeOf(body) ?? "");
@@ -1111,7 +1320,7 @@ class PromigenceClient {
1111
1320
  body = await res.text().catch(() => "");
1112
1321
  retryable = errorCodeOf(body) === undefined;
1113
1322
  }
1114
- if (retryable && attempt < this.maxRetries) {
1323
+ if (retryable && attempt < maxRetries) {
1115
1324
  const backoff = Math.min(MAX_RETRY_SLEEP_MS, this.retryBaseMs * 2 ** attempt);
1116
1325
  const jitter = Math.random();
1117
1326
  const waitMs = retryAfterMs !== undefined
@@ -1144,8 +1353,10 @@ class PromigenceClient {
1144
1353
  text = "";
1145
1354
  else {
1146
1355
  const e = err;
1356
+ if (execKey !== undefined && !opts.signal?.aborted)
1357
+ throw new HeldDrop(execKey, transportDetail(e));
1147
1358
  const timedOut = e?.name === "TimeoutError" || e?.name === "AbortError";
1148
- if (!timedOut && replayable && attempt < this.maxRetries && !opts.signal?.aborted) {
1359
+ if (!timedOut && replayable && attempt < maxRetries && !opts.signal?.aborted) {
1149
1360
  this.stats.retries++;
1150
1361
  if (opts.method === "DELETE")
1151
1362
  resentDelete = true;
@@ -1271,10 +1482,12 @@ class PromigenceClient {
1271
1482
  continue;
1272
1483
  }
1273
1484
  const code = payload.error?.code ?? codeForStatus(res.status);
1485
+ const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
1274
1486
  throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
1275
1487
  status: res.status,
1276
1488
  ...bodyExtra(payload),
1277
1489
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
1490
+ ...(rid ? { requestId: rid } : {}),
1278
1491
  });
1279
1492
  }
1280
1493
  }
@@ -1354,10 +1567,12 @@ class PromigenceClient {
1354
1567
  continue;
1355
1568
  }
1356
1569
  const code = payload.error?.code ?? codeForStatus(res.status);
1570
+ const rid = headerOf(res, "request-id") ?? headerOf(res, "x-request-id");
1357
1571
  throw errorFor(code, payload.error?.message ?? `HTTP ${res.status} from ${method} ${path}`, payload.error?.fix ?? "See the API response for details.", {
1358
1572
  status: res.status,
1359
1573
  ...bodyExtra(payload),
1360
1574
  ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
1575
+ ...(rid ? { requestId: rid } : {}),
1361
1576
  });
1362
1577
  }
1363
1578
  }
@@ -1518,6 +1733,7 @@ class PromigenceClient {
1518
1733
  method: "PATCH", path: `/orgs/${orgSeg(org)}/projects/${encodeURIComponent(project)}`, body: projectUpdateBody(update),
1519
1734
  }),
1520
1735
  },
1736
+ update: async (org, update) => this.request({ method: "PATCH", path: `/orgs/${orgSeg(org)}`, body: orgUpdateBody(update) }),
1521
1737
  };
1522
1738
  }
1523
1739
  group_credits() {
@@ -1597,6 +1813,8 @@ class PromigenceClient {
1597
1813
  body["network"] = opts.network;
1598
1814
  if (opts.optimizeFor !== undefined)
1599
1815
  body["optimize_for"] = opts.optimizeFor;
1816
+ if (opts.capture !== undefined)
1817
+ body["capture"] = opts.capture;
1600
1818
  if (opts.secrets?.length)
1601
1819
  body["secrets"] = opts.secrets;
1602
1820
  if (opts.labels && Object.keys(opts.labels).length)
@@ -1733,12 +1951,8 @@ class PromigenceClient {
1733
1951
  return { sandboxes: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
1734
1952
  },
1735
1953
  waitDone: async (runId, opts = {}) => {
1736
- const r = await this.waitLoop(opts, exports.LONG_POLL_MS, { run_id: runId, state: "running" }, (read) => this.runs.get(runId, read), (run) => {
1737
- const terminal = run.state === "completed" || run.state === "cap_killed" || run.state === "aborted";
1738
- const allDone = Array.isArray(run.sandboxes) && run.sandboxes.length > 0 &&
1739
- run.sandboxes.every((s) => s.state === "ended" || s.command_outcome != null);
1740
- return terminal || allDone;
1741
- });
1954
+ const endedSince = {};
1955
+ const r = await this.waitLoop(opts, exports.LONG_POLL_MS, { run_id: runId, state: "running" }, (read) => this.runs.get(runId, read), (run) => runWorkDone(run, endedSince));
1742
1956
  return { run: r.value, timedOut: r.timedOut, waitedMs: r.waitedMs };
1743
1957
  },
1744
1958
  };
@@ -1834,7 +2048,9 @@ class PromigenceClient {
1834
2048
  req.idempotencyKey = opts.idempotencyKey;
1835
2049
  if (opts.signal)
1836
2050
  req.signal = opts.signal;
1837
- return this.request(req);
2051
+ if (opts.terminal)
2052
+ return this.request(req);
2053
+ return this.heldExec(sandboxId, req);
1838
2054
  },
1839
2055
  execStream: async (sandboxId, opts, on = {}) => {
1840
2056
  if (!opts.argv?.length && !opts.sh) {
@@ -1863,7 +2079,9 @@ class PromigenceClient {
1863
2079
  body["timeout_ms"] = commandTimeoutMs(opts.timeoutMs) ?? 15 * 60_000;
1864
2080
  body["max_output_bytes"] = opts.maxOutputBytes ?? core_js_1.MAX_OUTPUT_BYTES_DEFAULT;
1865
2081
  const url = `${this.apiUrl}/sandboxes/${encodeURIComponent(sandboxId)}/execs`;
1866
- const idle = new AbortController();
2082
+ const keyed = opts.terminal !== true;
2083
+ let key;
2084
+ let idle = new AbortController();
1867
2085
  let silent = false;
1868
2086
  let reader;
1869
2087
  let timer;
@@ -1875,23 +2093,33 @@ class PromigenceClient {
1875
2093
  reader?.cancel().catch(() => { });
1876
2094
  }, STREAM_IDLE_MS);
1877
2095
  };
2096
+ const fresh = () => { idle = new AbortController(); silent = false; reader = undefined; };
2097
+ const abortedBefore = opts.signal?.aborted === true;
2098
+ const abandon = async (err) => {
2099
+ if (key !== undefined && !abortedBefore)
2100
+ await this.cancelHeldExec(sandboxId, key);
2101
+ throw err;
2102
+ };
1878
2103
  let stream;
1879
2104
  const bearer = `Bearer ${this.requireKey()}`;
1880
2105
  const dispatcher = this.guardDispatcher();
2106
+ const headers = () => ({
2107
+ accept: "text/event-stream",
2108
+ "content-type": "application/json",
2109
+ "user-agent": this.userAgent,
2110
+ authorization: bearer,
2111
+ ...(this.caller ? { [caller_js_1.CALLER_HEADER]: this.caller } : {}),
2112
+ });
1881
2113
  for (let attempt = 0; stream === undefined; attempt++) {
1882
2114
  let r;
1883
2115
  this.stats.requests++;
2116
+ if (keyed)
2117
+ key = newExecKey();
1884
2118
  arm();
1885
2119
  try {
1886
2120
  r = await this.fetchImpl(url, {
1887
2121
  method: "POST",
1888
- headers: {
1889
- accept: "text/event-stream",
1890
- "content-type": "application/json",
1891
- "user-agent": this.userAgent,
1892
- authorization: bearer,
1893
- ...(this.caller ? { [caller_js_1.CALLER_HEADER]: this.caller } : {}),
1894
- },
2122
+ headers: { ...headers(), ...(key === undefined ? {} : { [EXEC_KEY_HEADER]: key }) },
1895
2123
  body: JSON.stringify(body),
1896
2124
  signal: opts.signal ? AbortSignal.any([opts.signal, idle.signal]) : idle.signal,
1897
2125
  ...(dispatcher === undefined ? {} : { dispatcher }),
@@ -1901,7 +2129,10 @@ class PromigenceClient {
1901
2129
  clearTimeout(timer);
1902
2130
  const e = err;
1903
2131
  const why = silent ? `no answer in ${STREAM_IDLE_MS / 1000} s` : transportDetail(e);
1904
- throw new core_js_1.UnreachableError(`cannot reach ${url}: ${why}`);
2132
+ const failed = new core_js_1.UnreachableError(`cannot reach ${url}: ${why}`);
2133
+ if (opts.signal?.aborted)
2134
+ return abandon(failed);
2135
+ throw failed;
1905
2136
  }
1906
2137
  if (r.ok && r.body) {
1907
2138
  stream = r.body;
@@ -1932,87 +2163,166 @@ class PromigenceClient {
1932
2163
  const errOut = [];
1933
2164
  let exit;
1934
2165
  let failure;
1935
- const dec = new TextDecoder();
1936
- let buf = "";
2166
+ let holdS;
2167
+ const received = { stdout: 0, stderr: 0 };
1937
2168
  const lost = (how) => new core_js_1.UnreachableError(`the exec stream for sandbox ${sandboxId} ${how}`, "The connection dropped before the command finished; its result is unknown. " +
1938
2169
  `Read it by id (\`promigence exec output <exec_id> --sandbox ${(0, hint_js_1.hintArg)(sandboxId)}\`) or re-run.`);
1939
- reader = stream.getReader();
1940
- try {
1941
- for (;;) {
2170
+ const consume = async (from) => {
2171
+ const dec = new TextDecoder();
2172
+ let buf = "";
2173
+ const rd = from.getReader();
2174
+ reader = rd;
2175
+ try {
2176
+ for (;;) {
2177
+ arm();
2178
+ let chunk;
2179
+ try {
2180
+ chunk = await rd.read();
2181
+ }
2182
+ catch (err) {
2183
+ if (opts.signal?.aborted)
2184
+ return abandon(err);
2185
+ if (holdS !== undefined && (exit || failure))
2186
+ return { done: true };
2187
+ if (silent)
2188
+ return { done: false, why: `was silent for ${STREAM_IDLE_MS / 1000} s`, restart: false };
2189
+ const e = err;
2190
+ return { done: false, why: `was cut off (${e?.cause?.code ?? e?.message ?? "connection lost"})`, restart: false };
2191
+ }
2192
+ if (chunk.done)
2193
+ break;
2194
+ const value = chunk.value;
2195
+ buf += dec.decode(value, { stream: true });
2196
+ let sep;
2197
+ while ((sep = buf.indexOf("\n\n")) >= 0) {
2198
+ const block = buf.slice(0, sep);
2199
+ buf = buf.slice(sep + 2);
2200
+ let event = "";
2201
+ let data = "";
2202
+ for (const line of block.split("\n")) {
2203
+ if (line.startsWith("event:"))
2204
+ event = line.slice(6).trim();
2205
+ else if (line.startsWith("data:"))
2206
+ data = line.slice(5).replace(/^ /, "");
2207
+ }
2208
+ if (!event)
2209
+ continue;
2210
+ if (event === "heartbeat") {
2211
+ on.heartbeat?.();
2212
+ continue;
2213
+ }
2214
+ if (event === "reconnect") {
2215
+ rd.cancel().catch(() => { });
2216
+ return { done: false, why: "was ended by a service restart", restart: true };
2217
+ }
2218
+ let parsed = {};
2219
+ try {
2220
+ parsed = JSON.parse(data);
2221
+ }
2222
+ catch { }
2223
+ if (event === "held") {
2224
+ const h = parsed["hold_s"];
2225
+ holdS = typeof h === "number" && Number.isFinite(h) && h >= 0 ? h : EXEC_HOLD_DEFAULT_S;
2226
+ }
2227
+ else if (event === "stdout" || event === "stderr") {
2228
+ const bytes = b64ToBytes(String(parsed["data"] ?? ""));
2229
+ if (event === "stdout") {
2230
+ received.stdout += bytes.length;
2231
+ out.push(bytes);
2232
+ on.stdout?.(bytes);
2233
+ }
2234
+ else {
2235
+ received.stderr += bytes.length;
2236
+ errOut.push(bytes);
2237
+ on.stderr?.(bytes);
2238
+ }
2239
+ }
2240
+ else if (event === "exit") {
2241
+ exit = { ...(exit ?? {}), ...parsed };
2242
+ }
2243
+ else if (event === "error") {
2244
+ failure = parsed;
2245
+ }
2246
+ }
2247
+ }
2248
+ }
2249
+ finally {
2250
+ clearTimeout(timer);
2251
+ }
2252
+ if (exit || failure)
2253
+ return { done: true };
2254
+ if (silent)
2255
+ return { done: false, why: `was silent for ${STREAM_IDLE_MS / 1000} s`, restart: false };
2256
+ return { done: false, why: "ended without an exit event", restart: false };
2257
+ };
2258
+ const reattach = async () => {
2259
+ const droppedAt = Date.now();
2260
+ const hold = holdS ?? EXEC_HOLD_DEFAULT_S;
2261
+ const budgetMs = hold * 1000 + REATTACH_SLACK_MS;
2262
+ for (let attempt = 0;; attempt++) {
2263
+ if (opts.signal?.aborted)
2264
+ return abandon(opts.signal.reason);
2265
+ fresh();
2266
+ this.stats.requests++;
2267
+ this.stats.retries++;
1942
2268
  arm();
1943
- let chunk;
2269
+ let retryAfterMs;
2270
+ let r;
1944
2271
  try {
1945
- chunk = await reader.read();
2272
+ r = await this.fetchImpl(`${url}/attach`, {
2273
+ method: "POST",
2274
+ headers: headers(),
2275
+ body: JSON.stringify({ key, stdout_offset: received.stdout, stderr_offset: received.stderr }),
2276
+ signal: opts.signal ? AbortSignal.any([opts.signal, idle.signal]) : idle.signal,
2277
+ ...(dispatcher === undefined ? {} : { dispatcher }),
2278
+ });
1946
2279
  }
1947
2280
  catch (err) {
2281
+ clearTimeout(timer);
1948
2282
  if (opts.signal?.aborted)
1949
- throw err;
1950
- if (silent)
1951
- throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
1952
- const e = err;
1953
- throw lost(`was cut off (${e?.cause?.code ?? e?.message ?? "connection lost"})`);
2283
+ return abandon(err);
1954
2284
  }
1955
- if (chunk.done)
1956
- break;
1957
- const value = chunk.value;
1958
- buf += dec.decode(value, { stream: true });
1959
- let sep;
1960
- while ((sep = buf.indexOf("\n\n")) >= 0) {
1961
- const block = buf.slice(0, sep);
1962
- buf = buf.slice(sep + 2);
1963
- let event = "";
1964
- let data = "";
1965
- for (const line of block.split("\n")) {
1966
- if (line.startsWith("event:"))
1967
- event = line.slice(6).trim();
1968
- else if (line.startsWith("data:"))
1969
- data = line.slice(5).replace(/^ /, "");
1970
- }
1971
- if (!event)
1972
- continue;
1973
- if (event === "heartbeat") {
1974
- on.heartbeat?.();
1975
- continue;
1976
- }
1977
- if (event === "reconnect") {
1978
- reader.cancel().catch(() => { });
1979
- throw new core_js_1.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.");
1980
- }
1981
- let parsed = {};
2285
+ if (r !== undefined) {
2286
+ if (r.ok && r.body)
2287
+ return r.body;
2288
+ clearTimeout(timer);
2289
+ const text = await r.text().catch(() => "");
2290
+ let payload = {};
1982
2291
  try {
1983
- parsed = JSON.parse(data);
2292
+ payload = text ? JSON.parse(text) : {};
1984
2293
  }
1985
2294
  catch { }
1986
- if (event === "stdout" || event === "stderr") {
1987
- const bytes = b64ToBytes(String(parsed["data"] ?? ""));
1988
- if (event === "stdout") {
1989
- out.push(bytes);
1990
- on.stdout?.(bytes);
1991
- }
1992
- else {
1993
- errOut.push(bytes);
1994
- on.stderr?.(bytes);
1995
- }
2295
+ retryAfterMs = parseRetryAfter(r.headers.get("retry-after"));
2296
+ if (r.status === 404) {
2297
+ throw new core_js_1.ExecLostError(`the connection to the service dropped while the command on sandbox ${sandboxId} was running, and it could not be picked up again: it is no longer kept (it ended, or this service cannot keep it)`, { sandboxId }, "gone");
1996
2298
  }
1997
- else if (event === "exit") {
1998
- exit = { ...(exit ?? {}), ...parsed };
1999
- }
2000
- else if (event === "error") {
2001
- failure = parsed;
2299
+ if (!REATTACH_AGAIN.has(r.status)) {
2300
+ throw errorFor(payload.error?.code ?? codeForStatus(r.status), payload.error?.message ?? `HTTP ${r.status} from POST /sandboxes/${sandboxId}/execs/attach`, payload.error?.fix ?? "See the API response for details.", { status: r.status, ...bodyExtra(payload), ...(retryAfterMs === undefined ? {} : { retryAfterMs }) });
2002
2301
  }
2003
2302
  }
2303
+ const left = budgetMs - (Date.now() - droppedAt);
2304
+ if (left <= 0) {
2305
+ throw new core_js_1.ExecLostError(`the connection to the service dropped while the command on sandbox ${sandboxId} was running, and the service could not be reached again in time: the command is stopped ${hold} s after the drop`, { sandboxId }, "unreachable");
2306
+ }
2307
+ await sleepUnless(Math.min(left, reattachWait(attempt, retryAfterMs)), opts.signal);
2004
2308
  }
2005
- }
2006
- finally {
2007
- clearTimeout(timer);
2309
+ };
2310
+ for (let ended = await consume(stream); !ended.done; ended = await consume(stream)) {
2311
+ if (holdS === undefined || key === undefined) {
2312
+ if (ended.restart) {
2313
+ throw new core_js_1.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.");
2314
+ }
2315
+ throw lost(ended.why);
2316
+ }
2317
+ stream = await reattach();
2008
2318
  }
2009
2319
  if (failure) {
2010
2320
  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 }) });
2011
2321
  }
2012
- if (!exit && silent)
2013
- throw lost(`was silent for ${STREAM_IDLE_MS / 1000} s`);
2014
- if (!exit) {
2322
+ if (!exit)
2015
2323
  throw lost("ended without an exit event");
2324
+ if (stoppedUnattended(exit)) {
2325
+ throw new core_js_1.ExecLostError(`the command on sandbox ${sandboxId} was stopped: its connection to the service dropped and no client came back for it ${holdS === undefined ? "in time" : `within ${holdS} s`}`, { sandboxId }, "not_reattached");
2016
2326
  }
2017
2327
  const rawCode = exit["exit_code"];
2018
2328
  const signal = exit["signal"];
@@ -2133,12 +2443,14 @@ class PromigenceClient {
2133
2443
  }),
2134
2444
  },
2135
2445
  ports: {
2136
- expose: (sandboxId, port) => {
2446
+ expose: (sandboxId, port, opts = {}) => {
2137
2447
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
2138
2448
  throw new core_js_1.InvalidArgumentError(`port must be an integer 1..65535, got ${port}`, "Pass the port your process listens on inside the sandbox, e.g. 3000.");
2139
2449
  }
2450
+ checkPreviewLength(opts.length);
2140
2451
  return (0, core_js_1.withPreviewsFix)(this.request({
2141
- method: "POST", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`, body: { port },
2452
+ method: "POST", path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports`,
2453
+ body: { port, ...(opts.length === undefined ? {} : { length: opts.length }) },
2142
2454
  }), sandboxId, port);
2143
2455
  },
2144
2456
  list: async (sandboxId) => {
@@ -2147,10 +2459,19 @@ class PromigenceClient {
2147
2459
  }), sandboxId);
2148
2460
  return r.ports ?? [];
2149
2461
  },
2150
- revoke: (sandboxId, tokenOrPort) => this.request({
2151
- method: "DELETE",
2152
- path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${encodeURIComponent(String(tokenOrPort))}`,
2153
- }),
2462
+ revoke: (sandboxId, tokenOrPort) => {
2463
+ const which = String(tokenOrPort).trim();
2464
+ return /^[0-9]+$/.test(which)
2465
+ ? this.request({
2466
+ method: "DELETE",
2467
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/${which}`,
2468
+ })
2469
+ : this.request({
2470
+ method: "POST",
2471
+ path: `/sandboxes/${encodeURIComponent(sandboxId)}/ports/revoke`,
2472
+ body: { token: which },
2473
+ });
2474
+ },
2154
2475
  },
2155
2476
  ssh: {
2156
2477
  open: (sandboxId, opts) => {
@@ -2647,10 +2968,10 @@ class PromigenceClient {
2647
2968
  throw new core_js_1.InvalidArgumentError("snapshots.delete needs a snapshot", "Pass a snap_ id, an alias, or alias@commit7.");
2648
2969
  return this.request({ method: "DELETE", path: `/snapshots/${seg(idOrAlias)}` });
2649
2970
  },
2650
- alias: (idOrAlias, alias) => this.request({
2971
+ alias: (idOrAlias, alias, opts = {}) => this.request({
2651
2972
  method: "PUT",
2652
2973
  path: `/snapshots/${seg(idOrAlias)}/alias`,
2653
- body: { alias },
2974
+ body: { alias, ...(alias !== null && opts.moveAlias ? { move_alias: true } : {}) },
2654
2975
  }),
2655
2976
  waitBuild: async (buildId, opts = {}) => {
2656
2977
  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");
@@ -2756,5 +3077,6 @@ function toCommandResult(r) {
2756
3077
  stderr: r.stderr ?? "",
2757
3078
  ...(r.truncated === undefined ? {} : { truncated: r.truncated }),
2758
3079
  ...(r.exec_id === undefined ? {} : { execId: r.exec_id }),
3080
+ timedOut: (0, command_errors_js_1.answerTimedOut)(r),
2759
3081
  };
2760
3082
  }