@hasna/sandboxes 0.1.26 → 0.1.28

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/README.md CHANGED
@@ -17,6 +17,29 @@ npm install -g @hasna/sandboxes
17
17
  sandboxes --help
18
18
  ```
19
19
 
20
+ ## SDK One-shot Commands
21
+
22
+ Use the SDK to create a sandbox, upload a local project, run a command, and clean up:
23
+
24
+ ```ts
25
+ import { createSandboxesSDK } from "@hasna/sandboxes";
26
+
27
+ const sandboxes = createSandboxesSDK();
28
+
29
+ await sandboxes.runCommandInSandbox({
30
+ provider: "e2b",
31
+ command: "bun test",
32
+ upload: {
33
+ localDir: process.cwd(),
34
+ remoteDir: "/workspace/app",
35
+ syncStrategy: "rsync",
36
+ },
37
+ cleanup: "delete",
38
+ });
39
+ ```
40
+
41
+ Set `E2B_API_KEY` for E2B-backed runs. `syncStrategy: "rsync"` mirrors the local directory into a temporary staging tree with `rsync` before uploading it through the provider file APIs.
42
+
20
43
  ## MCP Server
21
44
 
22
45
  ```bash
package/dist/cli/index.js CHANGED
@@ -12060,7 +12060,9 @@ var init_config2 = __esm(() => {
12060
12060
  });
12061
12061
 
12062
12062
  // src/lib/archive.ts
12063
- import { existsSync as existsSync7, statSync } from "fs";
12063
+ import { existsSync as existsSync7, mkdtempSync, rmSync, statSync } from "fs";
12064
+ import { tmpdir } from "os";
12065
+ import { join as join8 } from "path";
12064
12066
  function shellQuote(value) {
12065
12067
  return "'" + value.replace(/'/g, "'\\''") + "'";
12066
12068
  }
@@ -12068,6 +12070,15 @@ async function tarDirectory(localDir, opts) {
12068
12070
  if (!existsSync7(localDir) || !statSync(localDir).isDirectory()) {
12069
12071
  throw new Error(`tarDirectory: not a directory: ${localDir}`);
12070
12072
  }
12073
+ if (opts?.syncStrategy === "rsync") {
12074
+ const stagingDir = mkdtempSync(join8(tmpdir(), "sandboxes-rsync-"));
12075
+ try {
12076
+ await rsyncDirectory(localDir, stagingDir, opts.exclude ?? DEFAULT_UPLOAD_EXCLUDES);
12077
+ return await tarDirectory(stagingDir, { exclude: [], syncStrategy: "archive" });
12078
+ } finally {
12079
+ rmSync(stagingDir, { recursive: true, force: true });
12080
+ }
12081
+ }
12071
12082
  const excludes = opts?.exclude ?? DEFAULT_UPLOAD_EXCLUDES;
12072
12083
  const args = ["-czf", "-"];
12073
12084
  for (const ex of excludes)
@@ -12084,6 +12095,23 @@ async function tarDirectory(localDir, opts) {
12084
12095
  }
12085
12096
  return Buffer.from(buf);
12086
12097
  }
12098
+ async function rsyncDirectory(localDir, stagingDir, excludes) {
12099
+ const args = [
12100
+ "-a",
12101
+ "--delete",
12102
+ ...excludes.flatMap((ex) => ["--exclude", ex]),
12103
+ `${localDir.replace(/\/+$/, "")}/`,
12104
+ `${stagingDir.replace(/\/+$/, "")}/`
12105
+ ];
12106
+ const proc = Bun.spawn(["rsync", ...args], { stdout: "pipe", stderr: "pipe" });
12107
+ const [stderr, exitCode] = await Promise.all([
12108
+ new Response(proc.stderr).text(),
12109
+ proc.exited
12110
+ ]);
12111
+ if (exitCode !== 0) {
12112
+ throw new Error(`rsyncDirectory: rsync exited ${exitCode}: ${stderr.trim()}`);
12113
+ }
12114
+ }
12087
12115
  function buildUntarCommand(remoteTarPath, remoteDir) {
12088
12116
  const tar = shellQuote(remoteTarPath);
12089
12117
  const dir = shellQuote(remoteDir);
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export { BUILTIN_IMAGES, resolveImage, getBuiltinImageSetupScript } from "./lib/
15
15
  export { getProvider } from "./providers/index.js";
16
16
  export type { SandboxProvider, ProviderSandbox, CreateSandboxOpts, ExecOptions } from "./providers/types.js";
17
17
  export { SandboxesSDK, createSandboxesSDK, } from "./sdk.js";
18
- export type { ExecCommandResult, ProviderFactory, RunAgentOptions, SandboxesSDKOptions, WaitForSessionOptions, } from "./sdk.js";
18
+ export type { ExecCommandResult, ProviderFactory, RunAgentOptions, RunCommandInSandboxOptions, RunCommandInSandboxResult, RunCommandInSandboxUploadOptions, SandboxesSDKOptions, OneShotSandboxCleanup, WaitForSessionOptions, } from "./sdk.js";
19
19
  export { createStreamCollector, addStreamListener, emitLifecycleEvent } from "./lib/stream.js";
20
20
  export { getAgentDriver, listAgentDrivers } from "./lib/agents/index.js";
21
21
  export type { AgentDriver } from "./lib/agents/types.js";
package/dist/index.js CHANGED
@@ -88,7 +88,9 @@ var init_types = __esm(() => {
88
88
  });
89
89
 
90
90
  // src/lib/archive.ts
91
- import { existsSync as existsSync7, statSync } from "fs";
91
+ import { existsSync as existsSync7, mkdtempSync, rmSync, statSync } from "fs";
92
+ import { tmpdir } from "os";
93
+ import { join as join8 } from "path";
92
94
  function shellQuote(value) {
93
95
  return "'" + value.replace(/'/g, "'\\''") + "'";
94
96
  }
@@ -96,6 +98,15 @@ async function tarDirectory(localDir, opts) {
96
98
  if (!existsSync7(localDir) || !statSync(localDir).isDirectory()) {
97
99
  throw new Error(`tarDirectory: not a directory: ${localDir}`);
98
100
  }
101
+ if (opts?.syncStrategy === "rsync") {
102
+ const stagingDir = mkdtempSync(join8(tmpdir(), "sandboxes-rsync-"));
103
+ try {
104
+ await rsyncDirectory(localDir, stagingDir, opts.exclude ?? DEFAULT_UPLOAD_EXCLUDES);
105
+ return await tarDirectory(stagingDir, { exclude: [], syncStrategy: "archive" });
106
+ } finally {
107
+ rmSync(stagingDir, { recursive: true, force: true });
108
+ }
109
+ }
99
110
  const excludes = opts?.exclude ?? DEFAULT_UPLOAD_EXCLUDES;
100
111
  const args = ["-czf", "-"];
101
112
  for (const ex of excludes)
@@ -112,6 +123,23 @@ async function tarDirectory(localDir, opts) {
112
123
  }
113
124
  return Buffer.from(buf);
114
125
  }
126
+ async function rsyncDirectory(localDir, stagingDir, excludes) {
127
+ const args = [
128
+ "-a",
129
+ "--delete",
130
+ ...excludes.flatMap((ex) => ["--exclude", ex]),
131
+ `${localDir.replace(/\/+$/, "")}/`,
132
+ `${stagingDir.replace(/\/+$/, "")}/`
133
+ ];
134
+ const proc = Bun.spawn(["rsync", ...args], { stdout: "pipe", stderr: "pipe" });
135
+ const [stderr, exitCode] = await Promise.all([
136
+ new Response(proc.stderr).text(),
137
+ proc.exited
138
+ ]);
139
+ if (exitCode !== 0) {
140
+ throw new Error(`rsyncDirectory: rsync exited ${exitCode}: ${stderr.trim()}`);
141
+ }
142
+ }
115
143
  function buildUntarCommand(remoteTarPath, remoteDir) {
116
144
  const tar = shellQuote(remoteTarPath);
117
145
  const dir = shellQuote(remoteDir);
@@ -11643,6 +11671,54 @@ class SandboxesSDK {
11643
11671
  emitLifecycleEvent(sandbox.id, `uploaded ${localDir} -> ${remoteDir} (${result.bytes} bytes)`);
11644
11672
  return result;
11645
11673
  }
11674
+ async runCommandInSandbox(input) {
11675
+ const cleanupMode = input.cleanup ?? "delete";
11676
+ const sandbox = await this.createSandbox({
11677
+ provider: input.provider,
11678
+ name: input.name,
11679
+ image: input.image,
11680
+ timeout: input.sandboxTimeout,
11681
+ env_vars: input.sandboxEnvVars,
11682
+ config: input.config,
11683
+ project_id: input.projectId
11684
+ });
11685
+ let upload;
11686
+ let exec;
11687
+ try {
11688
+ if (input.upload) {
11689
+ const uploadOptions = {};
11690
+ if (input.upload.exclude !== undefined)
11691
+ uploadOptions.exclude = input.upload.exclude;
11692
+ if (input.upload.syncStrategy !== undefined)
11693
+ uploadOptions.syncStrategy = input.upload.syncStrategy;
11694
+ upload = await this.uploadDir(sandbox.id, input.upload.localDir, input.upload.remoteDir, uploadOptions);
11695
+ }
11696
+ exec = await this.execCommand(sandbox.id, input.command, {
11697
+ cwd: input.cwd ?? input.upload?.remoteDir,
11698
+ env: input.callEnvVars,
11699
+ timeout: input.commandTimeoutMs,
11700
+ onStdout: input.onStdout,
11701
+ onStderr: input.onStderr
11702
+ });
11703
+ } finally {
11704
+ if (cleanupMode === "delete") {
11705
+ await this.deleteSandbox(sandbox.id);
11706
+ } else if (cleanupMode === "stop") {
11707
+ await this.stopSandbox(sandbox.id);
11708
+ }
11709
+ }
11710
+ if (!exec) {
11711
+ throw new Error("Sandbox command did not produce an execution result");
11712
+ }
11713
+ return {
11714
+ sandbox,
11715
+ session: exec.session,
11716
+ result: exec.result,
11717
+ upload,
11718
+ remoteDir: input.upload?.remoteDir,
11719
+ cleanup: cleanupMode === "delete" ? "deleted" : cleanupMode === "stop" ? "stopped" : "kept"
11720
+ };
11721
+ }
11646
11722
  async runAgent(sandboxId, opts) {
11647
11723
  const sandbox = this.requireProviderSandbox(sandboxId);
11648
11724
  const provider = await this.getProvider(sandbox.provider);
@@ -11,6 +11,8 @@ export interface TarDirectoryOptions {
11
11
  * {@link DEFAULT_UPLOAD_EXCLUDES}; pass `[]` to include everything.
12
12
  */
13
13
  exclude?: string[];
14
+ /** Prepare a temporary upload tree with rsync before creating the archive. */
15
+ syncStrategy?: "archive" | "rsync";
14
16
  }
15
17
  /** Single-quote a value for safe POSIX shell interpolation. */
16
18
  export declare function shellQuote(value: string): string;
package/dist/mcp/index.js CHANGED
@@ -61,7 +61,9 @@ var init_types2 = __esm(() => {
61
61
  });
62
62
 
63
63
  // src/lib/archive.ts
64
- import { existsSync as existsSync7, statSync } from "fs";
64
+ import { existsSync as existsSync7, mkdtempSync, rmSync, statSync } from "fs";
65
+ import { tmpdir } from "os";
66
+ import { join as join8 } from "path";
65
67
  function shellQuote(value) {
66
68
  return "'" + value.replace(/'/g, "'\\''") + "'";
67
69
  }
@@ -69,6 +71,15 @@ async function tarDirectory(localDir, opts) {
69
71
  if (!existsSync7(localDir) || !statSync(localDir).isDirectory()) {
70
72
  throw new Error(`tarDirectory: not a directory: ${localDir}`);
71
73
  }
74
+ if (opts?.syncStrategy === "rsync") {
75
+ const stagingDir = mkdtempSync(join8(tmpdir(), "sandboxes-rsync-"));
76
+ try {
77
+ await rsyncDirectory(localDir, stagingDir, opts.exclude ?? DEFAULT_UPLOAD_EXCLUDES);
78
+ return await tarDirectory(stagingDir, { exclude: [], syncStrategy: "archive" });
79
+ } finally {
80
+ rmSync(stagingDir, { recursive: true, force: true });
81
+ }
82
+ }
72
83
  const excludes = opts?.exclude ?? DEFAULT_UPLOAD_EXCLUDES;
73
84
  const args = ["-czf", "-"];
74
85
  for (const ex of excludes)
@@ -85,6 +96,23 @@ async function tarDirectory(localDir, opts) {
85
96
  }
86
97
  return Buffer.from(buf);
87
98
  }
99
+ async function rsyncDirectory(localDir, stagingDir, excludes) {
100
+ const args = [
101
+ "-a",
102
+ "--delete",
103
+ ...excludes.flatMap((ex) => ["--exclude", ex]),
104
+ `${localDir.replace(/\/+$/, "")}/`,
105
+ `${stagingDir.replace(/\/+$/, "")}/`
106
+ ];
107
+ const proc = Bun.spawn(["rsync", ...args], { stdout: "pipe", stderr: "pipe" });
108
+ const [stderr, exitCode] = await Promise.all([
109
+ new Response(proc.stderr).text(),
110
+ proc.exited
111
+ ]);
112
+ if (exitCode !== 0) {
113
+ throw new Error(`rsyncDirectory: rsync exited ${exitCode}: ${stderr.trim()}`);
114
+ }
115
+ }
88
116
  function buildUntarCommand(remoteTarPath, remoteDir) {
89
117
  const tar = shellQuote(remoteTarPath);
90
118
  const dir = shellQuote(remoteDir);
package/dist/sdk.d.ts CHANGED
@@ -15,6 +15,38 @@ export interface ExecCommandResult {
15
15
  session: SandboxSession;
16
16
  result: ExecResult;
17
17
  }
18
+ export type OneShotSandboxCleanup = "delete" | "stop" | "keep";
19
+ export interface RunCommandInSandboxUploadOptions {
20
+ localDir: string;
21
+ remoteDir: string;
22
+ exclude?: string[];
23
+ syncStrategy?: "archive" | "rsync";
24
+ }
25
+ export interface RunCommandInSandboxOptions {
26
+ command: string;
27
+ provider?: SandboxProviderName;
28
+ name?: string;
29
+ image?: string;
30
+ sandboxTimeout?: number;
31
+ commandTimeoutMs?: number;
32
+ projectId?: string;
33
+ config?: Record<string, unknown>;
34
+ sandboxEnvVars?: Record<string, string>;
35
+ callEnvVars?: Record<string, string>;
36
+ cwd?: string;
37
+ upload?: RunCommandInSandboxUploadOptions;
38
+ cleanup?: OneShotSandboxCleanup;
39
+ onStdout?: (data: string) => void;
40
+ onStderr?: (data: string) => void;
41
+ }
42
+ export interface RunCommandInSandboxResult {
43
+ sandbox: Sandbox;
44
+ session: SandboxSession;
45
+ result: ExecResult;
46
+ upload?: UploadDirResult;
47
+ remoteDir?: string;
48
+ cleanup: "deleted" | "stopped" | "kept";
49
+ }
18
50
  export interface RunAgentOptions {
19
51
  agentType: AgentType;
20
52
  prompt: string;
@@ -54,6 +86,7 @@ export declare class SandboxesSDK {
54
86
  glob?: string;
55
87
  }): Promise<FileInfo[]>;
56
88
  uploadDir(sandboxId: string, localDir: string, remoteDir: string, opts?: UploadDirOptions): Promise<UploadDirResult>;
89
+ runCommandInSandbox(input: RunCommandInSandboxOptions): Promise<RunCommandInSandboxResult>;
57
90
  runAgent(sandboxId: string, opts: RunAgentOptions): Promise<SandboxSession>;
58
91
  getSession(sessionId: string): SandboxSession;
59
92
  waitForSession(sessionId: string, opts?: WaitForSessionOptions): Promise<SandboxSession>;
@@ -97,7 +97,9 @@ var init_types2 = __esm(() => {
97
97
  });
98
98
 
99
99
  // src/lib/archive.ts
100
- import { existsSync as existsSync7, statSync } from "fs";
100
+ import { existsSync as existsSync7, mkdtempSync, rmSync, statSync } from "fs";
101
+ import { tmpdir } from "os";
102
+ import { join as join8 } from "path";
101
103
  function shellQuote(value) {
102
104
  return "'" + value.replace(/'/g, "'\\''") + "'";
103
105
  }
@@ -105,6 +107,15 @@ async function tarDirectory(localDir, opts) {
105
107
  if (!existsSync7(localDir) || !statSync(localDir).isDirectory()) {
106
108
  throw new Error(`tarDirectory: not a directory: ${localDir}`);
107
109
  }
110
+ if (opts?.syncStrategy === "rsync") {
111
+ const stagingDir = mkdtempSync(join8(tmpdir(), "sandboxes-rsync-"));
112
+ try {
113
+ await rsyncDirectory(localDir, stagingDir, opts.exclude ?? DEFAULT_UPLOAD_EXCLUDES);
114
+ return await tarDirectory(stagingDir, { exclude: [], syncStrategy: "archive" });
115
+ } finally {
116
+ rmSync(stagingDir, { recursive: true, force: true });
117
+ }
118
+ }
108
119
  const excludes = opts?.exclude ?? DEFAULT_UPLOAD_EXCLUDES;
109
120
  const args = ["-czf", "-"];
110
121
  for (const ex of excludes)
@@ -121,6 +132,23 @@ async function tarDirectory(localDir, opts) {
121
132
  }
122
133
  return Buffer.from(buf);
123
134
  }
135
+ async function rsyncDirectory(localDir, stagingDir, excludes) {
136
+ const args = [
137
+ "-a",
138
+ "--delete",
139
+ ...excludes.flatMap((ex) => ["--exclude", ex]),
140
+ `${localDir.replace(/\/+$/, "")}/`,
141
+ `${stagingDir.replace(/\/+$/, "")}/`
142
+ ];
143
+ const proc = Bun.spawn(["rsync", ...args], { stdout: "pipe", stderr: "pipe" });
144
+ const [stderr, exitCode] = await Promise.all([
145
+ new Response(proc.stderr).text(),
146
+ proc.exited
147
+ ]);
148
+ if (exitCode !== 0) {
149
+ throw new Error(`rsyncDirectory: rsync exited ${exitCode}: ${stderr.trim()}`);
150
+ }
151
+ }
124
152
  function buildUntarCommand(remoteTarPath, remoteDir) {
125
153
  const tar = shellQuote(remoteTarPath);
126
154
  const dir = shellQuote(remoteDir);
@@ -179,6 +179,12 @@ export interface FileInfo {
179
179
  export interface UploadDirOptions {
180
180
  /** Patterns to exclude (passed to `tar --exclude`); defaults applied by the archiver. */
181
181
  exclude?: string[];
182
+ /**
183
+ * How to prepare the upload payload. `archive` tars the source directory
184
+ * directly; `rsync` first mirrors the source into a temporary staging
185
+ * directory with rsync, then uploads the staged tree.
186
+ */
187
+ syncStrategy?: "archive" | "rsync";
182
188
  }
183
189
  export interface UploadDirResult {
184
190
  /** Number of bytes uploaded (compressed archive size). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/sandboxes",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "author": "Andrei Hasna <andrei@hasna.com>",
5
5
  "repository": {
6
6
  "type": "git",