@gigadrive/sdk 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -466,6 +466,16 @@ var HttpClient = class {
466
466
  }
467
467
  return response;
468
468
  }
469
+ /**
470
+ * Fetch an arbitrary URL without authentication and without throwing on a
471
+ * non-2xx status, using the client's configured `fetch`. Used to probe
472
+ * public URLs such as microVM preview URLs.
473
+ *
474
+ * @internal
475
+ */
476
+ async fetchPublic(url, init) {
477
+ return this.fetchFn(url, init);
478
+ }
469
479
  async request(method, path, options) {
470
480
  const response = await this.fetchWithAuth(method, path, options);
471
481
  return this.handleResponse(response);
@@ -490,8 +500,9 @@ var HttpClient = class {
490
500
  }
491
501
  }
492
502
  const signal = options?.signal;
493
- const response = await this.fetchFn(url, { method, headers, body: requestBody, signal });
494
503
  const bodyIsOneShot = typeof ReadableStream !== "undefined" && requestBody instanceof ReadableStream;
504
+ const init = bodyIsOneShot ? { method, headers, body: requestBody, signal, duplex: "half" } : { method, headers, body: requestBody, signal };
505
+ const response = await this.fetchFn(url, init);
495
506
  if (response.status === 401 && !bodyIsOneShot) {
496
507
  this.tokenManager.invalidate();
497
508
  const retryToken = await this.tokenManager.getToken();
@@ -552,12 +563,265 @@ var HttpClient = class {
552
563
  }
553
564
  };
554
565
 
566
+ // src/resources/base-resource.ts
567
+ var BaseResource = class {
568
+ constructor(httpClient) {
569
+ this.httpClient = httpClient;
570
+ }
571
+ httpClient;
572
+ };
573
+
574
+ // src/microvms/duration.ts
575
+ var PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)$/;
576
+ var UNIT_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
577
+ var durationToMs = (value) => {
578
+ let ms;
579
+ if (typeof value === "number") {
580
+ ms = value;
581
+ } else {
582
+ const match = PATTERN.exec(value.trim());
583
+ if (!match)
584
+ throw new RangeError(`Invalid duration "${value}". Use milliseconds or a string such as 30s, 10m or 2h.`);
585
+ ms = Number(match[1]) * UNIT_MS[match[2]];
586
+ }
587
+ if (!Number.isFinite(ms) || ms < 0) throw new RangeError(`Invalid duration "${String(value)}"`);
588
+ return Math.ceil(ms);
589
+ };
590
+ var MICROVM_MAX_TIMEOUT_MS = 24 * 36e5;
591
+ var microvmTimeoutMs = (value) => {
592
+ const ms = durationToMs(value);
593
+ if (ms < 1 || ms > MICROVM_MAX_TIMEOUT_MS) {
594
+ throw new RangeError(`A microVM timeout must be between 1 ms and 24 hours (got ${String(value)}).`);
595
+ }
596
+ return ms;
597
+ };
598
+ var sleep = (ms, signal) => new Promise((resolve, reject) => {
599
+ if (signal?.aborted) {
600
+ reject(abortReason(signal));
601
+ return;
602
+ }
603
+ const onAbort = () => {
604
+ clearTimeout(timer);
605
+ reject(abortReason(signal));
606
+ };
607
+ const timer = setTimeout(() => {
608
+ signal?.removeEventListener("abort", onAbort);
609
+ resolve();
610
+ }, ms);
611
+ signal?.addEventListener("abort", onAbort, { once: true });
612
+ });
613
+ var abortReason = (signal) => {
614
+ const reason = signal?.reason;
615
+ if (reason instanceof Error) return reason;
616
+ const error = new Error(typeof reason === "string" ? reason : "The operation was aborted");
617
+ error.name = "AbortError";
618
+ return error;
619
+ };
620
+
621
+ // src/microvms/errors.ts
622
+ var MicroVmNotRunningError = class extends ApiError {
623
+ constructor(message = "The microVM is not running", code = "microvm_not_running", reason) {
624
+ super(message, 409, code, reason);
625
+ this.name = "MicroVmNotRunningError";
626
+ }
627
+ };
628
+ var MicroVmNotFoundError = class extends ApiError {
629
+ constructor(message = "Not found", code, reason) {
630
+ super(message, 404, code, reason);
631
+ this.name = "MicroVmNotFoundError";
632
+ }
633
+ };
634
+ var MicroVmFileNotFoundError = class extends MicroVmNotFoundError {
635
+ /** The path that was not found. */
636
+ path;
637
+ constructor(path, message = `No such file or directory: ${path}`, code) {
638
+ super(message, code);
639
+ this.name = "MicroVmFileNotFoundError";
640
+ this.path = path;
641
+ }
642
+ };
643
+ var MicroVmBusyError = class extends ApiError {
644
+ constructor(message, status, code = "microvm_busy", reason) {
645
+ super(message, status, code, reason);
646
+ this.name = "MicroVmBusyError";
647
+ }
648
+ };
649
+ var MicroVmUnreachableError = class extends ApiError {
650
+ constructor(message, code = "microvm_unreachable", reason) {
651
+ super(message, 502, code, reason);
652
+ this.name = "MicroVmUnreachableError";
653
+ }
654
+ };
655
+ var MicroVmImageNotReadyError = class extends ApiError {
656
+ constructor(message, code = "image_not_ready", reason) {
657
+ super(message, 409, code, reason);
658
+ this.name = "MicroVmImageNotReadyError";
659
+ }
660
+ };
661
+ var MicroVmQuotaExceededError = class extends ApiError {
662
+ constructor(message, status, code, reason) {
663
+ super(message, status, code, reason);
664
+ this.name = "MicroVmQuotaExceededError";
665
+ }
666
+ };
667
+ var CommandFailedError = class _CommandFailedError extends GigadriveError {
668
+ commandId;
669
+ /** Exit code, or `null` when a signal killed the process. */
670
+ exitCode;
671
+ signal;
672
+ stdout;
673
+ stderr;
674
+ constructor(output, label = "Command") {
675
+ const how = output.exitCode !== null ? `exited with code ${output.exitCode}` : `was killed by ${output.signal ?? "a signal"}`;
676
+ const tail = output.stderr.trim().split("\n").slice(-5).join("\n");
677
+ super(tail ? `${label} ${how}:
678
+ ${tail}` : `${label} ${how}`);
679
+ this.name = "CommandFailedError";
680
+ this.commandId = output.commandId;
681
+ this.exitCode = output.exitCode;
682
+ this.signal = output.signal;
683
+ this.stdout = output.stdout;
684
+ this.stderr = output.stderr;
685
+ }
686
+ /** Builds the error from a finished command whose output was not collected. */
687
+ static fromInfo(info) {
688
+ return new _CommandFailedError({
689
+ commandId: info.id,
690
+ exitCode: info.exitCode,
691
+ signal: info.signal,
692
+ stdout: "",
693
+ stderr: "",
694
+ durationMs: 0
695
+ });
696
+ }
697
+ };
698
+ var MicroVmTimeoutError = class extends GigadriveError {
699
+ constructor(message) {
700
+ super(message);
701
+ this.name = "MicroVmTimeoutError";
702
+ }
703
+ };
704
+ var MicroVmPortNotExposedError = class extends GigadriveError {
705
+ port;
706
+ constructor(port, exposed) {
707
+ super(
708
+ exposed.length === 0 ? `Port ${port} is not exposed: this microVM exposes no ports. Pass \`ports: [${port}]\` when creating it.` : `Port ${port} is not exposed. Exposed ports: ${exposed.join(", ")}.`
709
+ );
710
+ this.name = "MicroVmPortNotExposedError";
711
+ this.port = port;
712
+ }
713
+ };
714
+ var MicroVmStartError = class extends GigadriveError {
715
+ microvmId;
716
+ status;
717
+ stopReason;
718
+ constructor(microvmId, status, stopReason) {
719
+ super(`The microVM ${microvmId} is ${status}${stopReason ? ` (${stopReason})` : ""}`);
720
+ this.name = "MicroVmStartError";
721
+ this.microvmId = microvmId;
722
+ this.status = status;
723
+ this.stopReason = stopReason;
724
+ }
725
+ };
726
+ var MicroVmCleanupError = class extends GigadriveError {
727
+ microvmId;
728
+ /** Why deleting the microVM failed. {@link Error.cause} is why creating it failed. */
729
+ cleanupError;
730
+ constructor(microvmId, cause, cleanupError) {
731
+ super(
732
+ `Creating the microVM ${microvmId} failed and deleting it failed too. Delete it with MicroVM.get('${microvmId}') and delete().`
733
+ );
734
+ this.name = "MicroVmCleanupError";
735
+ this.microvmId = microvmId;
736
+ this.cleanupError = cleanupError;
737
+ this.cause = cause;
738
+ }
739
+ };
740
+ var MicroVmImageBuildError = class extends GigadriveError {
741
+ imageId;
742
+ constructor(imageId, message) {
743
+ super(`The image build failed${message ? `: ${message}` : ""}`);
744
+ this.name = "MicroVmImageBuildError";
745
+ this.imageId = imageId;
746
+ }
747
+ };
748
+ var NOT_RUNNING = /microvm[_ ]not[_ ]running|not running/i;
749
+ var toMicroVmError = (error, filePath) => {
750
+ if (!(error instanceof ApiError) || Object.getPrototypeOf(error) !== ApiError.prototype) return error;
751
+ const { message, status, code, reason } = error;
752
+ if (code === "microvm_busy") return new MicroVmBusyError(message, status, code, reason);
753
+ if (code === "microvm_unreachable") return new MicroVmUnreachableError(message, code, reason);
754
+ if (code === "image_not_ready") return new MicroVmImageNotReadyError(message, code, reason);
755
+ if (code === "quota_exceeded" || code === "plan_limit_exceeded") {
756
+ return new MicroVmQuotaExceededError(message, status, code, reason);
757
+ }
758
+ if (status === 409 && (code === "microvm_not_running" || code === void 0 && NOT_RUNNING.test(message))) {
759
+ return new MicroVmNotRunningError(message, code ?? "microvm_not_running", reason);
760
+ }
761
+ if (status === 404) {
762
+ const vmMissing = code === "not_found" || code === "microvm_not_found";
763
+ return filePath !== void 0 && !vmMissing ? new MicroVmFileNotFoundError(filePath, message, code) : new MicroVmNotFoundError(message, code, reason);
764
+ }
765
+ return error;
766
+ };
767
+
768
+ // src/queues/encoding.ts
769
+ var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
770
+ var UNIT_SECONDS = { ms: 1e-3, s: 1, m: 60, h: 3600, d: 86400 };
771
+ var durationToSeconds = (value) => {
772
+ let seconds;
773
+ if (typeof value === "number") {
774
+ seconds = value;
775
+ } else {
776
+ const match = DURATION_PATTERN.exec(value.trim());
777
+ if (!match) throw new Error(`Invalid duration "${value}". Use seconds or a string such as 30s, 10m, 12h or 7d.`);
778
+ seconds = Number(match[1]) * UNIT_SECONDS[match[2]];
779
+ }
780
+ if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`Invalid duration "${String(value)}"`);
781
+ return Math.ceil(seconds);
782
+ };
783
+ var textEncoder = new TextEncoder();
784
+ var textDecoder = new TextDecoder();
785
+ var utf8Encode = (value) => textEncoder.encode(value);
786
+ var utf8Decode = (bytes) => textDecoder.decode(bytes);
787
+ var bytesToBase64 = (bytes) => {
788
+ let binary = "";
789
+ for (let index = 0; index < bytes.length; index += 32768) {
790
+ binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
791
+ }
792
+ return btoa(binary);
793
+ };
794
+ var base64ToBytes = (value) => {
795
+ const binary = atob(value);
796
+ const bytes = new Uint8Array(binary.length);
797
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
798
+ return bytes;
799
+ };
800
+ var toHex = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
801
+ var toArrayBuffer = (bytes) => {
802
+ const copy = new ArrayBuffer(bytes.byteLength);
803
+ new Uint8Array(copy).set(bytes);
804
+ return copy;
805
+ };
806
+
555
807
  // src/streaming.ts
556
- var extractData = (rawEvent) => {
557
- const dataLines = rawEvent.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice("data:".length).replace(/^ /, ""));
558
- if (dataLines.length === 0) return null;
559
- const joined = dataLines.join("\n");
560
- return joined.trim().length === 0 ? null : joined;
808
+ var parseBlock = (rawEvent) => {
809
+ let event = "message";
810
+ let id;
811
+ let retry;
812
+ const data = [];
813
+ for (const line of rawEvent.split("\n")) {
814
+ if (line === "" || line.startsWith(":")) continue;
815
+ const colon = line.indexOf(":");
816
+ const field = colon === -1 ? line : line.slice(0, colon);
817
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
818
+ if (field === "data") data.push(value);
819
+ else if (field === "event") event = value || "message";
820
+ else if (field === "id" && !value.includes("\0")) id = value;
821
+ else if (field === "retry" && /^\d+$/.test(value)) retry = Number(value);
822
+ }
823
+ if (data.length === 0) return null;
824
+ return { event, data: data.join("\n"), id, retry };
561
825
  };
562
826
  var parseEvent = (data) => {
563
827
  try {
@@ -567,7 +831,7 @@ var parseEvent = (data) => {
567
831
  throw new Error(`Failed to parse streamed SSE data as JSON: ${message}`);
568
832
  }
569
833
  };
570
- async function* parseSSEStream(response) {
834
+ async function* readEventBlocks(response) {
571
835
  const body = response.body;
572
836
  if (!body) {
573
837
  throw new Error("The response has no readable body to stream.");
@@ -575,44 +839,546 @@ async function* parseSSEStream(response) {
575
839
  const reader = body.getReader();
576
840
  const decoder = new TextDecoder();
577
841
  let buffer = "";
578
- const drain = function* (flush) {
579
- let separator = buffer.indexOf("\n\n");
580
- while (separator !== -1) {
581
- const rawEvent = buffer.slice(0, separator);
582
- buffer = buffer.slice(separator + 2);
583
- const data = extractData(rawEvent);
584
- if (data !== null && data !== "[DONE]") {
585
- yield parseEvent(data);
586
- }
587
- separator = buffer.indexOf("\n\n");
588
- }
589
- if (flush) {
590
- const data = extractData(buffer);
591
- buffer = "";
592
- if (data !== null && data !== "[DONE]") {
593
- yield parseEvent(data);
594
- }
595
- }
596
- };
597
842
  try {
598
843
  for (; ; ) {
599
844
  const { done, value } = await reader.read();
600
845
  if (done) break;
601
846
  buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
602
- yield* drain(false);
847
+ let separator = buffer.indexOf("\n\n");
848
+ while (separator !== -1) {
849
+ yield buffer.slice(0, separator);
850
+ buffer = buffer.slice(separator + 2);
851
+ separator = buffer.indexOf("\n\n");
852
+ }
603
853
  }
604
- yield* drain(true);
854
+ buffer += decoder.decode();
855
+ if (buffer.length > 0) yield buffer;
605
856
  } finally {
606
857
  await reader.cancel().catch(() => void 0);
607
858
  }
608
859
  }
860
+ async function* parseSSEEvents(response) {
861
+ for await (const block of readEventBlocks(response)) {
862
+ const event = parseBlock(block);
863
+ if (event) yield event;
864
+ }
865
+ }
866
+ async function* parseSSEStream(response) {
867
+ for await (const event of parseSSEEvents(response)) {
868
+ if (event.data.trim().length === 0 || event.data === "[DONE]") continue;
869
+ yield parseEvent(event.data);
870
+ }
871
+ }
609
872
 
610
- // src/resources/base-resource.ts
611
- var BaseResource = class {
612
- constructor(httpClient) {
613
- this.httpClient = httpClient;
873
+ // src/microvms/logs.ts
874
+ var RECONNECT_DELAYS_MS = [100, 250, 500, 1e3, 2e3];
875
+ var isRetryable = (error) => {
876
+ if (error instanceof ApiError) return error.status === 408 || error.status === 429 || error.status >= 500;
877
+ if (error instanceof Error && error.name === "AbortError") return false;
878
+ return true;
879
+ };
880
+ var parseJson = (data) => {
881
+ try {
882
+ const value = JSON.parse(data);
883
+ return value !== null && typeof value === "object" ? value : null;
884
+ } catch {
885
+ return null;
886
+ }
887
+ };
888
+ async function* streamCommandLogs(open, options = {}) {
889
+ const follow = options.follow ?? true;
890
+ const reconnect = options.reconnect ?? true;
891
+ const maxAttempts = options.maxReconnectAttempts ?? 5;
892
+ const next = {
893
+ stdout: options.stdoutOffset ?? 0,
894
+ stderr: options.stderrOffset ?? 0
895
+ };
896
+ let failures = 0;
897
+ for (; ; ) {
898
+ let progressed = false;
899
+ let serverError;
900
+ try {
901
+ const response = await open({
902
+ follow,
903
+ stdoutOffset: next.stdout,
904
+ stderrOffset: next.stderr,
905
+ signal: options.signal
906
+ });
907
+ for await (const event of parseSSEEvents(response)) {
908
+ if (event.event === "stdout" || event.event === "stderr") {
909
+ const payload = parseJson(event.data);
910
+ if (!payload || typeof payload.data !== "string") continue;
911
+ const stream = event.event;
912
+ let data = base64ToBytes(payload.data);
913
+ let offset = typeof payload.offset === "number" ? payload.offset : next[stream];
914
+ progressed = true;
915
+ if (offset < next[stream]) {
916
+ const skip = next[stream] - offset;
917
+ if (skip >= data.length) continue;
918
+ data = data.subarray(skip);
919
+ offset = next[stream];
920
+ }
921
+ next[stream] = offset + data.length;
922
+ if (data.length > 0) yield { type: "output", stream, data, offset };
923
+ } else if (event.event === "truncated") {
924
+ progressed = true;
925
+ yield { type: "truncated" };
926
+ } else if (event.event === "end") {
927
+ const payload = parseJson(event.data);
928
+ yield { type: "end", reason: typeof payload?.reason === "string" ? payload.reason : "snapshot" };
929
+ return;
930
+ } else if (event.event === "exit") {
931
+ const payload = parseJson(event.data) ?? {};
932
+ yield {
933
+ type: "exit",
934
+ exitCode: typeof payload.exitCode === "number" ? payload.exitCode : null,
935
+ signal: typeof payload.signal === "string" ? payload.signal : null,
936
+ ...typeof payload.timedOut === "boolean" && { timedOut: payload.timedOut },
937
+ ...(typeof payload.finishedAt === "string" || payload.finishedAt === null) && {
938
+ finishedAt: payload.finishedAt
939
+ }
940
+ };
941
+ return;
942
+ } else if (event.event === "error") {
943
+ const payload = parseJson(event.data);
944
+ const message = typeof payload?.message === "string" ? payload.message : typeof payload?.error === "string" ? payload.error : event.data;
945
+ const failure = new ApiError(message, 502, typeof payload?.code === "string" ? payload.code : void 0);
946
+ if (payload?.retryable !== true) serverError = failure;
947
+ throw failure;
948
+ }
949
+ }
950
+ if (!follow || !reconnect) return;
951
+ const exit = progressed ? null : await options.resolveExit?.();
952
+ if (exit) {
953
+ yield { type: "exit", exitCode: exit.exitCode, signal: exit.signal };
954
+ return;
955
+ }
956
+ } catch (error) {
957
+ if (error === serverError || options.signal?.aborted || !reconnect || !isRetryable(error)) throw error;
958
+ if (!progressed && failures >= maxAttempts) throw error;
959
+ }
960
+ failures = progressed ? 0 : failures + 1;
961
+ if (failures > maxAttempts) {
962
+ throw new ApiError("The command log stream kept closing without output", 502, "log_stream_closed");
963
+ }
964
+ await sleep(RECONNECT_DELAYS_MS[Math.min(failures, RECONNECT_DELAYS_MS.length - 1)], options.signal);
965
+ }
966
+ }
967
+
968
+ // src/microvms/resource.ts
969
+ var vmPath = (microvmId) => `/microvms/${encodeURIComponent(microvmId)}`;
970
+ var commandPath = (microvmId, commandId) => `${vmPath(microvmId)}/commands/${encodeURIComponent(commandId)}`;
971
+ var typed = (promise, filePath) => promise.catch((error) => {
972
+ throw toMicroVmError(error, filePath);
973
+ });
974
+ var BUSY_RETRY_DELAYS_MS = [250, 500, 1e3, 2e3, 4e3];
975
+ var read = async (run, signal, filePath) => {
976
+ for (let attempt = 0; ; attempt += 1) {
977
+ try {
978
+ return await typed(run(), filePath);
979
+ } catch (error) {
980
+ if (!(error instanceof MicroVmBusyError) || attempt >= BUSY_RETRY_DELAYS_MS.length || signal?.aborted) {
981
+ throw error;
982
+ }
983
+ await sleep(BUSY_RETRY_DELAYS_MS[attempt], signal);
984
+ }
985
+ }
986
+ };
987
+ var unwrapList = (body, key = "data") => {
988
+ if (Array.isArray(body)) return body;
989
+ if (body && typeof body === "object") {
990
+ const record = body;
991
+ const list = record[key] ?? record.data ?? record.items;
992
+ if (Array.isArray(list)) return list;
993
+ }
994
+ return [];
995
+ };
996
+ var normalizeCommand = (raw) => {
997
+ const withId = raw.id === void 0 && raw.commandId !== void 0 ? { ...raw, id: raw.commandId } : raw;
998
+ return typeof withId.timedOut === "boolean" ? withId : { ...withId, timedOut: false };
999
+ };
1000
+ var MicroVmCommandsResource = class extends BaseResource {
1001
+ /**
1002
+ * Run a command and wait for it to exit (`POST /microvms/:id/commands`, `wait: true`).
1003
+ *
1004
+ * @example
1005
+ * ```ts
1006
+ * const { exitCode, stdout } = await client.microvms.commands.run(id, { cmd: 'node', args: ['-v'] });
1007
+ * ```
1008
+ */
1009
+ run(microvmId, input, options = {}) {
1010
+ return typed(
1011
+ this.httpClient.post(`${vmPath(microvmId)}/commands`, { ...input, wait: true }, options)
1012
+ );
1013
+ }
1014
+ /**
1015
+ * Start a detached command and return at once (`POST /microvms/:id/commands`, `wait: false`).
1016
+ * Follow its output with {@link logs}.
1017
+ */
1018
+ async start(microvmId, input, options = {}) {
1019
+ const raw = await typed(
1020
+ this.httpClient.post(`${vmPath(microvmId)}/commands`, { ...input, wait: false }, options)
1021
+ );
1022
+ return normalizeCommand(raw);
1023
+ }
1024
+ /** List the microVM's commands, newest first (`GET /microvms/:id/commands`). */
1025
+ async list(microvmId, options = {}) {
1026
+ const body = await read(
1027
+ () => this.httpClient.get(`${vmPath(microvmId)}/commands`, options),
1028
+ options.signal
1029
+ );
1030
+ return unwrapList(body).map(normalizeCommand);
1031
+ }
1032
+ /** Get one command (`GET /microvms/:id/commands/:cmdId`). */
1033
+ async get(microvmId, commandId, options = {}) {
1034
+ return normalizeCommand(
1035
+ await read(() => this.httpClient.get(commandPath(microvmId, commandId), options), options.signal)
1036
+ );
1037
+ }
1038
+ /**
1039
+ * Stream a command's output (`GET /microvms/:id/commands/:cmdId/logs`, SSE).
1040
+ *
1041
+ * Yields `output` events with the raw bytes and a final `exit` event. A
1042
+ * dropped connection is reopened from the last received offsets, so no byte
1043
+ * is lost or repeated; pass `reconnect: false` to disable that.
1044
+ *
1045
+ * @example
1046
+ * ```ts
1047
+ * for await (const event of client.microvms.commands.logs(id, commandId)) {
1048
+ * if (event.type === 'output') process.stdout.write(event.data);
1049
+ * else console.log('exit', event.exitCode);
1050
+ * }
1051
+ * ```
1052
+ */
1053
+ logs(microvmId, commandId, options = {}) {
1054
+ return streamCommandLogs(
1055
+ ({ follow, stdoutOffset, stderrOffset, signal }) => typed(
1056
+ this.httpClient.requestStream("GET", `${commandPath(microvmId, commandId)}/logs`, {
1057
+ query: { follow, stdoutOffset, stderrOffset },
1058
+ headers: { Accept: "text/event-stream" },
1059
+ signal
1060
+ })
1061
+ ),
1062
+ {
1063
+ ...options,
1064
+ resolveExit: options.resolveExit ?? (async () => {
1065
+ const info = await this.get(microvmId, commandId);
1066
+ return info.status === "exited" ? { exitCode: info.exitCode, signal: info.signal } : null;
1067
+ })
1068
+ }
1069
+ );
1070
+ }
1071
+ /**
1072
+ * Wait up to `timeoutMs` (at most 60000) for the command to exit
1073
+ * (`POST /microvms/:id/commands/:cmdId/wait`). The returned command is still
1074
+ * `running` when the wait timed out.
1075
+ */
1076
+ async wait(microvmId, commandId, options = {}) {
1077
+ const { signal, timeoutMs } = options;
1078
+ const raw = await read(
1079
+ () => this.httpClient.post(
1080
+ `${commandPath(microvmId, commandId)}/wait`,
1081
+ timeoutMs === void 0 ? {} : { timeoutMs },
1082
+ { signal }
1083
+ ),
1084
+ signal
1085
+ );
1086
+ return normalizeCommand(raw);
1087
+ }
1088
+ /**
1089
+ * Write to the command's stdin (`POST /microvms/:id/commands/:cmdId/stdin`).
1090
+ * The command must have been started with `stdin: true` or a `pty`.
1091
+ *
1092
+ * @param options.eof - Close stdin after writing.
1093
+ */
1094
+ async stdin(microvmId, commandId, data, options = {}) {
1095
+ await typed(
1096
+ this.httpClient.post(`${commandPath(microvmId, commandId)}/stdin`, data, {
1097
+ headers: { "Content-Type": "application/octet-stream" },
1098
+ query: { eof: options.eof ? true : void 0 },
1099
+ signal: options.signal
1100
+ })
1101
+ );
1102
+ }
1103
+ /** Send a signal to the command's process group (`POST /microvms/:id/commands/:cmdId/signal`). */
1104
+ async signal(microvmId, commandId, signal, options = {}) {
1105
+ await typed(this.httpClient.post(`${commandPath(microvmId, commandId)}/signal`, { signal }, options));
1106
+ }
1107
+ /** Resize a PTY command's terminal (`POST /microvms/:id/commands/:cmdId/resize`). */
1108
+ async resize(microvmId, commandId, size, options = {}) {
1109
+ await typed(
1110
+ this.httpClient.post(
1111
+ `${commandPath(microvmId, commandId)}/resize`,
1112
+ { cols: size.cols, rows: size.rows },
1113
+ options
1114
+ )
1115
+ );
1116
+ }
1117
+ };
1118
+ var modeString = (mode) => typeof mode === "number" ? mode.toString(8).padStart(4, "0") : mode;
1119
+ var MicroVmFilesResource = class extends BaseResource {
1120
+ /** Read a file as bytes (`GET /microvms/:id/files/content`). */
1121
+ async read(microvmId, path, options = {}) {
1122
+ const response = await this.open(microvmId, path, options);
1123
+ return new Uint8Array(await response.arrayBuffer());
1124
+ }
1125
+ /** Read a file as a stream of bytes, without buffering it (`GET /microvms/:id/files/content`). */
1126
+ async readStream(microvmId, path, options = {}) {
1127
+ const response = await this.open(microvmId, path, options);
1128
+ if (!response.body) return new Blob([]).stream();
1129
+ return response.body;
1130
+ }
1131
+ /**
1132
+ * Write a file, replacing it atomically (`PUT /microvms/:id/files/content`).
1133
+ * Missing parent directories are created unless `mkdirs: false`.
1134
+ */
1135
+ write(microvmId, path, content, options = {}) {
1136
+ return typed(
1137
+ this.httpClient.put(`${vmPath(microvmId)}/files/content`, content, {
1138
+ headers: { "Content-Type": "application/octet-stream" },
1139
+ query: { path, mode: modeString(options.mode), mkdirs: options.mkdirs === false ? false : void 0 },
1140
+ signal: options.signal
1141
+ }),
1142
+ path
1143
+ );
1144
+ }
1145
+ /**
1146
+ * List a directory (`GET /microvms/:id/files`). `truncated` is set when the
1147
+ * listing hit its size limit; list a narrower directory then.
1148
+ */
1149
+ async list(microvmId, path, options = {}) {
1150
+ const body = await read(
1151
+ () => this.httpClient.get(`${vmPath(microvmId)}/files`, {
1152
+ query: { path, recursive: options.recursive ? true : void 0, depth: options.depth },
1153
+ signal: options.signal
1154
+ }),
1155
+ options.signal,
1156
+ path
1157
+ );
1158
+ const truncated = body?.truncated === true;
1159
+ return { entries: unwrapList(body, "entries"), truncated };
1160
+ }
1161
+ /** Describe a file or directory without following a final symlink (`GET /microvms/:id/files/stat`). */
1162
+ stat(microvmId, path, options = {}) {
1163
+ return read(
1164
+ () => this.httpClient.get(`${vmPath(microvmId)}/files/stat`, { query: { path }, signal: options.signal }),
1165
+ options.signal,
1166
+ path
1167
+ );
1168
+ }
1169
+ /** Create a directory, with its parents unless `parents: false` (`POST /microvms/:id/files/mkdir`). */
1170
+ mkdir(microvmId, path, options = {}) {
1171
+ const body = options.parents === void 0 ? { path } : { path, parents: options.parents };
1172
+ return typed(this.httpClient.post(`${vmPath(microvmId)}/files/mkdir`, body, options), path);
1173
+ }
1174
+ /** Move or rename a file or directory (`POST /microvms/:id/files/move`). */
1175
+ move(microvmId, from, to, options = {}) {
1176
+ return typed(this.httpClient.post(`${vmPath(microvmId)}/files/move`, { from, to }, options), from);
1177
+ }
1178
+ /** Delete a file, or a directory (non-empty ones need `recursive`) (`DELETE /microvms/:id/files`). */
1179
+ async remove(microvmId, path, options = {}) {
1180
+ await typed(
1181
+ this.httpClient.delete(`${vmPath(microvmId)}/files`, {
1182
+ query: { path, recursive: options.recursive ? true : void 0 },
1183
+ signal: options.signal
1184
+ }),
1185
+ path
1186
+ );
1187
+ }
1188
+ open(microvmId, path, options) {
1189
+ return read(
1190
+ () => this.httpClient.requestStream("GET", `${vmPath(microvmId)}/files/content`, {
1191
+ query: { path, offset: options.offset, length: options.length },
1192
+ signal: options.signal
1193
+ }),
1194
+ options.signal,
1195
+ path
1196
+ );
1197
+ }
1198
+ };
1199
+ var toImageSource = (input) => {
1200
+ if (input.source) return input.source;
1201
+ const from = input.from;
1202
+ if (typeof from === "string") return { type: "registry", ref: from };
1203
+ if (from && "dockerfile" in from) return { type: "dockerfile", dockerfile: from.dockerfile };
1204
+ if (from && "registry" in from) return { type: "registry", ref: from.registry };
1205
+ throw new TypeError("Pass `from` (a registry reference or { dockerfile }) or `source` to create an image.");
1206
+ };
1207
+ var MicroVmImagesResource = class extends BaseResource {
1208
+ defaultApplicationId;
1209
+ constructor(httpClient, defaultApplicationId) {
1210
+ super(httpClient);
1211
+ this.defaultApplicationId = defaultApplicationId || void 0;
1212
+ }
1213
+ /** List the built-in images and the application's custom images (`GET /microvm-images`). */
1214
+ async list(query = {}, options = {}) {
1215
+ const body = await read(
1216
+ () => this.httpClient.get("/microvm-images", {
1217
+ query: { applicationId: query.applicationId ?? this.defaultApplicationId },
1218
+ signal: options.signal
1219
+ }),
1220
+ options.signal
1221
+ );
1222
+ return unwrapList(body);
1223
+ }
1224
+ /**
1225
+ * Start building a custom image (`POST /microvm-images`). The image is
1226
+ * `building` until {@link waitUntilReady} resolves.
1227
+ *
1228
+ * @example
1229
+ * ```ts
1230
+ * const image = await client.microvms.images.create({ name: 'py-ml', from: 'python:3.13' });
1231
+ * await client.microvms.images.waitUntilReady(image.id);
1232
+ * const vm = await MicroVM.create({ image: 'py-ml' });
1233
+ * ```
1234
+ */
1235
+ async create(input, options = {}) {
1236
+ const applicationId = input.applicationId ?? this.defaultApplicationId;
1237
+ const source = toImageSource(input);
1238
+ return await typed(
1239
+ this.httpClient.post(
1240
+ "/microvm-images",
1241
+ { ...applicationId ? { applicationId } : {}, name: input.name, source },
1242
+ options
1243
+ )
1244
+ );
1245
+ }
1246
+ /** Get one image (`GET /microvm-images/:id`). */
1247
+ get(imageId, options = {}) {
1248
+ return read(
1249
+ () => this.httpClient.get(`/microvm-images/${encodeURIComponent(imageId)}`, options),
1250
+ options.signal
1251
+ );
1252
+ }
1253
+ /** Delete a custom image (`DELETE /microvm-images/:id`). */
1254
+ async delete(imageId, options = {}) {
1255
+ await typed(this.httpClient.delete(`/microvm-images/${encodeURIComponent(imageId)}`, options));
1256
+ }
1257
+ /**
1258
+ * Poll an image until its build finished.
1259
+ *
1260
+ * @throws {@link MicroVmImageBuildError} when the build failed.
1261
+ * @throws {@link MicroVmTimeoutError} when it is still building after `timeout`.
1262
+ */
1263
+ async waitUntilReady(imageId, options = {}) {
1264
+ const deadline = Date.now() + durationToMs(options.timeout ?? "15m");
1265
+ const interval = durationToMs(options.interval ?? "2s");
1266
+ let lastStatus;
1267
+ for (; ; ) {
1268
+ if (options.signal?.aborted) throw abortReason(options.signal);
1269
+ const image = await this.get(imageId, { signal: options.signal });
1270
+ if (image.status !== lastStatus) {
1271
+ lastStatus = image.status;
1272
+ options.onStatus?.(image);
1273
+ }
1274
+ if (image.status === "ready") return image;
1275
+ if (image.status === "failed") throw new MicroVmImageBuildError(image.id, image.error);
1276
+ if (Date.now() + interval > deadline) {
1277
+ throw new MicroVmTimeoutError(`The image ${image.name} is still ${image.status}; gave up waiting.`);
1278
+ }
1279
+ await sleep(interval, options.signal);
1280
+ }
1281
+ }
1282
+ };
1283
+ var MicroVmsResource = class extends BaseResource {
1284
+ /** Commands: run, start, logs, wait, stdin, signal, resize. */
1285
+ commands;
1286
+ /** Files: read, write, list, stat, mkdir, move, remove. */
1287
+ files;
1288
+ /** Built-in and custom images. */
1289
+ images;
1290
+ defaultApplicationId;
1291
+ constructor(httpClient, defaultApplicationId) {
1292
+ super(httpClient);
1293
+ this.defaultApplicationId = defaultApplicationId || void 0;
1294
+ this.commands = new MicroVmCommandsResource(httpClient);
1295
+ this.files = new MicroVmFilesResource(httpClient);
1296
+ this.images = new MicroVmImagesResource(httpClient, defaultApplicationId);
1297
+ }
1298
+ /**
1299
+ * Create a microVM (`POST /microvms`). Resolves once it is `running`; with
1300
+ * `wait: false` it resolves at once with a `starting` microVM.
1301
+ *
1302
+ * `applicationId` defaults to the client's `applicationId` or
1303
+ * `GIGADRIVE_APPLICATION_ID`, and may be omitted entirely with application
1304
+ * or deployment credentials.
1305
+ */
1306
+ create(input = {}, options = {}) {
1307
+ const applicationId = input.applicationId ?? this.defaultApplicationId;
1308
+ return typed(
1309
+ this.httpClient.post("/microvms", applicationId ? { ...input, applicationId } : input, {
1310
+ query: { wait: options.wait === false ? false : void 0 },
1311
+ signal: options.signal
1312
+ })
1313
+ );
1314
+ }
1315
+ /** List one page of microVMs, newest first (`GET /microvms`). */
1316
+ async list(query = {}, options = {}) {
1317
+ const body = await read(
1318
+ () => this.httpClient.get("/microvms", {
1319
+ query: {
1320
+ applicationId: query.applicationId ?? this.defaultApplicationId,
1321
+ status: query.status,
1322
+ cursor: query.cursor,
1323
+ limit: query.limit
1324
+ },
1325
+ signal: options.signal
1326
+ }),
1327
+ options.signal
1328
+ );
1329
+ const nextCursor = body.pagination?.nextCursor ?? body.nextCursor ?? void 0;
1330
+ const hasMore = body.pagination?.hasMore ?? nextCursor !== void 0;
1331
+ return {
1332
+ items: body.data ?? body.items ?? [],
1333
+ ...nextCursor && hasMore ? { nextCursor } : {},
1334
+ hasMore: hasMore && nextCursor !== void 0
1335
+ };
1336
+ }
1337
+ /**
1338
+ * Iterate every microVM across all pages.
1339
+ *
1340
+ * @example
1341
+ * ```ts
1342
+ * for await (const vm of client.microvms.listAll({ status: 'running' })) console.log(vm.id);
1343
+ * ```
1344
+ */
1345
+ async *listAll(query = {}, options = {}) {
1346
+ let cursor;
1347
+ do {
1348
+ const page = await this.list({ ...query, cursor }, options);
1349
+ yield* page.items;
1350
+ cursor = page.nextCursor;
1351
+ } while (cursor);
1352
+ }
1353
+ /** Get one microVM (`GET /microvms/:id`). */
1354
+ get(microvmId, options = {}) {
1355
+ return read(() => this.httpClient.get(vmPath(microvmId), options), options.signal);
1356
+ }
1357
+ /**
1358
+ * Set the remaining lifetime to `timeoutMs` from now (`POST /microvms/:id/timeout`),
1359
+ * up to the plan's maximum lifetime.
1360
+ */
1361
+ extendTimeout(microvmId, timeoutMs, options = {}) {
1362
+ return typed(this.httpClient.post(`${vmPath(microvmId)}/timeout`, { timeoutMs }, options));
1363
+ }
1364
+ /** Stop a microVM (`POST /microvms/:id/stop`). Its disk and memory are discarded. */
1365
+ stop(microvmId, options = {}) {
1366
+ return typed(this.httpClient.post(`${vmPath(microvmId)}/stop`, void 0, options));
1367
+ }
1368
+ /** Delete a microVM, stopping it first if needed (`DELETE /microvms/:id`). */
1369
+ async delete(microvmId, options = {}) {
1370
+ await typed(this.httpClient.delete(vmPath(microvmId), options));
1371
+ }
1372
+ /**
1373
+ * Request a public preview URL with the client's `fetch`, without
1374
+ * credentials and without throwing on error statuses. Used by
1375
+ * `MicroVM.waitForPort`.
1376
+ *
1377
+ * @internal
1378
+ */
1379
+ fetchPreview(url, init) {
1380
+ return this.httpClient.fetchPublic(url, init);
614
1381
  }
615
- httpClient;
616
1382
  };
617
1383
 
618
1384
  // src/resources/ai-gateway/index.ts
@@ -897,7 +1663,7 @@ var ApplicationRequestsResource = class extends BaseResource {
897
1663
 
898
1664
  // src/upload/checksum.ts
899
1665
  var EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
900
- var toHex = (buffer) => {
1666
+ var toHex2 = (buffer) => {
901
1667
  const bytes = new Uint8Array(buffer);
902
1668
  let hex = "";
903
1669
  for (let i = 0; i < bytes.length; i++) {
@@ -905,7 +1671,7 @@ var toHex = (buffer) => {
905
1671
  }
906
1672
  return hex;
907
1673
  };
908
- var subtleDigest = async (algorithm, bytes) => toHex(await crypto.subtle.digest(algorithm, bytes));
1674
+ var subtleDigest = async (algorithm, bytes) => toHex2(await crypto.subtle.digest(algorithm, bytes));
909
1675
  var importNodeCrypto = async () => {
910
1676
  if (typeof process === "undefined" || !process.versions?.node) return null;
911
1677
  try {
@@ -1714,7 +2480,7 @@ var pollSignal = (ms, signal) => {
1714
2480
  }
1715
2481
  };
1716
2482
  };
1717
- var sleep = (ms, signal) => new Promise((resolve, reject) => {
2483
+ var sleep2 = (ms, signal) => new Promise((resolve, reject) => {
1718
2484
  if (signal?.aborted) {
1719
2485
  reject(signal.reason instanceof Error ? signal.reason : new GigadriveError("Aborted"));
1720
2486
  return;
@@ -1838,7 +2604,7 @@ var ApplicationDomainsResource = class extends BaseResource {
1838
2604
  if (last) throw new DomainNotActiveError(last, "timeout");
1839
2605
  throw new GigadriveError("Timed out before the domain could be read");
1840
2606
  }
1841
- await sleep(Math.min(intervalMs, untilDeadline), signal);
2607
+ await sleep2(Math.min(intervalMs, untilDeadline), signal);
1842
2608
  }
1843
2609
  }
1844
2610
  };
@@ -2624,48 +3390,9 @@ var OrganizationsResource = class extends BaseResource {
2624
3390
  * console.log(org.id, org.slug);
2625
3391
  * ```
2626
3392
  */
2627
- async create(input) {
2628
- return this.httpClient.post("/organizations", input);
2629
- }
2630
- };
2631
-
2632
- // src/queues/encoding.ts
2633
- var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
2634
- var UNIT_SECONDS = { ms: 1e-3, s: 1, m: 60, h: 3600, d: 86400 };
2635
- var durationToSeconds = (value) => {
2636
- let seconds;
2637
- if (typeof value === "number") {
2638
- seconds = value;
2639
- } else {
2640
- const match = DURATION_PATTERN.exec(value.trim());
2641
- if (!match) throw new Error(`Invalid duration "${value}". Use seconds or a string such as 30s, 10m, 12h or 7d.`);
2642
- seconds = Number(match[1]) * UNIT_SECONDS[match[2]];
2643
- }
2644
- if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`Invalid duration "${String(value)}"`);
2645
- return Math.ceil(seconds);
2646
- };
2647
- var textEncoder = new TextEncoder();
2648
- var textDecoder = new TextDecoder();
2649
- var utf8Encode = (value) => textEncoder.encode(value);
2650
- var utf8Decode = (bytes) => textDecoder.decode(bytes);
2651
- var bytesToBase64 = (bytes) => {
2652
- let binary = "";
2653
- for (let index = 0; index < bytes.length; index += 32768) {
2654
- binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
3393
+ async create(input) {
3394
+ return this.httpClient.post("/organizations", input);
2655
3395
  }
2656
- return btoa(binary);
2657
- };
2658
- var base64ToBytes = (value) => {
2659
- const binary = atob(value);
2660
- const bytes = new Uint8Array(binary.length);
2661
- for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
2662
- return bytes;
2663
- };
2664
- var toHex2 = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
2665
- var toArrayBuffer = (bytes) => {
2666
- const copy = new ArrayBuffer(bytes.byteLength);
2667
- new Uint8Array(copy).set(bytes);
2668
- return copy;
2669
3396
  };
2670
3397
 
2671
3398
  // src/queues/errors.ts
@@ -2697,11 +3424,11 @@ var hmacSha256Hex = async (secret, payload) => {
2697
3424
  false,
2698
3425
  ["sign"]
2699
3426
  );
2700
- return toHex2(await crypto.subtle.sign("HMAC", key, toArrayBuffer(utf8Encode(payload))));
3427
+ return toHex(await crypto.subtle.sign("HMAC", key, toArrayBuffer(utf8Encode(payload))));
2701
3428
  };
2702
3429
  var signedPayload = async (input, timestamp) => {
2703
3430
  const body = typeof input.body === "string" ? utf8Encode(input.body) : input.body;
2704
- const bodyHash = toHex2(await crypto.subtle.digest("SHA-256", toArrayBuffer(body)));
3431
+ const bodyHash = toHex(await crypto.subtle.digest("SHA-256", toArrayBuffer(body)));
2705
3432
  return `${String(timestamp)}.${input.queue}.${input.messageId}.${String(input.attempt)}.${bodyHash}`;
2706
3433
  };
2707
3434
  var safeEqual = (left, right) => {
@@ -2784,7 +3511,7 @@ var ACK_FLUSH_MS = 25;
2784
3511
  var ACK_RETRY_DELAYS_MS = [200, 1e3];
2785
3512
  var EMPTY_POLL_MIN_MS = 1e3;
2786
3513
  var EMPTY_POLL_MAX_MS = 5e3;
2787
- var sleep2 = (ms, signal) => new Promise((resolve) => {
3514
+ var sleep3 = (ms, signal) => new Promise((resolve) => {
2788
3515
  if (signal?.aborted) return resolve();
2789
3516
  const timer = setTimeout(done, ms);
2790
3517
  function done() {
@@ -2979,7 +3706,7 @@ var Queue = class {
2979
3706
  const retryable = !(error instanceof ApiError) || error.status >= 500 || error.status === 429;
2980
3707
  const delay2 = ACK_RETRY_DELAYS_MS[attempt];
2981
3708
  if (!retryable || delay2 === void 0) throw error;
2982
- await sleep2(delay2, void 0);
3709
+ await sleep3(delay2, void 0);
2983
3710
  }
2984
3711
  }
2985
3712
  };
@@ -3019,7 +3746,7 @@ var Queue = class {
3019
3746
  if (messages.length === 0) {
3020
3747
  if (options.stopWhenEmpty) return;
3021
3748
  const remaining = Math.max(waitMs, EMPTY_POLL_MIN_MS) - (Date.now() - started);
3022
- if (remaining > 0) await sleep2(Math.min(remaining, EMPTY_POLL_MAX_MS), options.signal);
3749
+ if (remaining > 0) await sleep3(Math.min(remaining, EMPTY_POLL_MAX_MS), options.signal);
3023
3750
  continue;
3024
3751
  }
3025
3752
  await Promise.all(
@@ -3455,6 +4182,12 @@ var GigadriveClient = class {
3455
4182
  * handle bound to one queue.
3456
4183
  */
3457
4184
  queues;
4185
+ /**
4186
+ * microVMs: isolated Linux machines to run commands in, move files to and
4187
+ * from, and expose ports from. This is the low-level REST surface; the
4188
+ * {@link MicroVM} class (`MicroVM.create({ client })`) is the friendlier API.
4189
+ */
4190
+ microvms;
3458
4191
  constructor(config = {}) {
3459
4192
  const baseUrl = config.baseUrl ?? readEnv("GIGADRIVE_API_BASE_URL") ?? DEFAULT_BASE_URL2;
3460
4193
  const applicationId = config.applicationId ?? readEnv("GIGADRIVE_APPLICATION_ID");
@@ -3471,6 +4204,687 @@ var GigadriveClient = class {
3471
4204
  this.stickySessions = new StickySessionsResource(httpClient);
3472
4205
  this.imageOptimization = new ImageOptimizationResource(httpClient);
3473
4206
  this.queues = new QueuesResource(httpClient, applicationId);
4207
+ this.microvms = new MicroVmsResource(httpClient, applicationId);
4208
+ }
4209
+ };
4210
+
4211
+ // src/microvms/command.ts
4212
+ var MAX_WAIT_MS = 6e4;
4213
+ var toMs = (value) => {
4214
+ if (value === null || value === void 0) return null;
4215
+ const ms = typeof value === "number" ? value : Date.parse(value);
4216
+ return Number.isFinite(ms) ? ms : null;
4217
+ };
4218
+ var commandDurationMs = (info) => {
4219
+ const started = toMs(info.startedAt);
4220
+ const finished = toMs(info.finishedAt);
4221
+ return started !== null && finished !== null ? Math.max(0, finished - started) : null;
4222
+ };
4223
+ var Command = class {
4224
+ /** @internal Use {@link MicroVM.spawn} or `vm.commands.get(id)`. */
4225
+ constructor(resource, microvmId, info) {
4226
+ this.resource = resource;
4227
+ this.microvmId = microvmId;
4228
+ this.current = info;
4229
+ }
4230
+ resource;
4231
+ microvmId;
4232
+ current;
4233
+ pump;
4234
+ pumpError;
4235
+ skippedOutput = false;
4236
+ /** Command id (`cmd_...`). Pass it to `vm.commands.get(id)` to reattach later. */
4237
+ get id() {
4238
+ return this.current.id;
4239
+ }
4240
+ /** The latest known state. Updated by {@link refresh}, {@link wait} and the end of {@link logs}. */
4241
+ get info() {
4242
+ return this.current;
4243
+ }
4244
+ get status() {
4245
+ return this.current.status;
4246
+ }
4247
+ /** Exit code once exited; `null` while running or when a signal killed it. */
4248
+ get exitCode() {
4249
+ return this.current.exitCode;
4250
+ }
4251
+ /** Signal that killed the process, if any. */
4252
+ get signal() {
4253
+ return this.current.signal;
4254
+ }
4255
+ get pid() {
4256
+ return this.current.pid;
4257
+ }
4258
+ /** Whether the command was killed because it ran past its `timeout`. */
4259
+ get timedOut() {
4260
+ return this.current.timedOut;
4261
+ }
4262
+ /**
4263
+ * Whether a {@link logs} read found older output already dropped (each
4264
+ * stream keeps its last 8 MiB), so what it delivered starts mid-stream.
4265
+ */
4266
+ get truncated() {
4267
+ return this.skippedOutput;
4268
+ }
4269
+ /** Fetch the latest state from the API. */
4270
+ async refresh() {
4271
+ this.current = await this.resource.get(this.microvmId, this.id);
4272
+ return this.current;
4273
+ }
4274
+ /**
4275
+ * Stream the command's output, from the beginning of the retained output by
4276
+ * default, until it exits. With `follow: false` it delivers what is
4277
+ * buffered and ends, even while the command keeps running. Reconnects transparently after a dropped
4278
+ * connection. Breaking out of the loop closes the stream; the command keeps
4279
+ * running.
4280
+ *
4281
+ * @example
4282
+ * ```ts
4283
+ * for await (const { stream, text } of command.logs()) {
4284
+ * (stream === 'stdout' ? process.stdout : process.stderr).write(text);
4285
+ * }
4286
+ * ```
4287
+ */
4288
+ async *logs(options = {}) {
4289
+ const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() };
4290
+ for await (const event of this.resource.logs(this.microvmId, this.id, options)) {
4291
+ if (event.type === "exit") {
4292
+ this.current = {
4293
+ ...this.current,
4294
+ status: "exited",
4295
+ exitCode: event.exitCode,
4296
+ signal: event.signal,
4297
+ ...event.timedOut !== void 0 && { timedOut: event.timedOut },
4298
+ ...event.finishedAt !== void 0 && { finishedAt: event.finishedAt }
4299
+ };
4300
+ continue;
4301
+ }
4302
+ if (event.type === "truncated") {
4303
+ this.skippedOutput = true;
4304
+ continue;
4305
+ }
4306
+ if (event.type === "end") continue;
4307
+ const text = decoders[event.stream].decode(event.data, { stream: true });
4308
+ yield { stream: event.stream, data: event.data, text, offset: event.offset };
4309
+ }
4310
+ }
4311
+ /**
4312
+ * Collect the output from the start until the command exits.
4313
+ *
4314
+ * @example
4315
+ * ```ts
4316
+ * const { stdout } = await (await vm.spawn('ls', ['-la'])).output();
4317
+ * ```
4318
+ */
4319
+ async output(options = {}) {
4320
+ let stdout = "";
4321
+ let stderr = "";
4322
+ for await (const chunk of this.logs(options)) {
4323
+ if (chunk.stream === "stdout") stdout += chunk.text;
4324
+ else stderr += chunk.text;
4325
+ }
4326
+ return { stdout, stderr };
4327
+ }
4328
+ /** The complete stdout as text, once the command exited. */
4329
+ async stdout(options = {}) {
4330
+ return (await this.output(options)).stdout;
4331
+ }
4332
+ /** The complete stderr as text, once the command exited. */
4333
+ async stderr(options = {}) {
4334
+ return (await this.output(options)).stderr;
4335
+ }
4336
+ /**
4337
+ * Wait for the command to exit and return its final state. Output callbacks
4338
+ * given to `spawn` have received all output when this resolves.
4339
+ *
4340
+ * @throws {@link MicroVmTimeoutError} when `timeout` elapses first (the command keeps running).
4341
+ * @throws {@link CommandFailedError} with `throwOnError` when it exits non-zero or is killed.
4342
+ */
4343
+ async wait(options = {}) {
4344
+ const deadline = options.timeout === void 0 ? Infinity : Date.now() + durationToMs(options.timeout);
4345
+ while (this.current.status !== "exited") {
4346
+ const remaining = deadline - Date.now();
4347
+ if (remaining <= 0) {
4348
+ throw new MicroVmTimeoutError(`The command ${this.id} is still running; gave up waiting.`);
4349
+ }
4350
+ this.current = await this.resource.wait(this.microvmId, this.id, {
4351
+ timeoutMs: Math.min(MAX_WAIT_MS, remaining),
4352
+ signal: options.signal
4353
+ });
4354
+ }
4355
+ if (this.pump) {
4356
+ await this.pump;
4357
+ if (this.pumpError) throw this.pumpError;
4358
+ }
4359
+ if (options.throwOnError && this.current.exitCode !== 0) {
4360
+ throw CommandFailedError.fromInfo(this.current);
4361
+ }
4362
+ return this.current;
4363
+ }
4364
+ /**
4365
+ * Send a signal to the command's process group. Default `SIGTERM`; use
4366
+ * `SIGKILL` to stop it unconditionally.
4367
+ */
4368
+ async kill(signal = "SIGTERM") {
4369
+ await this.resource.signal(this.microvmId, this.id, signal);
4370
+ }
4371
+ /**
4372
+ * Write to stdin. The command must have been spawned with `stdin: true` or a `pty`.
4373
+ *
4374
+ * @example
4375
+ * ```ts
4376
+ * const cat = await vm.spawn('cat', [], { stdin: true });
4377
+ * await cat.write('hello\n');
4378
+ * await cat.end();
4379
+ * console.log(await cat.stdout()); // "hello\n"
4380
+ * ```
4381
+ */
4382
+ async write(data) {
4383
+ await this.resource.stdin(this.microvmId, this.id, data);
4384
+ }
4385
+ /** Close stdin, optionally writing a last piece of data first. */
4386
+ async end(data = new Uint8Array(0)) {
4387
+ await this.resource.stdin(this.microvmId, this.id, data, { eof: true });
4388
+ }
4389
+ /** Resize the terminal of a PTY command. */
4390
+ async resize(cols, rows) {
4391
+ await this.resource.resize(this.microvmId, this.id, { cols, rows });
4392
+ }
4393
+ /** JSON form: the command's latest state. */
4394
+ toJSON() {
4395
+ return this.current;
4396
+ }
4397
+ /**
4398
+ * Deliver output to callbacks in the background, for `spawn` with
4399
+ * `onStdout`/`onStderr`. {@link wait} waits for it and rethrows its error.
4400
+ *
4401
+ * @internal
4402
+ */
4403
+ startOutputPump(onStdout, onStderr) {
4404
+ this.pump = (async () => {
4405
+ for await (const chunk of this.logs()) {
4406
+ await (chunk.stream === "stdout" ? onStdout : onStderr)?.(chunk.text, chunk);
4407
+ }
4408
+ })().catch((error) => {
4409
+ this.pumpError = error instanceof Error ? error : new Error(String(error));
4410
+ });
4411
+ }
4412
+ };
4413
+
4414
+ // src/microvms/microvm.ts
4415
+ var defaultClient;
4416
+ var resolveClient = (client) => {
4417
+ if (client instanceof GigadriveClient) return client;
4418
+ if (client) return new GigadriveClient(client);
4419
+ defaultClient ??= new GigadriveClient();
4420
+ return defaultClient;
4421
+ };
4422
+ var CREATE_WAIT_TIMEOUT = "10m";
4423
+ var NOT_LISTENING = /* @__PURE__ */ new Set([502, 503, 504]);
4424
+ var PROBE_TIMEOUT_MS = 5e3;
4425
+ var TERMINAL = ["stopping", "stopped", "failed"];
4426
+ var toStartInput = (cmd, args, options) => {
4427
+ const user = options.sudo ? "root" : options.user;
4428
+ return {
4429
+ cmd,
4430
+ ...args ? { args } : {},
4431
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
4432
+ ...options.env ? { env: options.env } : {},
4433
+ ...user !== void 0 ? { user } : {},
4434
+ ...options.timeout !== void 0 ? { timeoutMs: durationToMs(options.timeout) } : {}
4435
+ };
4436
+ };
4437
+ var failIfRequested = (result, throwOnError) => {
4438
+ if (throwOnError && result.exitCode !== 0) throw new CommandFailedError(result);
4439
+ return result;
4440
+ };
4441
+ var MicroVmFiles = class {
4442
+ /** @internal */
4443
+ constructor(resource, microvmId) {
4444
+ this.resource = resource;
4445
+ this.microvmId = microvmId;
4446
+ }
4447
+ resource;
4448
+ microvmId;
4449
+ /** Read a file as bytes. @throws {@link MicroVmFileNotFoundError} */
4450
+ read(path, options) {
4451
+ return this.resource.read(this.microvmId, path, options);
4452
+ }
4453
+ /** Read a file as UTF-8 text. */
4454
+ async readText(path, options) {
4455
+ return new TextDecoder().decode(await this.read(path, options));
4456
+ }
4457
+ /** Read a file as a stream, for files too large to buffer. */
4458
+ readStream(path, options) {
4459
+ return this.resource.readStream(this.microvmId, path, options);
4460
+ }
4461
+ /**
4462
+ * Write a file atomically, creating parent directories.
4463
+ *
4464
+ * @example
4465
+ * ```ts
4466
+ * await vm.files.write('/app/index.js', 'console.log("hi")');
4467
+ * await vm.files.write('/app/logo.png', await (await fetch(url)).blob());
4468
+ * ```
4469
+ */
4470
+ write(path, content, options) {
4471
+ return this.resource.write(this.microvmId, path, content, options);
4472
+ }
4473
+ /**
4474
+ * Write several files, a few at a time. Resolves in input order once all
4475
+ * succeeded; rejects with the first failure.
4476
+ *
4477
+ * @example
4478
+ * ```ts
4479
+ * await vm.files.writeMany([
4480
+ * { path: 'app/package.json', content: JSON.stringify(pkg) },
4481
+ * { path: 'app/run.sh', content: '#!/bin/sh\nnode .', mode: '0755' },
4482
+ * ]);
4483
+ * ```
4484
+ */
4485
+ async writeMany(files, options = {}) {
4486
+ const results = new Array(files.length);
4487
+ let next = 0;
4488
+ const worker = async () => {
4489
+ while (next < files.length) {
4490
+ const index = next++;
4491
+ const file = files[index];
4492
+ results[index] = await this.write(file.path, file.content, { mode: file.mode, signal: options.signal });
4493
+ }
4494
+ };
4495
+ const workers = Math.max(1, Math.min(options.concurrency ?? 4, files.length));
4496
+ await Promise.all(Array.from({ length: workers }, worker));
4497
+ return results;
4498
+ }
4499
+ /**
4500
+ * List a directory. `recursive` descends into subdirectories (up to `depth`
4501
+ * levels). Very large listings are cut off; `client.microvms.files.list`
4502
+ * reports that as `truncated`.
4503
+ */
4504
+ async list(path = ".", options) {
4505
+ return (await this.resource.list(this.microvmId, path, options)).entries;
4506
+ }
4507
+ /** Describe a file or directory. @throws {@link MicroVmFileNotFoundError} */
4508
+ stat(path) {
4509
+ return this.resource.stat(this.microvmId, path);
4510
+ }
4511
+ /** Whether a file or directory exists. */
4512
+ async exists(path) {
4513
+ try {
4514
+ await this.stat(path);
4515
+ return true;
4516
+ } catch (error) {
4517
+ if (error instanceof MicroVmFileNotFoundError) return false;
4518
+ throw error;
4519
+ }
4520
+ }
4521
+ /** Create a directory and its parents (pass `parents: false` to require the parent). */
4522
+ mkdir(path, options) {
4523
+ return this.resource.mkdir(this.microvmId, path, options);
4524
+ }
4525
+ /** Delete a file, or a directory with `recursive: true`. */
4526
+ remove(path, options) {
4527
+ return this.resource.remove(this.microvmId, path, options);
4528
+ }
4529
+ /** Move or rename a file or directory. */
4530
+ move(from, to) {
4531
+ return this.resource.move(this.microvmId, from, to);
4532
+ }
4533
+ };
4534
+ var MicroVmCommands = class {
4535
+ /** @internal */
4536
+ constructor(resource, microvmId) {
4537
+ this.resource = resource;
4538
+ this.microvmId = microvmId;
4539
+ }
4540
+ resource;
4541
+ microvmId;
4542
+ /**
4543
+ * Reattach to a command, for example one spawned by another process.
4544
+ *
4545
+ * @example
4546
+ * ```ts
4547
+ * const command = await vm.commands.get(savedCommandId);
4548
+ * for await (const chunk of command.logs()) process.stdout.write(chunk.data);
4549
+ * ```
4550
+ */
4551
+ async get(commandId) {
4552
+ return new Command(this.resource, this.microvmId, await this.resource.get(this.microvmId, commandId));
4553
+ }
4554
+ /** All commands the microVM still remembers, newest first. */
4555
+ async list() {
4556
+ const infos = await this.resource.list(this.microvmId);
4557
+ return infos.map((info) => new Command(this.resource, this.microvmId, info));
4558
+ }
4559
+ };
4560
+ var MicroVM = class _MicroVM {
4561
+ /** @internal Use {@link MicroVM.create} or {@link MicroVM.get}. */
4562
+ constructor(resource, info) {
4563
+ this.resource = resource;
4564
+ this.current = info;
4565
+ this.files = new MicroVmFiles(resource.files, info.id);
4566
+ this.commands = new MicroVmCommands(resource.commands, info.id);
4567
+ }
4568
+ resource;
4569
+ /** Files inside the microVM. */
4570
+ files;
4571
+ /** Reattach to commands by id. */
4572
+ commands;
4573
+ current;
4574
+ /**
4575
+ * Create a microVM and wait until it runs.
4576
+ *
4577
+ * @throws {RangeError} When `timeout` is longer than 24 hours.
4578
+ *
4579
+ * The application is taken from `applicationId`, the client's
4580
+ * `applicationId` or `GIGADRIVE_APPLICATION_ID`; inside a Gigadrive
4581
+ * deployment or Function nothing needs configuring.
4582
+ *
4583
+ * @example
4584
+ * ```ts
4585
+ * const vm = await MicroVM.create({
4586
+ * image: 'python3.13',
4587
+ * vcpus: 2,
4588
+ * memoryMb: 4096,
4589
+ * timeout: '15m',
4590
+ * ports: [8000],
4591
+ * env: { PYTHONUNBUFFERED: '1' },
4592
+ * metadata: { user: 'u_123' },
4593
+ * network: 'deny',
4594
+ * });
4595
+ * ```
4596
+ */
4597
+ static async create(options = {}) {
4598
+ const { client, timeout, timeoutMs, network, wait, signal, ...input } = options;
4599
+ const resource = resolveClient(client).microvms;
4600
+ const info = await resource.create(
4601
+ {
4602
+ ...input,
4603
+ ...timeout !== void 0 ? { timeoutMs: microvmTimeoutMs(timeout) } : timeoutMs !== void 0 ? { timeoutMs } : {},
4604
+ ...network !== void 0 ? { network: typeof network === "string" ? { egress: network } : network } : {}
4605
+ },
4606
+ // Never hold one request open for the whole boot: a create can wait
4607
+ // minutes for capacity, longer than proxies in front of the API keep a
4608
+ // response open. Start it, then poll.
4609
+ { wait: false, signal }
4610
+ );
4611
+ const vm = new _MicroVM(resource, info);
4612
+ if (wait === false) return vm;
4613
+ try {
4614
+ return await vm.waitUntilRunning({ timeout: CREATE_WAIT_TIMEOUT, signal });
4615
+ } catch (error) {
4616
+ if (error instanceof MicroVmStartError) throw error;
4617
+ try {
4618
+ await vm.delete();
4619
+ } catch (cleanupError) {
4620
+ throw new MicroVmCleanupError(vm.id, error, cleanupError);
4621
+ }
4622
+ throw error;
4623
+ }
4624
+ }
4625
+ /**
4626
+ * Connect to an existing microVM by id.
4627
+ *
4628
+ * @throws {@link MicroVmNotFoundError} when it does not exist.
4629
+ */
4630
+ static async get(microvmId, options = {}) {
4631
+ const resource = resolveClient(options.client).microvms;
4632
+ return new _MicroVM(resource, await resource.get(microvmId));
4633
+ }
4634
+ /** Alias of {@link MicroVM.get}. */
4635
+ static connect(microvmId, options = {}) {
4636
+ return _MicroVM.get(microvmId, options);
4637
+ }
4638
+ /**
4639
+ * Iterate microVMs, newest first, across all pages.
4640
+ *
4641
+ * @example
4642
+ * ```ts
4643
+ * for await (const vm of MicroVM.list({ status: 'running' })) {
4644
+ * console.log(vm.id, vm.metadata);
4645
+ * }
4646
+ * ```
4647
+ */
4648
+ static async *list(options = {}) {
4649
+ const { client, ...query } = options;
4650
+ const resource = resolveClient(client).microvms;
4651
+ for await (const info of resource.listAll(query)) yield new _MicroVM(resource, info);
4652
+ }
4653
+ /** Stable UUID. */
4654
+ get id() {
4655
+ return this.current.id;
4656
+ }
4657
+ /** Label used in preview URLs. */
4658
+ get slug() {
4659
+ return this.current.slug;
4660
+ }
4661
+ get applicationId() {
4662
+ return this.current.applicationId;
4663
+ }
4664
+ /** Status as of the last API response; call {@link refresh} for the current one. */
4665
+ get status() {
4666
+ return this.current.status;
4667
+ }
4668
+ /** Exposed ports. */
4669
+ get ports() {
4670
+ return this.current.ports.map((entry) => entry.port);
4671
+ }
4672
+ get metadata() {
4673
+ return this.current.metadata;
4674
+ }
4675
+ /** When the microVM stops automatically. */
4676
+ get expiresAt() {
4677
+ return this.current.expiresAt ? new Date(this.current.expiresAt) : null;
4678
+ }
4679
+ /** The full API object as of the last response. */
4680
+ get info() {
4681
+ return this.current;
4682
+ }
4683
+ /**
4684
+ * The public preview URL of an exposed port.
4685
+ *
4686
+ * @throws {@link MicroVmPortNotExposedError} when the port was not in `ports` at create.
4687
+ *
4688
+ * @example
4689
+ * ```ts
4690
+ * vm.url(3000); // "https://3000-k3j9x2m1q8zt.vm.gigadrive.app"
4691
+ * ```
4692
+ */
4693
+ url(port) {
4694
+ const entry = this.current.ports.find((candidate) => candidate.port === port);
4695
+ if (!entry) throw new MicroVmPortNotExposedError(port, this.ports);
4696
+ return entry.url;
4697
+ }
4698
+ /** Fetch the latest state. */
4699
+ async refresh(options = {}) {
4700
+ this.current = await this.resource.get(this.id, { signal: options.signal });
4701
+ return this.current;
4702
+ }
4703
+ async run(cmd, argsOrOptions, maybeOptions) {
4704
+ const args = Array.isArray(argsOrOptions) ? argsOrOptions : void 0;
4705
+ const options = (Array.isArray(argsOrOptions) ? maybeOptions : argsOrOptions) ?? {};
4706
+ const input = toStartInput(cmd, args, options);
4707
+ const streaming = options.onStdout !== void 0 || options.onStderr !== void 0 || options.stdin !== void 0 || options.signal !== void 0;
4708
+ if (!streaming) {
4709
+ const raw = await this.resource.commands.run(this.id, input);
4710
+ return failIfRequested(
4711
+ {
4712
+ commandId: raw.id,
4713
+ exitCode: raw.exitCode,
4714
+ signal: raw.signal ?? null,
4715
+ timedOut: raw.timedOut === true,
4716
+ stdout: raw.stdout,
4717
+ stderr: raw.stderr,
4718
+ truncated: raw.truncated,
4719
+ durationMs: raw.durationMs
4720
+ },
4721
+ options.throwOnError
4722
+ );
4723
+ }
4724
+ const { signal } = options;
4725
+ if (signal?.aborted) throw abortReason(signal);
4726
+ const startedAt = Date.now();
4727
+ const info = await this.resource.commands.start(
4728
+ this.id,
4729
+ { ...input, ...options.stdin !== void 0 ? { stdin: true } : {} },
4730
+ { signal }
4731
+ );
4732
+ const command = new Command(this.resource.commands, this.id, info);
4733
+ const kill = () => void command.kill("SIGKILL").catch(() => void 0);
4734
+ signal?.addEventListener("abort", kill, { once: true });
4735
+ try {
4736
+ if (options.stdin !== void 0) await command.end(options.stdin);
4737
+ const output = { stdout: "", stderr: "" };
4738
+ const expected = { stdout: 0, stderr: 0 };
4739
+ let truncated = false;
4740
+ for await (const chunk of command.logs({ signal })) {
4741
+ if (chunk.offset > expected[chunk.stream]) truncated = true;
4742
+ expected[chunk.stream] = chunk.offset + chunk.data.length;
4743
+ output[chunk.stream] += chunk.text;
4744
+ await (chunk.stream === "stdout" ? options.onStdout : options.onStderr)?.(chunk.text, chunk);
4745
+ }
4746
+ const final = command.status === "exited" ? command.info : await command.wait({ signal });
4747
+ return failIfRequested(
4748
+ {
4749
+ commandId: command.id,
4750
+ exitCode: final.exitCode,
4751
+ signal: final.signal,
4752
+ timedOut: final.timedOut === true,
4753
+ ...output,
4754
+ truncated: truncated || command.truncated,
4755
+ durationMs: commandDurationMs(final) ?? Date.now() - startedAt
4756
+ },
4757
+ options.throwOnError
4758
+ );
4759
+ } finally {
4760
+ signal?.removeEventListener("abort", kill);
4761
+ }
4762
+ }
4763
+ async spawn(cmd, argsOrOptions, maybeOptions) {
4764
+ const args = Array.isArray(argsOrOptions) ? argsOrOptions : void 0;
4765
+ const options = (Array.isArray(argsOrOptions) ? maybeOptions : argsOrOptions) ?? {};
4766
+ const pty = options.pty === true ? { cols: 80, rows: 24 } : options.pty || void 0;
4767
+ const info = await this.resource.commands.start(this.id, {
4768
+ ...toStartInput(cmd, args, options),
4769
+ ...options.stdin ? { stdin: true } : {},
4770
+ ...pty ? { pty } : {}
4771
+ });
4772
+ const command = new Command(this.resource.commands, this.id, info);
4773
+ if (options.onStdout || options.onStderr) command.startOutputPump(options.onStdout, options.onStderr);
4774
+ return command;
4775
+ }
4776
+ /**
4777
+ * Set the remaining lifetime to `duration` from now (up to the plan's
4778
+ * maximum lifetime).
4779
+ *
4780
+ * @example
4781
+ * ```ts
4782
+ * await vm.extendTimeout('30m');
4783
+ * ```
4784
+ */
4785
+ async extendTimeout(duration) {
4786
+ this.current = await this.resource.extendTimeout(this.id, microvmTimeoutMs(duration));
4787
+ return this.current;
4788
+ }
4789
+ /** Stop the microVM. Its disk, memory and running commands are discarded. */
4790
+ async stop() {
4791
+ this.current = await this.resource.stop(this.id);
4792
+ return this.current;
4793
+ }
4794
+ /** Delete the microVM, stopping it first if needed. */
4795
+ async delete() {
4796
+ await this.resource.delete(this.id);
4797
+ }
4798
+ /**
4799
+ * Poll until the microVM is running, for microVMs created with `wait: false`.
4800
+ *
4801
+ * @throws {@link MicroVmStartError} when it failed or stopped instead.
4802
+ * @throws {@link MicroVmTimeoutError} when it is still starting after `timeout`.
4803
+ */
4804
+ async waitUntilRunning(options = {}) {
4805
+ const deadline = Date.now() + durationToMs(options.timeout ?? "2m");
4806
+ const interval = durationToMs(options.interval ?? "500ms");
4807
+ while (this.current.status !== "running") {
4808
+ if (TERMINAL.includes(this.current.status)) {
4809
+ throw new MicroVmStartError(this.id, this.current.status, this.current.stopReason);
4810
+ }
4811
+ if (Date.now() + interval > deadline) {
4812
+ throw new MicroVmTimeoutError(`The microVM ${this.id} is still ${this.current.status}; gave up waiting.`);
4813
+ }
4814
+ await sleep(interval, options.signal);
4815
+ await this.refresh({ signal: options.signal });
4816
+ if (options.signal?.aborted) throw abortReason(options.signal);
4817
+ }
4818
+ return this;
4819
+ }
4820
+ /**
4821
+ * Poll an exposed port's preview URL until a server answers (any status
4822
+ * other than 502, 503 or 504, which the gateway returns while nothing
4823
+ * listens).
4824
+ *
4825
+ * @returns The preview URL.
4826
+ * @throws {@link MicroVmPortNotExposedError} when the port is not exposed.
4827
+ * @throws {@link MicroVmNotRunningError} when the microVM has stopped.
4828
+ * @throws {@link MicroVmTimeoutError} when nothing answers within `timeout`.
4829
+ *
4830
+ * @example
4831
+ * ```ts
4832
+ * await vm.spawn('python3', ['-m', 'http.server', '8000']);
4833
+ * const url = await vm.waitForPort(8000);
4834
+ * ```
4835
+ */
4836
+ async waitForPort(port, options = {}) {
4837
+ const base = this.url(port);
4838
+ const path = options.path ?? "/";
4839
+ const target = `${base.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
4840
+ const deadline = Date.now() + durationToMs(options.timeout ?? "60s");
4841
+ const interval = durationToMs(options.interval ?? "500ms");
4842
+ for (; ; ) {
4843
+ if (options.signal?.aborted) throw abortReason(options.signal);
4844
+ const controller = new AbortController();
4845
+ const timer = setTimeout(
4846
+ () => controller.abort(),
4847
+ Math.min(PROBE_TIMEOUT_MS, Math.max(1, deadline - Date.now()))
4848
+ );
4849
+ const forward = () => controller.abort();
4850
+ options.signal?.addEventListener("abort", forward, { once: true });
4851
+ let stopped = false;
4852
+ try {
4853
+ const response = await this.resource.fetchPreview(target, {
4854
+ method: "GET",
4855
+ redirect: "manual",
4856
+ signal: controller.signal
4857
+ });
4858
+ await response.body?.cancel().catch(() => void 0);
4859
+ stopped = response.status === 410 && response.headers.get("x-gigadrive-error") === "EDGE_MICROVM_STOPPED";
4860
+ if (!stopped && !NOT_LISTENING.has(response.status)) return base;
4861
+ } catch {
4862
+ if (options.signal?.aborted) throw abortReason(options.signal);
4863
+ } finally {
4864
+ clearTimeout(timer);
4865
+ options.signal?.removeEventListener("abort", forward);
4866
+ }
4867
+ if (stopped) {
4868
+ throw new MicroVmNotRunningError(`The microVM ${this.id} has stopped; nothing will answer on port ${port}.`);
4869
+ }
4870
+ if (Date.now() + interval > deadline) {
4871
+ throw new MicroVmTimeoutError(`Nothing answered on port ${port} of the microVM ${this.id}.`);
4872
+ }
4873
+ await sleep(interval, options.signal);
4874
+ }
4875
+ }
4876
+ /** Stops the microVM at the end of an `await using` block. Already stopped microVMs are fine. */
4877
+ async [Symbol.asyncDispose]() {
4878
+ try {
4879
+ await this.stop();
4880
+ } catch (error) {
4881
+ if (error instanceof MicroVmNotRunningError || error instanceof MicroVmNotFoundError) return;
4882
+ throw error;
4883
+ }
4884
+ }
4885
+ /** JSON form: the API object. */
4886
+ toJSON() {
4887
+ return this.current;
3474
4888
  }
3475
4889
  };
3476
4890
 
@@ -3487,10 +4901,10 @@ async function* paginate(fetchPage) {
3487
4901
  }
3488
4902
 
3489
4903
  // src/queues/default-client.ts
3490
- var defaultClient;
4904
+ var defaultClient2;
3491
4905
  var defaultQueuesResource = () => {
3492
- defaultClient ??= new GigadriveClient();
3493
- return defaultClient.queues;
4906
+ defaultClient2 ??= new GigadriveClient();
4907
+ return defaultClient2.queues;
3494
4908
  };
3495
4909
  function queue(name, options = {}) {
3496
4910
  const { client, ...queueOptions } = options;
@@ -3514,7 +4928,7 @@ var decodeWorkflowMessage = (text) => JSON.parse(text, (_key, value) => {
3514
4928
  return value;
3515
4929
  });
3516
4930
  var readEnv2 = (name) => globalThis.process?.env?.[name] || void 0;
3517
- var deduplicationKeyFor = async (idempotencyKey) => idempotencyKey.length <= 256 ? idempotencyKey : `wkf:sha256:${toHex2(await crypto.subtle.digest("SHA-256", toArrayBuffer(utf8Encode(idempotencyKey))))}`;
4931
+ var deduplicationKeyFor = async (idempotencyKey) => idempotencyKey.length <= 256 ? idempotencyKey : `wkf:sha256:${toHex(await crypto.subtle.digest("SHA-256", toArrayBuffer(utf8Encode(idempotencyKey))))}`;
3518
4932
  function createWorkflowQueue(options = {}) {
3519
4933
  const resource = () => options.client?.queues ?? defaultQueuesResource();
3520
4934
  const scope = { applicationId: options.applicationId, environment: options.environment };
@@ -3655,12 +5069,34 @@ export {
3655
5069
  ApplicationStorageResource,
3656
5070
  ApplicationsResource,
3657
5071
  AuthenticationError,
5072
+ Command,
5073
+ CommandFailedError,
3658
5074
  ConfigurationError,
3659
5075
  DeploymentsResource,
3660
5076
  DomainNotActiveError,
3661
5077
  GigadriveClient,
3662
5078
  GigadriveError,
3663
5079
  ImageOptimizationResource,
5080
+ MICROVM_MAX_TIMEOUT_MS,
5081
+ MicroVM,
5082
+ MicroVmBusyError,
5083
+ MicroVmCleanupError,
5084
+ MicroVmCommands,
5085
+ MicroVmCommandsResource,
5086
+ MicroVmFileNotFoundError,
5087
+ MicroVmFiles,
5088
+ MicroVmFilesResource,
5089
+ MicroVmImageBuildError,
5090
+ MicroVmImageNotReadyError,
5091
+ MicroVmImagesResource,
5092
+ MicroVmNotFoundError,
5093
+ MicroVmNotRunningError,
5094
+ MicroVmPortNotExposedError,
5095
+ MicroVmQuotaExceededError,
5096
+ MicroVmStartError,
5097
+ MicroVmTimeoutError,
5098
+ MicroVmUnreachableError,
5099
+ MicroVmsResource,
3664
5100
  NonRetryableError,
3665
5101
  OrganizationAiGatewayResource,
3666
5102
  OrganizationDomainsResource,
@@ -3688,8 +5124,10 @@ export {
3688
5124
  createManagedImageUrl,
3689
5125
  createWorkflowQueue,
3690
5126
  defaultQueueCodec,
5127
+ durationToMs,
3691
5128
  durationToSeconds,
3692
5129
  paginate,
5130
+ parseSSEEvents,
3693
5131
  parseSSEStream,
3694
5132
  queue,
3695
5133
  signQueueDelivery,