withruntime 0.11.2 → 0.14.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.
Files changed (171) hide show
  1. package/BLAXEL.md +2 -2
  2. package/COMPATIBILITY.md +74 -9
  3. package/DAYTONA.md +33 -5
  4. package/E2B.md +85 -24
  5. package/README.md +11 -10
  6. package/README.src.md +10 -9
  7. package/VERCEL.md +43 -5
  8. package/compatibility-lock.json +58 -49
  9. package/dist/billing-cli.d.ts +1 -1
  10. package/dist/billing-cli.js +85 -5
  11. package/dist/blaxel/errors.d.ts +14 -0
  12. package/dist/blaxel/errors.js +29 -0
  13. package/dist/blaxel/index.d.ts +2 -2
  14. package/dist/blaxel/index.js +4 -4
  15. package/dist/blaxel/interpreter.d.ts +2 -5
  16. package/dist/blaxel/sandbox.d.ts +15 -6
  17. package/dist/blaxel/sandbox.js +64 -8
  18. package/dist/blaxel/snapshot.d.ts +4 -0
  19. package/dist/blaxel/snapshot.js +4 -0
  20. package/dist/cli/logs.d.ts +21 -0
  21. package/dist/cli/logs.js +188 -0
  22. package/dist/cli-extras.d.ts +3 -3
  23. package/dist/cli-extras.js +17 -12
  24. package/dist/cli-words.d.ts +10 -1
  25. package/dist/cli-words.js +18 -0
  26. package/dist/cli.d.ts +22 -3
  27. package/dist/cli.js +647 -273
  28. package/dist/client.d.ts +2 -4
  29. package/dist/client.js +27 -11
  30. package/dist/cloudflare/errors.d.ts +143 -0
  31. package/dist/cloudflare/errors.js +167 -0
  32. package/dist/cloudflare/index.d.ts +175 -6
  33. package/dist/cloudflare/index.js +416 -26
  34. package/dist/cloudflare/interpreter.d.ts +119 -0
  35. package/dist/cloudflare/interpreter.js +223 -0
  36. package/dist/cloudflare/streams.d.ts +38 -0
  37. package/dist/cloudflare/streams.js +145 -0
  38. package/dist/cloudflare/watch.d.ts +9 -0
  39. package/dist/cloudflare/watch.js +24 -0
  40. package/dist/codesandbox/index.d.ts +216 -28
  41. package/dist/codesandbox/index.js +478 -78
  42. package/dist/compat/core.d.ts +18 -0
  43. package/dist/compat/core.js +29 -0
  44. package/dist/compat/files.d.ts +1 -0
  45. package/dist/compat/files.js +25 -5
  46. package/dist/credentials.d.ts +3 -0
  47. package/dist/credentials.js +15 -0
  48. package/dist/daytona/computer-use.d.ts +13 -4
  49. package/dist/daytona/computer-use.js +66 -8
  50. package/dist/daytona/daytona.d.ts +7 -1
  51. package/dist/daytona/daytona.js +31 -4
  52. package/dist/daytona/errors.d.ts +11 -0
  53. package/dist/daytona/errors.js +18 -0
  54. package/dist/daytona/filesystem.js +2 -1
  55. package/dist/daytona/image.d.ts +13 -9
  56. package/dist/daytona/image.js +18 -12
  57. package/dist/daytona/index.d.ts +1 -1
  58. package/dist/daytona/sandbox.d.ts +8 -2
  59. package/dist/daytona/sandbox.js +16 -2
  60. package/dist/domains/index.d.ts +9 -0
  61. package/dist/domains/index.js +10 -0
  62. package/dist/e2b/commands.d.ts +1 -1
  63. package/dist/e2b/commands.js +6 -7
  64. package/dist/e2b/errors.d.ts +17 -0
  65. package/dist/e2b/errors.js +40 -1
  66. package/dist/e2b/index.d.ts +2 -2
  67. package/dist/e2b/index.js +1 -1
  68. package/dist/e2b/sandbox.d.ts +28 -22
  69. package/dist/e2b/sandbox.js +34 -8
  70. package/dist/e2b/unsupported.js +1 -1
  71. package/dist/errors.d.ts +11 -2
  72. package/dist/errors.js +17 -5
  73. package/dist/freestyle/index.d.ts +285 -37
  74. package/dist/freestyle/index.js +656 -144
  75. package/dist/identity.d.ts +3 -0
  76. package/dist/identity.js +27 -0
  77. package/dist/images/index.d.ts +9 -0
  78. package/dist/images/index.js +10 -0
  79. package/dist/index.d.ts +3 -1
  80. package/dist/ips/index.d.ts +9 -0
  81. package/dist/ips/index.js +10 -0
  82. package/dist/jobs/index.d.ts +9 -0
  83. package/dist/jobs/index.js +10 -0
  84. package/dist/keys.js +2 -0
  85. package/dist/login.js +2 -0
  86. package/dist/machine.d.ts +27 -4
  87. package/dist/machine.js +46 -14
  88. package/dist/mcp.js +144 -20
  89. package/dist/modal/index.d.ts +339 -61
  90. package/dist/modal/index.js +781 -121
  91. package/dist/network-cli.d.ts +3 -1
  92. package/dist/network-cli.js +78 -36
  93. package/dist/networks/index.d.ts +11 -0
  94. package/dist/networks/index.js +10 -0
  95. package/dist/ports/index.d.ts +9 -0
  96. package/dist/ports/index.js +10 -0
  97. package/dist/product-names.d.ts +52 -0
  98. package/dist/product-names.js +164 -0
  99. package/dist/product-renames.d.ts +103 -0
  100. package/dist/product-renames.js +189 -0
  101. package/dist/products/api-keys.d.ts +73 -0
  102. package/dist/products/api-keys.js +23 -0
  103. package/dist/products/apps.d.ts +157 -0
  104. package/dist/products/apps.js +60 -0
  105. package/dist/products/balancers.d.ts +87 -0
  106. package/dist/products/balancers.js +8 -0
  107. package/dist/products/billing.d.ts +41 -0
  108. package/dist/products/billing.js +13 -0
  109. package/dist/products/browser.d.ts +10 -0
  110. package/dist/products/browsers.d.ts +71 -0
  111. package/dist/products/browsers.js +102 -0
  112. package/dist/products/builds.d.ts +156 -0
  113. package/dist/products/builds.js +227 -0
  114. package/dist/products/index.d.ts +5 -16
  115. package/dist/products/index.js +5 -14
  116. package/dist/products/jobs.d.ts +19 -2
  117. package/dist/products/jobs.js +28 -11
  118. package/dist/products/logs.d.ts +245 -0
  119. package/dist/products/logs.js +274 -0
  120. package/dist/products/network-products.d.ts +38 -7
  121. package/dist/products/network-products.js +31 -18
  122. package/dist/products/observability.d.ts +8 -2
  123. package/dist/products/private-network.js +1 -1
  124. package/dist/products/queues.d.ts +149 -0
  125. package/dist/products/queues.js +213 -0
  126. package/dist/products/referrals.d.ts +21 -7
  127. package/dist/products/referrals.js +3 -3
  128. package/dist/products/resources.d.ts +54 -0
  129. package/dist/products/resources.js +122 -0
  130. package/dist/products/secrets.d.ts +29 -3
  131. package/dist/products/secrets.js +47 -18
  132. package/dist/products/signed-requests.d.ts +21 -0
  133. package/dist/products/signed-requests.js +11 -0
  134. package/dist/products/streams.d.ts +73 -0
  135. package/dist/products/streams.js +17 -0
  136. package/dist/products/switching.d.ts +35 -7
  137. package/dist/products/switching.js +4 -3
  138. package/dist/products/volumes.d.ts +6 -0
  139. package/dist/record-cli.d.ts +23 -0
  140. package/dist/record-cli.js +308 -0
  141. package/dist/renamed-products.d.ts +38 -0
  142. package/dist/renamed-products.js +34 -0
  143. package/dist/runloop/index.d.ts +9 -2
  144. package/dist/runloop/index.js +29 -8
  145. package/dist/runloop/sdk.d.ts +131 -0
  146. package/dist/runloop/sdk.js +167 -0
  147. package/dist/sandbox.d.ts +7 -2
  148. package/dist/sandbox.js +41 -11
  149. package/dist/sandboxes/index.d.ts +11 -0
  150. package/dist/sandboxes/index.js +12 -0
  151. package/dist/snapshots/index.d.ts +9 -0
  152. package/dist/snapshots/index.js +11 -0
  153. package/dist/snapshots.d.ts +7 -4
  154. package/dist/sprites/index.d.ts +343 -31
  155. package/dist/sprites/index.js +866 -84
  156. package/dist/ssh.d.ts +1 -1
  157. package/dist/ssh.js +11 -11
  158. package/dist/terminal.d.ts +24 -0
  159. package/dist/terminal.js +32 -0
  160. package/dist/transport.d.ts +18 -1
  161. package/dist/transport.js +80 -6
  162. package/dist/tunnel.js +1 -1
  163. package/dist/types.d.ts +71 -4
  164. package/dist/vercel/sandbox.d.ts +1 -1
  165. package/dist/vercel/sandbox.js +6 -4
  166. package/dist/vercel/snapshot.js +10 -2
  167. package/dist/volumes/index.d.ts +9 -0
  168. package/dist/volumes/index.js +10 -0
  169. package/dist/wallet.d.ts +4 -1
  170. package/dist/wallet.js +2 -1
  171. package/package.json +48 -14
@@ -1,4 +1,4 @@
1
- import { all, client, CompatibilityError, create, destroy, execOptions, only, quote, ready, } from "../compat/core.js";
1
+ import { all, client, CompatibilityError, create, destroy, execOptions, only, quote, ready, refuse, } from "../compat/core.js";
2
2
  import { GuestFiles } from "../compat/files.js";
3
3
  import { ManagedTasks } from "./guest-service.js";
4
4
  export { CompatibilityError };
@@ -41,10 +41,38 @@ export class VMTier {
41
41
  }
42
42
  export class CodeSandbox {
43
43
  sandboxes;
44
+ hosts = new HostTokens();
44
45
  constructor(apiToken, options = {}) {
45
- this.sandboxes = new Sandboxes(client({ ...options, apiKey: options.apiKey ?? apiToken }));
46
+ const { headers, tracer, ...rest } = options;
47
+ if (headers && Object.keys(headers).length)
48
+ throw new CompatibilityError("CodeSandbox", "custom API headers");
49
+ if (tracer)
50
+ throw new CompatibilityError("CodeSandbox", "an OpenTelemetry tracer; export Runtime's sandbox telemetry with runtime.observability");
51
+ this.sandboxes = new Sandboxes(client({ ...rest, apiKey: rest.apiKey ?? apiToken }));
46
52
  }
47
53
  }
54
+ /** CodeSandbox's host tokens open private previews. Runtime's private
55
+ * previews carry their own signed token: `sandbox.previews.create(port)`. */
56
+ export class HostTokens {
57
+ }
58
+ const PREVIEW_TOKENS = "use Runtime's private previews, sandbox.previews.create(port), which carry a signed token";
59
+ refuse("CodeSandbox", "HostTokens", HostTokens, {
60
+ createToken: PREVIEW_TOKENS,
61
+ listTokens: PREVIEW_TOKENS,
62
+ revokeToken: PREVIEW_TOKENS,
63
+ revokeAllTokens: PREVIEW_TOKENS,
64
+ updateToken: PREVIEW_TOKENS,
65
+ });
66
+ refuse("CodeSandbox", "HostTokens", HostTokens, { getUrl: PREVIEW_TOKENS, getHeaders: PREVIEW_TOKENS, getCookies: PREVIEW_TOKENS }, { sync: true });
67
+ /** Passing it to create() makes a fresh sandbox from Runtime's default image. */
68
+ const DEFAULT_TEMPLATE = "runtime-default";
69
+ function wakeup(config) {
70
+ if (!config)
71
+ return undefined;
72
+ if (config.http !== config.websocket)
73
+ throw new CompatibilityError("CodeSandbox", "waking on HTTP but not WebSocket requests, or the reverse; Runtime wakes on both");
74
+ return config.http;
75
+ }
48
76
  const metadata = (s) => ({
49
77
  id: s.id,
50
78
  createdAt: new Date(s.info.createdAt),
@@ -67,6 +95,9 @@ export class Sandboxes {
67
95
  constructor(runtime) {
68
96
  this.runtime = runtime;
69
97
  }
98
+ get defaultTemplateId() {
99
+ return DEFAULT_TEMPLATE;
100
+ }
70
101
  async create(options = {}) {
71
102
  only("CodeSandbox create", options, [
72
103
  "id",
@@ -76,22 +107,33 @@ export class Sandboxes {
76
107
  "privacy",
77
108
  "vmTier",
78
109
  "hibernationTimeoutSeconds",
110
+ "automaticWakeupConfig",
111
+ "ipcountry",
79
112
  ]);
113
+ if (options.ipcountry)
114
+ throw new CompatibilityError("CodeSandbox", "ipcountry; Runtime's sandboxes run in us-east");
80
115
  validateHibernationTimeout(options.hibernationTimeoutSeconds);
116
+ const autoWake = wakeup(options.automaticWakeupConfig);
81
117
  const labels = {
82
118
  "compat.provider": "codesandbox",
83
119
  "cs.tags": JSON.stringify([...new Set([...(options.tags ?? []), "sdk"])]),
84
120
  "cs.privacy": options.privacy ?? "public-hosts",
85
121
  "cs.description": options.description ?? "",
86
122
  };
87
- if (options.id) {
88
- if (options.vmTier)
89
- throw new CompatibilityError("CodeSandbox", "changing a fork's VM tier");
123
+ if (options.id && options.id !== DEFAULT_TEMPLATE) {
90
124
  const source = await this.runtime.sandboxes.get(options.id);
91
125
  const fork = await source.fork({ name: options.title, labels });
92
- if (options.hibernationTimeoutSeconds !== undefined)
93
- await fork.update({ idlePauseSeconds: options.hibernationTimeoutSeconds });
94
- return new Sandbox(fork, "FORK");
126
+ if (options.hibernationTimeoutSeconds !== undefined || autoWake !== undefined)
127
+ await fork.update({
128
+ ...(options.hibernationTimeoutSeconds !== undefined
129
+ ? { idlePauseSeconds: options.hibernationTimeoutSeconds }
130
+ : {}),
131
+ ...(autoWake !== undefined ? { autoWake } : {}),
132
+ });
133
+ const sandbox = new Sandbox(fork, "FORK");
134
+ if (options.vmTier)
135
+ await sandbox.updateTier(options.vmTier);
136
+ return sandbox;
95
137
  }
96
138
  const tier = options.vmTier ?? VMTier.Pico;
97
139
  const sandbox = await create(this.runtime, "codesandbox", {
@@ -102,6 +144,7 @@ export class Sandboxes {
102
144
  diskMiB: Math.ceil((tier.diskGB * 1e9) / 1048576),
103
145
  idlePauseSeconds: options.hibernationTimeoutSeconds,
104
146
  persistent: true,
147
+ ...(autoWake !== undefined ? { autoWake } : {}),
105
148
  }, {}, async (sandbox) => {
106
149
  await sandbox.exec(["sudo", "mkdir", "-p", "/project/sandbox"], { check: true });
107
150
  await sandbox.exec("sudo chown $(id -u):$(id -g) /project/sandbox", { check: true });
@@ -135,6 +178,26 @@ export class Sandboxes {
135
178
  fork(id, options = {}) {
136
179
  return this.create({ ...options, id });
137
180
  }
181
+ /** The account's running CodeSandbox-adapter sandboxes. Runtime has no
182
+ * per-account count of running machines, so the limit is null. */
183
+ async listRunning() {
184
+ const running = (await all(this.runtime, "codesandbox")).filter((s) => s.state === "running");
185
+ return {
186
+ concurrentVmCount: running.length,
187
+ concurrentVmLimit: null,
188
+ vms: running.map((s) => ({
189
+ id: s.id,
190
+ creditBasis: undefined,
191
+ lastActiveAt: s.info.lastActiveAt ? new Date(s.info.lastActiveAt) : undefined,
192
+ sessionStartedAt: s.info.readyAt ? new Date(s.info.readyAt) : undefined,
193
+ specs: {
194
+ cpu: s.info.vcpu,
195
+ memory: s.info.memoryMiB / 1024,
196
+ storage: s.info.diskMiB / 1024,
197
+ },
198
+ })),
199
+ };
200
+ }
138
201
  async get(id) {
139
202
  return metadata(await this.runtime.sandboxes.get(id));
140
203
  }
@@ -196,6 +259,14 @@ export class Sandbox {
196
259
  get cluster() {
197
260
  return this.native.info.region;
198
261
  }
262
+ /** Runtime's guest agent is updated with the host: always current. */
263
+ get isUpToDate() {
264
+ return true;
265
+ }
266
+ /** Resizes to the tier's CPUs and memory; a running sandbox restarts, as CodeSandbox's does. */
267
+ async updateTier(tier) {
268
+ await this.native.resize({ vcpu: tier.cpuCores, memoryMiB: tier.memoryGiB * 1024 });
269
+ }
199
270
  async updateHibernationTimeout(timeoutSeconds) {
200
271
  await this.native.update({ idlePauseSeconds: timeoutSeconds });
201
272
  }
@@ -208,6 +279,10 @@ export class Sandbox {
208
279
  return client;
209
280
  }
210
281
  }
282
+ refuse("CodeSandbox", "Sandbox", Sandbox, {
283
+ createSession: "use sandbox.connect(); a browser reaches a Runtime sandbox with a session token from the native sandbox.sessions.create()",
284
+ createBrowserSession: "make a session token with the native sandbox.sessions.create() and open it with Runtime's Sandbox.fromSession",
285
+ });
211
286
  export class CommandError extends Error {
212
287
  exitCode;
213
288
  output;
@@ -420,6 +495,34 @@ export class FileSystem {
420
495
  isSymlink: e.type === "symlink",
421
496
  }));
422
497
  }
498
+ /** Changes under a path, as CodeSandbox's add, change and remove events. */
499
+ async watch(path, options = {}) {
500
+ only("CodeSandbox watch", options, ["recursive", "excludes"]);
501
+ const events = emitter();
502
+ const handle = await this.native.files.watch(this.files.path(path), (e) => {
503
+ if (e.type === "rename") {
504
+ if (e.oldPath)
505
+ events.fire({ paths: [e.oldPath], type: "remove" });
506
+ events.fire({ paths: [e.path], type: "add" });
507
+ }
508
+ else
509
+ events.fire({
510
+ paths: [e.path],
511
+ type: e.type === "create" ? "add" : e.type === "remove" ? "remove" : "change",
512
+ });
513
+ }, {
514
+ recursive: options.recursive ?? false,
515
+ ...(options.excludes?.length ? { exclude: [...options.excludes] } : {}),
516
+ timeoutMs: 0,
517
+ });
518
+ return {
519
+ onEvent: events.event,
520
+ dispose: () => {
521
+ events.clear();
522
+ void handle.stop().catch(() => undefined);
523
+ },
524
+ };
525
+ }
423
526
  async stat(path) {
424
527
  const e = await this.files.stat(path);
425
528
  const r = await this.native.exec(["stat", "-c", "%X %Y %Z", "--", this.files.path(path)], {
@@ -436,19 +539,350 @@ export class FileSystem {
436
539
  };
437
540
  }
438
541
  }
542
+ refuse("CodeSandbox", "FileSystem", FileSystem, {
543
+ download: "read it with fs.readFile, or with the native sandbox.files.download(); Runtime has no signed download links",
544
+ });
439
545
  const commandPath = (id) => `/workspace/.runtime-compat/codesandbox/commands/${encodeURIComponent(id)}.json`;
546
+ /** A shell CodeSandbox lists: a command's name is JSON whose type is "command". */
547
+ export function isCommandShell(shell) {
548
+ try {
549
+ return JSON.parse(shell.name).type === "command";
550
+ }
551
+ catch {
552
+ return false;
553
+ }
554
+ }
555
+ export class SandboxCommands {
556
+ session;
557
+ constructor(session) {
558
+ this.session = session;
559
+ }
560
+ async runBackground(command, options = {}) {
561
+ only("CodeSandbox command", options, ["env", "cwd", "name", "dimensions", "asGlobalSession"]);
562
+ if (options.asGlobalSession)
563
+ throw new CompatibilityError("CodeSandbox", "global editor sessions");
564
+ const { native, env, workspacePath } = this.session;
565
+ this.session.assertConnected();
566
+ const { stdin: _stdin, ...spawnOptions } = await execOptions(native, {
567
+ env: { ...env, ...options.env },
568
+ cwd: options.cwd ?? workspacePath,
569
+ });
570
+ // The published SDK treats arrays as sequential shell commands, not argv.
571
+ const saved = {
572
+ version: 1,
573
+ command: Array.isArray(command) ? command.join(" && ") : command,
574
+ name: options.name,
575
+ options: {
576
+ ...spawnOptions,
577
+ env: { ...spawnOptions.env },
578
+ pty: { ...(options.dimensions ?? { cols: 128, rows: 24 }) },
579
+ },
580
+ };
581
+ return this.session.attach(await this.session.start(saved), saved);
582
+ }
583
+ async run(command, options) {
584
+ return (await this.runBackground(command, options)).waitUntilComplete();
585
+ }
586
+ async getAll() {
587
+ const { native } = this.session;
588
+ this.session.assertConnected();
589
+ const commands = [];
590
+ for (const saved of await savedShells(native))
591
+ if (saved.taskId === undefined && saved.kind !== "terminal")
592
+ commands.push(this.session.attach(await native.processes.get(saved.processId), saved));
593
+ return commands;
594
+ }
595
+ }
596
+ /** The running processes this adapter started, with what it saved about each. */
597
+ async function savedShells(native) {
598
+ const shells = [];
599
+ for (const process of await native.processes.list()) {
600
+ if (process.state !== "running" || !(await native.files.exists(commandPath(process.id))))
601
+ continue;
602
+ const saved = JSON.parse(await native.files.readText(commandPath(process.id)));
603
+ if (saved.version !== 1 || typeof saved.command !== "string" || !saved.options)
604
+ throw new Error(`Invalid saved CodeSandbox command: ${process.id}`);
605
+ shells.push({ ...saved, processId: process.id });
606
+ }
607
+ return shells;
608
+ }
609
+ /** The ports listening in the sandbox, from the kernel's socket tables. */
610
+ const LISTENING = String.raw `import sys
611
+ ports=set()
612
+ for name in ("/proc/net/tcp","/proc/net/tcp6"):
613
+ try:
614
+ for line in open(name).readlines()[1:]:
615
+ f=line.split()
616
+ if f[3]=="0A": ports.add(int(f[1].rsplit(":",1)[1],16))
617
+ except FileNotFoundError: pass
618
+ print(" ".join(str(p) for p in sorted(ports)))`;
619
+ export class Ports {
620
+ session;
621
+ opened = emitter();
622
+ closed = emitter();
623
+ watching;
624
+ known;
625
+ polling = false;
626
+ constructor(session) {
627
+ this.session = session;
628
+ }
629
+ /** A port's events start a watch of the sandbox's listening ports, once a second. */
630
+ get onDidPortOpen() {
631
+ return (listener) => this.subscribe(this.opened.event(listener));
632
+ }
633
+ get onDidPortClose() {
634
+ return (listener) => this.subscribe(this.closed.event(listener));
635
+ }
636
+ subscribe(subscription) {
637
+ this.watching ??= setInterval(() => void this.poll(), 1000);
638
+ void this.poll();
639
+ return {
640
+ dispose: () => {
641
+ subscription.dispose();
642
+ },
643
+ };
644
+ }
645
+ /** The ports listening now. */
646
+ async listening() {
647
+ const r = await this.session.native.exec(["python3", "-c", LISTENING], { check: true });
648
+ return r.stdout.trim() ? r.stdout.trim().split(" ").map(Number) : [];
649
+ }
650
+ async poll() {
651
+ if (this.polling)
652
+ return;
653
+ this.polling = true;
654
+ try {
655
+ const now = new Set(await this.listening());
656
+ const before = this.known;
657
+ this.known = now;
658
+ if (!before)
659
+ return;
660
+ for (const port of now)
661
+ if (!before.has(port))
662
+ this.opened.fire(await this.get(port));
663
+ for (const port of before)
664
+ if (!now.has(port))
665
+ this.closed.fire(port);
666
+ }
667
+ catch {
668
+ // A failed look is tried again at the next tick.
669
+ }
670
+ finally {
671
+ this.polling = false;
672
+ }
673
+ }
674
+ dispose() {
675
+ clearInterval(this.watching);
676
+ this.watching = undefined;
677
+ this.opened.clear();
678
+ this.closed.clear();
679
+ }
680
+ async get(port) {
681
+ const { native, previewURLs } = this.session;
682
+ this.session.assertConnected();
683
+ if (native.info.labels["cs.privacy"] === "private")
684
+ throw new CompatibilityError("CodeSandbox", "private port host authentication; use Runtime signed preview URLs");
685
+ const p = await native.previews.create(port, { visibility: "public" });
686
+ previewURLs.set(port, p.urlWithToken ?? p.url);
687
+ return { port, host: new URL(p.urlWithToken ?? p.url).host };
688
+ }
689
+ async getAll() {
690
+ this.session.assertConnected();
691
+ return (await this.session.native.previews.list()).map((p) => {
692
+ this.session.previewURLs.set(p.port, p.urlWithToken ?? p.url);
693
+ return { port: p.port, host: new URL(p.url).host };
694
+ });
695
+ }
696
+ async waitForPort(port, options = {}) {
697
+ only("CodeSandbox waitForPort", options, ["timeoutMs"]);
698
+ this.session.assertConnected();
699
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
700
+ throw new TypeError("Invalid port");
701
+ await this.session.native.exec([
702
+ "python3",
703
+ "-c",
704
+ 'import socket,sys,time\nport=int(sys.argv[1]); end=time.monotonic()+float(sys.argv[2])/1000\nwhile True:\n try:\n s=socket.create_connection(("127.0.0.1",port),timeout=0.25); s.close(); break\n except OSError:\n if time.monotonic() >= end: raise TimeoutError("Port did not open")\n time.sleep(0.1)',
705
+ String(port),
706
+ String(options.timeoutMs ?? 30000),
707
+ ], { check: true, timeoutMs: (options.timeoutMs ?? 30000) + 1000 });
708
+ return this.get(port);
709
+ }
710
+ }
711
+ /** URLs of the ports this client exposed with ports.get or listed with ports.getAll. */
712
+ export class Hosts {
713
+ session;
714
+ constructor(session) {
715
+ this.session = session;
716
+ }
717
+ getUrl(port, protocol = "https") {
718
+ const url = this.session.previewURLs.get(port);
719
+ if (!url)
720
+ throw new Error(`Port ${port} has not been exposed; call ports.get(${port}) first`);
721
+ const at = new URL(url);
722
+ at.protocol = `${protocol}:`;
723
+ return at.toString();
724
+ }
725
+ /** Public previews need no token: nothing to send. */
726
+ getHeaders() {
727
+ return {};
728
+ }
729
+ getCookies() {
730
+ return {};
731
+ }
732
+ }
733
+ /** Interactive shells, each a process with a terminal of its own. */
734
+ export class Terminals {
735
+ session;
736
+ constructor(session) {
737
+ this.session = session;
738
+ }
739
+ async create(command = "bash", options = {}) {
740
+ only("CodeSandbox terminal", options, ["env", "cwd", "name", "dimensions", "asGlobalSession"]);
741
+ if (options.asGlobalSession)
742
+ throw new CompatibilityError("CodeSandbox", "global editor sessions");
743
+ if (!["bash", "zsh", "fish", "ksh", "dash"].includes(command))
744
+ throw new TypeError(`Unknown shell: ${command}`);
745
+ const { native, env, workspacePath } = this.session;
746
+ this.session.assertConnected();
747
+ const { stdin: _stdin, ...spawnOptions } = await execOptions(native, {
748
+ env: { ...env, ...options.env },
749
+ cwd: options.cwd ?? workspacePath,
750
+ });
751
+ const saved = {
752
+ version: 1,
753
+ kind: "terminal",
754
+ command,
755
+ name: options.name,
756
+ options: {
757
+ ...spawnOptions,
758
+ env: { ...spawnOptions.env },
759
+ stdin: "pipe",
760
+ pty: { ...(options.dimensions ?? { cols: 80, rows: 24 }) },
761
+ },
762
+ };
763
+ const process = await this.session.start(saved);
764
+ return new Terminal(process, options.name ?? command);
765
+ }
766
+ async get(shellId) {
767
+ return (await this.getAll()).find((terminal) => terminal.id === shellId);
768
+ }
769
+ async getAll() {
770
+ const { native } = this.session;
771
+ this.session.assertConnected();
772
+ const terminals = [];
773
+ for (const saved of await savedShells(native))
774
+ if (saved.kind === "terminal")
775
+ terminals.push(new Terminal(await native.processes.get(saved.processId), saved.name ?? saved.command));
776
+ return terminals;
777
+ }
778
+ }
779
+ export class Terminal {
780
+ process;
781
+ name;
782
+ outputEvent = emitter();
783
+ onOutput = this.outputEvent.event;
784
+ output = [];
785
+ following;
786
+ constructor(process, name) {
787
+ this.process = process;
788
+ this.name = name;
789
+ }
790
+ get id() {
791
+ return this.process.id;
792
+ }
793
+ /** Its output so far, and what it prints from now on through onOutput. */
794
+ async open(dimensions = { cols: 80, rows: 24 }) {
795
+ await this.process.resize(dimensions.cols, dimensions.rows);
796
+ if (!this.following) {
797
+ const info = await this.process.refresh();
798
+ const end = info.outputBytes;
799
+ const signal = (this.following = new AbortController()).signal;
800
+ let replayed = "";
801
+ let caughtUp;
802
+ const ready = new Promise((resolve) => (caughtUp = resolve));
803
+ if (end <= info.firstOffset)
804
+ caughtUp();
805
+ void (async () => {
806
+ try {
807
+ for await (const e of this.process.output({ signal }))
808
+ if (e.type === "stdout" || e.type === "stderr") {
809
+ if (e.offset < end)
810
+ replayed += e.data;
811
+ else {
812
+ this.output.push(e.data);
813
+ this.outputEvent.fire(e.data);
814
+ }
815
+ if (e.offset + new TextEncoder().encode(e.data).length >= end)
816
+ caughtUp();
817
+ }
818
+ else if (e.type === "exit")
819
+ break;
820
+ }
821
+ catch {
822
+ // The terminal ended, or this client let go of it.
823
+ }
824
+ finally {
825
+ caughtUp();
826
+ }
827
+ })();
828
+ await ready;
829
+ this.output.unshift(replayed);
830
+ }
831
+ return this.output.join("");
832
+ }
833
+ async write(input, dimensions) {
834
+ if (dimensions)
835
+ await this.process.resize(dimensions.cols, dimensions.rows);
836
+ await this.process.write(input);
837
+ }
838
+ run(input, dimensions) {
839
+ return this.write(`${input}\n`, dimensions);
840
+ }
841
+ async kill() {
842
+ this.following?.abort();
843
+ await this.process.kill("SIGKILL");
844
+ }
845
+ }
846
+ /** One-off JavaScript and Python snippets, run as CodeSandbox runs them: the
847
+ * last line's value is printed. */
848
+ export class Interpreters {
849
+ commands;
850
+ constructor(commands) {
851
+ this.commands = commands;
852
+ }
853
+ javascript(code) {
854
+ const body = code
855
+ .split("\n")
856
+ .map((line, i, lines) => i === lines.length - 1 && !line.startsWith("return") ? `return ${line}` : line)
857
+ .join("\n");
858
+ return this.commands.run(`node -p "$(cat <<'EOF'\n(() => {${body}})()\nEOF\n)"`, {
859
+ env: { NO_COLOR: "true" },
860
+ });
861
+ }
862
+ python(code) {
863
+ const body = code
864
+ .split("\n")
865
+ .map((line, i, lines) => i === lines.length - 1 && !line.startsWith("print") ? `print(${line})` : line)
866
+ .join("\n");
867
+ return this.commands.run(`python3 <<'PYCODE'\n${body}\nPYCODE\n`);
868
+ }
869
+ }
440
870
  export class SandboxClient {
441
871
  native;
442
872
  env;
443
873
  fs;
444
874
  commands;
445
875
  ports;
876
+ terminals;
877
+ interpreters;
878
+ hosts;
446
879
  tasks;
447
880
  setup;
448
881
  workspacePath = "/project/sandbox";
449
882
  disconnected = false;
450
883
  attached = new Set();
451
884
  previewURLs = new Map();
885
+ stateEvent = emitter();
452
886
  stopKeepAlive;
453
887
  constructor(native, env) {
454
888
  this.native = native;
@@ -456,77 +890,31 @@ export class SandboxClient {
456
890
  this.fs = new FileSystem(native);
457
891
  this.tasks = new Tasks(this, native);
458
892
  this.setup = new Setup(this, native);
459
- const runBackground = async (command, options = {}) => {
460
- only("CodeSandbox command", options, ["env", "cwd", "name", "dimensions", "asGlobalSession"]);
461
- if (options.asGlobalSession)
462
- throw new CompatibilityError("CodeSandbox", "global editor sessions");
463
- if (this.disconnected)
464
- throw new Error("Sandbox client is disconnected.");
465
- const { stdin: _stdin, ...spawnOptions } = await execOptions(native, {
466
- env: { ...this.env, ...options.env },
467
- cwd: options.cwd ?? this.workspacePath,
468
- });
469
- // The published SDK treats arrays as sequential shell commands, not argv.
470
- const saved = {
471
- version: 1,
472
- command: Array.isArray(command) ? command.join(" && ") : command,
473
- name: options.name,
474
- options: {
475
- ...spawnOptions,
476
- env: { ...spawnOptions.env },
477
- pty: { ...(options.dimensions ?? { cols: 128, rows: 24 }) },
478
- },
479
- };
480
- return this.attach(await this.start(saved), saved);
481
- };
482
- this.commands = {
483
- runBackground,
484
- run: async (command, options) => (await runBackground(command, options)).waitUntilComplete(),
485
- getAll: async () => {
486
- this.assertConnected();
487
- const commands = [];
488
- for (const process of await native.processes.list()) {
489
- if (process.state !== "running" || !(await native.files.exists(commandPath(process.id))))
490
- continue;
491
- const saved = JSON.parse(await native.files.readText(commandPath(process.id)));
492
- if (saved.version !== 1 || typeof saved.command !== "string" || !saved.options)
493
- throw new Error(`Invalid saved CodeSandbox command: ${process.id}`);
494
- if (saved.taskId === undefined)
495
- commands.push(this.attach(await native.processes.get(process.id), saved));
496
- }
497
- return commands;
498
- },
499
- };
500
- this.ports = {
501
- get: async (port) => {
502
- this.assertConnected();
503
- if (native.info.labels["cs.privacy"] === "private")
504
- throw new CompatibilityError("CodeSandbox", "private port host authentication; use Runtime signed preview URLs");
505
- const p = await native.previews.create(port, { visibility: "public" });
506
- this.previewURLs.set(port, p.urlWithToken ?? p.url);
507
- return { port, host: new URL(p.urlWithToken ?? p.url).host };
508
- },
509
- getAll: async () => {
510
- this.assertConnected();
511
- return (await native.previews.list()).map((p) => ({
512
- port: p.port,
513
- host: new URL(p.url).host,
514
- }));
515
- },
516
- waitForPort: async (port, options = {}) => {
517
- this.assertConnected();
518
- if (!Number.isInteger(port) || port < 1 || port > 65535)
519
- throw new TypeError("Invalid port");
520
- await native.exec([
521
- "python3",
522
- "-c",
523
- 'import socket,sys,time\nport=int(sys.argv[1]); end=time.monotonic()+float(sys.argv[2])/1000\nwhile True:\n try:\n s=socket.create_connection(("127.0.0.1",port),timeout=0.25); s.close(); break\n except OSError:\n if time.monotonic() >= end: raise TimeoutError("Port did not open")\n time.sleep(0.1)',
524
- String(port),
525
- String(options.timeoutMs ?? 30000),
526
- ], { check: true, timeoutMs: (options.timeoutMs ?? 30000) + 1000 });
527
- return this.ports.get(port);
528
- },
893
+ const session = {
894
+ native,
895
+ env,
896
+ workspacePath: this.workspacePath,
897
+ assertConnected: () => this.assertConnected(),
898
+ start: (saved) => this.start(saved),
899
+ attach: (process, saved) => this.attach(process, saved),
900
+ previewURLs: this.previewURLs,
529
901
  };
902
+ this.commands = new SandboxCommands(session);
903
+ this.ports = new Ports(session);
904
+ this.terminals = new Terminals(session);
905
+ this.interpreters = new Interpreters(this.commands);
906
+ this.hosts = new Hosts(session);
907
+ }
908
+ /** Runtime's guest agent is updated with the host: always current. */
909
+ get isUpToDate() {
910
+ return true;
911
+ }
912
+ /** The sandbox's page in Runtime's console. */
913
+ get editorUrl() {
914
+ return `https://withruntime.com/account/sandboxes/${encodeURIComponent(this.native.id)}`;
915
+ }
916
+ get onStateChange() {
917
+ return this.stateEvent.event;
530
918
  }
531
919
  async initialize() {
532
920
  await this.tasks.getAll();
@@ -575,7 +963,10 @@ export class SandboxClient {
575
963
  return this.disconnected ? "DISCONNECTED" : "CONNECTED";
576
964
  }
577
965
  async disconnect() {
966
+ if (!this.disconnected)
967
+ this.stateEvent.fire("DISCONNECTED");
578
968
  this.disconnected = true;
969
+ this.ports.dispose();
579
970
  this.tasks.dispose();
580
971
  this.setup.dispose();
581
972
  for (const command of this.attached)
@@ -585,6 +976,8 @@ export class SandboxClient {
585
976
  }
586
977
  async reconnect() {
587
978
  await ready(await this.native.refresh());
979
+ if (this.disconnected)
980
+ this.stateEvent.fire("CONNECTED");
588
981
  this.disconnected = false;
589
982
  await this.tasks.reconnect();
590
983
  await this.setup.reconnect();
@@ -595,15 +988,22 @@ export class SandboxClient {
595
988
  this.stopKeepAlive = this.native.keepAlive();
596
989
  }
597
990
  dispose() {
991
+ if (!this.disconnected)
992
+ this.stateEvent.fire("DISCONNECTED");
598
993
  this.disconnected = true;
994
+ this.ports.dispose();
599
995
  this.tasks.dispose();
600
996
  this.setup.dispose();
601
997
  for (const command of this.attached)
602
998
  command.detach();
603
999
  this.attached.clear();
604
1000
  this.stopKeepAlive?.();
1001
+ this.stateEvent.clear();
605
1002
  }
606
1003
  }
1004
+ refuse("CodeSandbox", "SandboxClient", { prototype: SandboxClient }, {
1005
+ create: "connect with sandbox.connect(); Runtime has no separate session handshake",
1006
+ });
607
1007
  /** Configured tasks share durable native processes between client connections. */
608
1008
  export class Tasks {
609
1009
  client;