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
package/src/repair-main.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
|
-
import {
|
|
8
|
+
import { defaultStandaloneConcurrency, loadWorkerConfig } from "./config.js";
|
|
9
9
|
import { parallelMap } from "./parallel.js";
|
|
10
10
|
import { runCommand } from "./process.js";
|
|
11
11
|
import { createSandboxExecutor } from "./sandbox.js";
|
|
@@ -17,7 +17,7 @@ const parsed = parseArgs({
|
|
|
17
17
|
review: { type: "string" },
|
|
18
18
|
output: { type: "string" },
|
|
19
19
|
task: { type: "string" },
|
|
20
|
-
concurrency: { type: "string"
|
|
20
|
+
concurrency: { type: "string" },
|
|
21
21
|
model: { type: "string", default: "gpt-5.6-sol" },
|
|
22
22
|
help: { type: "boolean", short: "h" },
|
|
23
23
|
},
|
|
@@ -30,7 +30,7 @@ Usage:
|
|
|
30
30
|
self-bench-repair --tasks DIRECTORY --review REPORT.json --output DIRECTORY [options]
|
|
31
31
|
|
|
32
32
|
Options:
|
|
33
|
-
--concurrency N Concurrent repair sandboxes (default: 9)
|
|
33
|
+
--concurrency N Concurrent repair sandboxes (default: 9; Vercel: worker setting, default 4)
|
|
34
34
|
--model MODEL Codex subscription model (default: gpt-5.6-sol)
|
|
35
35
|
--task ID Repair only one coupled task
|
|
36
36
|
-h, --help Show this help`);
|
|
@@ -43,7 +43,11 @@ const outputDirectory = resolve(parsed.values.output ?? fail("--output is requir
|
|
|
43
43
|
if (outputDirectory === tasksDirectory || outputDirectory.startsWith(`${tasksDirectory}/`)) {
|
|
44
44
|
throw new Error("--output must be outside --tasks");
|
|
45
45
|
}
|
|
46
|
-
const
|
|
46
|
+
const config = loadWorkerConfig();
|
|
47
|
+
const concurrency = positiveInteger(
|
|
48
|
+
parsed.values.concurrency ?? String(defaultStandaloneConcurrency(config, 9)),
|
|
49
|
+
"--concurrency",
|
|
50
|
+
);
|
|
47
51
|
const rawReview = JSON.parse(await readFile(reviewPath, "utf8")) as unknown;
|
|
48
52
|
const review = reviewReport(rawReview);
|
|
49
53
|
const coupled = new Map(
|
|
@@ -77,7 +81,6 @@ for (const source of sourceTasks) {
|
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
const config = loadConfig();
|
|
81
84
|
const sandbox = createSandboxExecutor(config.execution);
|
|
82
85
|
const [modelAuth, repairer] = await Promise.all([
|
|
83
86
|
loadCodexModelAuth(),
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SandboxExecutor,
|
|
3
|
+
SandboxRequest,
|
|
4
|
+
SandboxResult,
|
|
5
|
+
SandboxRunOptions,
|
|
6
|
+
} from "./sandbox.js";
|
|
7
|
+
|
|
8
|
+
export const STANDARD_VERCEL_TIMEOUT_CAP_MS = 2 * 60 * 60 * 1_000;
|
|
9
|
+
export const HOBBY_VERCEL_TIMEOUT_CAP_MS = 45 * 60 * 1_000;
|
|
10
|
+
|
|
11
|
+
export function parseSandboxTimeoutCapText(value: string): number | undefined {
|
|
12
|
+
const normalized = value.trim().toLowerCase();
|
|
13
|
+
if (normalized === "") {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const match = /^(\d+)(ms|s|m|h)?$/.exec(normalized);
|
|
17
|
+
if (!match?.[1]) {
|
|
18
|
+
return Number.NaN;
|
|
19
|
+
}
|
|
20
|
+
const amount = Number(match[1]);
|
|
21
|
+
const multiplier =
|
|
22
|
+
match[2] === "h"
|
|
23
|
+
? 60 * 60 * 1_000
|
|
24
|
+
: match[2] === "m"
|
|
25
|
+
? 60 * 1_000
|
|
26
|
+
: match[2] === "s"
|
|
27
|
+
? 1_000
|
|
28
|
+
: 1;
|
|
29
|
+
return amount * multiplier;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class TimeoutCappedSandboxExecutor implements SandboxExecutor {
|
|
33
|
+
readonly #delegate: SandboxExecutor;
|
|
34
|
+
readonly #timeoutCapMs: number;
|
|
35
|
+
|
|
36
|
+
constructor(delegate: SandboxExecutor, timeoutCapMs: number) {
|
|
37
|
+
if (!Number.isInteger(timeoutCapMs) || timeoutCapMs < 100) {
|
|
38
|
+
throw new Error("sandbox timeout cap must be an integer of at least 100ms");
|
|
39
|
+
}
|
|
40
|
+
this.#delegate = delegate;
|
|
41
|
+
this.#timeoutCapMs = timeoutCapMs;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async run(request: SandboxRequest, options?: SandboxRunOptions): Promise<SandboxResult> {
|
|
45
|
+
return await this.#delegate.run(
|
|
46
|
+
request.timeoutMs > this.#timeoutCapMs
|
|
47
|
+
? { ...request, timeoutMs: this.#timeoutCapMs }
|
|
48
|
+
: request,
|
|
49
|
+
options,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
close(): void {
|
|
54
|
+
this.#delegate.close();
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/sandbox.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SelfBenchWorkerConfig } from "./config.js";
|
|
2
2
|
import { DockerSandboxExecutor } from "./docker-executor.js";
|
|
3
3
|
import { ModalSandboxExecutor } from "./modal-executor.js";
|
|
4
|
+
import { TimeoutCappedSandboxExecutor } from "./sandbox-timeout.js";
|
|
5
|
+
import { VercelSandboxExecutor } from "./vercel-executor.js";
|
|
4
6
|
|
|
5
7
|
export interface SandboxFile {
|
|
6
8
|
readonly path: string;
|
|
@@ -44,8 +46,16 @@ export interface SandboxExecutor {
|
|
|
44
46
|
close(): void;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
export function createSandboxExecutor(config:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
export function createSandboxExecutor(config: SelfBenchWorkerConfig["execution"]): SandboxExecutor {
|
|
50
|
+
switch (config.kind) {
|
|
51
|
+
case "docker":
|
|
52
|
+
return new DockerSandboxExecutor(config);
|
|
53
|
+
case "modal":
|
|
54
|
+
return new ModalSandboxExecutor(config);
|
|
55
|
+
case "vercel":
|
|
56
|
+
return new TimeoutCappedSandboxExecutor(
|
|
57
|
+
new VercelSandboxExecutor(config),
|
|
58
|
+
config.timeoutCapMs,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
51
61
|
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
couplingReviewInput,
|
|
13
13
|
couplingReviewSchema,
|
|
14
14
|
} from "../codex-review.js";
|
|
15
|
-
import type { SelfBenchConfig } from "../config.js";
|
|
15
|
+
import type { SelfBenchConfig, SelfBenchWorkerConfig } from "../config.js";
|
|
16
16
|
import {
|
|
17
17
|
type ArtifactRef,
|
|
18
18
|
type AuditResult,
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
scanBaseContractArtifacts,
|
|
35
35
|
} from "../coupling.js";
|
|
36
36
|
import { assertPullRequestBelongsToRepository } from "../github.js";
|
|
37
|
+
import { harborChildEnvironment } from "../harbor-environment.js";
|
|
37
38
|
import {
|
|
38
39
|
type HarborJobResult,
|
|
39
40
|
harborInfrastructureError,
|
|
@@ -114,7 +115,7 @@ export interface SelfBenchActivities {
|
|
|
114
115
|
buildExport(input: ExportInput): Promise<ArtifactRef>;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
export function createActivities(config:
|
|
118
|
+
export function createActivities(config: SelfBenchWorkerConfig): SelfBenchActivities {
|
|
118
119
|
const store = createArtifactStore(config.artifact);
|
|
119
120
|
const sandbox = createSandboxExecutor(config.execution);
|
|
120
121
|
return {
|
|
@@ -1054,7 +1055,12 @@ async function runHarborGate(
|
|
|
1054
1055
|
"--yes",
|
|
1055
1056
|
"--quiet",
|
|
1056
1057
|
],
|
|
1057
|
-
{
|
|
1058
|
+
{
|
|
1059
|
+
allowFailure: true,
|
|
1060
|
+
env: harborChildEnvironment(),
|
|
1061
|
+
timeoutMs: 3 * 60 * 60 * 1000,
|
|
1062
|
+
signal,
|
|
1063
|
+
},
|
|
1058
1064
|
);
|
|
1059
1065
|
if (result.exitCode !== 0) {
|
|
1060
1066
|
throw ApplicationFailure.create({
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import { Worker } from "@temporalio/worker";
|
|
3
|
-
import {
|
|
3
|
+
import { loadWorkerConfig } from "../config.js";
|
|
4
4
|
import { removeEmptyModalCredentialOverrides } from "../modal-auth.js";
|
|
5
5
|
import { runCommand } from "../process.js";
|
|
6
6
|
import { createActivities } from "./activities.js";
|
|
7
7
|
import { connectTemporalWorker } from "./connection.js";
|
|
8
8
|
|
|
9
9
|
removeEmptyModalCredentialOverrides();
|
|
10
|
-
const config =
|
|
10
|
+
const config = loadWorkerConfig();
|
|
11
11
|
if (config.execution.kind === "docker" || config.harborEnvironment === "docker") {
|
|
12
12
|
await runCommand("docker", ["info"], { timeoutMs: 30_000 });
|
|
13
13
|
await runCommand("docker", ["compose", "version"], { timeoutMs: 30_000 });
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
autocomplete,
|
|
3
|
+
cancel as cancelPrompt,
|
|
4
|
+
confirm as confirmPrompt,
|
|
5
|
+
isCancel,
|
|
6
|
+
password,
|
|
7
|
+
select as selectPrompt,
|
|
8
|
+
text as textPrompt,
|
|
9
|
+
} from "@clack/prompts";
|
|
10
|
+
|
|
11
|
+
export class SetupCanceledError extends Error {
|
|
12
|
+
constructor(message = "Setup canceled") {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "SetupCanceledError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PromptChoice {
|
|
19
|
+
readonly value: string;
|
|
20
|
+
readonly label: string;
|
|
21
|
+
readonly hint?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SetupPrompter {
|
|
25
|
+
select(message: string, choices: readonly PromptChoice[], defaultValue?: string): Promise<string>;
|
|
26
|
+
search(message: string, choices: readonly PromptChoice[], defaultValue?: string): Promise<string>;
|
|
27
|
+
text(message: string, defaultValue?: string): Promise<string>;
|
|
28
|
+
secret(message: string): Promise<string>;
|
|
29
|
+
confirm(message: string, defaultValue: boolean): Promise<boolean>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class TerminalPrompter implements SetupPrompter {
|
|
33
|
+
readonly #input: NodeJS.ReadStream;
|
|
34
|
+
readonly #output: NodeJS.WriteStream;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
input: NodeJS.ReadStream = process.stdin,
|
|
38
|
+
output: NodeJS.WriteStream = process.stdout,
|
|
39
|
+
) {
|
|
40
|
+
this.#input = input;
|
|
41
|
+
this.#output = output;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async select(
|
|
45
|
+
message: string,
|
|
46
|
+
choices: readonly PromptChoice[],
|
|
47
|
+
defaultValue?: string,
|
|
48
|
+
): Promise<string> {
|
|
49
|
+
this.#assertChoices(message, choices);
|
|
50
|
+
return await this.#run((signal) =>
|
|
51
|
+
selectPrompt<string>({
|
|
52
|
+
message,
|
|
53
|
+
options: choices.map(toPromptOption),
|
|
54
|
+
maxItems: this.#visibleItemCount(),
|
|
55
|
+
showInstructions: true,
|
|
56
|
+
...(defaultValue !== undefined && choices.some(({ value }) => value === defaultValue)
|
|
57
|
+
? { initialValue: defaultValue }
|
|
58
|
+
: {}),
|
|
59
|
+
input: this.#input,
|
|
60
|
+
output: this.#output,
|
|
61
|
+
signal,
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async search(
|
|
67
|
+
message: string,
|
|
68
|
+
choices: readonly PromptChoice[],
|
|
69
|
+
defaultValue?: string,
|
|
70
|
+
): Promise<string> {
|
|
71
|
+
this.#assertChoices(message, choices);
|
|
72
|
+
return await this.#run((signal) =>
|
|
73
|
+
autocomplete<string>({
|
|
74
|
+
message,
|
|
75
|
+
options: choices.map(toPromptOption),
|
|
76
|
+
placeholder: "Type to filter…",
|
|
77
|
+
maxItems: this.#visibleItemCount(),
|
|
78
|
+
filter: (query, option) =>
|
|
79
|
+
`${option.label ?? ""}\n${option.hint ?? ""}\n${option.value}`
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.includes(query.toLowerCase()),
|
|
82
|
+
...(defaultValue !== undefined && choices.some(({ value }) => value === defaultValue)
|
|
83
|
+
? { initialValue: defaultValue }
|
|
84
|
+
: {}),
|
|
85
|
+
input: this.#input,
|
|
86
|
+
output: this.#output,
|
|
87
|
+
signal,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async text(message: string, defaultValue?: string): Promise<string> {
|
|
93
|
+
return await this.#run((signal) =>
|
|
94
|
+
textPrompt({
|
|
95
|
+
message,
|
|
96
|
+
...(defaultValue === undefined ? {} : { placeholder: defaultValue, defaultValue }),
|
|
97
|
+
input: this.#input,
|
|
98
|
+
output: this.#output,
|
|
99
|
+
signal,
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async secret(message: string): Promise<string> {
|
|
105
|
+
if (!this.#input.isTTY || !this.#input.setRawMode) {
|
|
106
|
+
throw new Error("secure token input requires an interactive terminal");
|
|
107
|
+
}
|
|
108
|
+
return await this.#run((signal) =>
|
|
109
|
+
password({
|
|
110
|
+
message,
|
|
111
|
+
// An empty mask keeps both the token and its length out of terminal output.
|
|
112
|
+
mask: "",
|
|
113
|
+
validate: (value) => (value?.trim() ? undefined : "Enter a project-scoped token."),
|
|
114
|
+
input: this.#input,
|
|
115
|
+
output: this.#output,
|
|
116
|
+
signal,
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async confirm(message: string, defaultValue: boolean): Promise<boolean> {
|
|
122
|
+
return await this.#run((signal) =>
|
|
123
|
+
confirmPrompt({
|
|
124
|
+
message,
|
|
125
|
+
initialValue: defaultValue,
|
|
126
|
+
input: this.#input,
|
|
127
|
+
output: this.#output,
|
|
128
|
+
signal,
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async #run<T>(prompt: (signal: AbortSignal) => Promise<T | symbol>): Promise<T> {
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
const wasRaw = this.#input.isRaw;
|
|
136
|
+
const wasFlowing = this.#input.readableFlowing;
|
|
137
|
+
let inputFailure: Error | undefined;
|
|
138
|
+
const abort = (error: Error): void => {
|
|
139
|
+
inputFailure = error;
|
|
140
|
+
controller.abort(error);
|
|
141
|
+
};
|
|
142
|
+
const onEnd = (): void => abort(new SetupCanceledError("Setup canceled at end of input"));
|
|
143
|
+
const onError = (error: Error): void => abort(error);
|
|
144
|
+
this.#input.once("end", onEnd);
|
|
145
|
+
this.#input.once("error", onError);
|
|
146
|
+
if (this.#input.readableEnded) {
|
|
147
|
+
onEnd();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const result = await prompt(controller.signal);
|
|
152
|
+
if (isCancel(result)) {
|
|
153
|
+
const error = inputFailure ?? new SetupCanceledError();
|
|
154
|
+
if (error instanceof SetupCanceledError) {
|
|
155
|
+
cancelPrompt("Setup canceled.", { output: this.#output, withGuide: true });
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
} finally {
|
|
161
|
+
this.#input.off("end", onEnd);
|
|
162
|
+
this.#input.off("error", onError);
|
|
163
|
+
if (this.#input.isTTY && this.#input.setRawMode && this.#input.isRaw !== wasRaw) {
|
|
164
|
+
this.#input.setRawMode(Boolean(wasRaw));
|
|
165
|
+
}
|
|
166
|
+
if (wasFlowing !== true) {
|
|
167
|
+
this.#input.pause();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#assertChoices(message: string, choices: readonly PromptChoice[]): void {
|
|
173
|
+
if (choices.length === 0) {
|
|
174
|
+
throw new Error(`${message} has no available choices`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#visibleItemCount(): number {
|
|
179
|
+
return Math.max(3, Math.min(8, (this.#output.rows ?? 20) - 6));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function isInteractiveTerminal(): boolean {
|
|
184
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function toPromptOption(choice: PromptChoice): {
|
|
188
|
+
readonly value: string;
|
|
189
|
+
readonly label: string;
|
|
190
|
+
readonly hint?: string;
|
|
191
|
+
} {
|
|
192
|
+
return {
|
|
193
|
+
value: choice.value,
|
|
194
|
+
label: choice.label,
|
|
195
|
+
...(choice.hint ? { hint: choice.hint } : {}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Writable } from "node:stream";
|
|
2
|
+
import { stripVTControlCharacters } from "node:util";
|
|
3
|
+
import { cancel as cancelPrompt, intro, log, outro, spinner } from "@clack/prompts";
|
|
4
|
+
import { type CommandOutputHandler, RollingOutput } from "./process.js";
|
|
5
|
+
import { SetupCanceledError } from "./terminal-prompts.js";
|
|
6
|
+
|
|
7
|
+
export interface SetupTaskLabels {
|
|
8
|
+
readonly pending: string;
|
|
9
|
+
readonly success: string;
|
|
10
|
+
readonly failure: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SetupReporter {
|
|
14
|
+
intro(title: string): void;
|
|
15
|
+
message(message: string): void;
|
|
16
|
+
warn(message: string): void;
|
|
17
|
+
cancel(message: string): void;
|
|
18
|
+
task<T>(
|
|
19
|
+
labels: SetupTaskLabels,
|
|
20
|
+
operation: (onOutput: CommandOutputHandler) => Promise<T>,
|
|
21
|
+
): Promise<T>;
|
|
22
|
+
finish(title: string, details: readonly string[]): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class TerminalSetupReporter implements SetupReporter {
|
|
26
|
+
readonly #output: Writable;
|
|
27
|
+
readonly #errorOutput: Writable;
|
|
28
|
+
readonly #verbose: boolean;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
options: {
|
|
32
|
+
readonly output?: Writable;
|
|
33
|
+
readonly errorOutput?: Writable;
|
|
34
|
+
readonly verbose?: boolean;
|
|
35
|
+
} = {},
|
|
36
|
+
) {
|
|
37
|
+
this.#output = options.output ?? process.stdout;
|
|
38
|
+
this.#errorOutput = options.errorOutput ?? process.stderr;
|
|
39
|
+
this.#verbose = options.verbose ?? false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
intro(title: string): void {
|
|
43
|
+
intro(title, { output: this.#output, withGuide: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
message(message: string): void {
|
|
47
|
+
log.message(message, { output: this.#output, withGuide: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
warn(message: string): void {
|
|
51
|
+
log.warn(message, { output: this.#output, withGuide: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
cancel(message: string): void {
|
|
55
|
+
cancelPrompt(message, { output: this.#output, withGuide: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async task<T>(
|
|
59
|
+
labels: SetupTaskLabels,
|
|
60
|
+
operation: (onOutput: CommandOutputHandler) => Promise<T>,
|
|
61
|
+
): Promise<T> {
|
|
62
|
+
const captured = new RollingOutput();
|
|
63
|
+
const onOutput: CommandOutputHandler = (stream, chunk) => {
|
|
64
|
+
captured.push(Buffer.from(chunk));
|
|
65
|
+
if (this.#verbose) {
|
|
66
|
+
(stream === "stderr" ? this.#errorOutput : this.#output).write(chunk);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (this.#verbose) {
|
|
71
|
+
log.step(labels.pending, { output: this.#output, withGuide: true });
|
|
72
|
+
try {
|
|
73
|
+
const result = await operation(onOutput);
|
|
74
|
+
log.success(labels.success, { output: this.#output, withGuide: true });
|
|
75
|
+
return result;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof SetupCanceledError) {
|
|
78
|
+
this.cancel("Setup canceled.");
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
log.error(labels.failure, { output: this.#output, withGuide: true });
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const progress = spinner({ indicator: "timer", output: this.#output, withGuide: true });
|
|
87
|
+
progress.start(labels.pending);
|
|
88
|
+
try {
|
|
89
|
+
const result = await operation(onOutput);
|
|
90
|
+
progress.stop(labels.success);
|
|
91
|
+
return result;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof SetupCanceledError) {
|
|
94
|
+
progress.cancel("Setup canceled.");
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
progress.error(labels.failure);
|
|
98
|
+
this.#showFailureOutput(captured.text());
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
finish(title: string, details: readonly string[]): void {
|
|
104
|
+
log.success(title, { output: this.#output, withGuide: true });
|
|
105
|
+
log.message([...details], {
|
|
106
|
+
output: this.#output,
|
|
107
|
+
spacing: 0,
|
|
108
|
+
withGuide: true,
|
|
109
|
+
});
|
|
110
|
+
outro("Vercel Sandbox is ready.", { output: this.#output, withGuide: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#showFailureOutput(value: string): void {
|
|
114
|
+
const cleaned = stripVTControlCharacters(value)
|
|
115
|
+
.replace(/\r(?!\n)/g, "\n")
|
|
116
|
+
.trim();
|
|
117
|
+
if (!cleaned) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
log.message(["Command output:", ...cleaned.split("\n")], {
|
|
121
|
+
output: this.#output,
|
|
122
|
+
spacing: 0,
|
|
123
|
+
withGuide: true,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/validate-main.ts
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
import { access, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, join, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
|
+
import { harborChildEnvironment } from "./harbor-environment.js";
|
|
6
7
|
import { harborInfrastructureError, readHarborJobResult } from "./harbor-results.js";
|
|
7
8
|
import { parallelMap } from "./parallel.js";
|
|
8
9
|
import { runCommand } from "./process.js";
|
|
10
|
+
import { type HarborEnvironment, isHarborEnvironment } from "./providers.js";
|
|
9
11
|
|
|
10
12
|
const parsed = parseArgs({
|
|
11
13
|
options: {
|
|
@@ -33,7 +35,7 @@ Options:
|
|
|
33
35
|
const tasksDirectory = resolve(parsed.values.tasks ?? fail("--tasks is required"));
|
|
34
36
|
const jobsDirectory = resolve(parsed.values.jobs ?? fail("--jobs is required"));
|
|
35
37
|
const environment = parsed.values.environment ?? "modal";
|
|
36
|
-
if (environment
|
|
38
|
+
if (!isHarborEnvironment(environment)) {
|
|
37
39
|
throw new Error("--environment must be modal or docker");
|
|
38
40
|
}
|
|
39
41
|
const concurrency = positiveInteger(parsed.values.concurrency, "--concurrency");
|
|
@@ -90,7 +92,7 @@ async function runGate(
|
|
|
90
92
|
taskDirectory: string,
|
|
91
93
|
taskId: string,
|
|
92
94
|
agent: "nop" | "oracle",
|
|
93
|
-
environment:
|
|
95
|
+
environment: HarborEnvironment,
|
|
94
96
|
jobsDirectory: string,
|
|
95
97
|
): Promise<{ readonly passed: boolean; readonly rewards: Readonly<Record<string, number>> }> {
|
|
96
98
|
const jobName = `${taskId}-${agent}-${crypto.randomUUID().slice(0, 8)}`;
|
|
@@ -116,7 +118,11 @@ async function runGate(
|
|
|
116
118
|
"--yes",
|
|
117
119
|
"--quiet",
|
|
118
120
|
],
|
|
119
|
-
{
|
|
121
|
+
{
|
|
122
|
+
allowFailure: true,
|
|
123
|
+
env: harborChildEnvironment(),
|
|
124
|
+
timeoutMs: 4 * 60 * 60 * 1000,
|
|
125
|
+
},
|
|
120
126
|
);
|
|
121
127
|
if (process.exitCode !== 0) {
|
|
122
128
|
throw new Error(`Harbor ${agent} exited ${process.exitCode}: ${process.stderr.slice(-1_000)}`);
|