@vercel/sandbox 0.0.1
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 +4 -0
- package/CHANGELOG.md +7 -0
- package/README.md +79 -0
- package/dist/client/api-error.d.ts +14 -0
- package/dist/client/api-error.js +16 -0
- package/dist/client/base-client.d.ts +43 -0
- package/dist/client/base-client.js +111 -0
- package/dist/client/client.d.ts +63 -0
- package/dist/client/client.js +118 -0
- package/dist/client/validators.d.ts +70 -0
- package/dist/client/validators.js +26 -0
- package/dist/client/with-retry.d.ts +15 -0
- package/dist/client/with-retry.js +97 -0
- package/dist/create-sandbox.d.ts +175 -0
- package/dist/create-sandbox.js +212 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/utils/array.d.ts +9 -0
- package/dist/utils/array.js +18 -0
- package/dist/utils/deferred-generator.d.ts +5 -0
- package/dist/utils/deferred-generator.js +32 -0
- package/dist/utils/deferred.d.ts +6 -0
- package/dist/utils/deferred.js +12 -0
- package/package.json +30 -0
- package/src/client/api-error.ts +26 -0
- package/src/client/base-client.ts +144 -0
- package/src/client/client.ts +181 -0
- package/src/client/validators.ts +29 -0
- package/src/client/with-retry.ts +121 -0
- package/src/create-sandbox.ts +272 -0
- package/src/index.ts +2 -0
- package/src/utils/array.ts +15 -0
- package/src/utils/deferred-generator.ts +38 -0
- package/src/utils/deferred.ts +12 -0
- package/tsconfig.json +16 -0
- package/typedoc.json +7 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import FormData from "form-data";
|
|
2
|
+
import { APIClient, parseOrThrow, type RequestParams } from "./base-client";
|
|
3
|
+
import {
|
|
4
|
+
Command,
|
|
5
|
+
CreatedCommand,
|
|
6
|
+
CreatedSandbox,
|
|
7
|
+
LogLine,
|
|
8
|
+
WrittenFile,
|
|
9
|
+
} from "./validators";
|
|
10
|
+
import { Readable } from "stream";
|
|
11
|
+
import { APIError } from "./api-error";
|
|
12
|
+
import { createDeferredGenerator } from "../utils/deferred-generator";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import jsonlines from "jsonlines";
|
|
15
|
+
|
|
16
|
+
export class SandboxClient extends APIClient {
|
|
17
|
+
private teamId: string;
|
|
18
|
+
|
|
19
|
+
constructor(params: { host?: string; teamId: string; token: string }) {
|
|
20
|
+
super({
|
|
21
|
+
host: params.host ?? "https://api.vercel.com",
|
|
22
|
+
token: params.token,
|
|
23
|
+
debug: false,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
this.teamId = params.teamId;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected async request(path: string, params?: RequestParams) {
|
|
30
|
+
return super.request(path, {
|
|
31
|
+
...params,
|
|
32
|
+
query: { teamId: this.teamId, ...params?.query },
|
|
33
|
+
headers: {
|
|
34
|
+
"content-type": "application/json",
|
|
35
|
+
...params?.headers,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async createSandbox(params: {
|
|
41
|
+
source: { type: "git"; url: string };
|
|
42
|
+
ports: number[];
|
|
43
|
+
timeout?: number;
|
|
44
|
+
}) {
|
|
45
|
+
return parseOrThrow(
|
|
46
|
+
CreatedSandbox,
|
|
47
|
+
await this.request("/v1/sandboxes", {
|
|
48
|
+
method: "POST",
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
ports: params.ports,
|
|
51
|
+
source: params.source,
|
|
52
|
+
timeout: params.timeout,
|
|
53
|
+
}),
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async runCommand(params: {
|
|
59
|
+
sandboxId: string;
|
|
60
|
+
cwd?: string;
|
|
61
|
+
command: string;
|
|
62
|
+
args: string[];
|
|
63
|
+
}) {
|
|
64
|
+
return parseOrThrow(
|
|
65
|
+
CreatedCommand,
|
|
66
|
+
await this.request(`/v1/sandboxes/${params.sandboxId}/cmd`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
command: params.command,
|
|
70
|
+
args: params.args,
|
|
71
|
+
cwd: params.cwd,
|
|
72
|
+
}),
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getCommand(params: {
|
|
78
|
+
sandboxId: string;
|
|
79
|
+
cmdId: string;
|
|
80
|
+
wait?: boolean;
|
|
81
|
+
}) {
|
|
82
|
+
return parseOrThrow(
|
|
83
|
+
Command,
|
|
84
|
+
await this.request(
|
|
85
|
+
`/v1/sandboxes/${params.sandboxId}/cmd/${params.cmdId}`,
|
|
86
|
+
{ query: { wait: params.wait ? "true" : undefined } },
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async writeFiles(params: {
|
|
92
|
+
sandboxId: string;
|
|
93
|
+
files: { path: string; stream: Readable | Buffer }[];
|
|
94
|
+
}) {
|
|
95
|
+
const formData = new FormData();
|
|
96
|
+
|
|
97
|
+
for (const file of params.files) {
|
|
98
|
+
formData.append(file.path, file.stream, file.path);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await parseOrThrow(
|
|
102
|
+
WrittenFile,
|
|
103
|
+
await this.request(`/v1/sandboxes/${params.sandboxId}/fs/write`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { ...formData.getHeaders() },
|
|
106
|
+
body: formData,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async readFile(params: {
|
|
112
|
+
sandboxId: string;
|
|
113
|
+
path: string;
|
|
114
|
+
cwd?: string;
|
|
115
|
+
}): Promise<NodeJS.ReadableStream | null> {
|
|
116
|
+
const response = await this.request(
|
|
117
|
+
`/v1/sandboxes/${params.sandboxId}/fs/read`,
|
|
118
|
+
{
|
|
119
|
+
method: "POST",
|
|
120
|
+
body: JSON.stringify({ path: params.path, cwd: params.cwd }),
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (response.status === 404) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return response.body;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getLogs(params: { sandboxId: string; cmdId: string }) {
|
|
132
|
+
const deferred = createDeferredGenerator<z.infer<typeof LogLine>, void>();
|
|
133
|
+
|
|
134
|
+
(async () => {
|
|
135
|
+
const response = await this.request(
|
|
136
|
+
`/v1/sandboxes/${params.sandboxId}/cmd/${params.cmdId}/logs`,
|
|
137
|
+
{ method: "GET" },
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (response.headers.get("content-type") !== "application/x-ndjson") {
|
|
141
|
+
throw new APIError(response, {
|
|
142
|
+
message: "Expected a stream of logs",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const parser = jsonlines.parse();
|
|
147
|
+
response.body.pipe(parser);
|
|
148
|
+
|
|
149
|
+
parser.on("data", (data) => {
|
|
150
|
+
const parsed = LogLine.safeParse(data);
|
|
151
|
+
if (parsed.success) {
|
|
152
|
+
deferred.next({
|
|
153
|
+
value: parsed.data,
|
|
154
|
+
done: false,
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
deferred.next({
|
|
158
|
+
value: Promise.reject(parsed.error),
|
|
159
|
+
done: false,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
parser.on("error", (err) => {
|
|
165
|
+
deferred.next({
|
|
166
|
+
value: Promise.reject(err),
|
|
167
|
+
done: false,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
parser.on("end", () => {
|
|
172
|
+
deferred.next({
|
|
173
|
+
value: undefined,
|
|
174
|
+
done: true,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
})();
|
|
178
|
+
|
|
179
|
+
return deferred.generator();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const CreatedSandbox = z.object({
|
|
4
|
+
sandboxId: z.string(),
|
|
5
|
+
routes: z.array(z.object({ subdomain: z.string(), port: z.number() })),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const CreatedCommand = z.object({
|
|
9
|
+
cmdId: z.string(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const Command = z.object({
|
|
13
|
+
args: z.array(z.string()),
|
|
14
|
+
cmdId: z.string(),
|
|
15
|
+
cwd: z.string(),
|
|
16
|
+
exitCode: z.number().nullable(),
|
|
17
|
+
name: z.string(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const FinishedCommand = z.object({
|
|
21
|
+
cmdId: z.string(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const WrittenFile = z.object({});
|
|
25
|
+
|
|
26
|
+
export const LogLine = z.object({
|
|
27
|
+
stream: z.enum(["stdout", "stderr"]),
|
|
28
|
+
data: z.string(),
|
|
29
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { RequestInit, Response } from "node-fetch";
|
|
2
|
+
import type { Options as RetryOptions } from "async-retry";
|
|
3
|
+
import { APIError } from "./api-error";
|
|
4
|
+
import { setTimeout } from "timers/promises";
|
|
5
|
+
import retry from "async-retry";
|
|
6
|
+
|
|
7
|
+
export interface RequestOptions {
|
|
8
|
+
onRetry?(error: any, options: RequestOptions): void;
|
|
9
|
+
retry?: Partial<RetryOptions>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wraps a fetch function with retry logic. The retry logic will retry
|
|
14
|
+
* on network errors, 429 responses and 5xx responses. The retry logic
|
|
15
|
+
* will not retry on 4xx responses.
|
|
16
|
+
*
|
|
17
|
+
* @param rawFetch The fetch function to wrap.
|
|
18
|
+
* @returns The wrapped fetch function.
|
|
19
|
+
*/
|
|
20
|
+
export function withRetry<T extends RequestInit>(
|
|
21
|
+
rawFetch: (url: URL | string, init?: T) => Promise<Response>,
|
|
22
|
+
) {
|
|
23
|
+
return async (
|
|
24
|
+
url: URL | string,
|
|
25
|
+
opts: T & RequestOptions = <T & RequestOptions>{},
|
|
26
|
+
) => {
|
|
27
|
+
/**
|
|
28
|
+
* Timeouts by default will be [10, 60, 360, 2160, 12960]
|
|
29
|
+
* before randomization is added.
|
|
30
|
+
*/
|
|
31
|
+
const retryOpts = Object.assign(
|
|
32
|
+
{
|
|
33
|
+
minTimeout: 10,
|
|
34
|
+
retries: 5,
|
|
35
|
+
factor: 6,
|
|
36
|
+
maxRetryAfter: 20,
|
|
37
|
+
},
|
|
38
|
+
opts.retry,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (opts.onRetry) {
|
|
42
|
+
retryOpts.onRetry = (error, attempts) => {
|
|
43
|
+
opts.onRetry!(error, opts);
|
|
44
|
+
if (opts.retry && opts.retry.onRetry) {
|
|
45
|
+
opts.retry.onRetry(error, attempts);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
return (await retry(async (bail) => {
|
|
52
|
+
try {
|
|
53
|
+
const response = await rawFetch(url, opts);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* When the response is 429 we will try to parse the Retry-After
|
|
57
|
+
* header. If the header exists we will try to parse it and, if
|
|
58
|
+
* the wait time is higher than the maximum defined, we respond.
|
|
59
|
+
* Otherwise we wait for the time given in the header and throw
|
|
60
|
+
* to retry.
|
|
61
|
+
*/
|
|
62
|
+
if (response.status === 429) {
|
|
63
|
+
const retryAfter = parseInt(
|
|
64
|
+
response.headers.get("retry-after") || "",
|
|
65
|
+
10,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (retryAfter && !isNaN(retryAfter)) {
|
|
69
|
+
if (retryAfter > retryOpts.maxRetryAfter) {
|
|
70
|
+
return response;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await setTimeout(retryAfter * 1e3);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new APIError(response);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* If the response is a a retryable error, we throw in
|
|
81
|
+
* order to retry.
|
|
82
|
+
*/
|
|
83
|
+
if (response.status >= 500 && response.status < 600) {
|
|
84
|
+
throw new APIError(response);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return response;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
/**
|
|
90
|
+
* If the request was aborted using the AbortController
|
|
91
|
+
* we bail from retrying throwing the original error.
|
|
92
|
+
*/
|
|
93
|
+
if (isAbortError(error)) {
|
|
94
|
+
return bail(error);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}, retryOpts)) as Response;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
/**
|
|
102
|
+
* The ResponseError is only intended for retries so in case we
|
|
103
|
+
* ran out of attempts we will respond with the last response
|
|
104
|
+
* we obtained.
|
|
105
|
+
*/
|
|
106
|
+
if (error instanceof APIError) {
|
|
107
|
+
return error.response;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isAbortError(error: unknown): error is Error {
|
|
116
|
+
return (
|
|
117
|
+
error !== undefined &&
|
|
118
|
+
error !== null &&
|
|
119
|
+
(error as Error).name === "AbortError"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { SandboxClient } from "./client/client";
|
|
2
|
+
import { Readable } from "stream";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a new instance of the SandboxSDK.
|
|
6
|
+
*
|
|
7
|
+
* ````
|
|
8
|
+
* const teamId = process.env.VERCEL_TEAM_ID
|
|
9
|
+
* const token = process.env.VERCEL_TOKEN
|
|
10
|
+
* const sdk = new SandboxSDK({ teamId: teamId!, token: token! });
|
|
11
|
+
* ````
|
|
12
|
+
*
|
|
13
|
+
* @see {@link createSandbox} to start a sandbox.
|
|
14
|
+
*/
|
|
15
|
+
export class SandboxSDK {
|
|
16
|
+
public client: SandboxClient;
|
|
17
|
+
|
|
18
|
+
constructor({ teamId, token }: { teamId: string; token: string }) {
|
|
19
|
+
this.client = new SandboxClient({ teamId, token });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a new Sandbox with the given resources, source code, and ports.
|
|
24
|
+
*
|
|
25
|
+
* Upon start, the sandbox will perform a `git clone` of the repository given.
|
|
26
|
+
* This repo needs to be public.
|
|
27
|
+
*
|
|
28
|
+
* @param params
|
|
29
|
+
* @returns a
|
|
30
|
+
*/
|
|
31
|
+
async createSandbox(params: {
|
|
32
|
+
source: { type: "git"; url: string };
|
|
33
|
+
ports: number[];
|
|
34
|
+
timeout?: number;
|
|
35
|
+
}) {
|
|
36
|
+
const { client } = this;
|
|
37
|
+
const sandbox = await client.createSandbox({
|
|
38
|
+
source: params.source,
|
|
39
|
+
ports: params.ports,
|
|
40
|
+
timeout: params.timeout,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return new Sandbox({
|
|
44
|
+
client,
|
|
45
|
+
sandboxId: sandbox.json.sandboxId,
|
|
46
|
+
routes: sandbox.json.routes,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @hidden */
|
|
51
|
+
async getSandbox({
|
|
52
|
+
routes,
|
|
53
|
+
sandboxId,
|
|
54
|
+
}: {
|
|
55
|
+
routes: { subdomain: string; port: number }[];
|
|
56
|
+
sandboxId: string;
|
|
57
|
+
}) {
|
|
58
|
+
return new Sandbox({
|
|
59
|
+
client: this.client,
|
|
60
|
+
sandboxId: sandboxId,
|
|
61
|
+
routes: routes,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A Sandbox is an isolated Linux MicroVM that you can your experiments on.
|
|
68
|
+
*
|
|
69
|
+
* @see {@link SandboxSDK.createSandbox} to construct a Sandbox.
|
|
70
|
+
* @hideconstructor
|
|
71
|
+
*/
|
|
72
|
+
export class Sandbox {
|
|
73
|
+
private client: SandboxClient;
|
|
74
|
+
|
|
75
|
+
/** @hidden */
|
|
76
|
+
public routes: { subdomain: string; port: number }[];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The ID of this sandbox.
|
|
80
|
+
*/
|
|
81
|
+
public sandboxId: string;
|
|
82
|
+
|
|
83
|
+
constructor({
|
|
84
|
+
client,
|
|
85
|
+
routes,
|
|
86
|
+
sandboxId,
|
|
87
|
+
}: {
|
|
88
|
+
client: SandboxClient;
|
|
89
|
+
routes: { subdomain: string; port: number }[];
|
|
90
|
+
sandboxId: string;
|
|
91
|
+
}) {
|
|
92
|
+
this.client = client;
|
|
93
|
+
this.routes = routes;
|
|
94
|
+
this.sandboxId = sandboxId;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Start executing a command in this sandbox.
|
|
99
|
+
* @param command
|
|
100
|
+
* @param args
|
|
101
|
+
* @returns
|
|
102
|
+
*/
|
|
103
|
+
async runCommand(command: string, args: string[] = []) {
|
|
104
|
+
const commandResponse = await this.client.runCommand({
|
|
105
|
+
sandboxId: this.sandboxId,
|
|
106
|
+
command,
|
|
107
|
+
args,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return new Command({
|
|
111
|
+
client: this.client,
|
|
112
|
+
sandboxId: this.sandboxId,
|
|
113
|
+
cmdId: commandResponse.json.cmdId,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Write files to the filesystem of this sandbox.
|
|
119
|
+
*/
|
|
120
|
+
async writeFiles(files: { path: string; stream: Readable | Buffer }[]) {
|
|
121
|
+
return this.client.writeFiles({
|
|
122
|
+
sandboxId: this.sandboxId,
|
|
123
|
+
files: files,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get the public domain of a port of this sandbox.
|
|
129
|
+
*
|
|
130
|
+
* E.g. `2grza2l7imxe.vercel.run`
|
|
131
|
+
*/
|
|
132
|
+
domain(p: number): string {
|
|
133
|
+
const route = this.routes.find(({ port }) => port == p);
|
|
134
|
+
if (route) {
|
|
135
|
+
return `https://${route.subdomain}.vercel.run`;
|
|
136
|
+
} else {
|
|
137
|
+
throw new Error(`No route for port ${p}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A command executed in a Sandbox.
|
|
144
|
+
*
|
|
145
|
+
* You can {@link wait} on commands to access their {@link exitCode}, and
|
|
146
|
+
* iterate over their output with {@link logs}.
|
|
147
|
+
*
|
|
148
|
+
* @see {@link Sandbox.runCommand} to start a command.
|
|
149
|
+
*
|
|
150
|
+
* @hideconstructor
|
|
151
|
+
*/
|
|
152
|
+
export class Command {
|
|
153
|
+
private client: SandboxClient;
|
|
154
|
+
private sandboxId: string;
|
|
155
|
+
/**
|
|
156
|
+
* ID of the command execution.
|
|
157
|
+
*/
|
|
158
|
+
public cmdId: string;
|
|
159
|
+
/**
|
|
160
|
+
* The exit code of the command, if available. This is set after
|
|
161
|
+
* {@link wait} has returned.
|
|
162
|
+
*/
|
|
163
|
+
public exitCode: number | null;
|
|
164
|
+
|
|
165
|
+
constructor({
|
|
166
|
+
client,
|
|
167
|
+
sandboxId,
|
|
168
|
+
cmdId,
|
|
169
|
+
}: {
|
|
170
|
+
client: SandboxClient;
|
|
171
|
+
sandboxId: string;
|
|
172
|
+
cmdId: string;
|
|
173
|
+
}) {
|
|
174
|
+
this.client = client;
|
|
175
|
+
this.sandboxId = sandboxId;
|
|
176
|
+
this.cmdId = cmdId;
|
|
177
|
+
this.exitCode = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Iterate over the output of this command.
|
|
182
|
+
*
|
|
183
|
+
* ```
|
|
184
|
+
* for await (const log of cmd.logs()) {
|
|
185
|
+
* if (log.stream === "stdout") {
|
|
186
|
+
* process.stdout.write(log.data);
|
|
187
|
+
* } else {
|
|
188
|
+
* process.stderr.write(log.data);
|
|
189
|
+
* }
|
|
190
|
+
* }
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* @see {@link Command.stdout}, {@link Command.stderr}, and {@link Command.output}
|
|
194
|
+
* to access output as a string.
|
|
195
|
+
*/
|
|
196
|
+
logs() {
|
|
197
|
+
return this.client.getLogs({
|
|
198
|
+
sandboxId: this.sandboxId,
|
|
199
|
+
cmdId: this.cmdId,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Wait for a command to exit and populate it's exit code.
|
|
205
|
+
*
|
|
206
|
+
* ```
|
|
207
|
+
* await cmd.wait()
|
|
208
|
+
* if (cmd.exitCode != 0) {
|
|
209
|
+
* console.error("Something went wrong...")
|
|
210
|
+
* }
|
|
211
|
+
* ````
|
|
212
|
+
*/
|
|
213
|
+
async wait() {
|
|
214
|
+
const command = await this.client.getCommand({
|
|
215
|
+
sandboxId: this.sandboxId,
|
|
216
|
+
cmdId: this.cmdId,
|
|
217
|
+
wait: true,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
this.exitCode = command.json.exitCode;
|
|
221
|
+
return this;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Print command logs to stdout/stderr
|
|
226
|
+
*/
|
|
227
|
+
async printLogs() {
|
|
228
|
+
for await (const log of this.logs()) {
|
|
229
|
+
if (log.stream === "stdout") {
|
|
230
|
+
process.stdout.write(log.data);
|
|
231
|
+
} else {
|
|
232
|
+
process.stderr.write(log.data);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get the output of `stdout` or `stderr` as a string.
|
|
239
|
+
*
|
|
240
|
+
* NOTE: This may error with string conversion errors if the command does
|
|
241
|
+
* not ouptut valid unicode.
|
|
242
|
+
*/
|
|
243
|
+
async output(stream: "stdout" | "stderr" | "both" = "both") {
|
|
244
|
+
let data = "";
|
|
245
|
+
for await (const log of this.logs()) {
|
|
246
|
+
if (log.stream === stream) {
|
|
247
|
+
data += log.data;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return data;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get the output of `stdout` as a string.
|
|
255
|
+
*
|
|
256
|
+
* NOTE: This may error with string conversion errors if the command does
|
|
257
|
+
* not ouptut valid unicode.
|
|
258
|
+
*/
|
|
259
|
+
async stdout() {
|
|
260
|
+
return this.output("stdout");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get the output of `stderr` as a string.
|
|
265
|
+
*
|
|
266
|
+
* NOTE: This may error with string conversion errors if the command does
|
|
267
|
+
* not ouptut valid unicode.
|
|
268
|
+
*/
|
|
269
|
+
async stderr() {
|
|
270
|
+
return this.output("stderr");
|
|
271
|
+
}
|
|
272
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns an array from the given item. If the item is an array it will be
|
|
3
|
+
* returned as a it is, otherwise it will be returned as a single item array.
|
|
4
|
+
* If the item is undefined or null an empty array will be returned.
|
|
5
|
+
*
|
|
6
|
+
* @param item The item to convert to an array.
|
|
7
|
+
* @returns An array.
|
|
8
|
+
*/
|
|
9
|
+
export function array<T>(item?: null | T | T[]): T[] {
|
|
10
|
+
return item !== undefined && item !== null
|
|
11
|
+
? Array.isArray(item)
|
|
12
|
+
? item
|
|
13
|
+
: [item]
|
|
14
|
+
: [];
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Deferred } from "./deferred";
|
|
2
|
+
|
|
3
|
+
export interface DeferredGenerator<T, R> {
|
|
4
|
+
generator(): AsyncGenerator<T, R, void>;
|
|
5
|
+
next: (value: IteratorResult<T | Promise<T>>) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createDeferredGenerator<T, R>(): DeferredGenerator<T, R> {
|
|
9
|
+
const deferreds = [new Deferred<IteratorResult<Promise<T> | T>>()];
|
|
10
|
+
let currentIndex = 0;
|
|
11
|
+
|
|
12
|
+
const next = (value: IteratorResult<T | Promise<T>, R>) => {
|
|
13
|
+
const currentDeferred = deferreds[currentIndex];
|
|
14
|
+
if (!value.done) {
|
|
15
|
+
deferreds.push(new Deferred<IteratorResult<Promise<T> | T>>());
|
|
16
|
+
currentIndex++;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
currentDeferred.resolve(value);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function generator(): AsyncGenerator<T, R> {
|
|
23
|
+
let currentIndex = 0;
|
|
24
|
+
return (async function* () {
|
|
25
|
+
while (true) {
|
|
26
|
+
const result = await deferreds[currentIndex].promise;
|
|
27
|
+
if (result.done) return result.value;
|
|
28
|
+
yield result.value;
|
|
29
|
+
currentIndex++;
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
generator,
|
|
36
|
+
next,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class Deferred<T> {
|
|
2
|
+
promise: Promise<T>;
|
|
3
|
+
resolve!: (value: T | PromiseLike<T>) => void;
|
|
4
|
+
reject!: (reason?: any) => void;
|
|
5
|
+
|
|
6
|
+
constructor() {
|
|
7
|
+
this.promise = new Promise((res, rej) => {
|
|
8
|
+
this.resolve = res;
|
|
9
|
+
this.reject = rej;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
package/tsconfig.json
ADDED