@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 +23 -0
- package/dist/cli/index.js +29 -1
- package/dist/index.js +35 -2
- package/dist/lib/archive.d.ts +2 -0
- package/dist/mcp/http.d.ts +2 -1
- package/dist/mcp/index.js +37 -9
- package/dist/sdk.d.ts +1 -0
- package/dist/server/index.js +29 -1
- package/dist/types/index.d.ts +6 -0
- package/package.json +1 -1
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
|
-
|
|
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,
|
package/dist/lib/archive.d.ts
CHANGED
|
@@ -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/http.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Server } from "node:http";
|
|
2
|
-
export declare const DEFAULT_MCP_HTTP_PORT =
|
|
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 =
|
|
16934
|
-
function
|
|
16935
|
-
return argv.includes("--
|
|
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 (
|
|
17018
|
-
|
|
17045
|
+
if (isStdioMode(argv)) {
|
|
17046
|
+
const server = buildServer();
|
|
17047
|
+
const transport = new StdioServerTransport;
|
|
17048
|
+
await server.connect(transport);
|
|
17019
17049
|
return;
|
|
17020
17050
|
}
|
|
17021
|
-
|
|
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
package/dist/server/index.js
CHANGED
|
@@ -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);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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). */
|