computesdk 1.10.3 → 1.11.1

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
@@ -485,6 +485,17 @@ var WebSocketManager = class {
485
485
  data: { terminal_id: terminalId, cols, rows }
486
486
  });
487
487
  }
488
+ /**
489
+ * Start a pending streaming command
490
+ * Used in two-phase streaming flow: HTTP request creates pending command,
491
+ * then this signal triggers execution after client has subscribed.
492
+ */
493
+ startCommand(cmdId) {
494
+ this.sendRaw({
495
+ type: "command:start",
496
+ data: { cmd_id: cmdId }
497
+ });
498
+ }
488
499
  on(event, handler) {
489
500
  if (!this.eventHandlers.has(event)) {
490
501
  this.eventHandlers.set(event, /* @__PURE__ */ new Set());
@@ -958,6 +969,7 @@ var TerminalInstance = class {
958
969
  throw new Error("Destroy handler not set");
959
970
  }
960
971
  await this._destroyHandler();
972
+ this._status = "stopped";
961
973
  this.cleanup();
962
974
  }
963
975
  /**
@@ -1863,7 +1875,8 @@ var Sandbox = class {
1863
1875
  headers: config.headers || {},
1864
1876
  timeout: config.timeout || 3e4,
1865
1877
  protocol: config.protocol || "binary",
1866
- metadata: config.metadata
1878
+ metadata: config.metadata,
1879
+ destroyHandler: config.destroyHandler
1867
1880
  };
1868
1881
  this.WebSocketImpl = config.WebSocket || globalThis.WebSocket;
1869
1882
  if (!this.WebSocketImpl) {
@@ -2286,18 +2299,24 @@ API request failed (${response.status}): ${error}`
2286
2299
  * Get file metadata (without content)
2287
2300
  */
2288
2301
  async getFile(path) {
2289
- return this.request(`/files/${encodeURIComponent(path)}`);
2302
+ return this.request(`/files/${this.encodeFilePath(path)}`);
2303
+ }
2304
+ /**
2305
+ * Encode a file path for use in URLs
2306
+ * Strips leading slash and encodes each segment separately to preserve path structure
2307
+ */
2308
+ encodeFilePath(path) {
2309
+ const pathWithoutLeadingSlash = path.startsWith("/") ? path.slice(1) : path;
2310
+ const segments = pathWithoutLeadingSlash.split("/");
2311
+ return segments.map((s) => encodeURIComponent(s)).join("/");
2290
2312
  }
2291
2313
  /**
2292
2314
  * Read file content
2293
2315
  */
2294
2316
  async readFile(path) {
2295
2317
  const params = new URLSearchParams({ content: "true" });
2296
- const pathWithoutLeadingSlash = path.startsWith("/") ? path.slice(1) : path;
2297
- const segments = pathWithoutLeadingSlash.split("/");
2298
- const encodedPath = segments.map((s) => encodeURIComponent(s)).join("/");
2299
2318
  const response = await this.request(
2300
- `/files/${encodedPath}?${params}`
2319
+ `/files/${this.encodeFilePath(path)}?${params}`
2301
2320
  );
2302
2321
  return response.data.content || "";
2303
2322
  }
@@ -2314,7 +2333,7 @@ API request failed (${response.status}): ${error}`
2314
2333
  * Delete a file or directory
2315
2334
  */
2316
2335
  async deleteFile(path) {
2317
- return this.request(`/files/${encodeURIComponent(path)}`, {
2336
+ return this.request(`/files/${this.encodeFilePath(path)}`, {
2318
2337
  method: "DELETE"
2319
2338
  });
2320
2339
  }
@@ -2333,7 +2352,7 @@ API request failed (${response.status}): ${error}`
2333
2352
  headers["Authorization"] = `Bearer ${this._token}`;
2334
2353
  }
2335
2354
  const response = await fetch(
2336
- `${this.config.sandboxUrl}/files/${encodeURIComponent(path)}`,
2355
+ `${this.config.sandboxUrl}/files/${this.encodeFilePath(path)}`,
2337
2356
  {
2338
2357
  method: "HEAD",
2339
2358
  headers,
@@ -2855,6 +2874,8 @@ API request failed (${response.status}): ${error}`
2855
2874
  * @param options.background - Run in background (server uses goroutines)
2856
2875
  * @param options.cwd - Working directory (server uses cmd.Dir)
2857
2876
  * @param options.env - Environment variables (server uses cmd.Env)
2877
+ * @param options.onStdout - Callback for streaming stdout data
2878
+ * @param options.onStderr - Callback for streaming stderr data
2858
2879
  * @returns Command execution result
2859
2880
  *
2860
2881
  * @example
@@ -2870,10 +2891,77 @@ API request failed (${response.status}): ${error}`
2870
2891
  * background: true,
2871
2892
  * env: { PORT: '3000' }
2872
2893
  * })
2894
+ *
2895
+ * // With streaming output
2896
+ * await sandbox.runCommand('npm install', {
2897
+ * onStdout: (data) => console.log(data),
2898
+ * onStderr: (data) => console.error(data),
2899
+ * })
2873
2900
  * ```
2874
2901
  */
2875
2902
  async runCommand(command, options) {
2876
- return this.run.command(command, options);
2903
+ const hasStreamingCallbacks = options?.onStdout || options?.onStderr;
2904
+ if (!hasStreamingCallbacks) {
2905
+ return this.run.command(command, options);
2906
+ }
2907
+ const ws = await this.ensureWebSocket();
2908
+ const result = await this.runCommandRequest({
2909
+ command,
2910
+ stream: true,
2911
+ cwd: options?.cwd,
2912
+ env: options?.env
2913
+ });
2914
+ const { cmd_id, channel } = result.data;
2915
+ if (!cmd_id || !channel) {
2916
+ throw new Error("Server did not return streaming channel info");
2917
+ }
2918
+ ws.subscribe(channel);
2919
+ let stdout = "";
2920
+ let stderr = "";
2921
+ let exitCode = 0;
2922
+ let resolvePromise = null;
2923
+ const cleanup = () => {
2924
+ ws.off("command:stdout", handleStdout);
2925
+ ws.off("command:stderr", handleStderr);
2926
+ ws.off("command:exit", handleExit);
2927
+ ws.unsubscribe(channel);
2928
+ };
2929
+ const handleStdout = (msg) => {
2930
+ if (msg.channel === channel && msg.data.cmd_id === cmd_id) {
2931
+ stdout += msg.data.output;
2932
+ options?.onStdout?.(msg.data.output);
2933
+ }
2934
+ };
2935
+ const handleStderr = (msg) => {
2936
+ if (msg.channel === channel && msg.data.cmd_id === cmd_id) {
2937
+ stderr += msg.data.output;
2938
+ options?.onStderr?.(msg.data.output);
2939
+ }
2940
+ };
2941
+ const handleExit = (msg) => {
2942
+ if (msg.channel === channel && msg.data.cmd_id === cmd_id) {
2943
+ exitCode = msg.data.exit_code;
2944
+ cleanup();
2945
+ if (resolvePromise) {
2946
+ resolvePromise({ stdout, stderr, exitCode, durationMs: 0 });
2947
+ }
2948
+ }
2949
+ };
2950
+ ws.on("command:stdout", handleStdout);
2951
+ ws.on("command:stderr", handleStderr);
2952
+ ws.on("command:exit", handleExit);
2953
+ ws.startCommand(cmd_id);
2954
+ if (options?.background) {
2955
+ return {
2956
+ stdout: "",
2957
+ stderr: "",
2958
+ exitCode: 0,
2959
+ durationMs: 0
2960
+ };
2961
+ }
2962
+ return new Promise((resolve) => {
2963
+ resolvePromise = resolve;
2964
+ });
2877
2965
  }
2878
2966
  /**
2879
2967
  * Get server information
@@ -2926,9 +3014,15 @@ API request failed (${response.status}): ${error}`
2926
3014
  }
2927
3015
  /**
2928
3016
  * Destroy the sandbox (Sandbox interface method)
3017
+ *
3018
+ * If a destroyHandler was provided (e.g., from gateway), calls it to destroy
3019
+ * the sandbox on the backend. Otherwise, only disconnects the WebSocket.
2929
3020
  */
2930
3021
  async destroy() {
2931
3022
  await this.disconnect();
3023
+ if (this.config.destroyHandler) {
3024
+ await this.config.destroyHandler();
3025
+ }
2932
3026
  }
2933
3027
  /**
2934
3028
  * Disconnect WebSocket
@@ -3367,9 +3461,17 @@ function buildConfigExample(provider, authOptions) {
3367
3461
  return options.join("\n\n");
3368
3462
  }
3369
3463
  function createConfigFromExplicit(config) {
3370
- if (!config.apiKey) {
3464
+ const computesdkApiKey = config.computesdkApiKey || config.apiKey;
3465
+ if (!computesdkApiKey) {
3371
3466
  throw new Error(
3372
- `Missing ComputeSDK API key. The 'apiKey' field is required.
3467
+ `Missing ComputeSDK API key. Set 'computesdkApiKey' in your config.
3468
+
3469
+ Example:
3470
+ compute.setConfig({
3471
+ provider: 'e2b',
3472
+ computesdkApiKey: process.env.COMPUTESDK_API_KEY,
3473
+ e2b: { apiKey: process.env.E2B_API_KEY }
3474
+ })
3373
3475
 
3374
3476
  Get your API key at: https://computesdk.com/dashboard`
3375
3477
  );
@@ -3377,7 +3479,7 @@ Get your API key at: https://computesdk.com/dashboard`
3377
3479
  validateProviderConfig(config);
3378
3480
  const providerHeaders = buildProviderHeaders2(config);
3379
3481
  return {
3380
- apiKey: config.apiKey,
3482
+ apiKey: computesdkApiKey,
3381
3483
  gatewayUrl: config.gatewayUrl || GATEWAY_URL,
3382
3484
  provider: config.provider,
3383
3485
  providerHeaders
@@ -3465,6 +3567,34 @@ async function gatewayFetch(url, config, options = {}) {
3465
3567
  throw error;
3466
3568
  }
3467
3569
  }
3570
+ async function waitForSandboxStatus(config, endpoint, body, options = {}) {
3571
+ const maxWaitMs = options.maxWaitMs ?? 6e4;
3572
+ const initialDelayMs = 500;
3573
+ const maxDelayMs = 2e3;
3574
+ const backoffFactor = 1.5;
3575
+ const startTime = Date.now();
3576
+ let currentDelay = initialDelayMs;
3577
+ while (Date.now() - startTime < maxWaitMs) {
3578
+ const result = await gatewayFetch(endpoint, config, {
3579
+ method: "POST",
3580
+ body: JSON.stringify(body)
3581
+ });
3582
+ if (!result.success || !result.data) {
3583
+ return result;
3584
+ }
3585
+ if (result.data.status !== "creating") {
3586
+ return result;
3587
+ }
3588
+ if (process.env.COMPUTESDK_DEBUG) {
3589
+ console.log(`[Compute] Sandbox still creating, waiting ${currentDelay}ms...`);
3590
+ }
3591
+ await new Promise((resolve) => setTimeout(resolve, currentDelay));
3592
+ currentDelay = Math.min(currentDelay * backoffFactor, maxDelayMs);
3593
+ }
3594
+ throw new Error(
3595
+ `Sandbox is still being created after ${maxWaitMs}ms. This may indicate the sandbox failed to start. Check your provider dashboard.`
3596
+ );
3597
+ }
3468
3598
  var ComputeManager = class {
3469
3599
  constructor() {
3470
3600
  this.config = null;
@@ -3493,7 +3623,12 @@ var ComputeManager = class {
3493
3623
  ...name && { name },
3494
3624
  ...namespace && { namespace }
3495
3625
  },
3496
- WebSocket: globalThis.WebSocket
3626
+ WebSocket: globalThis.WebSocket,
3627
+ destroyHandler: async () => {
3628
+ await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/${sandboxId}`, config, {
3629
+ method: "DELETE"
3630
+ });
3631
+ }
3497
3632
  });
3498
3633
  await waitForComputeReady(sandbox);
3499
3634
  return sandbox;
@@ -3514,7 +3649,12 @@ var ComputeManager = class {
3514
3649
  provider,
3515
3650
  token: token || config.apiKey,
3516
3651
  metadata,
3517
- WebSocket: globalThis.WebSocket
3652
+ WebSocket: globalThis.WebSocket,
3653
+ destroyHandler: async () => {
3654
+ await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/${sandboxId}`, config, {
3655
+ method: "DELETE"
3656
+ });
3657
+ }
3518
3658
  });
3519
3659
  await waitForComputeReady(sandbox);
3520
3660
  return sandbox;
@@ -3542,14 +3682,15 @@ var ComputeManager = class {
3542
3682
  findOrCreate: async (options) => {
3543
3683
  const config = this.getGatewayConfig();
3544
3684
  const { name, namespace, ...restOptions } = options;
3545
- const result = await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/find-or-create`, config, {
3546
- method: "POST",
3547
- body: JSON.stringify({
3685
+ const result = await waitForSandboxStatus(
3686
+ config,
3687
+ `${config.gatewayUrl}/v1/sandboxes/find-or-create`,
3688
+ {
3548
3689
  namespace: namespace || "default",
3549
3690
  name,
3550
3691
  ...restOptions
3551
- })
3552
- });
3692
+ }
3693
+ );
3553
3694
  if (!result.success || !result.data) {
3554
3695
  throw new Error(`Gateway returned invalid response`);
3555
3696
  }
@@ -3564,7 +3705,12 @@ var ComputeManager = class {
3564
3705
  name: result.data.name,
3565
3706
  namespace: result.data.namespace
3566
3707
  },
3567
- WebSocket: globalThis.WebSocket
3708
+ WebSocket: globalThis.WebSocket,
3709
+ destroyHandler: async () => {
3710
+ await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/${sandboxId}`, config, {
3711
+ method: "DELETE"
3712
+ });
3713
+ }
3568
3714
  });
3569
3715
  await waitForComputeReady(sandbox);
3570
3716
  return sandbox;
@@ -3574,13 +3720,14 @@ var ComputeManager = class {
3574
3720
  */
3575
3721
  find: async (options) => {
3576
3722
  const config = this.getGatewayConfig();
3577
- const result = await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/find`, config, {
3578
- method: "POST",
3579
- body: JSON.stringify({
3723
+ const result = await waitForSandboxStatus(
3724
+ config,
3725
+ `${config.gatewayUrl}/v1/sandboxes/find`,
3726
+ {
3580
3727
  namespace: options.namespace || "default",
3581
3728
  name: options.name
3582
- })
3583
- });
3729
+ }
3730
+ );
3584
3731
  if (!result.success || !result.data) {
3585
3732
  return null;
3586
3733
  }
@@ -3595,7 +3742,12 @@ var ComputeManager = class {
3595
3742
  name,
3596
3743
  namespace
3597
3744
  },
3598
- WebSocket: globalThis.WebSocket
3745
+ WebSocket: globalThis.WebSocket,
3746
+ destroyHandler: async () => {
3747
+ await gatewayFetch(`${config.gatewayUrl}/v1/sandboxes/${sandboxId}`, config, {
3748
+ method: "DELETE"
3749
+ });
3750
+ }
3599
3751
  });
3600
3752
  await waitForComputeReady(sandbox);
3601
3753
  return sandbox;
@@ -3636,7 +3788,7 @@ var ComputeManager = class {
3636
3788
 
3637
3789
  Options:
3638
3790
  1. Zero-config: Set COMPUTESDK_API_KEY and provider credentials (e.g., E2B_API_KEY)
3639
- 2. Explicit: Call compute.setConfig({ provider: "e2b", apiKey: "...", e2b: { apiKey: "..." } })
3791
+ 2. Explicit: Call compute.setConfig({ provider: "e2b", computesdkApiKey: "...", e2b: { apiKey: "..." } })
3640
3792
  3. Use provider directly: import { e2b } from '@computesdk/e2b'
3641
3793
 
3642
3794
  Docs: https://computesdk.com/docs/quickstart`