@workbench-ai/workbench-core 0.0.64 → 0.0.66
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/dist/execution-events.d.ts +2 -2
- package/dist/execution-events.d.ts.map +1 -1
- package/dist/execution-events.js +6 -6
- package/dist/execution-evidence.d.ts +11 -11
- package/dist/execution-jobs.d.ts +5 -5
- package/dist/execution-jobs.js +0 -3
- package/dist/execution-outputs.d.ts.map +1 -1
- package/dist/execution-outputs.js +5 -16
- package/dist/execution-runtime-types.d.ts +3 -3
- package/dist/execution-scheduler.d.ts +12 -12
- package/dist/execution-scheduler.d.ts.map +1 -1
- package/dist/execution-scheduler.js +5 -5
- package/dist/execution-traces.d.ts +3 -3
- package/dist/index.d.ts +26 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -148
- package/dist/inspection.d.ts +111 -0
- package/dist/inspection.d.ts.map +1 -0
- package/dist/inspection.js +217 -0
- package/dist/runtime-utils.d.ts +6 -1
- package/dist/runtime-utils.d.ts.map +1 -1
- package/dist/runtime-utils.js +92 -0
- package/dist/sandbox-backends/index.d.ts +12 -20
- package/dist/sandbox-backends/index.d.ts.map +1 -1
- package/dist/sandbox-backends/index.js +15 -26
- package/dist/sandbox-backends/names.d.ts +2 -3
- package/dist/sandbox-backends/names.d.ts.map +1 -1
- package/dist/sandbox-backends/names.js +5 -5
- package/dist/sandbox-inputs.d.ts +6 -6
- package/dist/trace-files.d.ts.map +1 -1
- package/dist/trace-files.js +7 -50
- package/package.json +8 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { buildCandidateLineage, buildWorkbenchEvaluationComparison, } from "@workbench-ai/workbench-contract";
|
|
2
|
+
import { buildCandidateCaseExecutionRefs, buildWorkbenchExecutionEvidence, } from "./execution-evidence.js";
|
|
3
|
+
import { candidateRecordWithoutDerivedFields, createCandidateFilePreview, createCaseReview, summarizeCandidateFiles, } from "./index.js";
|
|
4
|
+
export class WorkbenchInspectionError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
statusCode;
|
|
7
|
+
constructor(message, options = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "WorkbenchInspectionError";
|
|
10
|
+
this.status = options.status ?? 400;
|
|
11
|
+
this.statusCode = this.status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function createWorkbenchInspection(backend) {
|
|
15
|
+
return {
|
|
16
|
+
snapshot: () => backend.snapshot(),
|
|
17
|
+
spec: (input = {}) => backend.spec(input),
|
|
18
|
+
sourceFiles: async (input = {}) => {
|
|
19
|
+
const files = await backend.sourceFiles(input);
|
|
20
|
+
return summarizeCandidateFiles(files, files.map((file) => file.path));
|
|
21
|
+
},
|
|
22
|
+
sourcePreview: async (input) => createCandidateFilePreview({
|
|
23
|
+
files: await backend.sourceFiles(input),
|
|
24
|
+
path: input.path,
|
|
25
|
+
view: input.view,
|
|
26
|
+
}),
|
|
27
|
+
candidate: async (input) => candidateRecordWithoutDerivedFields(await backend.candidate(input)),
|
|
28
|
+
candidateFiles: async (input) => {
|
|
29
|
+
const result = await backend.candidateFiles(input);
|
|
30
|
+
return summarizeCandidateFiles(result.files, result.changedPaths);
|
|
31
|
+
},
|
|
32
|
+
candidatePreview: async (input) => createCandidateFilePreview({
|
|
33
|
+
files: (await backend.candidateFiles(input)).files,
|
|
34
|
+
path: input.path,
|
|
35
|
+
view: input.view,
|
|
36
|
+
}),
|
|
37
|
+
evaluations: async () => {
|
|
38
|
+
const snapshot = await backend.snapshot();
|
|
39
|
+
return buildWorkbenchEvaluationComparison(snapshot.evaluations);
|
|
40
|
+
},
|
|
41
|
+
evaluation: (input) => backend.evaluation(input),
|
|
42
|
+
caseReview: async (input) => {
|
|
43
|
+
if (backend.caseReview) {
|
|
44
|
+
return await backend.caseReview(input);
|
|
45
|
+
}
|
|
46
|
+
const candidate = await backend.candidate({ id: input.candidateId });
|
|
47
|
+
const jobs = (await backend.run({ id: input.runId, includeJobs: true })).jobs ?? [];
|
|
48
|
+
return createCaseReview({
|
|
49
|
+
candidate,
|
|
50
|
+
caseId: input.caseId,
|
|
51
|
+
executions: buildCandidateCaseExecutionRefs({
|
|
52
|
+
jobs,
|
|
53
|
+
candidateId: input.candidateId,
|
|
54
|
+
caseId: input.caseId,
|
|
55
|
+
sampleIndex: input.sampleIndex,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
run: (input) => backend.run(input),
|
|
60
|
+
executionTrace: async (input) => {
|
|
61
|
+
if (backend.executionTrace) {
|
|
62
|
+
return await backend.executionTrace(input);
|
|
63
|
+
}
|
|
64
|
+
if (!backend.jobInRun || !backend.traceForJob) {
|
|
65
|
+
throw new WorkbenchInspectionError("Execution traces are not available for this Workbench inspection backend.", { status: 404 });
|
|
66
|
+
}
|
|
67
|
+
const jobs = [await backend.jobInRun(input)];
|
|
68
|
+
return {
|
|
69
|
+
projectId: backend.projectId,
|
|
70
|
+
runId: input.runId,
|
|
71
|
+
executions: buildWorkbenchExecutionEvidence({
|
|
72
|
+
jobs,
|
|
73
|
+
traceIdPrefix: `${backend.projectId}-execution`,
|
|
74
|
+
traceForJob: backend.traceForJob,
|
|
75
|
+
traceSessionsForJob: backend.traceSessionsForJob,
|
|
76
|
+
}),
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
executionFiles: async (input) => {
|
|
80
|
+
const files = await backend.executionFiles(input);
|
|
81
|
+
return summarizeCandidateFiles(files, files.map((file) => file.path));
|
|
82
|
+
},
|
|
83
|
+
executionPreview: async (input) => createCandidateFilePreview({
|
|
84
|
+
files: await backend.executionFiles(input),
|
|
85
|
+
path: input.path,
|
|
86
|
+
view: input.view,
|
|
87
|
+
}),
|
|
88
|
+
lineage: async () => {
|
|
89
|
+
const snapshot = await backend.snapshot();
|
|
90
|
+
return buildCandidateLineage({
|
|
91
|
+
summaries: snapshot.summaries,
|
|
92
|
+
activeId: snapshot.activeId,
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
diagnose: async (input = {}) => {
|
|
96
|
+
const snapshot = await backend.snapshot();
|
|
97
|
+
return await diagnoseWorkbenchFailures({
|
|
98
|
+
snapshot,
|
|
99
|
+
backend,
|
|
100
|
+
targetId: input.targetId?.trim() || null,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function diagnoseWorkbenchFailures(args) {
|
|
106
|
+
const targetRun = args.targetId
|
|
107
|
+
? args.snapshot.runs.find((run) => run.id === args.targetId)
|
|
108
|
+
: null;
|
|
109
|
+
const targetEvaluation = args.targetId
|
|
110
|
+
? args.snapshot.evaluations.find((evaluation) => evaluation.id === args.targetId)
|
|
111
|
+
: null;
|
|
112
|
+
const failures = [];
|
|
113
|
+
if (args.targetId && targetRun) {
|
|
114
|
+
const detail = await args.backend.run({ id: targetRun.id, includeJobs: true });
|
|
115
|
+
failures.push(...runFailures(detail.run));
|
|
116
|
+
failures.push(...jobFailures(detail.jobs ?? []));
|
|
117
|
+
}
|
|
118
|
+
else if (args.targetId && targetEvaluation) {
|
|
119
|
+
const evaluation = await args.backend.evaluation({ id: targetEvaluation.id });
|
|
120
|
+
failures.push(...evaluationFailures(evaluation));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
for (const run of args.snapshot.runs) {
|
|
124
|
+
failures.push(...runFailures(run));
|
|
125
|
+
}
|
|
126
|
+
for (const evaluation of args.snapshot.evaluations) {
|
|
127
|
+
failures.push(...evaluationSummaryFailures(evaluation));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
targetId: args.targetId,
|
|
132
|
+
failures,
|
|
133
|
+
failedRunCount: failures.filter((failure) => failure.kind === "run").length,
|
|
134
|
+
failedEvaluationCount: failures.filter((failure) => failure.kind === "evaluation").length,
|
|
135
|
+
failedJobCount: failures.filter((failure) => failure.kind === "job").length,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function runFailures(run) {
|
|
139
|
+
if (run.status !== "finished" || (run.outcome !== "error" && run.outcome !== "cancelled")) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
return [{
|
|
143
|
+
kind: "run",
|
|
144
|
+
id: run.id,
|
|
145
|
+
runId: run.id,
|
|
146
|
+
candidateId: run.outputCandidateId ?? run.candidateId ?? undefined,
|
|
147
|
+
status: run.outcome,
|
|
148
|
+
...(run.error ? { error: run.error } : {}),
|
|
149
|
+
}];
|
|
150
|
+
}
|
|
151
|
+
function evaluationSummaryFailures(evaluation) {
|
|
152
|
+
if (evaluation.status === "completed" &&
|
|
153
|
+
evaluation.errorSampleCount === 0 &&
|
|
154
|
+
!evaluation.error) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
return [{
|
|
158
|
+
kind: "evaluation",
|
|
159
|
+
id: evaluation.id,
|
|
160
|
+
evaluationId: evaluation.id,
|
|
161
|
+
runId: evaluation.runId,
|
|
162
|
+
candidateId: evaluation.candidateId,
|
|
163
|
+
status: evaluation.status,
|
|
164
|
+
...(evaluation.error ? { error: evaluation.error } : {}),
|
|
165
|
+
}];
|
|
166
|
+
}
|
|
167
|
+
function evaluationFailures(evaluation) {
|
|
168
|
+
const failures = evaluationSummaryFailures(evaluation);
|
|
169
|
+
for (const sample of evaluation.evaluation.samples) {
|
|
170
|
+
if (!sample.error && !(sample.cases ?? []).some((entry) => entry.status && entry.status !== "completed")) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
failures.push({
|
|
174
|
+
kind: "sample",
|
|
175
|
+
id: `${evaluation.id}:sample:${sample.index}`,
|
|
176
|
+
evaluationId: evaluation.id,
|
|
177
|
+
runId: evaluation.runId,
|
|
178
|
+
candidateId: evaluation.candidateId,
|
|
179
|
+
sampleIndex: sample.index,
|
|
180
|
+
status: sample.status,
|
|
181
|
+
...(sample.error ? { error: sample.error } : {}),
|
|
182
|
+
});
|
|
183
|
+
for (const result of sample.cases ?? []) {
|
|
184
|
+
if (!result.status || result.status === "completed") {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
failures.push({
|
|
188
|
+
kind: "case",
|
|
189
|
+
id: `${evaluation.id}:case:${result.id}:sample:${sample.index}`,
|
|
190
|
+
evaluationId: evaluation.id,
|
|
191
|
+
runId: evaluation.runId,
|
|
192
|
+
candidateId: evaluation.candidateId,
|
|
193
|
+
caseId: result.id,
|
|
194
|
+
sampleIndex: sample.index,
|
|
195
|
+
status: result.status,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return failures;
|
|
200
|
+
}
|
|
201
|
+
function jobFailures(jobs) {
|
|
202
|
+
return jobs
|
|
203
|
+
.filter((job) => isFailedJobStatus(job.status))
|
|
204
|
+
.map((job) => ({
|
|
205
|
+
kind: "job",
|
|
206
|
+
id: job.id,
|
|
207
|
+
jobId: job.id,
|
|
208
|
+
runId: job.runId,
|
|
209
|
+
candidateId: job.candidateId,
|
|
210
|
+
status: job.status,
|
|
211
|
+
attemptIndex: typeof job.attempt === "number" ? job.attempt : undefined,
|
|
212
|
+
...(job.error ? { error: job.error } : {}),
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
function isFailedJobStatus(status) {
|
|
216
|
+
return status === "failed" || status === "cancelled";
|
|
217
|
+
}
|
package/dist/runtime-utils.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import type { Json } from "@workbench-ai/workbench-contract";
|
|
1
|
+
import type { Json, SurfaceSnapshotFile } from "@workbench-ai/workbench-contract";
|
|
2
2
|
export declare function importNodeModule<T>(specifier: string): Promise<T>;
|
|
3
3
|
export declare function nodeBuiltin(name: string): string;
|
|
4
4
|
export declare function asRuntimeRecord(value: unknown): Record<string, unknown>;
|
|
5
5
|
export declare function jsonRecord(value: unknown): Record<string, Json>;
|
|
6
6
|
export declare function numberValue(value: unknown): number | undefined;
|
|
7
7
|
export declare function stringValue(value: unknown): string | undefined;
|
|
8
|
+
export declare function normalizeRelativePath(filePath: string): string;
|
|
9
|
+
export declare function writeSurfaceFiles(root: string, files: readonly SurfaceSnapshotFile[]): Promise<void>;
|
|
10
|
+
export declare function readSurfaceFiles(root: string, options?: {
|
|
11
|
+
ignorePath?: (path: string) => boolean;
|
|
12
|
+
}): Promise<SurfaceSnapshotFile[]>;
|
|
8
13
|
export declare function isJsonPayload(value: unknown): value is import("@workbench-ai/workbench-contract").Json;
|
|
9
14
|
export declare function resolveDockerRuntimeImageRef(imageRef: string, options: {
|
|
10
15
|
runtimeRegistry: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-utils.d.ts","sourceRoot":"","sources":["../src/runtime-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,
|
|
1
|
+
{"version":3,"file":"runtime-utils.d.ts","sourceRoot":"","sources":["../src/runtime-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,mBAAmB,EACpB,MAAM,kCAAkC,CAAC;AAE1C,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAEvE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIvE;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAI/D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE9D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE9D;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAU9D;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,mBAAmB,EAAE,GACpC,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CAAO,GACvD,OAAO,CAAC,mBAAmB,EAAE,CAAC,CA+ChC;AAwBD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,kCAAkC,EAAE,IAAI,CAWtG;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAE,GAChF,MAAM,CASR;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9D;AAeD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAS7E;AAED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,SAAS,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CASjG;AAoBD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnD"}
|
package/dist/runtime-utils.js
CHANGED
|
@@ -28,6 +28,98 @@ export function numberValue(value) {
|
|
|
28
28
|
export function stringValue(value) {
|
|
29
29
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
30
30
|
}
|
|
31
|
+
export function normalizeRelativePath(filePath) {
|
|
32
|
+
const normalized = filePath.replace(/\\/gu, "/").replace(/^\/+/u, "");
|
|
33
|
+
if (!normalized || normalized.includes("\0")) {
|
|
34
|
+
throw new Error("File paths must be non-empty relative paths.");
|
|
35
|
+
}
|
|
36
|
+
const parts = normalized.split("/");
|
|
37
|
+
if (parts.some((part) => part === ".." || part === "." || part === "")) {
|
|
38
|
+
throw new Error(`Unsafe relative file path: ${filePath}`);
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
export async function writeSurfaceFiles(root, files) {
|
|
43
|
+
const fs = await importNodeModule(nodeBuiltin("fs/promises"));
|
|
44
|
+
const path = await importNodeModule(nodeBuiltin("path"));
|
|
45
|
+
await fs.mkdir(root, { recursive: true });
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const target = path.join(root, normalizeRelativePath(file.path));
|
|
48
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
49
|
+
const body = file.encoding === "base64"
|
|
50
|
+
? Buffer.from(file.content, "base64")
|
|
51
|
+
: Buffer.from(file.content, "utf8");
|
|
52
|
+
await fs.writeFile(target, body);
|
|
53
|
+
if (file.executable) {
|
|
54
|
+
await fs.chmod(target, 0o755).catch(() => undefined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function readSurfaceFiles(root, options = {}) {
|
|
59
|
+
const fs = await importNodeModule(nodeBuiltin("fs/promises"));
|
|
60
|
+
const path = await importNodeModule(nodeBuiltin("path"));
|
|
61
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
62
|
+
const files = [];
|
|
63
|
+
async function walk(directory) {
|
|
64
|
+
const entries = await fs
|
|
65
|
+
.readdir(directory, { withFileTypes: true })
|
|
66
|
+
.catch(() => []);
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const absolutePath = path.join(directory, entry.name);
|
|
69
|
+
const relativePath = normalizeRelativePath(path.relative(root, absolutePath).replace(/\\/gu, "/"));
|
|
70
|
+
if (options.ignorePath?.(relativePath)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
await walk(absolutePath);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!entry.isFile()) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
let body;
|
|
81
|
+
let stats;
|
|
82
|
+
try {
|
|
83
|
+
body = await fs.readFile(absolutePath);
|
|
84
|
+
stats = await fs.stat(absolutePath);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (isVanishedWalkEntry(error)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
const content = encodeSurfaceSnapshotContent(body, utf8Decoder);
|
|
93
|
+
files.push({
|
|
94
|
+
path: relativePath,
|
|
95
|
+
kind: content.encoding === "base64" ? "binary" : "text",
|
|
96
|
+
encoding: content.encoding,
|
|
97
|
+
content: content.content,
|
|
98
|
+
executable: (stats.mode & 0o111) !== 0,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
await walk(root);
|
|
103
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
104
|
+
}
|
|
105
|
+
function isVanishedWalkEntry(error) {
|
|
106
|
+
const code = error?.code;
|
|
107
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
108
|
+
}
|
|
109
|
+
function encodeSurfaceSnapshotContent(body, utf8Decoder) {
|
|
110
|
+
try {
|
|
111
|
+
return {
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
content: utf8Decoder.decode(body),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return {
|
|
118
|
+
encoding: "base64",
|
|
119
|
+
content: body.toString("base64"),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
31
123
|
export function isJsonPayload(value) {
|
|
32
124
|
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
33
125
|
return Number.isFinite(value) || typeof value !== "number";
|
|
@@ -1,37 +1,29 @@
|
|
|
1
1
|
import type { WorkbenchExecutionRuntimeInput } from "../execution-runtime-types.ts";
|
|
2
2
|
import type { SandboxBackendCapabilities, SandboxExecutionFileStore, SandboxPlane } from "../sandbox-plane.ts";
|
|
3
|
-
import { type
|
|
4
|
-
export { DOCKER_SANDBOX_BACKEND,
|
|
3
|
+
import { type WorkbenchSandboxBackendName } from "./names.ts";
|
|
4
|
+
export { DOCKER_SANDBOX_BACKEND, resolveWorkbenchSandboxBackendName, type WorkbenchSandboxBackendName, } from "./names.ts";
|
|
5
5
|
export { createDockerSandboxBackendDescriptor, createDockerSandboxPlane, } from "./docker.ts";
|
|
6
6
|
export interface SandboxHostHealthExpectation {
|
|
7
|
-
|
|
8
|
-
backend: string;
|
|
7
|
+
backend: WorkbenchSandboxBackendName;
|
|
9
8
|
capabilities: SandboxBackendCapabilities;
|
|
10
9
|
}
|
|
11
|
-
export interface
|
|
10
|
+
export interface SandboxBackendRequestedResources {
|
|
12
11
|
cpu: number;
|
|
13
12
|
memoryGb: number;
|
|
14
13
|
diskGb?: number;
|
|
15
14
|
timeoutMinutes?: number;
|
|
16
15
|
}
|
|
17
|
-
export interface
|
|
16
|
+
export interface SandboxBackendHostCost {
|
|
18
17
|
cpu: number;
|
|
19
18
|
memoryGb: number;
|
|
20
19
|
diskGb: number;
|
|
21
20
|
}
|
|
22
|
-
export interface
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
export interface SandboxBackendAdmission {
|
|
22
|
+
backend: WorkbenchSandboxBackendName;
|
|
23
|
+
hostCost: SandboxBackendHostCost;
|
|
25
24
|
}
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
export declare function createSandboxBackendPlaneForProvider(provider: string, args: WorkbenchExecutionRuntimeInput, startedAt: string, fileStore: SandboxExecutionFileStore): SandboxPlane;
|
|
32
|
-
export declare function sandboxHostHealthExpectationForProvider(provider: WorkbenchSandboxProviderName): SandboxHostHealthExpectation;
|
|
33
|
-
export declare function assertSandboxHostHealthForProvider(value: unknown, provider: WorkbenchSandboxProviderName): void;
|
|
34
|
-
export declare function sandboxProviderDefaultMaxConcurrentJobs(_provider: WorkbenchSandboxProviderName): number | null;
|
|
35
|
-
export declare function sandboxProviderAdmissionForResources(provider: WorkbenchSandboxProviderName, resources: SandboxProviderRequestedResources): SandboxProviderAdmission;
|
|
36
|
-
export declare function sandboxProviderLeaseScope(provider: WorkbenchSandboxProviderName): string;
|
|
25
|
+
export declare function createSandboxBackendPlaneForBackend(backend: string, args: WorkbenchExecutionRuntimeInput, startedAt: string, fileStore: SandboxExecutionFileStore): SandboxPlane;
|
|
26
|
+
export declare function sandboxHostHealthExpectationForBackend(backend: WorkbenchSandboxBackendName): SandboxHostHealthExpectation;
|
|
27
|
+
export declare function assertSandboxHostHealthForBackend(value: unknown, backend: WorkbenchSandboxBackendName): void;
|
|
28
|
+
export declare function sandboxBackendAdmissionForResources(backend: WorkbenchSandboxBackendName, resources: SandboxBackendRequestedResources): SandboxBackendAdmission;
|
|
37
29
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sandbox-backends/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,8BAA8B,EAC/B,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EACV,0BAA0B,EAE1B,yBAAyB,EACzB,YAAY,EACb,MAAM,qBAAqB,CAAC;AAK7B,OAAO,EAEL,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sandbox-backends/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,8BAA8B,EAC/B,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EACV,0BAA0B,EAE1B,yBAAyB,EACzB,YAAY,EACb,MAAM,qBAAqB,CAAC;AAK7B,OAAO,EAEL,KAAK,2BAA2B,EAEjC,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,sBAAsB,EACtB,kCAAkC,EAClC,KAAK,2BAA2B,GACjC,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oCAAoC,EACpC,wBAAwB,GACzB,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,2BAA2B,CAAC;IACrC,YAAY,EAAE,0BAA0B,CAAC;CAC1C;AAED,MAAM,WAAW,gCAAgC;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,2BAA2B,CAAC;IACrC,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAED,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,8BAA8B,EACpC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,yBAAyB,GACnC,YAAY,CAMd;AAED,wBAAgB,sCAAsC,CACpD,OAAO,EAAE,2BAA2B,GACnC,4BAA4B,CAQ9B;AAED,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,2BAA2B,GACnC,IAAI,CAYN;AAED,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,2BAA2B,EACpC,SAAS,EAAE,gCAAgC,GAC1C,uBAAuB,CAiBzB"}
|
|
@@ -1,34 +1,30 @@
|
|
|
1
1
|
import { isWorkbenchExecutionNetworkEgress, } from "@workbench-ai/workbench-contract";
|
|
2
2
|
import { createDockerSandboxBackendDescriptor, createDockerSandboxPlane, } from "./docker.js";
|
|
3
|
-
import { DOCKER_SANDBOX_BACKEND,
|
|
4
|
-
export { DOCKER_SANDBOX_BACKEND,
|
|
3
|
+
import { DOCKER_SANDBOX_BACKEND, resolveWorkbenchSandboxBackendName, } from "./names.js";
|
|
4
|
+
export { DOCKER_SANDBOX_BACKEND, resolveWorkbenchSandboxBackendName, } from "./names.js";
|
|
5
5
|
export { createDockerSandboxBackendDescriptor, createDockerSandboxPlane, } from "./docker.js";
|
|
6
|
-
export function
|
|
7
|
-
const resolved =
|
|
6
|
+
export function createSandboxBackendPlaneForBackend(backend, args, startedAt, fileStore) {
|
|
7
|
+
const resolved = resolveWorkbenchSandboxBackendName(backend);
|
|
8
8
|
if (resolved !== DOCKER_SANDBOX_BACKEND) {
|
|
9
|
-
throw new Error(`Unsupported local sandbox
|
|
9
|
+
throw new Error(`Unsupported local sandbox backend ${backend}.`);
|
|
10
10
|
}
|
|
11
11
|
return createDockerSandboxPlane(args, startedAt, fileStore);
|
|
12
12
|
}
|
|
13
|
-
export function
|
|
14
|
-
if (
|
|
15
|
-
|
|
13
|
+
export function sandboxHostHealthExpectationForBackend(backend) {
|
|
14
|
+
if (backend !== DOCKER_SANDBOX_BACKEND) {
|
|
15
|
+
resolveWorkbenchSandboxBackendName(backend);
|
|
16
16
|
}
|
|
17
17
|
return {
|
|
18
|
-
|
|
19
|
-
backend: provider,
|
|
18
|
+
backend,
|
|
20
19
|
capabilities: createDockerSandboxBackendDescriptor().capabilities,
|
|
21
20
|
};
|
|
22
21
|
}
|
|
23
|
-
export function
|
|
24
|
-
const expected =
|
|
22
|
+
export function assertSandboxHostHealthForBackend(value, backend) {
|
|
23
|
+
const expected = sandboxHostHealthExpectationForBackend(backend);
|
|
25
24
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
26
25
|
throw new Error("sandbox host health response must be an object.");
|
|
27
26
|
}
|
|
28
27
|
const record = value;
|
|
29
|
-
if (record.provider !== expected.provider) {
|
|
30
|
-
throw new Error(`sandbox host provider ${String(record.provider ?? "missing")} does not match expected ${expected.provider}.`);
|
|
31
|
-
}
|
|
32
28
|
if (record.backend !== expected.backend) {
|
|
33
29
|
throw new Error(`sandbox host backend ${String(record.backend ?? "missing")} does not match expected ${expected.backend}.`);
|
|
34
30
|
}
|
|
@@ -36,12 +32,9 @@ export function assertSandboxHostHealthForProvider(value, provider) {
|
|
|
36
32
|
throw new Error(`sandbox host capabilities are invalid for backend ${expected.backend}.`);
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
|
-
export function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export function sandboxProviderAdmissionForResources(provider, resources) {
|
|
43
|
-
if (provider !== DOCKER_SANDBOX_BACKEND) {
|
|
44
|
-
resolveWorkbenchSandboxProviderName(provider);
|
|
35
|
+
export function sandboxBackendAdmissionForResources(backend, resources) {
|
|
36
|
+
if (backend !== DOCKER_SANDBOX_BACKEND) {
|
|
37
|
+
resolveWorkbenchSandboxBackendName(backend);
|
|
45
38
|
}
|
|
46
39
|
assertPositiveResource(resources.cpu, "resources.cpu");
|
|
47
40
|
assertPositiveResource(resources.memoryGb, "resources.memoryGb");
|
|
@@ -49,18 +42,14 @@ export function sandboxProviderAdmissionForResources(provider, resources) {
|
|
|
49
42
|
assertPositiveResource(resources.diskGb, "resources.diskGb");
|
|
50
43
|
}
|
|
51
44
|
return {
|
|
52
|
-
|
|
45
|
+
backend,
|
|
53
46
|
hostCost: {
|
|
54
47
|
cpu: resources.cpu,
|
|
55
48
|
memoryGb: resources.memoryGb,
|
|
56
49
|
diskGb: resources.diskGb ?? 1,
|
|
57
50
|
},
|
|
58
|
-
providerLeases: [],
|
|
59
51
|
};
|
|
60
52
|
}
|
|
61
|
-
export function sandboxProviderLeaseScope(provider) {
|
|
62
|
-
throw new Error(`Local sandbox provider ${provider} does not use provider leases.`);
|
|
63
|
-
}
|
|
64
53
|
function isSandboxBackendCapabilities(value) {
|
|
65
54
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
66
55
|
return false;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export declare const DOCKER_SANDBOX_BACKEND = "docker";
|
|
2
2
|
export type WorkbenchSandboxBackendName = typeof DOCKER_SANDBOX_BACKEND;
|
|
3
|
-
export
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function resolveWorkbenchSandboxProviderName(value: string | null | undefined): WorkbenchSandboxProviderName;
|
|
3
|
+
export declare function isWorkbenchSandboxBackendName(value: string): value is WorkbenchSandboxBackendName;
|
|
4
|
+
export declare function resolveWorkbenchSandboxBackendName(value: string | null | undefined): WorkbenchSandboxBackendName;
|
|
6
5
|
//# sourceMappingURL=names.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../../src/sandbox-backends/names.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,WAAW,CAAC;AAE/C,MAAM,MAAM,2BAA2B,GAAG,OAAO,sBAAsB,CAAC;AAExE,
|
|
1
|
+
{"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../../src/sandbox-backends/names.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,WAAW,CAAC;AAE/C,MAAM,MAAM,2BAA2B,GAAG,OAAO,sBAAsB,CAAC;AAExE,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,2BAA2B,CAEjG;AAED,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,2BAA2B,CAS7B"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
export const DOCKER_SANDBOX_BACKEND = "docker";
|
|
2
|
-
export function
|
|
2
|
+
export function isWorkbenchSandboxBackendName(value) {
|
|
3
3
|
return value === DOCKER_SANDBOX_BACKEND;
|
|
4
4
|
}
|
|
5
|
-
export function
|
|
5
|
+
export function resolveWorkbenchSandboxBackendName(value) {
|
|
6
6
|
const normalized = value?.trim();
|
|
7
7
|
if (!normalized) {
|
|
8
|
-
throw new Error("Sandbox
|
|
8
|
+
throw new Error("Sandbox backend is required.");
|
|
9
9
|
}
|
|
10
|
-
if (
|
|
10
|
+
if (isWorkbenchSandboxBackendName(normalized)) {
|
|
11
11
|
return normalized;
|
|
12
12
|
}
|
|
13
|
-
throw new Error(`Unsupported local sandbox
|
|
13
|
+
throw new Error(`Unsupported local sandbox backend ${normalized}. Supported backends: docker.`);
|
|
14
14
|
}
|
package/dist/sandbox-inputs.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type { BlobObjectRef,
|
|
1
|
+
import type { BlobObjectRef, RemoteWorkbenchJob, Json, SurfaceSnapshotFile, WorkbenchExecutionCapability, WorkbenchExecutionResult, WorkbenchExecutionSpec, WorkbenchSandboxExecutionMetadata } from "@workbench-ai/workbench-contract";
|
|
2
2
|
import { type SandboxCreateRequest, type SandboxExecutionFileStore, type SandboxMaterializedInput } from "./sandbox-plane.ts";
|
|
3
3
|
import type { WorkbenchExecutionRuntimeInput } from "./execution-runtime-types.ts";
|
|
4
|
-
export declare function readWorkbenchExecutionSpec(job:
|
|
4
|
+
export declare function readWorkbenchExecutionSpec(job: RemoteWorkbenchJob): WorkbenchExecutionSpec;
|
|
5
5
|
export declare function createWorkbenchSandboxFileStore(args: WorkbenchExecutionRuntimeInput): SandboxExecutionFileStore;
|
|
6
6
|
export declare function materializeWorkbenchSandboxInput(args: WorkbenchExecutionRuntimeInput, execution: WorkbenchExecutionSpec, input: WorkbenchExecutionSpec["inputs"][number]): SandboxMaterializedInput;
|
|
7
7
|
export declare function materializedFileInput(input: WorkbenchExecutionSpec["inputs"][number], files: readonly SurfaceSnapshotFile[]): SandboxMaterializedInput;
|
|
8
8
|
export declare function createSandboxAdapterRequest(args: WorkbenchExecutionRuntimeInput, request: SandboxCreateRequest, startedAt: string): Json;
|
|
9
|
-
export declare function sanitizeWorkbenchExecutionJobForSandbox(job:
|
|
9
|
+
export declare function sanitizeWorkbenchExecutionJobForSandbox(job: RemoteWorkbenchJob, execution: WorkbenchExecutionSpec): RemoteWorkbenchJob;
|
|
10
10
|
export declare function materializedInputForSandboxRequest(input: SandboxMaterializedInput): Record<string, Json>;
|
|
11
11
|
export declare function executionResultFromCompletedSandboxJob(args: {
|
|
12
|
-
completedJob:
|
|
12
|
+
completedJob: RemoteWorkbenchJob;
|
|
13
13
|
execution: WorkbenchExecutionSpec;
|
|
14
14
|
startedAt: string;
|
|
15
15
|
backend: string;
|
|
@@ -21,7 +21,7 @@ export declare function executionResultFromCompletedSandboxJob(args: {
|
|
|
21
21
|
export declare function outputPayloadForContract(output: Record<string, unknown>, outputName: string): Json | undefined;
|
|
22
22
|
export declare function sandboxOutputRef(capability: WorkbenchExecutionCapability, outputName: string, body: string): Promise<BlobObjectRef>;
|
|
23
23
|
export declare function sha256Hex(body: string): Promise<string>;
|
|
24
|
-
export declare function withSandboxCompletionMetadata(job:
|
|
25
|
-
export declare function attachSandboxMetadataToJob(job:
|
|
24
|
+
export declare function withSandboxCompletionMetadata(job: RemoteWorkbenchJob, metadata: WorkbenchSandboxExecutionMetadata): RemoteWorkbenchJob;
|
|
25
|
+
export declare function attachSandboxMetadataToJob(job: RemoteWorkbenchJob, metadata: unknown): RemoteWorkbenchJob;
|
|
26
26
|
export declare function isSurfaceSnapshotFile(value: unknown): value is SurfaceSnapshotFile;
|
|
27
27
|
//# sourceMappingURL=sandbox-inputs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace-files.d.ts","sourceRoot":"","sources":["../src/trace-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,yBAAyB,EAC1B,MAAM,kCAAkC,CAAC;AAO1C,eAAO,MAAM,oBAAoB,sBAAsB,CAAC;AAExD,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"trace-files.d.ts","sourceRoot":"","sources":["../src/trace-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,yBAAyB,EAC1B,MAAM,kCAAkC,CAAC;AAO1C,eAAO,MAAM,oBAAoB,sBAAsB,CAAC;AAExD,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAOhH;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,mBAAmB,EAAE,GAAG,MAAM,EAAE,CAK9E;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAET;AAED,wBAAgB,gCAAgC,CAAC,IAAI,EAAE;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,yBAAyB,CAAC;CACpC,GAAG,MAAM,CAET;AAED,wBAAgB,8BAA8B,CAAC,IAAI,EAAE;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAKT"}
|
package/dist/trace-files.js
CHANGED
|
@@ -1,38 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeRelativePath, readSurfaceFiles, } from "./runtime-utils.js";
|
|
2
2
|
export const WORKBENCH_TRACE_ROOT = ".workbench/traces";
|
|
3
3
|
export async function readOutputTraceFiles(outputRoot, traceRoot) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
for (const entry of entries) {
|
|
11
|
-
const absolutePath = path.join(directory, entry.name);
|
|
12
|
-
if (entry.isDirectory()) {
|
|
13
|
-
const relativeDirectory = normalizeRelativePath(path.relative(outputRoot, absolutePath).replace(/\\/gu, "/"));
|
|
14
|
-
if (shouldSkipTraceDirectory(relativeDirectory)) {
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
await walk(absolutePath);
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
if (!entry.isFile()) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const body = await fs.readFile(absolutePath);
|
|
24
|
-
const content = encodeTraceFileContent(body, decoder);
|
|
25
|
-
files.push({
|
|
26
|
-
path: normalizeRelativePath(`${traceRoot}/${path.relative(outputRoot, absolutePath).replace(/\\/gu, "/")}`),
|
|
27
|
-
kind: content.encoding === "base64" ? "binary" : "text",
|
|
28
|
-
encoding: content.encoding,
|
|
29
|
-
content: content.content,
|
|
30
|
-
executable: false,
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
await walk(outputRoot);
|
|
35
|
-
return files;
|
|
4
|
+
return (await readSurfaceFiles(outputRoot, { ignorePath: shouldSkipTraceDirectory }))
|
|
5
|
+
.map((file) => ({
|
|
6
|
+
...file,
|
|
7
|
+
path: normalizeRelativePath(`${traceRoot}/${file.path}`),
|
|
8
|
+
executable: false,
|
|
9
|
+
}));
|
|
36
10
|
}
|
|
37
11
|
export function traceFilePaths(files) {
|
|
38
12
|
return files
|
|
@@ -62,20 +36,6 @@ function shouldSkipTraceDirectory(relativeDirectory) {
|
|
|
62
36
|
|| relativeDirectory.endsWith("/session/workspace")
|
|
63
37
|
|| relativeDirectory.includes("/session/workspace/");
|
|
64
38
|
}
|
|
65
|
-
function encodeTraceFileContent(body, utf8Decoder) {
|
|
66
|
-
try {
|
|
67
|
-
return {
|
|
68
|
-
encoding: "utf8",
|
|
69
|
-
content: utf8Decoder.decode(body),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return {
|
|
74
|
-
encoding: "base64",
|
|
75
|
-
content: body.toString("base64"),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
39
|
function sanitizeTracePathSegment(value) {
|
|
80
40
|
const sanitized = value
|
|
81
41
|
.trim()
|
|
@@ -89,6 +49,3 @@ function tracePurposeSequence(purpose) {
|
|
|
89
49
|
}
|
|
90
50
|
return 2;
|
|
91
51
|
}
|
|
92
|
-
function normalizeRelativePath(filePath) {
|
|
93
|
-
return filePath.replace(/\\/gu, "/").replace(/^\/+/u, "").replace(/\/+/gu, "/");
|
|
94
|
-
}
|