@hasna/sandboxes 0.1.27 → 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.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);
@@ -11658,7 +11686,12 @@ class SandboxesSDK {
11658
11686
  let exec;
11659
11687
  try {
11660
11688
  if (input.upload) {
11661
- upload = await this.uploadDir(sandbox.id, input.upload.localDir, input.upload.remoteDir, { exclude: input.upload.exclude });
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);
11662
11695
  }
11663
11696
  exec = await this.execCommand(sandbox.id, input.command, {
11664
11697
  cwd: input.cwd ?? input.upload?.remoteDir,
@@ -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;
@@ -1,6 +1,7 @@
1
1
  import { type Server } from "node:http";
2
- export declare const DEFAULT_MCP_HTTP_PORT = 8831;
2
+ export declare const DEFAULT_MCP_HTTP_PORT = 8875;
3
3
  export declare function isHttpMode(argv: string[]): boolean;
4
+ export declare function isStdioMode(argv: string[]): boolean;
4
5
  export declare function resolveMcpHttpPort(argv: string[]): number;
5
6
  export declare function healthPayload(name?: string): {
6
7
  status: 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);
@@ -16930,9 +16958,9 @@ function buildServer() {
16930
16958
  import { createServer } from "http";
16931
16959
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
16932
16960
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
16933
- var DEFAULT_MCP_HTTP_PORT = 8831;
16934
- function isHttpMode(argv) {
16935
- return argv.includes("--http") || process.env["MCP_HTTP"] === "1";
16961
+ var DEFAULT_MCP_HTTP_PORT = 8875;
16962
+ function isStdioMode(argv) {
16963
+ return argv.includes("--stdio") || process.env["MCP_STDIO"] === "1";
16936
16964
  }
16937
16965
  function resolveMcpHttpPort(argv) {
16938
16966
  const portIdx = argv.indexOf("--port");
@@ -17014,12 +17042,12 @@ if (handleCliFlags(argv)) {
17014
17042
  process.exit(0);
17015
17043
  }
17016
17044
  async function main() {
17017
- if (isHttpMode(argv)) {
17018
- startMcpHttpServer({ port: resolveMcpHttpPort(argv) });
17045
+ if (isStdioMode(argv)) {
17046
+ const server = buildServer();
17047
+ const transport = new StdioServerTransport;
17048
+ await server.connect(transport);
17019
17049
  return;
17020
17050
  }
17021
- const server = buildServer();
17022
- const transport = new StdioServerTransport;
17023
- await server.connect(transport);
17051
+ startMcpHttpServer({ port: resolveMcpHttpPort(argv) });
17024
17052
  }
17025
17053
  main().catch(console.error);
package/dist/sdk.d.ts CHANGED
@@ -20,6 +20,7 @@ export interface RunCommandInSandboxUploadOptions {
20
20
  localDir: string;
21
21
  remoteDir: string;
22
22
  exclude?: string[];
23
+ syncStrategy?: "archive" | "rsync";
23
24
  }
24
25
  export interface RunCommandInSandboxOptions {
25
26
  command: string;
@@ -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.27",
3
+ "version": "0.1.28",
4
4
  "author": "Andrei Hasna <andrei@hasna.com>",
5
5
  "repository": {
6
6
  "type": "git",