@lizard-build/sdk 0.1.49 → 0.1.52

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
@@ -59,18 +59,44 @@ var TimeoutError = class extends LizardError {
59
59
  this.name = "TimeoutError";
60
60
  }
61
61
  };
62
+ var PaymentRequiredError = class extends LizardError {
63
+ constructor(message = "Payment required") {
64
+ super(message);
65
+ this.name = "PaymentRequiredError";
66
+ }
67
+ /** The one page to open next for this {@link paymentStatus}. */
68
+ get url() {
69
+ const s = this.paymentStatus;
70
+ if (this.invoiceUrl) return this.invoiceUrl;
71
+ if ((s === "trial_available" || s === "subscription_required") && this.subscribeUrl) return this.subscribeUrl;
72
+ if (s && PREPAID_STATUSES.has(s) && this.topupUrl) return this.topupUrl;
73
+ return this.billingUrl ?? this.subscribeUrl ?? this.topupUrl;
74
+ }
75
+ };
76
+ var PREPAID_STATUSES = /* @__PURE__ */ new Set(["grace", "frozen", "card_required", "credits_required"]);
77
+ var ERROR_CODE = /^[A-Z][A-Z0-9_]*$/;
62
78
  async function handleApiError(res) {
63
- let message;
64
- let code;
79
+ let body = {};
65
80
  try {
66
- const body = await res.json();
67
- message = body.error ?? res.statusText;
68
- if (typeof body.code === "string") code = body.code;
81
+ const parsed = await res.json();
82
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) body = parsed;
69
83
  } catch {
70
- message = res.statusText;
71
84
  }
85
+ const text = (key) => typeof body[key] === "string" && body[key] ? body[key] : void 0;
86
+ const error = text("error");
87
+ const errorIsCode = !!error && ERROR_CODE.test(error);
88
+ const message = (errorIsCode ? text("message") ?? error : error ?? text("message")) ?? res.statusText;
89
+ const code = text("code") ?? (errorIsCode ? error : void 0);
72
90
  let err;
73
- if (res.status === 401 || res.status === 403) err = new AuthenticationError(message);
91
+ if (res.status === 402) {
92
+ const paid = new PaymentRequiredError(message);
93
+ paid.paymentStatus = text("status");
94
+ paid.subscribeUrl = text("subscribeUrl");
95
+ paid.billingUrl = text("billingUrl");
96
+ paid.topupUrl = text("topupUrl");
97
+ paid.invoiceUrl = text("invoiceUrl");
98
+ err = paid;
99
+ } else if (res.status === 401 || res.status === 403) err = new AuthenticationError(message);
74
100
  else if (res.status === 404) err = new NotFoundError(message);
75
101
  else if (res.status === 409) err = new ConflictError(message);
76
102
  else if (res.status === 408 || res.status === 504) err = new TimeoutError(message);
@@ -496,7 +522,9 @@ var Process = class {
496
522
  envs: opts?.envs,
497
523
  user: opts?.user,
498
524
  workdir: opts?.workdir,
499
- timeoutMs: opts?.timeoutMs
525
+ timeoutMs: opts?.timeoutMs,
526
+ // The pid event is opt-in: older CLIs print any unknown data line as output.
527
+ pidEvent: opts?.onPid ? true : void 0
500
528
  }),
501
529
  signal: AbortSignal.timeout(opts?.timeoutMs ?? 6e4)
502
530
  });
@@ -504,11 +532,6 @@ var Process = class {
504
532
  if (!streaming) return res.json();
505
533
  return this.consumeStream(res, opts);
506
534
  }
507
- /**
508
- * Read an SSE exec stream, handing each line to the caller as it arrives while
509
- * still accumulating the full result. Events are `{stream, line}` for output and
510
- * `{exitCode}` at the end.
511
- */
512
535
  /**
513
536
  * List the processes this sandbox is currently running.
514
537
  *
@@ -532,8 +555,8 @@ var Process = class {
532
555
  /**
533
556
  * Signal a running process. Defaults to `SIGTERM`.
534
557
  *
535
- * The signal goes to the process group, so a shell's children die with it —
536
- * killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
558
+ * The signal goes to the whole process tree, so a shell's children die with
559
+ * it — killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
537
560
  *
538
561
  * The pid comes from {@link list}, or from the `pid` event at the start of a
539
562
  * streaming `exec`.
@@ -552,6 +575,11 @@ var Process = class {
552
575
  });
553
576
  if (!res.ok) await handleApiError(res);
554
577
  }
578
+ /**
579
+ * Read an SSE exec stream, handing each line to the caller as it arrives while
580
+ * still accumulating the full result. Events are `{pid}` first (when asked for
581
+ * with `pidEvent`), `{stream, line}` for output and `{exitCode}` at the end.
582
+ */
555
583
  async consumeStream(res, opts) {
556
584
  const reader = res.body?.getReader();
557
585
  if (!reader) return { stdout: "", stderr: "", exitCode: 0 };
@@ -602,33 +630,37 @@ var Fs = class {
602
630
  /**
603
631
  * Write a file into the sandbox filesystem.
604
632
  *
605
- * Creates parent directories automatically if they don't exist.
606
- * Byte input must contain valid UTF-8; arbitrary binary uploads are unsupported.
633
+ * Creates parent directories automatically if they don't exist. A string is
634
+ * written as UTF-8; bytes (`Uint8Array`, `Buffer` or `ArrayBuffer`) are written
635
+ * exactly as given, so binary files round-trip with {@link readBytes}.
636
+ *
637
+ * With `opts.user` the file is written as (and owned by) that user.
607
638
  *
608
639
  * @example
609
640
  * ```ts
610
641
  * await sandbox.fs.write('/app/index.js', 'console.log("hello")')
611
642
  * ```
612
643
  *
613
- * @example Write UTF-8 bytes:
644
+ * @example Write binary data:
614
645
  * ```ts
615
- * await sandbox.fs.write('/app/notes.txt', new TextEncoder().encode('hello'))
646
+ * await sandbox.fs.write('/app/logo.png', await readFile('logo.png'))
616
647
  * ```
617
648
  */
618
649
  async write(path, data, opts) {
619
- const content = typeof data === "string" ? data : new TextDecoder("utf-8", { fatal: true }).decode(data);
650
+ const body = typeof data === "string" ? { path, content: data, user: opts?.user } : { path, content: bytesToBase64(data instanceof ArrayBuffer ? new Uint8Array(data) : data), encoding: "base64", user: opts?.user };
620
651
  const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files`, {
621
652
  method: "POST",
622
653
  headers: this.config.headers,
623
- body: JSON.stringify({ path, content, user: opts?.user })
654
+ body: JSON.stringify(body)
624
655
  });
625
656
  if (!res.ok) await handleApiError(res);
626
657
  }
627
658
  /**
628
659
  * Read a file from the sandbox filesystem.
629
660
  *
630
- * @returns The file contents as a UTF-8 string.
661
+ * @returns The file contents as a UTF-8 string. Use {@link readBytes} for binary files.
631
662
  *
663
+ * Throws `NotFoundError` if the path does not exist.
632
664
  * @example
633
665
  * ```ts
634
666
  * const content = await sandbox.fs.read('/app/index.js')
@@ -696,9 +728,7 @@ var Fs = class {
696
728
  * console.log(info.size, info.type)
697
729
  * ```
698
730
  *
699
- * Throws `NotFoundError` if the path does not exist. Sandboxes created before
700
- * this shipped run a guest agent without it and throw `LizardError` (501) —
701
- * recreate the sandbox to use it.
731
+ * Throws `NotFoundError` if the path does not exist.
702
732
  */
703
733
  async stat(path, opts) {
704
734
  const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/stat`);
@@ -715,9 +745,6 @@ var Fs = class {
715
745
  * ```ts
716
746
  * await sandbox.fs.move('/tmp/build.log', '/app/logs/build.log')
717
747
  * ```
718
- *
719
- * Sandboxes created before this shipped run a guest agent without it and throw
720
- * `LizardError` (501) — recreate the sandbox to use it.
721
748
  */
722
749
  async move(from, to) {
723
750
  const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/move`, {
@@ -746,8 +773,7 @@ var Fs = class {
746
773
  * }, 1000)
747
774
  * ```
748
775
  *
749
- * Sandboxes created before this shipped run a guest agent without it and throw
750
- * `LizardError` (501) — recreate the sandbox to use it.
776
+ * Not available on Firecracker sandboxes yet: the call throws `LizardError` (501).
751
777
  */
752
778
  async watch(path, opts) {
753
779
  const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/watch`, {
@@ -793,6 +819,12 @@ var Watcher = class {
793
819
  if (!res.ok) await handleApiError(res);
794
820
  }
795
821
  };
822
+ function bytesToBase64(bytes) {
823
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
824
+ let s = "";
825
+ for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
826
+ return btoa(s);
827
+ }
796
828
 
797
829
  // src/sandbox/desktop.ts
798
830
  var BUTTONS = { left: "1", middle: "2", right: "3" };
@@ -1093,11 +1125,17 @@ var Sandbox = class extends SandboxClient {
1093
1125
  async kill(opts) {
1094
1126
  return SandboxClient.killSandbox(this.sandboxId, this.resolveOpts(opts));
1095
1127
  }
1096
- /** Queue a CRIU checkpoint and stop compute. Use waitForStatus('paused') before resuming. */
1128
+ /**
1129
+ * Pause the sandbox: its memory, running processes and files are kept and
1130
+ * compute stops. Wait with `waitForStatus('paused')` before resuming.
1131
+ */
1097
1132
  async pause(opts) {
1098
1133
  return SandboxClient.pauseSandbox(this.sandboxId, this.resolveOpts(opts));
1099
1134
  }
1100
- /** Queue restoration of saved memory and files. Use waitForStatus('running') before executing. */
1135
+ /**
1136
+ * Resume a paused sandbox exactly where it stopped, processes included. Wait
1137
+ * with `waitForStatus('running')` before running commands.
1138
+ */
1101
1139
  async resume(opts) {
1102
1140
  return SandboxClient.resumeSandbox(this.sandboxId, this.resolveOpts(opts));
1103
1141
  }
@@ -1130,33 +1168,82 @@ var Sandbox = class extends SandboxClient {
1130
1168
  return SandboxClient.setTimeoutSandbox(this.sandboxId, timeoutMs, this.resolveOpts(opts));
1131
1169
  }
1132
1170
  /**
1133
- * Register a public HTTPS route and return its hostname without a scheme.
1171
+ * Publish a port on a public HTTPS hostname and return the hostname without a scheme.
1134
1172
  *
1135
- * Useful for accessing HTTP servers started inside the sandbox from your
1136
- * agent or tests without additional tunneling.
1173
+ * The port is private: requests need {@link accessToken} (set by this call) as
1174
+ * the `X-Lizard-Access-Token` header, or `?lizard_token=<token>` in a browser.
1175
+ * {@link exposePort} returns the token and a ready-made browser URL together.
1137
1176
  *
1138
1177
  * @example
1139
1178
  * ```ts
1140
1179
  * // Start an HTTP server on port 3000 before exposing it.
1141
1180
  * const hostname = await sandbox.getHost(3000)
1142
- * console.log(`https://${hostname}`)
1181
+ * const res = await fetch(`https://${hostname}/`, {
1182
+ * headers: { 'X-Lizard-Access-Token': sandbox.accessToken! },
1183
+ * })
1143
1184
  * ```
1144
1185
  */
1145
1186
  async getHost(port, opts) {
1146
- const { hostname } = await SandboxClient.exposeSandboxPort(this.sandboxId, port, this.resolveOpts(opts));
1147
- return hostname;
1187
+ return (await this.exposePort(port, opts)).hostname;
1148
1188
  }
1149
- /** Fork is unsupported on Kubernetes; the backend returns HTTP 501. */
1150
- fork(opts = {}) {
1151
- return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/fork`, { count: opts.count ?? 1, timeoutMs: opts.timeoutMs ?? 0 });
1189
+ /**
1190
+ * Publish a port and return everything needed to reach it: the hostname, a
1191
+ * browser `url` that carries the token, and the `accessToken` for API clients.
1192
+ *
1193
+ * @example
1194
+ * ```ts
1195
+ * const { url, hostname, accessToken } = await sandbox.exposePort(3000)
1196
+ * console.log(url) // open in a browser
1197
+ * await fetch(`https://${hostname}/api`, { headers: { 'X-Lizard-Access-Token': accessToken } })
1198
+ * ```
1199
+ */
1200
+ async exposePort(port, opts) {
1201
+ const exposed = await SandboxClient.exposeSandboxPort(this.sandboxId, port, this.resolveOpts(opts));
1202
+ if (exposed.accessToken) this.accessToken = exposed.accessToken;
1203
+ return exposed;
1204
+ }
1205
+ /**
1206
+ * Clone this running sandbox `count` times (1-10). Each fork is a copy of the
1207
+ * microVM at this instant -- memory, running processes and files -- and is
1208
+ * billed like its source. The source keeps running.
1209
+ *
1210
+ * Returns one entry per requested fork, in order: `{ sandbox, info }`, or
1211
+ * `{ error }` for a copy that could not be made (e.g. the account's sandbox
1212
+ * limit). A sandbox with a volume attached cannot be forked (`ConflictError`, 409).
1213
+ *
1214
+ * @param opts.timeoutMs Lifetime of each fork; defaults to the source's timeout.
1215
+ *
1216
+ * @example
1217
+ * ```ts
1218
+ * const forks = await sandbox.fork({ count: 2 })
1219
+ * for (const f of forks) if (f.sandbox) await f.sandbox.process.exec('echo hi')
1220
+ * ```
1221
+ */
1222
+ async fork(opts = {}) {
1223
+ const result = await new PlatformClient(this.resolveOpts()).post(
1224
+ `/api/sandboxes/${this.sandboxId}/fork`,
1225
+ { count: opts.count ?? 1, timeoutMs: opts.timeoutMs ?? 0 }
1226
+ );
1227
+ const Ctor = this.constructor;
1228
+ return (Array.isArray(result) ? result : []).map((entry) => {
1229
+ const id = entry.sandbox?.sandboxId ?? entry.sandbox?.id;
1230
+ if (!entry.sandbox || !id) return { error: entry.error ?? "fork failed" };
1231
+ return { sandbox: new Ctor({ ...this.resolveOpts(), sandboxId: id }), info: { ...entry.sandbox, sandboxId: id } };
1232
+ });
1152
1233
  }
1153
- /** Capture memory and workspace files. Disconnect active clients before capturing. */
1234
+ /**
1235
+ * Save this sandbox -- memory, running processes and files -- as a private
1236
+ * snapshot. A Firecracker snapshot is `ready` as soon as this returns (about 2 s);
1237
+ * start copies of it with {@link Sandbox.restore} (about 0.4 s each). The source
1238
+ * keeps running. `poolSize` applies to container sandboxes only.
1239
+ */
1154
1240
  snapshot(name = `Snapshot ${this.sandboxId}`, opts = {}) {
1155
1241
  return new PlatformClient(this.resolveOpts()).post(`/api/sandboxes/${this.sandboxId}/snapshot`, { name, poolSize: opts.poolSize ?? 5 });
1156
1242
  }
1157
1243
  unexpose(port) {
1158
1244
  return new PlatformClient(this.resolveOpts()).delete(`/api/sandboxes/${this.sandboxId}/expose/${port}`);
1159
1245
  }
1246
+ /** Stream the sandbox's logs. Not available on Firecracker sandboxes yet (`LizardError`, 501). */
1160
1247
  logs(opts = {}) {
1161
1248
  return new PlatformClient(this.resolveOpts()).events(query(`/api/sandboxes/${this.sandboxId}/logs`, { tail: opts.tail }), { signal: opts.signal });
1162
1249
  }
@@ -1166,7 +1253,7 @@ var Sandbox = class extends SandboxClient {
1166
1253
  static getSnapshot(snapshotId, opts) {
1167
1254
  return new PlatformClient(opts ?? {}).get(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}`);
1168
1255
  }
1169
- /** Start a sandbox from an available warm copy in the snapshot's project and region. */
1256
+ /** Start a new sandbox from a snapshot, in the snapshot's project and region. */
1170
1257
  static async restore(snapshotId, opts) {
1171
1258
  const client = new PlatformClient(opts ?? {});
1172
1259
  const snapshot = await this.getSnapshot(snapshotId, opts);
@@ -1181,7 +1268,7 @@ var Sandbox = class extends SandboxClient {
1181
1268
  static setSnapshotWarmPool(snapshotId, poolSize, opts) {
1182
1269
  return new PlatformClient(opts ?? {}).patch(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}`, { poolSize });
1183
1270
  }
1184
- /** Release idle warm copies; retain saved state and existing claimed sandboxes. */
1271
+ /** Release idle warm copies (container snapshots); retain saved state and existing claimed sandboxes. */
1185
1272
  static pauseSnapshot(snapshotId, opts) {
1186
1273
  return new PlatformClient(opts ?? {}).post(`/api/sandbox-snapshots/${encodeURIComponent(snapshotId)}/pause`, {});
1187
1274
  }
@@ -1559,88 +1646,54 @@ var RegionsAPI = class {
1559
1646
  }
1560
1647
  };
1561
1648
 
1562
- // src/cli.ts
1563
- import { spawn } from "child_process";
1564
- var LizardCLI = class {
1565
- constructor(opts = {}) {
1566
- this.opts = opts;
1567
- }
1568
- run(args, opts = {}) {
1569
- return new Promise((resolve, reject) => {
1570
- const env = { ...process.env };
1571
- if (this.opts.apiKey) {
1572
- env.LIZARD_API_KEY = this.opts.apiKey;
1573
- env.LIZARD_TOKEN = this.opts.apiKey;
1574
- }
1575
- if (this.opts.apiUrl) env.LIZARD_API_URL = this.opts.apiUrl;
1576
- const child = spawn(this.opts.executable ?? "lizard", ["--json", ...args], {
1577
- cwd: this.opts.cwd,
1578
- env,
1579
- shell: false,
1580
- stdio: "pipe",
1581
- signal: opts.signal,
1582
- timeout: this.opts.timeoutMs ?? 6e5
1583
- });
1584
- child.stdout.setEncoding("utf8");
1585
- child.stderr.setEncoding("utf8");
1586
- let stdout = "", stderr = "", bytes = 0;
1587
- const collect = (stream, data) => {
1588
- bytes += Buffer.byteLength(data);
1589
- if (bytes > 16 * 1024 * 1024) {
1590
- child.kill();
1591
- reject(new Error("CLI output exceeds 16 MiB"));
1592
- return;
1593
- }
1594
- if (stream === "stdout") stdout += data;
1595
- else stderr += data;
1596
- };
1597
- child.stdout.on("data", (data) => collect("stdout", data));
1598
- child.stderr.on("data", (data) => collect("stderr", data));
1599
- child.on("error", reject);
1600
- child.on("close", (code, signal) => {
1601
- if (signal) {
1602
- reject(new Error(`CLI ended with signal ${signal}`));
1603
- return;
1604
- }
1605
- let events = [];
1606
- try {
1607
- events = [JSON.parse(stdout)];
1608
- } catch {
1609
- events = stdout.split("\n").filter(Boolean).flatMap((line) => {
1610
- try {
1611
- return [JSON.parse(line)];
1612
- } catch {
1613
- return [];
1614
- }
1615
- });
1616
- }
1617
- resolve({ code: code ?? 1, stdout, stderr, events });
1618
- });
1619
- child.stdin.on("error", (error) => {
1620
- if (error.code !== "EPIPE") reject(error);
1621
- });
1622
- child.stdin.end(opts.stdin);
1623
- });
1624
- }
1625
- };
1626
-
1627
1649
  // src/platform/billing.ts
1628
1650
  var BillingAPI = class {
1629
1651
  constructor(client) {
1630
1652
  this.client = client;
1631
1653
  }
1632
- /** Current balance, status, burn rate, and runway. */
1654
+ /**
1655
+ * The plan: trial, this month's credits, overage, next charge, cancel date. With
1656
+ * `workspaceId`, the plan of that workspace's owner (`isOwner: false`, no card or
1657
+ * invoice fields).
1658
+ */
1659
+ subscription(opts) {
1660
+ return this.client.get(query("/api/billing/subscription", { workspaceId: opts?.workspaceId }));
1661
+ }
1662
+ /**
1663
+ * Opens Stripe Checkout for Pro: the trial when the account can have one, otherwise
1664
+ * Pro at once ($19 today). Returns the page for the user to finish; a second call
1665
+ * while it is open returns the same session.
1666
+ */
1667
+ startCheckout(opts = {}) {
1668
+ return this.client.post("/api/billing/subscription/checkout", { returnUrl: opts.returnUrl });
1669
+ }
1670
+ /** Ends the trial now: charges $19 and starts the first paid month with $19 of credits. */
1671
+ startProNow() {
1672
+ return this.client.post("/api/billing/subscription/start-now", {});
1673
+ }
1674
+ /** Cancels Pro at the end of the current month or trial. Cancelling in the trial costs nothing. */
1675
+ cancel() {
1676
+ return this.client.post("/api/billing/subscription/cancel", {});
1677
+ }
1678
+ /** Undoes {@link cancel}: Pro renews as usual. */
1679
+ resume() {
1680
+ return this.client.post("/api/billing/subscription/resume", {});
1681
+ }
1682
+ /** Redeems a promo code: a longer trial with more trial credits. Works only before the first payment. */
1683
+ redeemPromo(code) {
1684
+ return this.client.post("/api/billing/promo/redeem", { code });
1685
+ }
1686
+ /** Prepaid credits balance, status, burn rate and runway (old `payg` accounts and enterprise). */
1633
1687
  balance() {
1634
1688
  return this.client.get("/api/billing/balance");
1635
1689
  }
1636
- /** A page of balance transactions, newest first. */
1690
+ /** A page of balance transactions, newest first (old `payg` accounts and enterprise). */
1637
1691
  transactions(opts) {
1638
- const qs = new URLSearchParams();
1639
- if (opts?.limit !== void 0) qs.set("limit", String(opts.limit));
1640
- if (opts?.cursor) qs.set("cursor", opts.cursor);
1641
- if (opts?.includeUsage) qs.set("includeUsage", "1");
1642
- const q = qs.toString();
1643
- return this.client.get(`/api/billing/transactions${q ? `?${q}` : ""}`);
1692
+ return this.client.get(query("/api/billing/transactions", {
1693
+ limit: opts?.limit,
1694
+ cursor: opts?.cursor || void 0,
1695
+ includeUsage: opts?.includeUsage ? "1" : void 0
1696
+ }));
1644
1697
  }
1645
1698
  /** Cost summary for the current billing period, broken down by resource. */
1646
1699
  summary() {
@@ -1650,46 +1703,13 @@ var BillingAPI = class {
1650
1703
  live() {
1651
1704
  return this.client.get("/api/billing/live");
1652
1705
  }
1706
+ /** Saved cards. Cards are added in Checkout or on the Billing page. */
1653
1707
  paymentMethods() {
1654
1708
  return this.client.get("/api/billing/payment-methods");
1655
1709
  }
1656
- setupPaymentMethod(returnUrl) {
1657
- return this.client.post("/api/billing/payment-methods/setup", { returnUrl });
1658
- }
1659
1710
  removePaymentMethod(id) {
1660
1711
  return this.client.delete(`/api/billing/payment-methods/${encodeURIComponent(id)}`);
1661
1712
  }
1662
- purchase(creditCents, opts = {}) {
1663
- if (!Number.isSafeInteger(creditCents) || creditCents <= 0) throw new Error("creditCents must be a positive integer");
1664
- return this.client.post("/api/billing/purchase", { creditCents, paymentMethod: opts.paymentMethod ?? "card", returnUrl: opts.returnUrl });
1665
- }
1666
- autoTopup() {
1667
- return this.client.get("/api/billing/auto-topup");
1668
- }
1669
- setAutoTopup(opts) {
1670
- return this.client.put("/api/billing/auto-topup", opts);
1671
- }
1672
- runAutoTopup() {
1673
- return this.client.post("/api/billing/auto-topup/run", {});
1674
- }
1675
- redeemPromo(code) {
1676
- return this.client.post("/api/billing/promo/redeem", { code });
1677
- }
1678
- x402Quote(creditCents) {
1679
- return this.client.post("/api/billing/purchase/x402/quote", { creditCents });
1680
- }
1681
- paymentStatus(attemptId) {
1682
- return this.client.get(`/api/billing/purchase/x402/${encodeURIComponent(attemptId)}`);
1683
- }
1684
- /** Pay with the CLI's durable x402 journal. Requires CLI >= 4.0.8. Amounts are cents. */
1685
- async payX402(creditCents, opts) {
1686
- if (![creditCents, opts.maxTotalCents].every((n) => Number.isSafeInteger(n) && n > 0)) throw new Error("Payment amounts must be positive integer cents");
1687
- const args = ["credits", "topup", (creditCents / 100).toFixed(2), "--method", "x402", "--max-total", (opts.maxTotalCents / 100).toFixed(2), "--yes"];
1688
- if (opts.requestId) args.push("--request-id", opts.requestId);
1689
- const result = await new LizardCLI({ apiKey: this.client.config.apiKey, apiUrl: this.client.config.apiUrl, executable: opts.executable }).run(args);
1690
- if (result.code !== 0) throw new Error(`x402 CLI exited with code ${result.code}; inspect the payment journal or query paymentStatus before retrying`);
1691
- return result.events[0];
1692
- }
1693
1713
  };
1694
1714
 
1695
1715
  // src/volume/index.ts
@@ -1720,7 +1740,7 @@ var Volume = class _Volume {
1720
1740
  * "the scratch disk for this task" no longer has to store an id between runs.
1721
1741
  *
1722
1742
  * An existing volume is returned as-is — `sizeGb` applies only to a fresh create,
1723
- * so an existing volume keeps its size. Call {@link Volume.resize} to change it.
1743
+ * so an existing volume keeps its size.
1724
1744
  */
1725
1745
  static async getOrCreate(projectId, name, opts) {
1726
1746
  const config = new ConnectionConfig(opts);
@@ -1763,11 +1783,15 @@ var Volume = class _Volume {
1763
1783
  /**
1764
1784
  * Resize a volume, by name or by id, to `sizeGb`.
1765
1785
  *
1766
- * The change is in place and online: the size is a quota, so no data is copied,
1767
- * it completes in well under a second, and a sandbox that has the volume mounted
1768
- * keeps running and sees the new size immediately. Both growing and shrinking are
1769
- * allowed, but a shrink must leave at least 10% of the new size free — otherwise
1770
- * it throws `ConflictError` with `code: 'volume_too_full_to_shrink'`.
1786
+ * **Firecracker volumes cannot be resized yet**: volumes created for the default
1787
+ * Firecracker sandboxes throw `LizardError` (400) with `code: 'volume_not_resizable'`.
1788
+ * Create the volume at the size you need, or copy the data to a new, larger one.
1789
+ *
1790
+ * On a container volume the change is in place and online: the size is a quota,
1791
+ * so no data is copied and a mounted sandbox sees the new size immediately. Both
1792
+ * growing and shrinking are allowed, but a shrink must leave at least 10% of the
1793
+ * new size free — otherwise it throws `ConflictError` with
1794
+ * `code: 'volume_too_full_to_shrink'`.
1771
1795
  *
1772
1796
  * Other failures carry a `code` too: `invalid_volume_size`, `volume_not_resizable`,
1773
1797
  * `volume_resize_in_progress`, `volume_not_provisioned`,
@@ -1793,7 +1817,7 @@ var Volume = class _Volume {
1793
1817
  if (!res.ok) await handleApiError(res);
1794
1818
  return res.json();
1795
1819
  }
1796
- /** Resize this volume in place — see the static {@link Volume.resize}. */
1820
+ /** Resize this volume — see the static {@link Volume.resize} (not supported for Firecracker volumes yet). */
1797
1821
  async resize(projectId, sizeGb) {
1798
1822
  return _Volume.resize(projectId, this.volumeId, sizeGb, {
1799
1823
  apiKey: this.config.apiKey,
@@ -1916,7 +1940,7 @@ var Execution = class {
1916
1940
  this.stderr = "";
1917
1941
  /** Rich output items (images, JSON, HTML, plain values). */
1918
1942
  this.results = [];
1919
- /** Monotonically increasing counter for this context. */
1943
+ /** Monotonically increasing counter for this context (the Python kernel's `In[n]`). */
1920
1944
  this.executionCount = 0;
1921
1945
  }
1922
1946
  get success() {
@@ -1927,25 +1951,317 @@ var Execution = class {
1927
1951
  }
1928
1952
  };
1929
1953
 
1954
+ // src/code-interpreter/runner.ts
1955
+ var RUNNER_SOURCE = String.raw`# Lizard code runner: executes code for CodeSandbox.run_code / runCode inside the sandbox.
1956
+ # Python runs in a persistent Jupyter kernel per context when the template has one
1957
+ # (the interpreter template does); everything else runs as a fresh process.
1958
+ # Events are JSON lines on stdout; an event over 24000 chars is sent as "part" lines.
1959
+ import base64, json, os, re, shutil, signal, subprocess, sys, threading, time
1960
+ ROOT = "/tmp/.lizard-code"
1961
+ LANGS = {"python": "python3", "javascript": "node", "bash": "bash"}
1962
+ ALIASES = {"py": "python", "python3": "python", "js": "javascript", "node": "javascript", "sh": "bash"}
1963
+ ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
1964
+ LOCK = threading.Lock()
1965
+
1966
+
1967
+ def emit(ev):
1968
+ s = json.dumps(ev)
1969
+ with LOCK:
1970
+ if len(s) <= 24000:
1971
+ print(s, flush=True)
1972
+ return
1973
+ n = (len(s) + 23999) // 24000
1974
+ for i in range(n):
1975
+ print(json.dumps({"type": "part", "data": s[i * 24000:(i + 1) * 24000], "end": i == n - 1}), flush=True)
1976
+
1977
+
1978
+ def fail(msg):
1979
+ emit({"type": "fatal", "message": msg})
1980
+ sys.exit(3)
1981
+
1982
+
1983
+ def meta_path(ctx):
1984
+ return os.path.join(ROOT, ctx, "meta.json")
1985
+
1986
+
1987
+ def load(ctx):
1988
+ try:
1989
+ with open(meta_path(ctx)) as f:
1990
+ return json.load(f)
1991
+ except OSError:
1992
+ return None
1993
+
1994
+
1995
+ def save(m):
1996
+ os.makedirs(os.path.join(ROOT, m["id"]), exist_ok=True)
1997
+ with open(meta_path(m["id"]), "w") as f:
1998
+ json.dump(m, f)
1999
+
2000
+
2001
+ def alive(pid):
2002
+ try:
2003
+ os.kill(pid, 0)
2004
+ except OSError:
2005
+ return False
2006
+ try:
2007
+ with open("/proc/%d/stat" % pid) as f:
2008
+ return f.read().split(") ")[-1][:1] != "Z"
2009
+ except OSError:
2010
+ return False
2011
+
2012
+
2013
+ def kernel_pid(m):
2014
+ try:
2015
+ with open(os.path.join(ROOT, m["id"], "kernel.pid")) as f:
2016
+ return int(f.read().strip())
2017
+ except (OSError, ValueError):
2018
+ return 0
2019
+
2020
+
2021
+ def stop_kernel(m):
2022
+ pid = kernel_pid(m)
2023
+ if pid and alive(pid):
2024
+ try:
2025
+ os.killpg(pid, signal.SIGKILL)
2026
+ except OSError:
2027
+ pass
2028
+ for name in ("kernel.pid", "kernel.json"):
2029
+ try:
2030
+ os.remove(os.path.join(ROOT, m["id"], name))
2031
+ except OSError:
2032
+ pass
2033
+
2034
+
2035
+ def start_kernel(m):
2036
+ d = os.path.join(ROOT, m["id"])
2037
+ conn = os.path.join(d, "kernel.json")
2038
+ stop_kernel(m)
2039
+ os.makedirs(m["cwd"], exist_ok=True)
2040
+ log = open(os.path.join(d, "kernel.log"), "ab")
2041
+ p = subprocess.Popen([sys.executable, "-m", "ipykernel_launcher", "-f", conn], cwd=m["cwd"],
2042
+ stdin=subprocess.DEVNULL, stdout=log, stderr=log, start_new_session=True,
2043
+ env=dict(os.environ, PYTHONUNBUFFERED="1"))
2044
+ with open(os.path.join(d, "kernel.pid"), "w") as f:
2045
+ f.write(str(p.pid))
2046
+ end = time.time() + 30
2047
+ while time.time() < end:
2048
+ if os.path.exists(conn) and os.path.getsize(conn) > 0:
2049
+ return
2050
+ if p.poll() is not None:
2051
+ break
2052
+ time.sleep(0.05)
2053
+ with open(os.path.join(d, "kernel.log"), "rb") as f:
2054
+ tail = f.read()[-2000:].decode("utf-8", "replace")
2055
+ fail("python kernel failed to start: " + (tail or "no output"))
2056
+
2057
+
2058
+ def has_kernel():
2059
+ try:
2060
+ import jupyter_client, ipykernel # noqa: F401
2061
+ return True
2062
+ except ImportError:
2063
+ return False
2064
+
2065
+
2066
+ def client(m):
2067
+ from jupyter_client import BlockingKernelClient
2068
+ if not (kernel_pid(m) and alive(kernel_pid(m))):
2069
+ start_kernel(m)
2070
+ kc = BlockingKernelClient()
2071
+ kc.load_connection_file(os.path.join(ROOT, m["id"], "kernel.json"))
2072
+ kc.start_channels()
2073
+ kc.wait_for_ready(timeout=30)
2074
+ return kc
2075
+
2076
+
2077
+ def shell_reply(kc, msg_id, timeout):
2078
+ end = time.time() + timeout
2079
+ while time.time() < end:
2080
+ try:
2081
+ r = kc.get_shell_msg(timeout=max(0.01, end - time.time()))
2082
+ except Exception:
2083
+ return None
2084
+ if r["parent_header"].get("msg_id") == msg_id:
2085
+ return r
2086
+ return None
2087
+
2088
+
2089
+ def silent(kc, code):
2090
+ shell_reply(kc, kc.execute(code, silent=True, store_history=False), 10)
2091
+
2092
+
2093
+ BEST = ("image/png", "image/jpeg", "image/svg+xml", "text/html", "text/markdown", "application/json", "text/latex", "text/plain")
2094
+
2095
+
2096
+ def run_python(m, code, envs, timeout):
2097
+ kc = client(m)
2098
+ if envs:
2099
+ silent(kc, "import os as _lz_os\n_lz_saved = {k: _lz_os.environ.get(k) for k in %r}\n_lz_os.environ.update(%r)" % (list(envs), envs))
2100
+ msg_id = kc.execute(code, store_history=True, allow_stdin=False)
2101
+ end = time.time() + timeout
2102
+ timed_out = False
2103
+ while True:
2104
+ left = end - time.time()
2105
+ if left <= 0:
2106
+ timed_out = True
2107
+ break
2108
+ try:
2109
+ msg = kc.get_iopub_msg(timeout=left)
2110
+ except Exception:
2111
+ continue
2112
+ if msg["parent_header"].get("msg_id") != msg_id:
2113
+ continue
2114
+ t, c = msg["msg_type"], msg["content"]
2115
+ if t == "stream":
2116
+ emit({"type": "stderr" if c.get("name") == "stderr" else "stdout", "data": c.get("text", "")})
2117
+ elif t in ("execute_result", "display_data"):
2118
+ data = c.get("data", {})
2119
+ mime = next((k for k in BEST if k in data), next(iter(data), None))
2120
+ if mime:
2121
+ v = data[mime]
2122
+ emit({"type": "result", "mime": mime, "data": v if isinstance(v, str) else json.dumps(v)})
2123
+ elif t == "error":
2124
+ emit({"type": "error", "name": c.get("ename", "Error"), "message": c.get("evalue", ""),
2125
+ "traceback": ANSI.sub("", "\n".join(c.get("traceback", [])))})
2126
+ elif t == "status" and c.get("execution_state") == "idle":
2127
+ break
2128
+ count = 0
2129
+ if timed_out:
2130
+ pid = kernel_pid(m)
2131
+ if pid:
2132
+ try:
2133
+ os.kill(pid, signal.SIGINT)
2134
+ except OSError:
2135
+ pass
2136
+ emit({"type": "error", "name": "TimeoutError", "message": "execution timed out after %gs; the kernel was interrupted" % timeout, "traceback": ""})
2137
+ else:
2138
+ r = shell_reply(kc, msg_id, 10)
2139
+ count = (r or {}).get("content", {}).get("execution_count") or 0
2140
+ if envs and not timed_out:
2141
+ silent(kc, "for _lz_k, _lz_v in _lz_saved.items():\n _lz_os.environ.pop(_lz_k, None) if _lz_v is None else _lz_os.environ.__setitem__(_lz_k, _lz_v)")
2142
+ kc.stop_channels()
2143
+ emit({"type": "done", "execution_count": count})
2144
+
2145
+
2146
+ def run_process(m, code, envs, timeout):
2147
+ interp = LANGS[m["language"]]
2148
+ if shutil.which(interp) is None:
2149
+ emit({"type": "error", "name": "LanguageUnavailable", "message": "%s is not installed in this sandbox" % interp, "traceback": ""})
2150
+ emit({"type": "done", "execution_count": 0})
2151
+ return
2152
+ d = os.path.join(ROOT, m["id"])
2153
+ src = os.path.join(d, "cell" + {"node": ".js", "bash": ".sh"}.get(interp, ".py"))
2154
+ with open(src, "w") as f:
2155
+ f.write(code)
2156
+ os.makedirs(m["cwd"], exist_ok=True)
2157
+ p = subprocess.Popen([interp, src], cwd=m["cwd"], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
2158
+ stderr=subprocess.PIPE, env=dict(os.environ, **(envs or {})), start_new_session=True)
2159
+ err_tail = []
2160
+
2161
+ def pump(stream, kind):
2162
+ for line in iter(stream.readline, b""):
2163
+ text = line.decode("utf-8", "replace")
2164
+ if kind == "stderr":
2165
+ err_tail.append(text)
2166
+ del err_tail[:-50]
2167
+ emit({"type": kind, "data": text})
2168
+
2169
+ ts = [threading.Thread(target=pump, args=(p.stdout, "stdout")), threading.Thread(target=pump, args=(p.stderr, "stderr"))]
2170
+ for t in ts:
2171
+ t.start()
2172
+ try:
2173
+ rc = p.wait(timeout=timeout)
2174
+ except subprocess.TimeoutExpired:
2175
+ os.killpg(p.pid, signal.SIGKILL)
2176
+ rc = None
2177
+ for t in ts:
2178
+ t.join(5)
2179
+ m["count"] = m.get("count", 0) + 1
2180
+ save(m)
2181
+ if rc is None:
2182
+ emit({"type": "error", "name": "TimeoutError", "message": "execution timed out after %gs" % timeout, "traceback": ""})
2183
+ elif rc != 0:
2184
+ emit({"type": "error", "name": "ExitError", "message": "exited with code %d" % rc, "traceback": "".join(err_tail)})
2185
+ emit({"type": "done", "execution_count": m["count"]})
2186
+
2187
+
2188
+ def ctx_for(a):
2189
+ lang = (a.get("language") or "python").lower()
2190
+ lang = ALIASES.get(lang, lang)
2191
+ if lang not in LANGS:
2192
+ fail("unsupported language %r; use python, javascript or bash" % lang)
2193
+ return lang
2194
+
2195
+
2196
+ def main():
2197
+ a = json.loads(base64.b64decode(sys.argv[1]))
2198
+ act = a["action"]
2199
+ os.makedirs(ROOT, exist_ok=True)
2200
+ if act == "create" or (act == "run" and not a.get("context")):
2201
+ lang = ctx_for(a)
2202
+ cid = a.get("context") or ("default-" + lang)
2203
+ m = load(cid) if act == "run" else None
2204
+ if m is None:
2205
+ m = {"id": cid if act == "run" else a["id"], "language": lang, "cwd": a.get("cwd") or "/home/user"}
2206
+ save(m)
2207
+ if lang == "python" and act == "create" and has_kernel():
2208
+ client(m).stop_channels()
2209
+ if act == "create":
2210
+ emit({"type": "context", "id": m["id"], "language": m["language"], "cwd": m["cwd"]})
2211
+ return
2212
+ elif act == "list":
2213
+ out = []
2214
+ for name in sorted(os.listdir(ROOT)):
2215
+ m = load(name)
2216
+ if m and not name.startswith("default-"):
2217
+ out.append({"id": m["id"], "language": m["language"], "cwd": m["cwd"]})
2218
+ emit({"type": "contexts", "data": out})
2219
+ return
2220
+ else:
2221
+ m = load(a["context"])
2222
+ if m is None:
2223
+ fail("context %r not found" % a["context"])
2224
+ if act == "delete":
2225
+ stop_kernel(m)
2226
+ shutil.rmtree(os.path.join(ROOT, m["id"]), ignore_errors=True)
2227
+ emit({"type": "ok"})
2228
+ return
2229
+ if act == "restart":
2230
+ if m["language"] == "python" and has_kernel():
2231
+ start_kernel(m)
2232
+ m["count"] = 0
2233
+ save(m)
2234
+ emit({"type": "ok"})
2235
+ return
2236
+ code = a.get("code")
2237
+ if code is None:
2238
+ with open(a["codeFile"]) as f:
2239
+ code = f.read()
2240
+ os.remove(a["codeFile"])
2241
+ if m["language"] == "python" and has_kernel():
2242
+ run_python(m, code, a.get("envs") or {}, a.get("timeout") or 60)
2243
+ else:
2244
+ run_process(m, code, a.get("envs") or {}, a.get("timeout") or 60)
2245
+
2246
+
2247
+ main()
2248
+ `;
2249
+
1930
2250
  // src/code-interpreter/index.ts
1931
- var CODE_INTERPRETER_PORT = 8080;
2251
+ var DEFAULT_RUN_TIMEOUT_MS = 6e4;
2252
+ var RUNNER_OVERHEAD_MS = 45e3;
2253
+ var MAX_INLINE_ARGS = 1e5;
1932
2254
  var CodeSandbox = class extends Sandbox {
1933
- get serverUrl() {
1934
- return this.getHost(CODE_INTERPRETER_PORT).then((h) => `https://${h}`);
1935
- }
1936
2255
  /**
1937
- * Execute code in a persistent kernel.
2256
+ * Execute code and wait for it to finish.
1938
2257
  *
1939
- * Variables, imports, and function definitions from previous calls are
1940
- * available in subsequent calls within the same context.
2258
+ * Python keeps its state per context (the default context, per language,
2259
+ * when none is given). Code errors appear in `execution.error`; request
2260
+ * failures throw.
1941
2261
  *
1942
2262
  * @param code Source code to run.
1943
2263
  * @param opts Language, context, env vars, timeout, and streaming callbacks.
1944
2264
  *
1945
- * @returns Execution result with stdout, stderr, results, and any error.
1946
- *
1947
- * Code errors appear in `execution.error`; request failures throw.
1948
- *
1949
2265
  * @example
1950
2266
  * ```ts
1951
2267
  * const result = await sandbox.runCode(`
@@ -1959,63 +2275,42 @@ var CodeSandbox = class extends Sandbox {
1959
2275
  if (opts?.context && opts?.language) {
1960
2276
  throw new Error("Provide context or language, not both");
1961
2277
  }
1962
- const body = { code, env_vars: opts?.envs ?? {} };
1963
- if (opts?.context) body.context_id = opts.context.id;
1964
- else if (opts?.language) body.language = opts.language;
1965
- const controller = new AbortController();
1966
- const timer = opts?.timeoutMs ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
1967
- try {
1968
- const res = await fetch(`${await this.serverUrl}/execute`, {
1969
- method: "POST",
1970
- headers: { "Content-Type": "application/json" },
1971
- body: JSON.stringify(body),
1972
- signal: controller.signal
1973
- });
1974
- if (!res.ok) {
1975
- throw new Error(`Code interpreter returned ${res.status}: ${await res.text()}`);
1976
- }
1977
- if (!res.body) throw new Error("Empty response body");
1978
- const execution = new Execution();
1979
- for await (const line of readLines(res.body)) {
1980
- let item;
1981
- try {
1982
- item = JSON.parse(line);
1983
- } catch {
1984
- continue;
1985
- }
1986
- if (item.type === "stdout") {
1987
- execution.stdout += item.data;
1988
- opts?.onStdout?.(item.data);
1989
- } else if (item.type === "stderr") {
1990
- execution.stderr += item.data;
1991
- opts?.onStderr?.(item.data);
1992
- } else if (item.type === "result") {
1993
- const out = item;
1994
- execution.results.push(out);
1995
- opts?.onResult?.(out);
1996
- } else if (item.type === "error") {
1997
- const err = new ExecutionError(
1998
- item.name,
1999
- item.message,
2000
- item.traceback
2001
- );
2002
- execution.error = err;
2003
- opts?.onError?.(err);
2004
- } else if (item.type === "done") {
2005
- execution.executionCount = item.execution_count ?? 0;
2006
- break;
2007
- }
2278
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS;
2279
+ const args = {
2280
+ action: "run",
2281
+ code,
2282
+ envs: opts?.envs ?? {},
2283
+ timeout: timeoutMs / 1e3
2284
+ };
2285
+ if (opts?.context) args.context = opts.context.id;
2286
+ else args.language = opts?.language ?? "python";
2287
+ const execution = new Execution();
2288
+ await this.runner(args, timeoutMs, (item) => {
2289
+ if (item.type === "stdout") {
2290
+ execution.stdout += item.data;
2291
+ opts?.onStdout?.(item.data);
2292
+ } else if (item.type === "stderr") {
2293
+ execution.stderr += item.data;
2294
+ opts?.onStderr?.(item.data);
2295
+ } else if (item.type === "result") {
2296
+ const out = { type: "result", mime: item.mime, data: item.data };
2297
+ execution.results.push(out);
2298
+ opts?.onResult?.(out);
2299
+ } else if (item.type === "error") {
2300
+ const err = new ExecutionError(item.name, item.message, item.traceback);
2301
+ execution.error = err;
2302
+ opts?.onError?.(err);
2303
+ } else if (item.type === "done") {
2304
+ execution.executionCount = item.execution_count ?? 0;
2008
2305
  }
2009
- return execution;
2010
- } finally {
2011
- clearTimeout(timer);
2012
- }
2306
+ });
2307
+ return execution;
2013
2308
  }
2014
2309
  /**
2015
2310
  * Create a new isolated execution context.
2016
2311
  *
2017
- * Each context maintains its own variable namespace and process state.
2018
- * Useful for running multiple independent sessions in the same sandbox.
2312
+ * A Python context is its own kernel: its own variables, and `cwd` as its
2313
+ * working directory. Other languages get the working directory only.
2019
2314
  *
2020
2315
  * @example
2021
2316
  * ```ts
@@ -2025,40 +2320,80 @@ var CodeSandbox = class extends Sandbox {
2025
2320
  * ```
2026
2321
  */
2027
2322
  async createContext(opts) {
2028
- const res = await fetch(`${await this.serverUrl}/contexts`, {
2029
- method: "POST",
2030
- headers: { "Content-Type": "application/json" },
2031
- body: JSON.stringify({
2032
- language: opts?.language ?? "python",
2033
- cwd: opts?.cwd ?? "/home/user"
2034
- })
2035
- });
2036
- if (!res.ok) throw new Error(`Failed to create context: ${await res.text()}`);
2037
- return res.json();
2323
+ const id = `ctx-${randomId()}`;
2324
+ let ctx;
2325
+ await this.runner(
2326
+ { action: "create", id, language: opts?.language ?? "python", cwd: opts?.cwd ?? "/home/user" },
2327
+ DEFAULT_RUN_TIMEOUT_MS,
2328
+ (ev) => {
2329
+ if (ev.type === "context") ctx = { id: ev.id, language: ev.language, cwd: ev.cwd };
2330
+ }
2331
+ );
2332
+ if (!ctx) throw new LizardError("Failed to create context");
2333
+ return ctx;
2038
2334
  }
2039
- /**
2040
- * List all active execution contexts in this sandbox.
2041
- */
2335
+ /** List the contexts created with {@link createContext} in this sandbox. */
2042
2336
  async listContexts() {
2043
- const res = await fetch(`${await this.serverUrl}/contexts`);
2044
- if (!res.ok) throw new Error(`Failed to list contexts: ${await res.text()}`);
2045
- return res.json();
2337
+ let out = [];
2338
+ await this.runner({ action: "list" }, DEFAULT_RUN_TIMEOUT_MS, (ev) => {
2339
+ if (ev.type === "contexts") out = ev.data;
2340
+ });
2341
+ return out;
2046
2342
  }
2047
- /**
2048
- * Delete an execution context and free its resources.
2049
- */
2343
+ /** Delete an execution context, stopping its kernel. */
2050
2344
  async deleteContext(context) {
2051
2345
  const id = typeof context === "string" ? context : context.id;
2052
- const res = await fetch(`${await this.serverUrl}/contexts/${id}`, { method: "DELETE" });
2053
- if (!res.ok) throw new Error(`Failed to delete context: ${await res.text()}`);
2346
+ await this.runner({ action: "delete", context: id }, DEFAULT_RUN_TIMEOUT_MS, () => {
2347
+ });
2054
2348
  }
2055
- /**
2056
- * Restart a context, clearing all variables and state.
2057
- */
2349
+ /** Restart a context, clearing all variables and state. */
2058
2350
  async restartContext(context) {
2059
2351
  const id = typeof context === "string" ? context : context.id;
2060
- const res = await fetch(`${await this.serverUrl}/contexts/${id}/restart`, { method: "POST" });
2061
- if (!res.ok) throw new Error(`Failed to restart context: ${await res.text()}`);
2352
+ await this.runner({ action: "restart", context: id }, DEFAULT_RUN_TIMEOUT_MS, () => {
2353
+ });
2354
+ }
2355
+ /** Run the in-sandbox runner with `args` and hand each of its events to `onEvent`. */
2356
+ async runner(args, timeoutMs, onEvent) {
2357
+ let encoded = toBase64(JSON.stringify(args));
2358
+ if (encoded.length > MAX_INLINE_ARGS && typeof args.code === "string") {
2359
+ const codeFile = `/tmp/.lizard-code/in-${randomId()}`;
2360
+ await this.fs.write(codeFile, args.code);
2361
+ const { code: _code, ...rest } = args;
2362
+ encoded = toBase64(JSON.stringify({ ...rest, codeFile }));
2363
+ }
2364
+ let part = "";
2365
+ let fatal;
2366
+ let sawEvent = false;
2367
+ const stderr = [];
2368
+ const result = await this.process.exec(["python3", "-c", RUNNER_SOURCE, encoded], {
2369
+ timeoutMs: timeoutMs + RUNNER_OVERHEAD_MS,
2370
+ onStdout: (line) => {
2371
+ let ev;
2372
+ try {
2373
+ ev = JSON.parse(line);
2374
+ if (ev.type === "part") {
2375
+ part += ev.data;
2376
+ if (!ev.end) return;
2377
+ ev = JSON.parse(part);
2378
+ part = "";
2379
+ }
2380
+ } catch {
2381
+ part = "";
2382
+ return;
2383
+ }
2384
+ sawEvent = true;
2385
+ if (ev.type === "fatal") fatal = ev.message;
2386
+ else onEvent(ev);
2387
+ },
2388
+ onStderr: (line) => {
2389
+ stderr.push(line);
2390
+ }
2391
+ });
2392
+ if (fatal) throw new LizardError(fatal);
2393
+ if (result.exitCode !== 0 || !sawEvent) {
2394
+ const detail = stderr.slice(-20).join("\n").trim() || `exit code ${result.exitCode}`;
2395
+ throw new LizardError(`Code runner failed (python3 is required in the sandbox): ${detail}`);
2396
+ }
2062
2397
  }
2063
2398
  static async create(templateOrOpts, opts) {
2064
2399
  return super.create(templateOrOpts, opts);
@@ -2067,27 +2402,83 @@ var CodeSandbox = class extends Sandbox {
2067
2402
  return super.connect(sandboxId, opts);
2068
2403
  }
2069
2404
  };
2070
- CodeSandbox.defaultTemplate = "code-interpreter-v1";
2071
- async function* readLines(body) {
2072
- const reader = body.getReader();
2073
- const decoder = new TextDecoder();
2074
- let buf = "";
2075
- try {
2076
- while (true) {
2077
- const { done, value } = await reader.read();
2078
- if (done) break;
2079
- buf += decoder.decode(value, { stream: true });
2080
- const lines = buf.split("\n");
2081
- buf = lines.pop() ?? "";
2082
- for (const line of lines) {
2083
- if (line.trim()) yield line;
2405
+ CodeSandbox.defaultTemplate = "interpreter";
2406
+ function randomId() {
2407
+ const c = globalThis.crypto;
2408
+ return c?.randomUUID ? c.randomUUID().replace(/-/g, "").slice(0, 16) : Math.random().toString(16).slice(2, 18);
2409
+ }
2410
+ function toBase64(text) {
2411
+ if (typeof Buffer !== "undefined") return Buffer.from(text, "utf8").toString("base64");
2412
+ const bytes = new TextEncoder().encode(text);
2413
+ let s = "";
2414
+ for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
2415
+ return btoa(s);
2416
+ }
2417
+
2418
+ // src/cli.ts
2419
+ import { spawn } from "child_process";
2420
+ var LizardCLI = class {
2421
+ constructor(opts = {}) {
2422
+ this.opts = opts;
2423
+ }
2424
+ run(args, opts = {}) {
2425
+ return new Promise((resolve, reject) => {
2426
+ const env = { ...process.env };
2427
+ if (this.opts.apiKey) {
2428
+ env.LIZARD_API_KEY = this.opts.apiKey;
2429
+ env.LIZARD_TOKEN = this.opts.apiKey;
2084
2430
  }
2085
- }
2086
- if (buf.trim()) yield buf;
2087
- } finally {
2088
- reader.releaseLock();
2431
+ if (this.opts.apiUrl) env.LIZARD_API_URL = this.opts.apiUrl;
2432
+ const child = spawn(this.opts.executable ?? "lizard", ["--json", ...args], {
2433
+ cwd: this.opts.cwd,
2434
+ env,
2435
+ shell: false,
2436
+ stdio: "pipe",
2437
+ signal: opts.signal,
2438
+ timeout: this.opts.timeoutMs ?? 6e5
2439
+ });
2440
+ child.stdout.setEncoding("utf8");
2441
+ child.stderr.setEncoding("utf8");
2442
+ let stdout = "", stderr = "", bytes = 0;
2443
+ const collect = (stream, data) => {
2444
+ bytes += Buffer.byteLength(data);
2445
+ if (bytes > 16 * 1024 * 1024) {
2446
+ child.kill();
2447
+ reject(new Error("CLI output exceeds 16 MiB"));
2448
+ return;
2449
+ }
2450
+ if (stream === "stdout") stdout += data;
2451
+ else stderr += data;
2452
+ };
2453
+ child.stdout.on("data", (data) => collect("stdout", data));
2454
+ child.stderr.on("data", (data) => collect("stderr", data));
2455
+ child.on("error", reject);
2456
+ child.on("close", (code, signal) => {
2457
+ if (signal) {
2458
+ reject(new Error(`CLI ended with signal ${signal}`));
2459
+ return;
2460
+ }
2461
+ let events = [];
2462
+ try {
2463
+ events = [JSON.parse(stdout)];
2464
+ } catch {
2465
+ events = stdout.split("\n").filter(Boolean).flatMap((line) => {
2466
+ try {
2467
+ return [JSON.parse(line)];
2468
+ } catch {
2469
+ return [];
2470
+ }
2471
+ });
2472
+ }
2473
+ resolve({ code: code ?? 1, stdout, stderr, events });
2474
+ });
2475
+ child.stdin.on("error", (error) => {
2476
+ if (error.code !== "EPIPE") reject(error);
2477
+ });
2478
+ child.stdin.end(opts.stdin);
2479
+ });
2089
2480
  }
2090
- }
2481
+ };
2091
2482
  export {
2092
2483
  AddonsAPI,
2093
2484
  ApiKeysAPI,
@@ -2105,6 +2496,7 @@ export {
2105
2496
  LizardError,
2106
2497
  MetricsAPI,
2107
2498
  NotFoundError,
2499
+ PaymentRequiredError,
2108
2500
  PlatformClient,
2109
2501
  ProjectsAPI,
2110
2502
  RegionsAPI,