@vercel/sandbox 1.2.0 → 1.3.0
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 +5 -0
- package/dist/api-client/api-client.js +7 -0
- package/dist/api-client/api-client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/get-credentials.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -1
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-test.log +0 -24
- package/.turbo/turbo-typecheck.log +0 -4
- package/CHANGELOG.md +0 -277
- package/__mocks__/picocolors.ts +0 -13
- package/scripts/inject-version.ts +0 -11
- package/src/api-client/api-client.test.ts +0 -228
- package/src/api-client/api-client.ts +0 -592
- package/src/api-client/api-error.ts +0 -46
- package/src/api-client/base-client.ts +0 -171
- package/src/api-client/file-writer.ts +0 -90
- package/src/api-client/index.ts +0 -2
- package/src/api-client/validators.ts +0 -146
- package/src/api-client/with-retry.ts +0 -131
- package/src/auth/api.ts +0 -31
- package/src/auth/error.ts +0 -8
- package/src/auth/file.ts +0 -69
- package/src/auth/index.ts +0 -9
- package/src/auth/infer-scope.test.ts +0 -178
- package/src/auth/linked-project.test.ts +0 -86
- package/src/auth/linked-project.ts +0 -40
- package/src/auth/oauth.ts +0 -333
- package/src/auth/poll-for-token.ts +0 -89
- package/src/auth/project.ts +0 -92
- package/src/auth/zod.ts +0 -16
- package/src/command.test.ts +0 -103
- package/src/command.ts +0 -287
- package/src/constants.ts +0 -1
- package/src/index.ts +0 -4
- package/src/sandbox.test.ts +0 -171
- package/src/sandbox.ts +0 -677
- package/src/snapshot.ts +0 -110
- package/src/utils/array.ts +0 -15
- package/src/utils/consume-readable.ts +0 -12
- package/src/utils/decode-base64-url.ts +0 -14
- package/src/utils/dev-credentials.test.ts +0 -217
- package/src/utils/dev-credentials.ts +0 -196
- package/src/utils/get-credentials.test.ts +0 -20
- package/src/utils/get-credentials.ts +0 -183
- package/src/utils/jwt-expiry.test.ts +0 -125
- package/src/utils/jwt-expiry.ts +0 -105
- package/src/utils/log.ts +0 -20
- package/src/utils/normalizePath.test.ts +0 -114
- package/src/utils/normalizePath.ts +0 -33
- package/src/utils/resolveSignal.ts +0 -24
- package/src/utils/types.test.js +0 -7
- package/src/utils/types.ts +0 -23
- package/src/version.ts +0 -2
- package/test-utils/mock-response.ts +0 -12
- package/tsconfig.json +0 -16
- package/turbo.json +0 -9
- package/typedoc.json +0 -13
- package/vercel.json +0 -9
- package/vitest.config.ts +0 -9
- package/vitest.setup.ts +0 -4
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { setTimeout } from "node:timers/promises";
|
|
2
|
-
import { updateAuthConfig } from "./file";
|
|
3
|
-
import { DeviceAuthorizationRequest, isOAuthError, OAuth } from "./oauth";
|
|
4
|
-
|
|
5
|
-
export type PollTokenItem =
|
|
6
|
-
| { _tag: "Timeout"; newInterval: number }
|
|
7
|
-
| { _tag: "SlowDown"; newInterval: number }
|
|
8
|
-
| { _tag: "Error"; error: Error }
|
|
9
|
-
| {
|
|
10
|
-
_tag: "Response";
|
|
11
|
-
response: { text(): Promise<string> };
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export async function* pollForToken({
|
|
15
|
-
request,
|
|
16
|
-
oauth,
|
|
17
|
-
}: {
|
|
18
|
-
request: DeviceAuthorizationRequest;
|
|
19
|
-
oauth: OAuth;
|
|
20
|
-
}): AsyncGenerator<PollTokenItem, void, void> {
|
|
21
|
-
const controller = new AbortController();
|
|
22
|
-
try {
|
|
23
|
-
let intervalMs = request.interval * 1000;
|
|
24
|
-
while (Date.now() < request.expiresAt) {
|
|
25
|
-
const [tokenResponseError, tokenResponse] =
|
|
26
|
-
await oauth.deviceAccessTokenRequest(request.device_code);
|
|
27
|
-
|
|
28
|
-
if (tokenResponseError) {
|
|
29
|
-
// 2x backoff on connection timeouts per spec https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
|
|
30
|
-
if (tokenResponseError.message.includes("timeout")) {
|
|
31
|
-
intervalMs *= 2;
|
|
32
|
-
yield { _tag: "Timeout" as const, newInterval: intervalMs };
|
|
33
|
-
await setTimeout(intervalMs, { signal: controller.signal });
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
yield { _tag: "Error" as const, error: tokenResponseError };
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
yield {
|
|
41
|
-
_tag: "Response" as const,
|
|
42
|
-
response: tokenResponse.clone() as { text(): Promise<string> },
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const [tokensError, tokens] =
|
|
46
|
-
await oauth.processTokenResponse(tokenResponse);
|
|
47
|
-
|
|
48
|
-
if (isOAuthError(tokensError)) {
|
|
49
|
-
const { code } = tokensError;
|
|
50
|
-
switch (code) {
|
|
51
|
-
case "authorization_pending":
|
|
52
|
-
await setTimeout(intervalMs, { signal: controller.signal });
|
|
53
|
-
continue;
|
|
54
|
-
case "slow_down":
|
|
55
|
-
intervalMs += 5 * 1000;
|
|
56
|
-
yield { _tag: "SlowDown" as const, newInterval: intervalMs };
|
|
57
|
-
await setTimeout(intervalMs, { signal: controller.signal });
|
|
58
|
-
continue;
|
|
59
|
-
default:
|
|
60
|
-
yield { _tag: "Error", error: tokensError.cause };
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (tokensError) {
|
|
66
|
-
yield { _tag: "Error", error: tokensError };
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
updateAuthConfig({
|
|
71
|
-
token: tokens.access_token,
|
|
72
|
-
expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
|
|
73
|
-
refreshToken: tokens.refresh_token,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
yield {
|
|
80
|
-
_tag: "Error" as const,
|
|
81
|
-
error: new Error(
|
|
82
|
-
"Timed out waiting for authentication. Please try again.",
|
|
83
|
-
),
|
|
84
|
-
};
|
|
85
|
-
return;
|
|
86
|
-
} finally {
|
|
87
|
-
controller.abort();
|
|
88
|
-
}
|
|
89
|
-
}
|
package/src/auth/project.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { fetchApi } from "./api";
|
|
3
|
-
import { NotOk } from "./error";
|
|
4
|
-
import { readLinkedProject } from "./linked-project";
|
|
5
|
-
|
|
6
|
-
const TeamsSchema = z.object({
|
|
7
|
-
teams: z
|
|
8
|
-
.array(
|
|
9
|
-
z.object({
|
|
10
|
-
slug: z.string(),
|
|
11
|
-
}),
|
|
12
|
-
)
|
|
13
|
-
.min(1, `No teams found. Please create a team first.`),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const DEFAULT_PROJECT_NAME = "vercel-sandbox-default-project";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Resolves the team and project scope for sandbox operations.
|
|
20
|
-
*
|
|
21
|
-
* First checks for a locally linked project in `.vercel/project.json`.
|
|
22
|
-
* If found, uses the `projectId` and `orgId` from there.
|
|
23
|
-
*
|
|
24
|
-
* Otherwise, if `teamId` is not provided, selects the first available team for the account.
|
|
25
|
-
* Ensures a default project exists within the team, creating it if necessary.
|
|
26
|
-
*
|
|
27
|
-
* @param opts.token - Vercel API authentication token.
|
|
28
|
-
* @param opts.teamId - Optional team slug. If omitted, the first team is selected.
|
|
29
|
-
* @param opts.cwd - Optional directory to search for `.vercel/project.json`. Defaults to `process.cwd()`.
|
|
30
|
-
* @returns The resolved scope with `projectId`, `teamId`, and whether the project was `created`.
|
|
31
|
-
*
|
|
32
|
-
* @throws {NotOk} If the API returns an error other than 404 when checking the project.
|
|
33
|
-
* @throws {ZodError} If no teams exist for the account.
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* ```ts
|
|
37
|
-
* const scope = await inferScope({ token: "vercel_..." });
|
|
38
|
-
* // => { projectId: "vercel-sandbox-default-project", teamId: "my-team", created: false }
|
|
39
|
-
* ```
|
|
40
|
-
*/
|
|
41
|
-
export async function inferScope(opts: {
|
|
42
|
-
token: string;
|
|
43
|
-
teamId?: string;
|
|
44
|
-
cwd?: string;
|
|
45
|
-
}): Promise<{ projectId: string; teamId: string; created: boolean }> {
|
|
46
|
-
const linkedProject = await readLinkedProject(opts.cwd ?? process.cwd());
|
|
47
|
-
if (linkedProject) {
|
|
48
|
-
return { ...linkedProject, created: false };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const teamId = opts.teamId ?? (await selectTeam(opts.token));
|
|
52
|
-
|
|
53
|
-
let created = false;
|
|
54
|
-
try {
|
|
55
|
-
await fetchApi({
|
|
56
|
-
token: opts.token,
|
|
57
|
-
endpoint: `/v2/projects/${encodeURIComponent(DEFAULT_PROJECT_NAME)}?slug=${encodeURIComponent(teamId)}`,
|
|
58
|
-
});
|
|
59
|
-
} catch (e) {
|
|
60
|
-
if (!(e instanceof NotOk) || e.response.statusCode !== 404) {
|
|
61
|
-
throw e;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
await fetchApi({
|
|
65
|
-
token: opts.token,
|
|
66
|
-
endpoint: `/v11/projects?slug=${encodeURIComponent(teamId)}`,
|
|
67
|
-
method: "POST",
|
|
68
|
-
body: JSON.stringify({
|
|
69
|
-
name: DEFAULT_PROJECT_NAME,
|
|
70
|
-
}),
|
|
71
|
-
});
|
|
72
|
-
created = true;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { projectId: DEFAULT_PROJECT_NAME, teamId, created };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Selects a team for the current token by querying the Teams API and
|
|
80
|
-
* returning the slug of the first team in the result set.
|
|
81
|
-
*
|
|
82
|
-
* @param token - Authentication token used to call the Vercel API.
|
|
83
|
-
* @returns A promise that resolves to the first team's slug.
|
|
84
|
-
*/
|
|
85
|
-
export async function selectTeam(token: string) {
|
|
86
|
-
const {
|
|
87
|
-
teams: [team],
|
|
88
|
-
} = await fetchApi({ token, endpoint: "/v2/teams?limit=1" }).then(
|
|
89
|
-
TeamsSchema.parse,
|
|
90
|
-
);
|
|
91
|
-
return team.slug;
|
|
92
|
-
}
|
package/src/auth/zod.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A Zod codec that serializes and deserializes JSON strings.
|
|
5
|
-
*/
|
|
6
|
-
export const json = z.string().transform((jsonString: string, ctx): unknown => {
|
|
7
|
-
try {
|
|
8
|
-
return JSON.parse(jsonString);
|
|
9
|
-
} catch (err: any) {
|
|
10
|
-
ctx.addIssue({
|
|
11
|
-
code: z.ZodIssueCode.custom,
|
|
12
|
-
message: `Invalid JSON: ${err.message}`,
|
|
13
|
-
});
|
|
14
|
-
return z.NEVER;
|
|
15
|
-
}
|
|
16
|
-
});
|
package/src/command.test.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { expect, it, vi, beforeEach, afterEach, describe } from "vitest";
|
|
2
|
-
import { Sandbox } from "./sandbox";
|
|
3
|
-
|
|
4
|
-
describe.skipIf(process.env.RUN_INTEGRATION_TESTS !== "1")("Command", () => {
|
|
5
|
-
let sandbox: Sandbox;
|
|
6
|
-
|
|
7
|
-
beforeEach(async () => {
|
|
8
|
-
sandbox = await Sandbox.create();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
await sandbox.stop();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("supports more than one logs consumer", async () => {
|
|
16
|
-
const stdoutSpy = vi
|
|
17
|
-
.spyOn(process.stdout, "write")
|
|
18
|
-
.mockImplementation(() => true);
|
|
19
|
-
|
|
20
|
-
const cmd = await sandbox.runCommand({
|
|
21
|
-
cmd: "echo",
|
|
22
|
-
args: ["Hello World!"],
|
|
23
|
-
stdout: process.stdout,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
expect(await cmd.stdout()).toEqual("Hello World!\n");
|
|
27
|
-
expect(stdoutSpy).toHaveBeenCalledWith("Hello World!\n");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("does not warn when there is only one logs consumer", async () => {
|
|
31
|
-
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
32
|
-
|
|
33
|
-
const cmd = await sandbox.runCommand({
|
|
34
|
-
cmd: "echo",
|
|
35
|
-
args: ["Hello World!"],
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
expect(await cmd.stdout()).toEqual("Hello World!\n");
|
|
39
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("Kills a command with a SIGINT", async () => {
|
|
43
|
-
const cmd = await sandbox.runCommand({
|
|
44
|
-
cmd: "sleep",
|
|
45
|
-
args: ["200000"],
|
|
46
|
-
detached: true,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
await cmd.kill("SIGINT");
|
|
50
|
-
const result = await cmd.wait();
|
|
51
|
-
expect(result.exitCode).toBe(130); // 128 + 2
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("Kills a command with a SIGTERM", async () => {
|
|
55
|
-
const cmd = await sandbox.runCommand({
|
|
56
|
-
cmd: "sleep",
|
|
57
|
-
args: ["200000"],
|
|
58
|
-
detached: true,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
await cmd.kill("SIGTERM");
|
|
62
|
-
|
|
63
|
-
const result = await cmd.wait();
|
|
64
|
-
expect(result.exitCode).toBe(143); // 128 + 15
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("can execute commands with sudo", async () => {
|
|
68
|
-
const cmd = await sandbox.runCommand({
|
|
69
|
-
cmd: "env",
|
|
70
|
-
sudo: true,
|
|
71
|
-
env: {
|
|
72
|
-
FOO: "bar",
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
expect(cmd.exitCode).toBe(0);
|
|
77
|
-
|
|
78
|
-
const output = await cmd.stdout();
|
|
79
|
-
expect(output).toContain("FOO=bar\n");
|
|
80
|
-
expect(output).toContain("USER=root\n");
|
|
81
|
-
expect(output).toContain("SUDO_USER=vercel-sandbox\n");
|
|
82
|
-
|
|
83
|
-
const pathLine = output
|
|
84
|
-
.split("\n")
|
|
85
|
-
.find((line) => line.startsWith("PATH="));
|
|
86
|
-
expect(pathLine).toBeDefined();
|
|
87
|
-
|
|
88
|
-
const pathSegments = pathLine!.slice(5).split(":");
|
|
89
|
-
expect(pathSegments).toContain("/vercel/bin");
|
|
90
|
-
expect(pathSegments).toContain("/vercel/runtimes/node22/bin");
|
|
91
|
-
|
|
92
|
-
const dnf = await sandbox.runCommand({
|
|
93
|
-
cmd: "dnf",
|
|
94
|
-
args: ["install", "-y", "golang"],
|
|
95
|
-
sudo: true,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
expect(dnf.exitCode).toBe(0);
|
|
99
|
-
|
|
100
|
-
const which = await sandbox.runCommand("which", ["go"]);
|
|
101
|
-
expect(await which.output()).toContain("/usr/bin/go");
|
|
102
|
-
});
|
|
103
|
-
});
|
package/src/command.ts
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import { APIClient, type CommandData } from "./api-client";
|
|
2
|
-
import { Signal, resolveSignal } from "./utils/resolveSignal";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* A command executed in a Sandbox.
|
|
6
|
-
*
|
|
7
|
-
* For detached commands, you can {@link wait} to get a {@link CommandFinished} instance
|
|
8
|
-
* with the populated exit code. For non-detached commands, {@link Sandbox.runCommand}
|
|
9
|
-
* automatically waits and returns a {@link CommandFinished} instance.
|
|
10
|
-
*
|
|
11
|
-
* You can iterate over command output with {@link logs}.
|
|
12
|
-
*
|
|
13
|
-
* @see {@link Sandbox.runCommand} to start a command.
|
|
14
|
-
*
|
|
15
|
-
* @hideconstructor
|
|
16
|
-
*/
|
|
17
|
-
export class Command {
|
|
18
|
-
/**
|
|
19
|
-
* @internal
|
|
20
|
-
* @private
|
|
21
|
-
*/
|
|
22
|
-
protected client: APIClient;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* ID of the sandbox this command is running in.
|
|
26
|
-
*/
|
|
27
|
-
private sandboxId: string;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Data for the command execution.
|
|
31
|
-
*/
|
|
32
|
-
private cmd: CommandData;
|
|
33
|
-
|
|
34
|
-
public exitCode: number | null;
|
|
35
|
-
|
|
36
|
-
private outputCache: Promise<{
|
|
37
|
-
stdout: string;
|
|
38
|
-
stderr: string;
|
|
39
|
-
both: string;
|
|
40
|
-
}> | null = null;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* ID of the command execution.
|
|
44
|
-
*/
|
|
45
|
-
get cmdId() {
|
|
46
|
-
return this.cmd.id;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
get cwd() {
|
|
50
|
-
return this.cmd.cwd;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
get startedAt() {
|
|
54
|
-
return this.cmd.startedAt;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* @param params - Object containing the client, sandbox ID, and command ID.
|
|
59
|
-
* @param params.client - API client used to interact with the backend.
|
|
60
|
-
* @param params.sandboxId - The ID of the sandbox where the command is running.
|
|
61
|
-
* @param params.cmdId - The ID of the command execution.
|
|
62
|
-
*/
|
|
63
|
-
constructor({
|
|
64
|
-
client,
|
|
65
|
-
sandboxId,
|
|
66
|
-
cmd,
|
|
67
|
-
}: {
|
|
68
|
-
client: APIClient;
|
|
69
|
-
sandboxId: string;
|
|
70
|
-
cmd: CommandData;
|
|
71
|
-
}) {
|
|
72
|
-
this.client = client;
|
|
73
|
-
this.sandboxId = sandboxId;
|
|
74
|
-
this.cmd = cmd;
|
|
75
|
-
this.exitCode = cmd.exitCode ?? null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Iterate over the output of this command.
|
|
80
|
-
*
|
|
81
|
-
* ```
|
|
82
|
-
* for await (const log of cmd.logs()) {
|
|
83
|
-
* if (log.stream === "stdout") {
|
|
84
|
-
* process.stdout.write(log.data);
|
|
85
|
-
* } else {
|
|
86
|
-
* process.stderr.write(log.data);
|
|
87
|
-
* }
|
|
88
|
-
* }
|
|
89
|
-
* ```
|
|
90
|
-
*
|
|
91
|
-
* @param opts - Optional parameters.
|
|
92
|
-
* @param opts.signal - An AbortSignal to cancel log streaming.
|
|
93
|
-
* @returns An async iterable of log entries from the command output.
|
|
94
|
-
*
|
|
95
|
-
* @see {@link Command.stdout}, {@link Command.stderr}, and {@link Command.output}
|
|
96
|
-
* to access output as a string.
|
|
97
|
-
*/
|
|
98
|
-
logs(opts?: { signal?: AbortSignal }) {
|
|
99
|
-
return this.client.getLogs({
|
|
100
|
-
sandboxId: this.sandboxId,
|
|
101
|
-
cmdId: this.cmd.id,
|
|
102
|
-
signal: opts?.signal,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Wait for a command to exit and populate its exit code.
|
|
108
|
-
*
|
|
109
|
-
* This method is useful for detached commands where you need to wait
|
|
110
|
-
* for completion. For non-detached commands, {@link Sandbox.runCommand}
|
|
111
|
-
* automatically waits and returns a {@link CommandFinished} instance.
|
|
112
|
-
*
|
|
113
|
-
* ```
|
|
114
|
-
* const detachedCmd = await sandbox.runCommand({ cmd: 'sleep', args: ['5'], detached: true });
|
|
115
|
-
* const result = await detachedCmd.wait();
|
|
116
|
-
* if (result.exitCode !== 0) {
|
|
117
|
-
* console.error("Something went wrong...")
|
|
118
|
-
* }
|
|
119
|
-
* ```
|
|
120
|
-
*
|
|
121
|
-
* @param params - Optional parameters.
|
|
122
|
-
* @param params.signal - An AbortSignal to cancel waiting.
|
|
123
|
-
* @returns A {@link CommandFinished} instance with populated exit code.
|
|
124
|
-
*/
|
|
125
|
-
async wait(params?: { signal?: AbortSignal }) {
|
|
126
|
-
params?.signal?.throwIfAborted();
|
|
127
|
-
|
|
128
|
-
const command = await this.client.getCommand({
|
|
129
|
-
sandboxId: this.sandboxId,
|
|
130
|
-
cmdId: this.cmd.id,
|
|
131
|
-
wait: true,
|
|
132
|
-
signal: params?.signal,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
return new CommandFinished({
|
|
136
|
-
client: this.client,
|
|
137
|
-
sandboxId: this.sandboxId,
|
|
138
|
-
cmd: command.json.command,
|
|
139
|
-
exitCode: command.json.command.exitCode,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Get cached output, fetching logs only once and reusing for concurrent calls.
|
|
145
|
-
* This prevents race conditions when stdout() and stderr() are called in parallel.
|
|
146
|
-
*/
|
|
147
|
-
private async getCachedOutput(opts?: { signal?: AbortSignal }): Promise<{
|
|
148
|
-
stdout: string;
|
|
149
|
-
stderr: string;
|
|
150
|
-
both: string;
|
|
151
|
-
}> {
|
|
152
|
-
if (!this.outputCache) {
|
|
153
|
-
this.outputCache = (async () => {
|
|
154
|
-
try {
|
|
155
|
-
let stdout = "";
|
|
156
|
-
let stderr = "";
|
|
157
|
-
let both = "";
|
|
158
|
-
for await (const log of this.logs({ signal: opts?.signal })) {
|
|
159
|
-
both += log.data;
|
|
160
|
-
if (log.stream === "stdout") {
|
|
161
|
-
stdout += log.data;
|
|
162
|
-
} else {
|
|
163
|
-
stderr += log.data;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return { stdout, stderr, both };
|
|
167
|
-
} catch (err) {
|
|
168
|
-
// Clear the promise so future calls can retry
|
|
169
|
-
this.outputCache = null;
|
|
170
|
-
throw err;
|
|
171
|
-
}
|
|
172
|
-
})();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return this.outputCache;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Get the output of `stdout`, `stderr`, or both as a string.
|
|
180
|
-
*
|
|
181
|
-
* NOTE: This may throw string conversion errors if the command does
|
|
182
|
-
* not output valid Unicode.
|
|
183
|
-
*
|
|
184
|
-
* @param stream - The output stream to read: "stdout", "stderr", or "both".
|
|
185
|
-
* @param opts - Optional parameters.
|
|
186
|
-
* @param opts.signal - An AbortSignal to cancel output streaming.
|
|
187
|
-
* @returns The output of the specified stream(s) as a string.
|
|
188
|
-
*/
|
|
189
|
-
async output(
|
|
190
|
-
stream: "stdout" | "stderr" | "both" = "both",
|
|
191
|
-
opts?: { signal?: AbortSignal },
|
|
192
|
-
) {
|
|
193
|
-
const cached = await this.getCachedOutput(opts);
|
|
194
|
-
return cached[stream];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Get the output of `stdout` as a string.
|
|
199
|
-
*
|
|
200
|
-
* NOTE: This may throw string conversion errors if the command does
|
|
201
|
-
* not output valid Unicode.
|
|
202
|
-
*
|
|
203
|
-
* @param opts - Optional parameters.
|
|
204
|
-
* @param opts.signal - An AbortSignal to cancel output streaming.
|
|
205
|
-
* @returns The standard output of the command.
|
|
206
|
-
*/
|
|
207
|
-
async stdout(opts?: { signal?: AbortSignal }) {
|
|
208
|
-
return this.output("stdout", opts);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Get the output of `stderr` as a string.
|
|
213
|
-
*
|
|
214
|
-
* NOTE: This may throw string conversion errors if the command does
|
|
215
|
-
* not output valid Unicode.
|
|
216
|
-
*
|
|
217
|
-
* @param opts - Optional parameters.
|
|
218
|
-
* @param opts.signal - An AbortSignal to cancel output streaming.
|
|
219
|
-
* @returns The standard error output of the command.
|
|
220
|
-
*/
|
|
221
|
-
async stderr(opts?: { signal?: AbortSignal }) {
|
|
222
|
-
return this.output("stderr", opts);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Kill a running command in a sandbox.
|
|
227
|
-
*
|
|
228
|
-
* @param signal - The signal to send the running process. Defaults to SIGTERM.
|
|
229
|
-
* @param opts - Optional parameters.
|
|
230
|
-
* @param opts.abortSignal - An AbortSignal to cancel the kill operation.
|
|
231
|
-
* @returns Promise<void>.
|
|
232
|
-
*/
|
|
233
|
-
async kill(signal?: Signal, opts?: { abortSignal?: AbortSignal }) {
|
|
234
|
-
await this.client.killCommand({
|
|
235
|
-
sandboxId: this.sandboxId,
|
|
236
|
-
commandId: this.cmd.id,
|
|
237
|
-
signal: resolveSignal(signal ?? "SIGTERM"),
|
|
238
|
-
abortSignal: opts?.abortSignal,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* A command that has finished executing.
|
|
245
|
-
*
|
|
246
|
-
* The exit code is immediately available and populated upon creation.
|
|
247
|
-
* Unlike {@link Command}, you don't need to call wait() - the command
|
|
248
|
-
* has already completed execution.
|
|
249
|
-
*
|
|
250
|
-
* @hideconstructor
|
|
251
|
-
*/
|
|
252
|
-
export class CommandFinished extends Command {
|
|
253
|
-
/**
|
|
254
|
-
* The exit code of the command. This is always populated for
|
|
255
|
-
* CommandFinished instances.
|
|
256
|
-
*/
|
|
257
|
-
public exitCode: number;
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* @param params - Object containing client, sandbox ID, command ID, and exit code.
|
|
261
|
-
* @param params.client - API client used to interact with the backend.
|
|
262
|
-
* @param params.sandboxId - The ID of the sandbox where the command ran.
|
|
263
|
-
* @param params.cmdId - The ID of the command execution.
|
|
264
|
-
* @param params.exitCode - The exit code of the completed command.
|
|
265
|
-
*/
|
|
266
|
-
constructor(params: {
|
|
267
|
-
client: APIClient;
|
|
268
|
-
sandboxId: string;
|
|
269
|
-
cmd: CommandData;
|
|
270
|
-
exitCode: number;
|
|
271
|
-
}) {
|
|
272
|
-
super({ ...params });
|
|
273
|
-
this.exitCode = params.exitCode;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* The wait method is not needed for CommandFinished instances since
|
|
278
|
-
* the command has already completed and exitCode is populated.
|
|
279
|
-
*
|
|
280
|
-
* @deprecated This method is redundant for CommandFinished instances.
|
|
281
|
-
* The exitCode is already available.
|
|
282
|
-
* @returns This CommandFinished instance.
|
|
283
|
-
*/
|
|
284
|
-
async wait(): Promise<CommandFinished> {
|
|
285
|
-
return this;
|
|
286
|
-
}
|
|
287
|
-
}
|
package/src/constants.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export type RUNTIMES = "node24" | "node22" | "python3.13";
|
package/src/index.ts
DELETED