self-bench 0.3.3 → 0.3.4
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/Dockerfile.sandbox +1 -1
- package/README.md +20 -0
- package/bun.lock +48 -0
- package/compose.yaml +7 -1
- package/dist/agent-smoke-main.js +3 -2
- package/dist/agent-smoke-main.js.map +1 -1
- package/dist/agent-smoke.d.ts +2 -1
- package/dist/agent-smoke.d.ts.map +1 -1
- package/dist/agent-smoke.js +6 -1
- package/dist/agent-smoke.js.map +1 -1
- package/dist/api.d.ts +24 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +25 -18
- package/dist/api.js.map +1 -1
- package/dist/build-metadata.js +1 -1
- package/dist/cli.js +67 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +36 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +89 -15
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +22 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +31 -6
- package/dist/contracts.js.map +1 -1
- package/dist/eval-main.js +3 -2
- package/dist/eval-main.js.map +1 -1
- package/dist/evaluate.d.ts +2 -1
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +2 -1
- package/dist/evaluate.js.map +1 -1
- package/dist/harbor-environment.d.ts +2 -0
- package/dist/harbor-environment.d.ts.map +1 -0
- package/dist/harbor-environment.js +15 -0
- package/dist/harbor-environment.js.map +1 -0
- package/dist/harbor-results.d.ts.map +1 -1
- package/dist/harbor-results.js +2 -0
- package/dist/harbor-results.js.map +1 -1
- package/dist/process.d.ts +2 -1
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js.map +1 -1
- package/dist/providers.d.ts +8 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +14 -0
- package/dist/providers.js.map +1 -0
- package/dist/reaudit-main.js +5 -5
- package/dist/reaudit-main.js.map +1 -1
- package/dist/repair-main.js +5 -5
- package/dist/repair-main.js.map +1 -1
- package/dist/sandbox-author.bundle.js +40 -6
- package/dist/sandbox-timeout.d.ts +11 -0
- package/dist/sandbox-timeout.d.ts.map +1 -0
- package/dist/sandbox-timeout.js +41 -0
- package/dist/sandbox-timeout.js.map +1 -0
- package/dist/sandbox-validation-repair.bundle.js +40 -6
- package/dist/sandbox.d.ts +2 -2
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +10 -3
- package/dist/sandbox.js.map +1 -1
- package/dist/temporal/activities.d.ts +2 -2
- package/dist/temporal/activities.d.ts.map +1 -1
- package/dist/temporal/activities.js +7 -1
- package/dist/temporal/activities.js.map +1 -1
- package/dist/temporal/worker-main.js +2 -2
- package/dist/temporal/worker-main.js.map +1 -1
- package/dist/terminal-prompts.d.ts +26 -0
- package/dist/terminal-prompts.d.ts.map +1 -0
- package/dist/terminal-prompts.js +137 -0
- package/dist/terminal-prompts.js.map +1 -0
- package/dist/terminal-reporter.d.ts +30 -0
- package/dist/terminal-reporter.d.ts.map +1 -0
- package/dist/terminal-reporter.js +90 -0
- package/dist/terminal-reporter.js.map +1 -0
- package/dist/validate-main.js +8 -2
- package/dist/validate-main.js.map +1 -1
- package/dist/vercel-cli.d.ts +64 -0
- package/dist/vercel-cli.d.ts.map +1 -0
- package/dist/vercel-cli.js +303 -0
- package/dist/vercel-cli.js.map +1 -0
- package/dist/vercel-command.d.ts +14 -0
- package/dist/vercel-command.d.ts.map +1 -0
- package/dist/vercel-command.js +93 -0
- package/dist/vercel-command.js.map +1 -0
- package/dist/vercel-executor.d.ts +14 -0
- package/dist/vercel-executor.d.ts.map +1 -0
- package/dist/vercel-executor.js +356 -0
- package/dist/vercel-executor.js.map +1 -0
- package/dist/vercel-fetch.d.ts +6 -0
- package/dist/vercel-fetch.d.ts.map +1 -0
- package/dist/vercel-fetch.js +39 -0
- package/dist/vercel-fetch.js.map +1 -0
- package/dist/vercel-profile.d.ts +34 -0
- package/dist/vercel-profile.d.ts.map +1 -0
- package/dist/vercel-profile.js +244 -0
- package/dist/vercel-profile.js.map +1 -0
- package/dist/vercel-runtime-image.d.ts +51 -0
- package/dist/vercel-runtime-image.d.ts.map +1 -0
- package/dist/vercel-runtime-image.js +123 -0
- package/dist/vercel-runtime-image.js.map +1 -0
- package/dist/vercel-setup-probe.d.ts +49 -0
- package/dist/vercel-setup-probe.d.ts.map +1 -0
- package/dist/vercel-setup-probe.js +151 -0
- package/dist/vercel-setup-probe.js.map +1 -0
- package/dist/vercel-setup.d.ts +42 -0
- package/dist/vercel-setup.d.ts.map +1 -0
- package/dist/vercel-setup.js +280 -0
- package/dist/vercel-setup.js.map +1 -0
- package/docs/evaluations.md +2 -0
- package/docs/operations.md +128 -6
- package/package.json +3 -1
- package/src/agent-smoke-main.ts +3 -2
- package/src/agent-smoke.ts +8 -2
- package/src/api.ts +31 -18
- package/src/cli.ts +72 -11
- package/src/config.ts +150 -25
- package/src/contracts.ts +33 -6
- package/src/eval-main.ts +3 -2
- package/src/evaluate.ts +5 -3
- package/src/harbor-environment.ts +17 -0
- package/src/harbor-results.ts +2 -0
- package/src/process.ts +3 -1
- package/src/providers.ts +22 -0
- package/src/reaudit-main.ts +8 -5
- package/src/repair-main.ts +8 -5
- package/src/sandbox-timeout.ts +56 -0
- package/src/sandbox.ts +15 -5
- package/src/temporal/activities.ts +9 -3
- package/src/temporal/worker-main.ts +2 -2
- package/src/terminal-prompts.ts +197 -0
- package/src/terminal-reporter.ts +126 -0
- package/src/validate-main.ts +9 -3
- package/src/vercel-cli.ts +403 -0
- package/src/vercel-command.ts +127 -0
- package/src/vercel-executor.ts +448 -0
- package/src/vercel-fetch.ts +45 -0
- package/src/vercel-profile.ts +319 -0
- package/src/vercel-runtime-image.ts +204 -0
- package/src/vercel-setup-probe.ts +229 -0
- package/src/vercel-setup.ts +419 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { type CommandOutputHandler, type CommandResult, runCommand } from "./process.js";
|
|
5
|
+
|
|
6
|
+
const MINIMUM_CLI_VERSION = [59, 1, 3] as const;
|
|
7
|
+
const PAGE_SIZE = "100";
|
|
8
|
+
|
|
9
|
+
const teamSchema = z.object({
|
|
10
|
+
id: z.string().min(1),
|
|
11
|
+
slug: z.string().min(1),
|
|
12
|
+
name: z.string().min(1),
|
|
13
|
+
current: z.boolean().optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const projectSchema = z.object({
|
|
17
|
+
id: z.string().min(1),
|
|
18
|
+
name: z.string().min(1),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const vcrTagSchema = z.object({
|
|
22
|
+
tag: z.string().min(1),
|
|
23
|
+
manifestDigest: z.string().regex(/^sha256:[0-9a-f]{64}$/i),
|
|
24
|
+
kind: z.enum(["manifest", "index"]).optional(),
|
|
25
|
+
status: z.string().min(1).nullable(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type VercelTeam = z.infer<typeof teamSchema>;
|
|
29
|
+
export type VercelProject = z.infer<typeof projectSchema>;
|
|
30
|
+
export type VcrTag = z.infer<typeof vcrTagSchema>;
|
|
31
|
+
|
|
32
|
+
export interface VercelCommandRunner {
|
|
33
|
+
capture(
|
|
34
|
+
args: readonly string[],
|
|
35
|
+
options?: { readonly onOutput?: CommandOutputHandler },
|
|
36
|
+
): Promise<CommandResult>;
|
|
37
|
+
interactive(args: readonly string[]): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class VercelCli {
|
|
41
|
+
readonly #runner: VercelCommandRunner;
|
|
42
|
+
|
|
43
|
+
constructor(runner: VercelCommandRunner = new ProcessVercelCommandRunner()) {
|
|
44
|
+
this.#runner = runner;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async ensureAvailable(): Promise<void> {
|
|
48
|
+
let result: CommandResult;
|
|
49
|
+
try {
|
|
50
|
+
result = await this.#runner.capture(["--version"]);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Vercel CLI 59.1.3 or newer is required. Install it with: npm install --global vercel@latest",
|
|
54
|
+
{ cause: error },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const version = parseCliVersion(`${result.stdout}\n${result.stderr}`);
|
|
58
|
+
if (result.exitCode !== 0 || !version || compareVersion(version, MINIMUM_CLI_VERSION) < 0) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Vercel CLI 59.1.3 or newer is required. Install or update it with: npm install --global vercel@latest",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async ensureLoggedIn(): Promise<void> {
|
|
66
|
+
const status = await this.#runner.capture(["whoami", "--json", "--non-interactive"]);
|
|
67
|
+
if (status.exitCode === 0 && parseLoggedIn(status.stdout)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await this.#runner.interactive(["login"]);
|
|
71
|
+
const verified = await this.#runner.capture(["whoami", "--json", "--non-interactive"]);
|
|
72
|
+
if (verified.exitCode !== 0 || !parseLoggedIn(verified.stdout)) {
|
|
73
|
+
throw new Error("Vercel CLI login did not complete; run vercel login and retry setup");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async listTeams(): Promise<readonly VercelTeam[]> {
|
|
78
|
+
const teams: VercelTeam[] = [];
|
|
79
|
+
const seenPages = new Set<string>();
|
|
80
|
+
let next: string | undefined;
|
|
81
|
+
do {
|
|
82
|
+
const pageKey = next ?? "first";
|
|
83
|
+
if (seenPages.has(pageKey)) {
|
|
84
|
+
throw new Error("Vercel CLI returned a repeated teams pagination cursor");
|
|
85
|
+
}
|
|
86
|
+
seenPages.add(pageKey);
|
|
87
|
+
const args = ["teams", "list", "--json", "--limit", PAGE_SIZE, "--non-interactive"];
|
|
88
|
+
if (next) {
|
|
89
|
+
args.push("--next", next);
|
|
90
|
+
}
|
|
91
|
+
const result = await this.#runJson(args);
|
|
92
|
+
const parsed = z
|
|
93
|
+
.object({
|
|
94
|
+
teams: z.array(teamSchema),
|
|
95
|
+
pagination: z.object({ next: z.union([z.string(), z.number()]).nullable().optional() }),
|
|
96
|
+
})
|
|
97
|
+
.parse(result);
|
|
98
|
+
teams.push(...parsed.teams);
|
|
99
|
+
next = paginationValue(parsed.pagination.next);
|
|
100
|
+
} while (next);
|
|
101
|
+
return uniqueById(teams);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async listProjects(teamSlug: string): Promise<readonly VercelProject[]> {
|
|
105
|
+
const projects: VercelProject[] = [];
|
|
106
|
+
const seenPages = new Set<string>();
|
|
107
|
+
let next: string | undefined;
|
|
108
|
+
do {
|
|
109
|
+
const pageKey = next ?? "first";
|
|
110
|
+
if (seenPages.has(pageKey)) {
|
|
111
|
+
throw new Error("Vercel CLI returned a repeated projects pagination cursor");
|
|
112
|
+
}
|
|
113
|
+
seenPages.add(pageKey);
|
|
114
|
+
const args = [
|
|
115
|
+
"project",
|
|
116
|
+
"list",
|
|
117
|
+
"--scope",
|
|
118
|
+
teamSlug,
|
|
119
|
+
"--json",
|
|
120
|
+
"--limit",
|
|
121
|
+
PAGE_SIZE,
|
|
122
|
+
"--non-interactive",
|
|
123
|
+
];
|
|
124
|
+
if (next) {
|
|
125
|
+
args.push("--next", next);
|
|
126
|
+
}
|
|
127
|
+
const result = await this.#runJson(args);
|
|
128
|
+
const parsed = z
|
|
129
|
+
.object({
|
|
130
|
+
projects: z.array(projectSchema),
|
|
131
|
+
pagination: z.object({ next: z.union([z.string(), z.number()]).nullable().optional() }),
|
|
132
|
+
})
|
|
133
|
+
.parse(result);
|
|
134
|
+
projects.push(...parsed.projects);
|
|
135
|
+
next = paginationValue(parsed.pagination.next);
|
|
136
|
+
} while (next);
|
|
137
|
+
return uniqueById(projects);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async createProject(teamSlug: string, projectName: string): Promise<VercelProject> {
|
|
141
|
+
const result = await this.#runner.capture([
|
|
142
|
+
"project",
|
|
143
|
+
"add",
|
|
144
|
+
projectName,
|
|
145
|
+
"--scope",
|
|
146
|
+
teamSlug,
|
|
147
|
+
"--non-interactive",
|
|
148
|
+
]);
|
|
149
|
+
if (result.exitCode !== 0) {
|
|
150
|
+
throw commandFailure("create Vercel project", result);
|
|
151
|
+
}
|
|
152
|
+
const project = (await this.listProjects(teamSlug)).find(
|
|
153
|
+
({ name }) => name.toLowerCase() === projectName.toLowerCase(),
|
|
154
|
+
);
|
|
155
|
+
if (!project) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Vercel created project ${projectName}, but it was not returned by project list`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return project;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async repositoryExists(input: VcrScope & { readonly repository: string }): Promise<boolean> {
|
|
164
|
+
const result = await this.#runner.capture(
|
|
165
|
+
this.#vcrArgs(
|
|
166
|
+
["vcr", "inspect", input.repository, "--json"],
|
|
167
|
+
input.teamSlug,
|
|
168
|
+
input.projectId,
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
if (result.exitCode === 0) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (isNotFound(result)) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
throw commandFailure("inspect VCR repository", result);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async createRepository(input: VcrScope & { readonly repository: string }): Promise<void> {
|
|
181
|
+
const result = await this.#runner.capture(
|
|
182
|
+
this.#vcrArgs(["vcr", "add", input.repository, "--json"], input.teamSlug, input.projectId),
|
|
183
|
+
);
|
|
184
|
+
if (result.exitCode !== 0) {
|
|
185
|
+
throw commandFailure("create VCR repository", result);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async listTags(input: VcrScope & { readonly repository: string }): Promise<readonly VcrTag[]> {
|
|
190
|
+
const tags: VcrTag[] = [];
|
|
191
|
+
const seenPages = new Set<string>();
|
|
192
|
+
let next: string | undefined;
|
|
193
|
+
do {
|
|
194
|
+
const pageKey = next ?? "first";
|
|
195
|
+
if (seenPages.has(pageKey)) {
|
|
196
|
+
throw new Error("Vercel CLI returned a repeated VCR pagination cursor");
|
|
197
|
+
}
|
|
198
|
+
seenPages.add(pageKey);
|
|
199
|
+
const args = this.#vcrArgs(
|
|
200
|
+
["vcr", "tag", "list", input.repository, "--json", "--limit", PAGE_SIZE],
|
|
201
|
+
input.teamSlug,
|
|
202
|
+
input.projectId,
|
|
203
|
+
);
|
|
204
|
+
if (next) {
|
|
205
|
+
args.push("--next", next);
|
|
206
|
+
}
|
|
207
|
+
const result = await this.#runJson(args);
|
|
208
|
+
const parsed = z
|
|
209
|
+
.object({
|
|
210
|
+
tags: z.array(vcrTagSchema),
|
|
211
|
+
nextCursor: z.string().nullable().optional(),
|
|
212
|
+
})
|
|
213
|
+
.parse(result);
|
|
214
|
+
tags.push(...parsed.tags);
|
|
215
|
+
next = parsed.nextCursor ?? undefined;
|
|
216
|
+
} while (next);
|
|
217
|
+
return tags;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async inspectTag(
|
|
221
|
+
input: VcrScope & { readonly repository: string; readonly tag: string },
|
|
222
|
+
): Promise<VcrTag | undefined> {
|
|
223
|
+
const result = await this.#runner.capture(
|
|
224
|
+
this.#vcrArgs(
|
|
225
|
+
["vcr", "tag", "inspect", input.repository, input.tag, "--json"],
|
|
226
|
+
input.teamSlug,
|
|
227
|
+
input.projectId,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
if (result.exitCode === 0) {
|
|
231
|
+
return vcrTagSchema.parse(parseJson(result.stdout, "VCR tag inspect"));
|
|
232
|
+
}
|
|
233
|
+
if (isNotFound(result)) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
throw commandFailure("inspect VCR tag", result);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async buildImage(
|
|
240
|
+
input: VcrScope & {
|
|
241
|
+
readonly repository: string;
|
|
242
|
+
readonly tag: string;
|
|
243
|
+
readonly projectRoot: string;
|
|
244
|
+
readonly onOutput?: CommandOutputHandler;
|
|
245
|
+
},
|
|
246
|
+
): Promise<void> {
|
|
247
|
+
await this.#run(
|
|
248
|
+
[
|
|
249
|
+
"vcr",
|
|
250
|
+
"login",
|
|
251
|
+
"docker",
|
|
252
|
+
"--project",
|
|
253
|
+
input.projectId,
|
|
254
|
+
"--scope",
|
|
255
|
+
input.teamSlug,
|
|
256
|
+
"--non-interactive",
|
|
257
|
+
],
|
|
258
|
+
"authenticate Docker with VCR",
|
|
259
|
+
input.onOutput,
|
|
260
|
+
);
|
|
261
|
+
await this.#run(
|
|
262
|
+
[
|
|
263
|
+
"vcr",
|
|
264
|
+
"build",
|
|
265
|
+
"docker",
|
|
266
|
+
input.projectRoot,
|
|
267
|
+
`${input.repository}:${input.tag}`,
|
|
268
|
+
"--project",
|
|
269
|
+
input.projectId,
|
|
270
|
+
"--platform",
|
|
271
|
+
"linux/amd64",
|
|
272
|
+
"--push",
|
|
273
|
+
"--scope",
|
|
274
|
+
input.teamSlug,
|
|
275
|
+
"--non-interactive",
|
|
276
|
+
"--",
|
|
277
|
+
"--file",
|
|
278
|
+
resolve(input.projectRoot, "Dockerfile.sandbox"),
|
|
279
|
+
// Buildx provenance attestations turn this into an OCI index. Vercel
|
|
280
|
+
// Sandbox accepts the single linux/amd64 manifest produced without them.
|
|
281
|
+
"--provenance=false",
|
|
282
|
+
],
|
|
283
|
+
"build and publish the VCR image",
|
|
284
|
+
input.onOutput,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async #runJson(args: readonly string[]): Promise<unknown> {
|
|
289
|
+
const result = await this.#runner.capture(args);
|
|
290
|
+
if (result.exitCode !== 0) {
|
|
291
|
+
throw commandFailure(`run vercel ${args.slice(0, 2).join(" ")}`, result);
|
|
292
|
+
}
|
|
293
|
+
return parseJson(result.stdout, `vercel ${args.slice(0, 2).join(" ")}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async #run(
|
|
297
|
+
args: readonly string[],
|
|
298
|
+
action: string,
|
|
299
|
+
onOutput?: CommandOutputHandler,
|
|
300
|
+
): Promise<void> {
|
|
301
|
+
const result = await this.#runner.capture(args, onOutput ? { onOutput } : undefined);
|
|
302
|
+
if (result.exitCode !== 0) {
|
|
303
|
+
throw commandFailure(action, result);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#vcrArgs(args: string[], teamSlug: string, projectId: string): string[] {
|
|
308
|
+
return [...args, "--project", projectId, "--scope", teamSlug, "--non-interactive"];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
interface VcrScope {
|
|
313
|
+
readonly teamSlug: string;
|
|
314
|
+
readonly projectId: string;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
class ProcessVercelCommandRunner implements VercelCommandRunner {
|
|
318
|
+
async capture(
|
|
319
|
+
args: readonly string[],
|
|
320
|
+
options?: { readonly onOutput?: CommandOutputHandler },
|
|
321
|
+
): Promise<CommandResult> {
|
|
322
|
+
return await runCommand("vercel", args, {
|
|
323
|
+
allowFailure: true,
|
|
324
|
+
...(options?.onOutput ? { onOutput: options.onOutput } : {}),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async interactive(args: readonly string[]): Promise<void> {
|
|
329
|
+
await new Promise<void>((resolve, reject) => {
|
|
330
|
+
const child = spawn("vercel", args, { stdio: "inherit" });
|
|
331
|
+
child.once("error", reject);
|
|
332
|
+
child.once("exit", (code, signal) => {
|
|
333
|
+
if (code === 0) {
|
|
334
|
+
resolve();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
reject(
|
|
338
|
+
new Error(
|
|
339
|
+
`vercel ${args.slice(0, 2).join(" ")} exited ${code ?? `after ${signal ?? "signal"}`}`,
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function parseCliVersion(value: string): readonly [number, number, number] | undefined {
|
|
348
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(value);
|
|
349
|
+
if (!match?.[1] || !match[2] || !match[3]) {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function compareVersion(
|
|
356
|
+
left: readonly [number, number, number],
|
|
357
|
+
right: readonly [number, number, number],
|
|
358
|
+
): number {
|
|
359
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
360
|
+
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
361
|
+
if (difference !== 0) {
|
|
362
|
+
return difference;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function parseLoggedIn(value: string): boolean {
|
|
369
|
+
try {
|
|
370
|
+
const parsed = z
|
|
371
|
+
.object({ loggedIn: z.boolean().optional() })
|
|
372
|
+
.passthrough()
|
|
373
|
+
.parse(JSON.parse(value));
|
|
374
|
+
return parsed.loggedIn ?? true;
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function parseJson(value: string, context: string): unknown {
|
|
381
|
+
try {
|
|
382
|
+
return JSON.parse(value);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
throw new Error(`${context} returned invalid JSON`, { cause: error });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function paginationValue(value: string | number | null | undefined): string | undefined {
|
|
389
|
+
return value === undefined || value === null ? undefined : String(value);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function uniqueById<T extends { readonly id: string }>(values: readonly T[]): readonly T[] {
|
|
393
|
+
return [...new Map(values.map((value) => [value.id, value])).values()];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function isNotFound(result: CommandResult): boolean {
|
|
397
|
+
return /(?:not[_ -]?found|does not exist|\b404\b)/i.test(`${result.stdout}\n${result.stderr}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function commandFailure(action: string, result: CommandResult): Error {
|
|
401
|
+
const detail = (result.stderr.trim() || result.stdout.trim()).slice(0, 1_000);
|
|
402
|
+
return new Error(`${action} failed with exit ${result.exitCode}${detail ? `: ${detail}` : ""}`);
|
|
403
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Command, Session } from "@vercel/sandbox";
|
|
2
|
+
import { InactivityTimeoutError, type RollingOutput } from "./process.js";
|
|
3
|
+
import type { SandboxRequest, SandboxResult, SandboxRunOptions } from "./sandbox.js";
|
|
4
|
+
import { VercelCommandStartError } from "./vercel-fetch.js";
|
|
5
|
+
|
|
6
|
+
export const VERCEL_WORK_DIRECTORY = "/work";
|
|
7
|
+
|
|
8
|
+
const COMPLETION_REQUEST_TIMEOUT_MS = 15_000;
|
|
9
|
+
|
|
10
|
+
export async function executeVercelCommand(input: {
|
|
11
|
+
readonly session: Session;
|
|
12
|
+
readonly request: SandboxRequest;
|
|
13
|
+
readonly options: SandboxRunOptions;
|
|
14
|
+
readonly signal: AbortSignal;
|
|
15
|
+
readonly terminate: (error: unknown) => void;
|
|
16
|
+
readonly stdout: RollingOutput;
|
|
17
|
+
readonly stderr: RollingOutput;
|
|
18
|
+
}): Promise<Omit<SandboxResult, "sandboxId">> {
|
|
19
|
+
const { session, request, options, signal, terminate, stdout, stderr } = input;
|
|
20
|
+
let inactivityTimer: ReturnType<typeof setTimeout> | undefined;
|
|
21
|
+
const clearInactivityTimer = (): void => {
|
|
22
|
+
if (inactivityTimer) {
|
|
23
|
+
clearTimeout(inactivityTimer);
|
|
24
|
+
inactivityTimer = undefined;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const armInactivityTimer = (): void => {
|
|
28
|
+
clearInactivityTimer();
|
|
29
|
+
if (!request.inactivityTimeoutMs) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
inactivityTimer = setTimeout(() => {
|
|
33
|
+
terminate(
|
|
34
|
+
new InactivityTimeoutError(
|
|
35
|
+
`Vercel sandbox ${session.sessionId} stage ${request.stage}`,
|
|
36
|
+
request.inactivityTimeoutMs ?? 0,
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
}, request.inactivityTimeoutMs);
|
|
40
|
+
inactivityTimer.unref();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const [commandName, ...commandArguments] = request.command;
|
|
45
|
+
if (!commandName) {
|
|
46
|
+
throw new Error("sandbox command must not be empty");
|
|
47
|
+
}
|
|
48
|
+
const command = await startCommand(session, commandName, commandArguments, request, signal);
|
|
49
|
+
|
|
50
|
+
armInactivityTimer();
|
|
51
|
+
await consumeLogs(command, stdout, stderr, options, armInactivityTimer, signal);
|
|
52
|
+
// The log stream closes when remote execution ends. From here on, the
|
|
53
|
+
// bounded completion request is provider settlement rather than command
|
|
54
|
+
// output inactivity.
|
|
55
|
+
clearInactivityTimer();
|
|
56
|
+
const completed = await command.wait({
|
|
57
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(COMPLETION_REQUEST_TIMEOUT_MS)]),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const outputs: Record<string, Uint8Array> = {};
|
|
61
|
+
for (const path of request.outputPaths ?? []) {
|
|
62
|
+
const output = await session.readFileToBuffer({ path }, { signal });
|
|
63
|
+
if (output === null) {
|
|
64
|
+
if (completed.exitCode === 0) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`sandbox ${session.sessionId} exited successfully without output ${path}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
outputs[path] = output;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
exitCode: completed.exitCode,
|
|
75
|
+
stdout: stdout.text(),
|
|
76
|
+
stderr: stderr.text(),
|
|
77
|
+
outputs,
|
|
78
|
+
};
|
|
79
|
+
} finally {
|
|
80
|
+
clearInactivityTimer();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function startCommand(
|
|
85
|
+
session: Session,
|
|
86
|
+
commandName: string,
|
|
87
|
+
commandArguments: readonly string[],
|
|
88
|
+
request: SandboxRequest,
|
|
89
|
+
signal: AbortSignal,
|
|
90
|
+
): Promise<Command> {
|
|
91
|
+
try {
|
|
92
|
+
return await session.runCommand({
|
|
93
|
+
cmd: commandName,
|
|
94
|
+
args: [...commandArguments],
|
|
95
|
+
cwd: VERCEL_WORK_DIRECTORY,
|
|
96
|
+
detached: true,
|
|
97
|
+
env: { ...request.environment, ...request.secrets },
|
|
98
|
+
signal,
|
|
99
|
+
timeoutMs: request.timeoutMs,
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error instanceof VercelCommandStartError) {
|
|
103
|
+
error.restorePublicName();
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function consumeLogs(
|
|
110
|
+
command: Command,
|
|
111
|
+
stdout: RollingOutput,
|
|
112
|
+
stderr: RollingOutput,
|
|
113
|
+
options: SandboxRunOptions,
|
|
114
|
+
onOutput: () => void,
|
|
115
|
+
signal: AbortSignal,
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
for await (const event of command.logs({ signal })) {
|
|
118
|
+
const chunk = Buffer.from(event.data);
|
|
119
|
+
if (chunk.byteLength === 0) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const output = event.stream === "stdout" ? stdout : stderr;
|
|
123
|
+
output.push(chunk);
|
|
124
|
+
options.onProgress?.({ stream: event.stream, bytes: chunk.byteLength });
|
|
125
|
+
onOutput();
|
|
126
|
+
}
|
|
127
|
+
}
|