@vercel/sandbox 0.0.7 → 0.0.9
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +15 -0
- package/dist/api-client/api-client.d.ts +81 -8
- package/dist/api-client/api-client.js +34 -14
- package/dist/api-client/file-writer.d.ts +52 -0
- package/dist/api-client/file-writer.js +62 -0
- package/dist/api-client/index.d.ts +1 -0
- package/dist/api-client/index.js +15 -0
- package/dist/api-client/validators.d.ts +343 -50
- package/dist/api-client/validators.js +37 -16
- package/dist/command.d.ts +18 -5
- package/dist/command.js +27 -6
- package/dist/sandbox.d.ts +25 -15
- package/dist/sandbox.js +28 -14
- package/dist/utils/resolveSignal.d.ts +13 -0
- package/dist/utils/resolveSignal.js +21 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
- package/src/api-client/api-client.ts +63 -29
- package/src/api-client/file-writer.ts +90 -0
- package/src/api-client/index.ts +1 -0
- package/src/api-client/validators.ts +45 -15
- package/src/command.test.ts +39 -16
- package/src/command.ts +33 -10
- package/src/sandbox.ts +53 -23
- package/src/utils/resolveSignal.ts +24 -0
- package/src/version.ts +1 -1
package/src/command.test.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { Sandbox } from "./sandbox";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
6
|
-
const stdoutSpy = vi
|
|
7
|
-
.spyOn(process.stdout, "write")
|
|
8
|
-
.mockImplementation(() => true);
|
|
4
|
+
let sandbox: Sandbox;
|
|
9
5
|
|
|
10
|
-
|
|
6
|
+
beforeEach(async () => {
|
|
7
|
+
sandbox = await Sandbox.create({
|
|
11
8
|
projectId: process.env.VERCEL_PROJECT_ID!,
|
|
12
9
|
teamId: process.env.VERCEL_TEAM_ID!,
|
|
13
10
|
token: process.env.VERCEL_TOKEN!,
|
|
14
11
|
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await sandbox.stop();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("warns when there is more than one logs consumer", async () => {
|
|
19
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
20
|
+
const stdoutSpy = vi
|
|
21
|
+
.spyOn(process.stdout, "write")
|
|
22
|
+
.mockImplementation(() => true);
|
|
15
23
|
|
|
16
24
|
const cmd = await sandbox.runCommand({
|
|
17
25
|
cmd: "echo",
|
|
@@ -26,19 +34,11 @@ it("warns when there is more than one logs consumer", async () => {
|
|
|
26
34
|
/Multiple consumers for logs of command `[^`]+`\.\sThis may lead to unexpected behavior\./,
|
|
27
35
|
),
|
|
28
36
|
);
|
|
29
|
-
|
|
30
|
-
await sandbox.stop();
|
|
31
37
|
});
|
|
32
38
|
|
|
33
39
|
it("does not warn when there is only one logs consumer", async () => {
|
|
34
40
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
35
41
|
|
|
36
|
-
const sandbox = await Sandbox.create({
|
|
37
|
-
projectId: process.env.VERCEL_PROJECT_ID!,
|
|
38
|
-
teamId: process.env.VERCEL_TEAM_ID!,
|
|
39
|
-
token: process.env.VERCEL_TOKEN!,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
42
|
const cmd = await sandbox.runCommand({
|
|
43
43
|
cmd: "echo",
|
|
44
44
|
args: ["Hello World!"],
|
|
@@ -46,6 +46,29 @@ it("does not warn when there is only one logs consumer", async () => {
|
|
|
46
46
|
|
|
47
47
|
expect(await cmd.stdout()).toEqual("Hello World!\n");
|
|
48
48
|
expect(warnSpy).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
it("Kills a command with a SIGINT", async () => {
|
|
52
|
+
const cmd = await sandbox.runCommand({
|
|
53
|
+
cmd: "sleep",
|
|
54
|
+
args: ["200000"],
|
|
55
|
+
detached: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await cmd.kill("SIGINT");
|
|
59
|
+
const result = await cmd.wait();
|
|
60
|
+
expect(result.exitCode).toBe(130); // 128 + 2
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("Kills a command with a SIGTERM", async () => {
|
|
64
|
+
const cmd = await sandbox.runCommand({
|
|
65
|
+
cmd: "sleep",
|
|
66
|
+
args: ["200000"],
|
|
67
|
+
detached: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await cmd.kill("SIGTERM");
|
|
71
|
+
|
|
72
|
+
const result = await cmd.wait();
|
|
73
|
+
expect(result.exitCode).toBe(143); // 128 + 15
|
|
51
74
|
});
|
package/src/command.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { APIClient } from "./api-client";
|
|
1
|
+
import { APIClient, type CommandData } from "./api-client";
|
|
2
|
+
import { Signal, resolveSignal } from "./utils/resolveSignal";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A command executed in a Sandbox.
|
|
@@ -22,10 +23,17 @@ export class Command {
|
|
|
22
23
|
*/
|
|
23
24
|
private sandboxId: string;
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Data for the command execution.
|
|
28
|
+
*/
|
|
29
|
+
private cmd: CommandData;
|
|
30
|
+
|
|
25
31
|
/**
|
|
26
32
|
* ID of the command execution.
|
|
27
33
|
*/
|
|
28
|
-
|
|
34
|
+
get cmdId() {
|
|
35
|
+
return this.cmd.id;
|
|
36
|
+
}
|
|
29
37
|
|
|
30
38
|
/**
|
|
31
39
|
* @param params - Object containing the client, sandbox ID, and command ID.
|
|
@@ -36,15 +44,15 @@ export class Command {
|
|
|
36
44
|
constructor({
|
|
37
45
|
client,
|
|
38
46
|
sandboxId,
|
|
39
|
-
|
|
47
|
+
cmd,
|
|
40
48
|
}: {
|
|
41
49
|
client: APIClient;
|
|
42
50
|
sandboxId: string;
|
|
43
|
-
|
|
51
|
+
cmd: CommandData;
|
|
44
52
|
}) {
|
|
45
53
|
this.client = client;
|
|
46
54
|
this.sandboxId = sandboxId;
|
|
47
|
-
this.
|
|
55
|
+
this.cmd = cmd;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
/**
|
|
@@ -68,7 +76,7 @@ export class Command {
|
|
|
68
76
|
logs() {
|
|
69
77
|
return this.client.getLogs({
|
|
70
78
|
sandboxId: this.sandboxId,
|
|
71
|
-
cmdId: this.
|
|
79
|
+
cmdId: this.cmd.id,
|
|
72
80
|
});
|
|
73
81
|
}
|
|
74
82
|
|
|
@@ -87,15 +95,15 @@ export class Command {
|
|
|
87
95
|
async wait() {
|
|
88
96
|
const command = await this.client.getCommand({
|
|
89
97
|
sandboxId: this.sandboxId,
|
|
90
|
-
cmdId: this.
|
|
98
|
+
cmdId: this.cmd.id,
|
|
91
99
|
wait: true,
|
|
92
100
|
});
|
|
93
101
|
|
|
94
102
|
return new CommandFinished({
|
|
95
103
|
client: this.client,
|
|
96
104
|
sandboxId: this.sandboxId,
|
|
97
|
-
|
|
98
|
-
exitCode: command.json.exitCode,
|
|
105
|
+
cmd: command.json.command,
|
|
106
|
+
exitCode: command.json.command.exitCode,
|
|
99
107
|
});
|
|
100
108
|
}
|
|
101
109
|
|
|
@@ -141,6 +149,21 @@ export class Command {
|
|
|
141
149
|
async stderr() {
|
|
142
150
|
return this.output("stderr");
|
|
143
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Kill a running command in a sandbox.
|
|
155
|
+
*
|
|
156
|
+
* @param params - commandId and the signal to send the running process.
|
|
157
|
+
* Defaults to SIGTERM.
|
|
158
|
+
* @returns Promise<void>.
|
|
159
|
+
*/
|
|
160
|
+
async kill(signal?: Signal) {
|
|
161
|
+
await this.client.killCommand({
|
|
162
|
+
sandboxId: this.sandboxId,
|
|
163
|
+
commandId: this.cmd.id,
|
|
164
|
+
signal: resolveSignal(signal ?? "SIGTERM"),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
144
167
|
}
|
|
145
168
|
|
|
146
169
|
/**
|
|
@@ -167,7 +190,7 @@ export class CommandFinished extends Command {
|
|
|
167
190
|
constructor(params: {
|
|
168
191
|
client: APIClient;
|
|
169
192
|
sandboxId: string;
|
|
170
|
-
|
|
193
|
+
cmd: CommandData;
|
|
171
194
|
exitCode: number;
|
|
172
195
|
}) {
|
|
173
196
|
super({ ...params });
|
package/src/sandbox.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SandboxData, SandboxRouteData } from "./api-client";
|
|
2
|
+
import type { Writable } from "stream";
|
|
2
3
|
import { APIClient } from "./api-client";
|
|
3
|
-
import { Command, CommandFinished } from "./command";
|
|
4
|
-
import {
|
|
4
|
+
import { Command, type CommandFinished } from "./command";
|
|
5
|
+
import { type Credentials, getCredentials } from "./utils/get-credentials";
|
|
5
6
|
|
|
6
7
|
/** @inline */
|
|
7
8
|
export interface CreateSandboxParams {
|
|
@@ -9,8 +10,14 @@ export interface CreateSandboxParams {
|
|
|
9
10
|
* The source of the sandbox.
|
|
10
11
|
*
|
|
11
12
|
* Omit this parameter start a sandbox without a source.
|
|
13
|
+
*
|
|
14
|
+
* For git sources:
|
|
15
|
+
* - `depth`: Creates shallow clones with limited commit history (minimum: 1)
|
|
16
|
+
* - `revision`: Clones and checks out a specific commit, branch, or tag
|
|
12
17
|
*/
|
|
13
|
-
source?:
|
|
18
|
+
source?:
|
|
19
|
+
| { type: "git"; url: string; depth?: number; revision?: string }
|
|
20
|
+
| { type: "tarball"; url: string };
|
|
14
21
|
/**
|
|
15
22
|
* Array of port numbers to expose from the sandbox.
|
|
16
23
|
*/
|
|
@@ -26,6 +33,12 @@ export interface CreateSandboxParams {
|
|
|
26
33
|
* 2048 MB of memory per vCPU.
|
|
27
34
|
*/
|
|
28
35
|
resources?: { vcpus: number };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The runtime of the sandbox, currently only `node22` and `python3.13` are supported.
|
|
39
|
+
* If not specified, the default runtime `node22` will be used.
|
|
40
|
+
*/
|
|
41
|
+
runtime?: "node22" | "python3.13";
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
/** @inline */
|
|
@@ -85,12 +98,19 @@ export class Sandbox {
|
|
|
85
98
|
* Routes from ports to subdomains.
|
|
86
99
|
/* @hidden
|
|
87
100
|
*/
|
|
88
|
-
public readonly routes:
|
|
101
|
+
public readonly routes: SandboxRouteData[];
|
|
89
102
|
|
|
90
103
|
/**
|
|
91
104
|
* Unique ID of this sandbox.
|
|
92
105
|
*/
|
|
93
|
-
public
|
|
106
|
+
public get sandboxId(): string {
|
|
107
|
+
return this.sandbox.id;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Data about this sandbox.
|
|
112
|
+
*/
|
|
113
|
+
private readonly sandbox: SandboxData;
|
|
94
114
|
|
|
95
115
|
/**
|
|
96
116
|
* Create a new sandbox.
|
|
@@ -113,11 +133,12 @@ export class Sandbox {
|
|
|
113
133
|
ports: params?.ports ?? [],
|
|
114
134
|
timeout: params?.timeout,
|
|
115
135
|
resources: params?.resources,
|
|
136
|
+
runtime: params?.runtime,
|
|
116
137
|
});
|
|
117
138
|
|
|
118
139
|
return new Sandbox({
|
|
119
140
|
client,
|
|
120
|
-
|
|
141
|
+
sandbox: sandbox.json.sandbox,
|
|
121
142
|
routes: sandbox.json.routes,
|
|
122
143
|
});
|
|
123
144
|
}
|
|
@@ -137,10 +158,14 @@ export class Sandbox {
|
|
|
137
158
|
token: credentials.token,
|
|
138
159
|
});
|
|
139
160
|
|
|
161
|
+
const sandbox = await client.getSandbox({
|
|
162
|
+
sandboxId: params.sandboxId,
|
|
163
|
+
});
|
|
164
|
+
|
|
140
165
|
return new Sandbox({
|
|
141
166
|
client,
|
|
142
|
-
|
|
143
|
-
routes:
|
|
167
|
+
sandbox: sandbox.json.sandbox,
|
|
168
|
+
routes: sandbox.json.routes,
|
|
144
169
|
});
|
|
145
170
|
}
|
|
146
171
|
|
|
@@ -154,15 +179,15 @@ export class Sandbox {
|
|
|
154
179
|
constructor({
|
|
155
180
|
client,
|
|
156
181
|
routes,
|
|
157
|
-
|
|
182
|
+
sandbox,
|
|
158
183
|
}: {
|
|
159
184
|
client: APIClient;
|
|
160
|
-
routes:
|
|
161
|
-
|
|
185
|
+
routes: SandboxRouteData[];
|
|
186
|
+
sandbox: SandboxData;
|
|
162
187
|
}) {
|
|
163
188
|
this.client = client;
|
|
164
189
|
this.routes = routes;
|
|
165
|
-
this.
|
|
190
|
+
this.sandbox = sandbox;
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
/**
|
|
@@ -171,11 +196,16 @@ export class Sandbox {
|
|
|
171
196
|
* @param cmdId - ID of the command to retrieve
|
|
172
197
|
* @returns A {@link Command} instance representing the command
|
|
173
198
|
*/
|
|
174
|
-
getCommand(cmdId: string): Command {
|
|
199
|
+
async getCommand(cmdId: string): Promise<Command> {
|
|
200
|
+
const command = await this.client.getCommand({
|
|
201
|
+
sandboxId: this.sandbox.id,
|
|
202
|
+
cmdId,
|
|
203
|
+
});
|
|
204
|
+
|
|
175
205
|
return new Command({
|
|
176
206
|
client: this.client,
|
|
177
|
-
sandboxId: this.
|
|
178
|
-
|
|
207
|
+
sandboxId: this.sandbox.id,
|
|
208
|
+
cmd: command.json.command,
|
|
179
209
|
});
|
|
180
210
|
}
|
|
181
211
|
|
|
@@ -224,7 +254,7 @@ export class Sandbox {
|
|
|
224
254
|
*/
|
|
225
255
|
async _runCommand(params: RunCommandParams) {
|
|
226
256
|
const commandResponse = await this.client.runCommand({
|
|
227
|
-
sandboxId: this.
|
|
257
|
+
sandboxId: this.sandbox.id,
|
|
228
258
|
command: params.cmd,
|
|
229
259
|
args: params.args ?? [],
|
|
230
260
|
cwd: params.cwd,
|
|
@@ -233,8 +263,8 @@ export class Sandbox {
|
|
|
233
263
|
|
|
234
264
|
const command = new Command({
|
|
235
265
|
client: this.client,
|
|
236
|
-
sandboxId: this.
|
|
237
|
-
|
|
266
|
+
sandboxId: this.sandbox.id,
|
|
267
|
+
cmd: commandResponse.json.command,
|
|
238
268
|
});
|
|
239
269
|
|
|
240
270
|
if (params.stdout || params.stderr) {
|
|
@@ -259,7 +289,7 @@ export class Sandbox {
|
|
|
259
289
|
*/
|
|
260
290
|
async mkDir(path: string): Promise<void> {
|
|
261
291
|
await this.client.mkDir({
|
|
262
|
-
sandboxId: this.
|
|
292
|
+
sandboxId: this.sandbox.id,
|
|
263
293
|
path: path,
|
|
264
294
|
});
|
|
265
295
|
}
|
|
@@ -270,9 +300,9 @@ export class Sandbox {
|
|
|
270
300
|
* @param files - Array of files with path and stream/buffer contents
|
|
271
301
|
* @returns A promise that resolves when the files are written
|
|
272
302
|
*/
|
|
273
|
-
async writeFiles(files: { path: string; stream:
|
|
303
|
+
async writeFiles(files: { path: string; stream: Buffer }[]) {
|
|
274
304
|
return this.client.writeFiles({
|
|
275
|
-
sandboxId: this.
|
|
305
|
+
sandboxId: this.sandbox.id,
|
|
276
306
|
files: files,
|
|
277
307
|
});
|
|
278
308
|
}
|
|
@@ -300,7 +330,7 @@ export class Sandbox {
|
|
|
300
330
|
*/
|
|
301
331
|
async stop() {
|
|
302
332
|
await this.client.stopSandbox({
|
|
303
|
-
sandboxId: this.
|
|
333
|
+
sandboxId: this.sandbox.id,
|
|
304
334
|
});
|
|
305
335
|
}
|
|
306
336
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const linuxSignalMapping = {
|
|
2
|
+
SIGHUP: 1,
|
|
3
|
+
SIGINT: 2,
|
|
4
|
+
SIGQUIT: 3,
|
|
5
|
+
SIGKILL: 9,
|
|
6
|
+
SIGTERM: 15,
|
|
7
|
+
SIGCONT: 18,
|
|
8
|
+
SIGSTOP: 19,
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
type CommonLinuxSignals = keyof typeof linuxSignalMapping;
|
|
12
|
+
|
|
13
|
+
export type Signal = CommonLinuxSignals | number;
|
|
14
|
+
|
|
15
|
+
export function resolveSignal(signal: Signal): number {
|
|
16
|
+
if (typeof signal === "number") {
|
|
17
|
+
return signal;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (signal in linuxSignalMapping) {
|
|
21
|
+
return linuxSignalMapping[signal];
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`Unknown signal name: ${String(signal)}`);
|
|
24
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Autogenerated by inject-version.ts
|
|
2
|
-
export const VERSION = "0.0.
|
|
2
|
+
export const VERSION = "0.0.9";
|