@mastra/e2b 0.14.2-alpha.0 → 0.15.0-alpha.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/dist/_types/@internal_workspace/dist/file-commands.d.ts +69 -0
- package/dist/_types/@internal_workspace/dist/files-api-filesystem.d.ts +85 -0
- package/dist/_types/@internal_workspace/dist/index.d.ts +5 -0
- package/dist/_types/@internal_workspace/dist/repo-clone.d.ts +22 -0
- package/dist/_types/@internal_workspace/dist/setup-marker.d.ts +56 -0
- package/dist/_types/@internal_workspace/package.json +1 -0
- package/dist/index.cjs +535 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +537 -93
- package/dist/index.js.map +1 -1
- package/dist/sandbox/filesystem.d.ts +31 -0
- package/dist/sandbox/filesystem.d.ts.map +1 -0
- package/dist/sandbox/index.d.ts +8 -0
- package/dist/sandbox/index.d.ts.map +1 -1
- package/package.json +7 -8
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { FactorySandbox, MastraSandbox, ProcessHandle, SandboxError, SandboxNotReadyError, SandboxProcessManager, UnsupportedStdinCloseError, assertModesUnsupported } from "@mastra/core/workspace";
|
|
2
|
-
import { ApiClient, ConnectionConfig, Sandbox, Template } from "e2b";
|
|
1
|
+
import { DirectoryNotEmptyError, DirectoryNotFoundError, FactorySandbox, FileExistsError, FileNotFoundError, IsDirectoryError, MastraFilesystem, MastraSandbox, NotDirectoryError, ProcessHandle, SandboxError, SandboxNotReadyError, SandboxProcessManager, UnsupportedStdinCloseError, assertModesUnsupported } from "@mastra/core/workspace";
|
|
2
|
+
import { ApiClient, ConnectionConfig, InvalidArgumentError, NotFoundError, Sandbox, SandboxNotFoundError, Template } from "e2b";
|
|
3
3
|
import { createHash } from "crypto";
|
|
4
|
+
import { posix } from "path";
|
|
5
|
+
import { Buffer as Buffer$1 } from "buffer";
|
|
4
6
|
import { execFile } from "child_process";
|
|
5
7
|
import { promisify } from "util";
|
|
6
8
|
import { FRAME_PREFIX, buildProgramModule, buildRunner, sanitizeToolId } from "@mastra/core/tools";
|
|
@@ -74,6 +76,514 @@ function createDefaultMountableTemplate(options) {
|
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
78
|
//#endregion
|
|
79
|
+
//#region ../../packages/_internals/workspace/dist/index.js
|
|
80
|
+
/** Directory, relative to the template's build cwd, that holds every marker below. */
|
|
81
|
+
const SETUP_MARKER_DIR = ".mastra-sandbox";
|
|
82
|
+
/**
|
|
83
|
+
* Setup completion marker shared by repo templates and their consumers.
|
|
84
|
+
*
|
|
85
|
+
* A repo template writes this file beside the checkout as its last build
|
|
86
|
+
* step, so it exists only in images where every setup command succeeded. Its
|
|
87
|
+
* content is a digest of the setup commands the image ran, letting a sandbox
|
|
88
|
+
* booted from the image tell whether the setup it is about to run already
|
|
89
|
+
* happened. Relative to the template's build cwd, which is also the runtime
|
|
90
|
+
* working directory the repo was cloned into.
|
|
91
|
+
*
|
|
92
|
+
* This is the single-repository marker. Templates built from a repository
|
|
93
|
+
* list write one marker per repository instead ({@link repoSetupMarkerPath}).
|
|
94
|
+
*/
|
|
95
|
+
const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
|
|
96
|
+
/**
|
|
97
|
+
* Marker for the workspace-level setup of a multi-repository template: the
|
|
98
|
+
* commands that ran at the working directory after every repository was set
|
|
99
|
+
* up. Written even when there were no such commands, so a consumer can tell
|
|
100
|
+
* "ran nothing" from "never built".
|
|
101
|
+
*/
|
|
102
|
+
const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
|
|
103
|
+
/**
|
|
104
|
+
* One line per repository directory whose setup failed while the build was
|
|
105
|
+
* told to continue. Absent when nothing failed. Consumers check this before
|
|
106
|
+
* trusting a per-repository marker: that marker means every step ran, not
|
|
107
|
+
* that every step succeeded.
|
|
108
|
+
*/
|
|
109
|
+
const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
|
|
110
|
+
/** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
|
|
111
|
+
function repoSetupMarkerPath(repoDir) {
|
|
112
|
+
return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
|
|
113
|
+
}
|
|
114
|
+
/** Blank entries never become build steps, so they never count toward the digest either. */
|
|
115
|
+
function normalizeSetupCommands$1(setupCommand) {
|
|
116
|
+
return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
|
|
117
|
+
}
|
|
118
|
+
/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */
|
|
119
|
+
function setupMarkerContent(setupCommand) {
|
|
120
|
+
return `sha256:${createHash("sha256").update(normalizeSetupCommands$1(setupCommand).join("\n")).digest("hex")}`;
|
|
121
|
+
}
|
|
122
|
+
/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
|
|
123
|
+
function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
|
|
124
|
+
return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
|
|
125
|
+
}
|
|
126
|
+
/** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
|
|
127
|
+
const REPO_DIR_PATTERN = /^[\w.-]+$/;
|
|
128
|
+
/**
|
|
129
|
+
* Shell step that runs one setup command inside a repository clone.
|
|
130
|
+
*
|
|
131
|
+
* Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
|
|
132
|
+
* the `cd` sits inside the parenthesized group so that the failure branch
|
|
133
|
+
* runs back at the build cwd and the failure list lands at the workspace
|
|
134
|
+
* level, never inside the repository.
|
|
135
|
+
*/
|
|
136
|
+
function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
|
|
137
|
+
if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
|
|
138
|
+
if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
|
|
139
|
+
return `( cd "${repoDir}" && sh -c ${shellQuote$2(command)} ) || { mkdir -p "${SETUP_MARKER_DIR}" && grep -qxF -- '${repoDir}' "${SETUP_FAILED_MARKER_PATH}" 2>/dev/null || printf '%s\\n' '${repoDir}' >> "${SETUP_FAILED_MARKER_PATH}"; }`;
|
|
140
|
+
}
|
|
141
|
+
function shellQuote$2(value) {
|
|
142
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
143
|
+
}
|
|
144
|
+
function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
|
|
145
|
+
return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote$1(branch)} ` : ""}${shellQuote$1(cloneUrl)} ${shellQuote$1(destination)}`;
|
|
146
|
+
}
|
|
147
|
+
/** Per-invocation auth header; `-c` config never reaches `.git/config`. */
|
|
148
|
+
function gitAuthFlag$1(tokenEnv) {
|
|
149
|
+
return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
|
|
150
|
+
}
|
|
151
|
+
function shellQuote$1(value) {
|
|
152
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Shell scripts for the file operations a sandbox can only do atomically (or
|
|
156
|
+
* without moving file bytes over the network) by running a command inside the
|
|
157
|
+
* VM. Each builder takes absolute paths and returns one POSIX `sh` script; the
|
|
158
|
+
* caller runs it with `sh -c` and maps the exit codes below to typed errors.
|
|
159
|
+
*/
|
|
160
|
+
/** Exit codes the scripts use for outcomes the caller turns into typed errors. Any other non-zero code is a plain failure. */
|
|
161
|
+
const FILE_COMMAND_EXIT = {
|
|
162
|
+
notFound: 20,
|
|
163
|
+
isDirectory: 21,
|
|
164
|
+
exists: 22,
|
|
165
|
+
notDirectory: 23,
|
|
166
|
+
destIsDirectory: 24
|
|
167
|
+
};
|
|
168
|
+
/** Single-quote a string for safe POSIX shell interpolation. */
|
|
169
|
+
function shellQuote$3(value) {
|
|
170
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
171
|
+
}
|
|
172
|
+
const { notFound, isDirectory, exists, notDirectory, destIsDirectory } = FILE_COMMAND_EXIT;
|
|
173
|
+
/** `[ -e ]` is false for a broken symlink, which still occupies the path. */
|
|
174
|
+
const present = (variable) => `{ [ -e "$${variable}" ] || [ -L "$${variable}" ]; }`;
|
|
175
|
+
/**
|
|
176
|
+
* A sibling temp path for `path`, so a later `ln` / `mv` onto `path` stays on
|
|
177
|
+
* one filesystem. Hidden, and unique per call.
|
|
178
|
+
*/
|
|
179
|
+
function siblingTempPath(path) {
|
|
180
|
+
const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
181
|
+
return posix.join(posix.dirname(path), `.${posix.basename(path)}.mastra-tmp-${suffix}`);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Hard-link `$tmp` to `$dest` and delete `$tmp`, failing with `exists` when
|
|
185
|
+
* something is at `$dest`. `ln` onto a directory (or a link to one) would
|
|
186
|
+
* create the link inside it and succeed, so that case is undone and reported
|
|
187
|
+
* as taken too.
|
|
188
|
+
*/
|
|
189
|
+
function linkTempIntoPlace() {
|
|
190
|
+
return [
|
|
191
|
+
`ln "$tmp" "$dest" 2>/dev/null || { rm -f "$tmp"; ${present("dest")} && exit ${exists}; exit 1; }`,
|
|
192
|
+
`if [ -d "$dest" ]; then rm -f "$dest/$(basename "$tmp")" "$tmp"; exit ${exists}; fi`,
|
|
193
|
+
`rm -f "$tmp"`
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Copy `src` to `dest` inside the sandbox, creating `dest`'s parent.
|
|
198
|
+
*
|
|
199
|
+
* A file source copies its content (a symlinked source is followed). A
|
|
200
|
+
* directory source with `recursive` copies its contents into `dest`, keeping
|
|
201
|
+
* symlinks inside the tree as links. With `overwrite: false` the destination
|
|
202
|
+
* is claimed atomically: a directory with an exclusive `mkdir`, a file by
|
|
203
|
+
* copying to a temp name and hard-linking it into place (`link(2)` fails when
|
|
204
|
+
* the destination exists).
|
|
205
|
+
*
|
|
206
|
+
* Exits: `notFound` (no source), `isDirectory` (directory source without
|
|
207
|
+
* `recursive`), `destIsDirectory` (a file onto an existing directory or a link
|
|
208
|
+
* to one), `exists`.
|
|
209
|
+
*/
|
|
210
|
+
function copyCommand(src, dest, options = {}) {
|
|
211
|
+
const noClobber = options.overwrite === false;
|
|
212
|
+
return [
|
|
213
|
+
`src=${shellQuote$3(src)}`,
|
|
214
|
+
`dest=${shellQuote$3(dest)}`,
|
|
215
|
+
`[ -e "$src" ] || exit ${notFound}`,
|
|
216
|
+
`mkdir -p "$(dirname "$dest")" || exit 1`,
|
|
217
|
+
`if [ -d "$src" ]; then`,
|
|
218
|
+
options.recursive ? ` :` : ` exit ${isDirectory}`,
|
|
219
|
+
noClobber ? ` mkdir "$dest" 2>/dev/null || exit ${exists}` : ` mkdir -p "$dest" || exit 1`,
|
|
220
|
+
` cp -R -P "$src"/. "$dest"/ || exit 1`,
|
|
221
|
+
` exit 0`,
|
|
222
|
+
`fi`,
|
|
223
|
+
noClobber ? `${present("dest")} && exit ${exists}` : `if [ -d "$dest" ]; then exit ${destIsDirectory}; fi`,
|
|
224
|
+
...noClobber ? [
|
|
225
|
+
`tmp=${shellQuote$3(siblingTempPath(dest))}`,
|
|
226
|
+
`cp "$src" "$tmp" || { rm -f "$tmp"; exit 1; }`,
|
|
227
|
+
...linkTempIntoPlace()
|
|
228
|
+
] : [`cp "$src" "$dest" || exit 1`]
|
|
229
|
+
].join("\n");
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Move `src` to `dest` without replacing anything at `dest`, creating `dest`'s
|
|
233
|
+
* parent. A skipped `mv -n` is detected by the source surviving, whatever
|
|
234
|
+
* its exit code. If a directory appears at `dest` between the check and the
|
|
235
|
+
* move, `mv` moves `src` into it (BSD `mv` has no `-T`), and that is reported
|
|
236
|
+
* as success; the no-clobber rename itself is atomic.
|
|
237
|
+
*
|
|
238
|
+
* Exits: `notFound` (no source), `exists`.
|
|
239
|
+
*/
|
|
240
|
+
function moveNoClobberCommand(src, dest) {
|
|
241
|
+
return [
|
|
242
|
+
`src=${shellQuote$3(src)}`,
|
|
243
|
+
`dest=${shellQuote$3(dest)}`,
|
|
244
|
+
`${present("src")} || exit ${notFound}`,
|
|
245
|
+
`${present("dest")} && exit ${exists}`,
|
|
246
|
+
`mkdir -p "$(dirname "$dest")" || exit 1`,
|
|
247
|
+
`mv -n "$src" "$dest" 2>/dev/null || { ${present("src")} && ${present("dest")} && exit ${exists}; exit 1; }`,
|
|
248
|
+
`${present("src")} && exit ${exists}`,
|
|
249
|
+
`exit 0`
|
|
250
|
+
].join("\n");
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Append the bytes already written to `tmp` onto `dest`, then delete `tmp`.
|
|
254
|
+
* The content travels as a file rather than a command argument, so it has no
|
|
255
|
+
* size limit. Creates `dest` (and its parent) when missing.
|
|
256
|
+
*
|
|
257
|
+
* Exits: `isDirectory` (dest is a directory).
|
|
258
|
+
*/
|
|
259
|
+
function appendFromCommand(tmp, dest) {
|
|
260
|
+
return [
|
|
261
|
+
`tmp=${shellQuote$3(tmp)}`,
|
|
262
|
+
`dest=${shellQuote$3(dest)}`,
|
|
263
|
+
`if [ -d "$dest" ]; then rm -f "$tmp"; exit ${isDirectory}; fi`,
|
|
264
|
+
`mkdir -p "$(dirname "$dest")" && cat "$tmp" >> "$dest"`,
|
|
265
|
+
`status=$?`,
|
|
266
|
+
`rm -f "$tmp"`,
|
|
267
|
+
`exit $status`
|
|
268
|
+
].join("\n");
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Publish the file already written to `tmp` at `dest` only if nothing is
|
|
272
|
+
* there, then delete `tmp`. `ln` (`link(2)`) is the atomic exclusivity check;
|
|
273
|
+
* `tmp` must be on the same filesystem, e.g. from {@link siblingTempPath}.
|
|
274
|
+
*
|
|
275
|
+
* Exits: `exists`, `notDirectory` (dest's parent is not a directory).
|
|
276
|
+
*/
|
|
277
|
+
function linkNoClobberCommand(tmp, dest) {
|
|
278
|
+
return [
|
|
279
|
+
`tmp=${shellQuote$3(tmp)}`,
|
|
280
|
+
`dest=${shellQuote$3(dest)}`,
|
|
281
|
+
`if [ ! -d "$(dirname "$dest")" ]; then rm -f "$tmp"; exit ${notDirectory}; fi`,
|
|
282
|
+
`if ${present("dest")}; then rm -f "$tmp"; exit ${exists}; fi`,
|
|
283
|
+
...linkTempIntoPlace()
|
|
284
|
+
].join("\n");
|
|
285
|
+
}
|
|
286
|
+
/** Default recursion depth for `readdir({ recursive: true })`, matching `LocalFilesystem`. */
|
|
287
|
+
const DEFAULT_MAX_DEPTH = 100;
|
|
288
|
+
const toAbsolute = (path) => posix.resolve("/", path);
|
|
289
|
+
/** The entry's own type: a symlink is a link even when it points at a directory. */
|
|
290
|
+
const isSymlink = (info) => info.symlinkTarget !== void 0;
|
|
291
|
+
function toWriteData(content) {
|
|
292
|
+
if (typeof content === "string") return content;
|
|
293
|
+
return content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
|
|
294
|
+
}
|
|
295
|
+
function matchesExtension(name, extension) {
|
|
296
|
+
if (!extension) return true;
|
|
297
|
+
const ext = posix.extname(name);
|
|
298
|
+
return (Array.isArray(extension) ? extension : [extension]).some((e) => e === ext || e === ext.slice(1));
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* A sandbox's disk, read and written through E2B's envd files API, with the
|
|
302
|
+
* operations the files API can't do in one call (copy, append, no-clobber
|
|
303
|
+
* writes and moves) run as one shell command inside the VM so they are atomic
|
|
304
|
+
* and file bytes never round-trip through the host. Paths are absolute inside
|
|
305
|
+
* the sandbox; this class does not confine paths or symlinks to any root.
|
|
306
|
+
*
|
|
307
|
+
* Providers supply how to reach the files API and the shell, and how to read
|
|
308
|
+
* their SDK's errors.
|
|
309
|
+
*
|
|
310
|
+
* If a provider's `runCommand` re-runs a command whose first run already
|
|
311
|
+
* finished (e.g. a retry after a dropped connection), the second run sees the
|
|
312
|
+
* first's result: a no-clobber copy, write or move reports `FileExistsError`
|
|
313
|
+
* or `FileNotFoundError`, and an append fails, even though the work was done.
|
|
314
|
+
*/
|
|
315
|
+
var FilesApiFilesystem = class extends MastraFilesystem {
|
|
316
|
+
async readFile(path, options) {
|
|
317
|
+
const abs = toAbsolute(path);
|
|
318
|
+
const bytes = await this.runFiles("readFile", async (files) => {
|
|
319
|
+
try {
|
|
320
|
+
return await files.read(abs, { format: "bytes" });
|
|
321
|
+
} catch (error) {
|
|
322
|
+
const kind = this.errorKind(error);
|
|
323
|
+
if (kind === "not-found") throw new FileNotFoundError(path);
|
|
324
|
+
if (kind === "invalid-argument") throw new IsDirectoryError(path);
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
const buffer = Buffer$1.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
329
|
+
return options?.encoding ? buffer.toString(options.encoding) : buffer;
|
|
330
|
+
}
|
|
331
|
+
async writeFile(path, content, options) {
|
|
332
|
+
const abs = toAbsolute(path);
|
|
333
|
+
if (options?.overwrite !== false) {
|
|
334
|
+
await this.runFiles("writeFile", async (files) => {
|
|
335
|
+
if (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);
|
|
336
|
+
await this.#write(files, abs, path, toWriteData(content));
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const tmp = siblingTempPath(abs);
|
|
341
|
+
const upload = async (files) => {
|
|
342
|
+
if (options.recursive === false) await this.#assertParentDirectory(files, abs, path);
|
|
343
|
+
await this.#write(files, tmp, path, toWriteData(content));
|
|
344
|
+
};
|
|
345
|
+
await this.#publishTemp(tmp, "writeFile", upload, linkNoClobberCommand(tmp, abs), path, {
|
|
346
|
+
exists: () => new FileExistsError(path),
|
|
347
|
+
notDirectory: () => new NotDirectoryError(posix.dirname(path))
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
async appendFile(path, content) {
|
|
351
|
+
const abs = toAbsolute(path);
|
|
352
|
+
const tmp = siblingTempPath(abs);
|
|
353
|
+
const upload = (files) => this.#write(files, tmp, path, toWriteData(content));
|
|
354
|
+
await this.#publishTemp(tmp, "appendFile", upload, appendFromCommand(tmp, abs), path, { isDirectory: () => new IsDirectoryError(path) });
|
|
355
|
+
}
|
|
356
|
+
async deleteFile(path, options) {
|
|
357
|
+
const abs = toAbsolute(path);
|
|
358
|
+
await this.runFiles("deleteFile", async (files) => {
|
|
359
|
+
const info = await this.#info(files, abs);
|
|
360
|
+
if (!info) {
|
|
361
|
+
if (options?.force) return;
|
|
362
|
+
throw new FileNotFoundError(path);
|
|
363
|
+
}
|
|
364
|
+
if (!isSymlink(info) && info.type === "dir") throw new IsDirectoryError(path);
|
|
365
|
+
await files.remove(abs);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* One `cp` inside the VM. A recursive copy keeps symlinks inside the tree as
|
|
370
|
+
* links; `overwrite: false` claims the destination atomically.
|
|
371
|
+
*/
|
|
372
|
+
async copyFile(src, dest, options) {
|
|
373
|
+
const command = copyCommand(toAbsolute(src), toAbsolute(dest), {
|
|
374
|
+
recursive: options?.recursive,
|
|
375
|
+
overwrite: options?.overwrite
|
|
376
|
+
});
|
|
377
|
+
await this.#command("copyFile", command, `${src} -> ${dest}`, {
|
|
378
|
+
notFound: () => new FileNotFoundError(src),
|
|
379
|
+
isDirectory: () => new IsDirectoryError(src),
|
|
380
|
+
destIsDirectory: () => new IsDirectoryError(dest),
|
|
381
|
+
exists: () => new FileExistsError(dest)
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
async moveFile(src, dest, options) {
|
|
385
|
+
const srcAbs = toAbsolute(src);
|
|
386
|
+
const destAbs = toAbsolute(dest);
|
|
387
|
+
if (options?.overwrite === false) {
|
|
388
|
+
await this.#command("moveFile", moveNoClobberCommand(srcAbs, destAbs), `${src} -> ${dest}`, {
|
|
389
|
+
notFound: () => new FileNotFoundError(src),
|
|
390
|
+
exists: () => new FileExistsError(dest)
|
|
391
|
+
});
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
await this.runFiles("moveFile", async (files) => {
|
|
395
|
+
try {
|
|
396
|
+
await files.rename(srcAbs, destAbs);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (this.errorKind(error) === "not-found") throw new FileNotFoundError(src);
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
async mkdir(path, options) {
|
|
404
|
+
const abs = toAbsolute(path);
|
|
405
|
+
await this.runFiles("mkdir", async (files) => {
|
|
406
|
+
if (options?.recursive === false) await this.#assertParentDirectory(files, abs, path);
|
|
407
|
+
try {
|
|
408
|
+
await files.makeDir(abs);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (this.errorKind(error) === "invalid-argument") throw new FileExistsError(path);
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
async rmdir(path, options) {
|
|
416
|
+
const abs = toAbsolute(path);
|
|
417
|
+
await this.runFiles("rmdir", async (files) => {
|
|
418
|
+
const info = await this.#info(files, abs);
|
|
419
|
+
if (!info) {
|
|
420
|
+
if (options?.force) return;
|
|
421
|
+
throw new DirectoryNotFoundError(path);
|
|
422
|
+
}
|
|
423
|
+
if (!options?.recursive) {
|
|
424
|
+
if (isSymlink(info) || info.type !== "dir") throw new NotDirectoryError(path);
|
|
425
|
+
if ((await files.list(abs)).length > 0) throw new DirectoryNotEmptyError(path);
|
|
426
|
+
} else if (!isSymlink(info) && info.type !== "dir") throw new NotDirectoryError(path);
|
|
427
|
+
await files.remove(abs);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
async readdir(path, options) {
|
|
431
|
+
const abs = toAbsolute(path);
|
|
432
|
+
const depth = options?.recursive ? (options.maxDepth ?? DEFAULT_MAX_DEPTH) + 1 : 1;
|
|
433
|
+
const entries = await this.runFiles("readdir", async (files) => {
|
|
434
|
+
try {
|
|
435
|
+
return await files.list(abs, { depth });
|
|
436
|
+
} catch (error) {
|
|
437
|
+
const kind = this.errorKind(error);
|
|
438
|
+
if (kind === "not-found") throw new DirectoryNotFoundError(path);
|
|
439
|
+
if (kind === "invalid-argument") throw new NotDirectoryError(path);
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
const result = [];
|
|
444
|
+
for (const entry of entries) {
|
|
445
|
+
if (!entry.type) continue;
|
|
446
|
+
const name = posix.relative(abs, entry.path);
|
|
447
|
+
const type = entry.type === "dir" ? "directory" : "file";
|
|
448
|
+
if (type === "file" && !matchesExtension(name, options?.extension)) continue;
|
|
449
|
+
const link = isSymlink(entry);
|
|
450
|
+
result.push({
|
|
451
|
+
name,
|
|
452
|
+
type,
|
|
453
|
+
...type === "file" && !link && { size: entry.size },
|
|
454
|
+
...link && {
|
|
455
|
+
isSymlink: true,
|
|
456
|
+
symlinkTarget: entry.symlinkTarget
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
async exists(path) {
|
|
463
|
+
const abs = toAbsolute(path);
|
|
464
|
+
return this.runFiles("exists", async (files) => Boolean((await this.#info(files, abs))?.type));
|
|
465
|
+
}
|
|
466
|
+
async stat(path) {
|
|
467
|
+
const abs = toAbsolute(path);
|
|
468
|
+
return this.runFiles("stat", async (files) => {
|
|
469
|
+
let info = await this.#info(files, abs);
|
|
470
|
+
if (info?.type && isSymlink(info)) info = await this.#info(files, posix.resolve(posix.dirname(abs), info.symlinkTarget));
|
|
471
|
+
if (!info?.type) throw new FileNotFoundError(path);
|
|
472
|
+
const modifiedAt = info.modifiedTime ?? /* @__PURE__ */ new Date(0);
|
|
473
|
+
return {
|
|
474
|
+
name: posix.basename(abs),
|
|
475
|
+
path: abs,
|
|
476
|
+
type: info.type === "dir" ? "directory" : "file",
|
|
477
|
+
size: info.type === "dir" ? 0 : info.size,
|
|
478
|
+
createdAt: modifiedAt,
|
|
479
|
+
modifiedAt
|
|
480
|
+
};
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
/** `getInfo` without following the entry itself, or `undefined` when nothing is there. */
|
|
484
|
+
async #info(files, abs) {
|
|
485
|
+
try {
|
|
486
|
+
return await files.getInfo(abs);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
if (this.errorKind(error) === "not-found") return void 0;
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async #assertParentDirectory(files, abs, path) {
|
|
493
|
+
const parent = await this.#info(files, posix.dirname(abs));
|
|
494
|
+
if (!parent?.type) throw new DirectoryNotFoundError(posix.dirname(path));
|
|
495
|
+
if (parent.type !== "dir") throw new NotDirectoryError(posix.dirname(path));
|
|
496
|
+
}
|
|
497
|
+
async #write(files, abs, path, data) {
|
|
498
|
+
try {
|
|
499
|
+
await files.write(abs, data);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
if (this.errorKind(error) === "invalid-argument") throw new IsDirectoryError(path);
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Upload the temp file `tmp`, then run the command that consumes it. The
|
|
507
|
+
* script deletes it on every path it controls; if the upload or the command
|
|
508
|
+
* fails part-way, delete it here so it doesn't linger in the user's tree.
|
|
509
|
+
*/
|
|
510
|
+
async #publishTemp(tmp, operation, upload, script, subject, errors) {
|
|
511
|
+
try {
|
|
512
|
+
await this.runFiles(operation, upload);
|
|
513
|
+
await this.#command(operation, script, subject, errors);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
await this.runFiles(operation, (files) => files.remove(tmp)).catch(() => {});
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/** Run a file command and turn its known exit codes into typed errors. */
|
|
520
|
+
async #command(operation, script, subject, errors) {
|
|
521
|
+
const result = await this.runCommand(operation, script);
|
|
522
|
+
if (result.exitCode === 0) return;
|
|
523
|
+
for (const [outcome, code] of Object.entries(FILE_COMMAND_EXIT)) {
|
|
524
|
+
const toError = errors[outcome];
|
|
525
|
+
if (code === result.exitCode && toError) throw toError();
|
|
526
|
+
}
|
|
527
|
+
throw new Error(`${operation} ${subject} failed (exit ${result.exitCode}): ${result.stderr.trim()}`);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
//#endregion
|
|
531
|
+
//#region src/sandbox/filesystem.ts
|
|
532
|
+
/**
|
|
533
|
+
* envd's edge answers a paused or missing sandbox with a 404 that the SDK
|
|
534
|
+
* reports as `FileNotFoundError`. Same pattern as `E2BSandbox`'s dead-sandbox
|
|
535
|
+
* check, so those errors reach `retryOnDead` instead of reading as a missing file.
|
|
536
|
+
*/
|
|
537
|
+
const DEAD_SANDBOX = /\b(?:paused\s+)?sandbox(?:\s+\S+)?\s+(?:was\s+)?not found\b/i;
|
|
538
|
+
/**
|
|
539
|
+
* The disk of an {@link E2BSandbox}, read and written through E2B's files API
|
|
540
|
+
* (`sandbox.e2b.files`). Each operation starts the sandbox if needed, and each
|
|
541
|
+
* files call is retried once on a sandbox that died server-side, like
|
|
542
|
+
* `executeCommand`. Copies, appends and no-clobber writes and moves run as one
|
|
543
|
+
* shell command in the VM, so they are atomic and file bytes don't round-trip
|
|
544
|
+
* through the host. Paths are absolute inside the sandbox; this class does not
|
|
545
|
+
* confine paths or symlinks to any root.
|
|
546
|
+
*/
|
|
547
|
+
var E2BSandboxFilesystem = class extends FilesApiFilesystem {
|
|
548
|
+
id;
|
|
549
|
+
name = "E2BSandboxFilesystem";
|
|
550
|
+
provider = "e2b";
|
|
551
|
+
status = "ready";
|
|
552
|
+
#sandbox;
|
|
553
|
+
constructor(sandbox) {
|
|
554
|
+
super({ name: "E2BSandboxFilesystem" });
|
|
555
|
+
this.id = `${sandbox.id}-filesystem`;
|
|
556
|
+
this.#sandbox = sandbox;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Wrap each files call in `retryOnDead`, so a sandbox that died mid-operation
|
|
560
|
+
* is restarted and only the call that failed runs again; the calls before it
|
|
561
|
+
* are never replayed. If the sandbox was killed and replaced rather than
|
|
562
|
+
* paused, the rest of a multi-call operation runs against the new sandbox's disk.
|
|
563
|
+
*/
|
|
564
|
+
async runFiles(_operation, fn) {
|
|
565
|
+
await this.#sandbox.ensureRunning();
|
|
566
|
+
const call = (invoke) => this.#sandbox.retryOnDead(() => invoke(this.#sandbox.e2b.files));
|
|
567
|
+
return fn({
|
|
568
|
+
read: (path) => call((files) => files.read(path, { format: "bytes" })),
|
|
569
|
+
write: (path, data) => call((files) => files.write(path, data)),
|
|
570
|
+
list: (path, opts) => call((files) => files.list(path, opts)),
|
|
571
|
+
makeDir: (path) => call((files) => files.makeDir(path)),
|
|
572
|
+
rename: (oldPath, newPath) => call((files) => files.rename(oldPath, newPath)),
|
|
573
|
+
remove: (path) => call((files) => files.remove(path)),
|
|
574
|
+
getInfo: (path) => call((files) => files.getInfo(path))
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
runCommand(operation, script) {
|
|
578
|
+
if (!this.#sandbox.executeCommand) throw new Error(`${operation} needs E2BSandbox.executeCommand`);
|
|
579
|
+
return this.#sandbox.executeCommand("sh", ["-c", script]);
|
|
580
|
+
}
|
|
581
|
+
errorKind(error) {
|
|
582
|
+
if (error instanceof NotFoundError && !(error instanceof SandboxNotFoundError) && !(error instanceof Error && DEAD_SANDBOX.test(error.message))) return "not-found";
|
|
583
|
+
if (error instanceof InvalidArgumentError) return "invalid-argument";
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
//#endregion
|
|
77
587
|
//#region src/sandbox/mounts/types.ts
|
|
78
588
|
const LOG_PREFIX = "[@mastra/e2b]";
|
|
79
589
|
const SAFE_S3_BUCKET_NAME = /^[a-z0-9][a-z0-9.\-]{1,61}[a-z0-9]$/;
|
|
@@ -126,7 +636,7 @@ function validatePrefix(prefix) {
|
|
|
126
636
|
* Arguments containing only safe characters are returned as-is.
|
|
127
637
|
* All others are wrapped in single quotes with embedded single quotes escaped.
|
|
128
638
|
*/
|
|
129
|
-
function shellQuote
|
|
639
|
+
function shellQuote(arg) {
|
|
130
640
|
if (/^[a-zA-Z0-9._\-/@:=]+$/.test(arg)) return arg;
|
|
131
641
|
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
132
642
|
}
|
|
@@ -179,12 +689,12 @@ Error details: ${installResult.stderr || installResult.stdout}`);
|
|
|
179
689
|
};
|
|
180
690
|
if (hasCredentials) {
|
|
181
691
|
const credentialsContent = useSessionToken ? [
|
|
182
|
-
`export AWSACCESSKEYID=${shellQuote
|
|
183
|
-
`export AWSSECRETACCESSKEY=${shellQuote
|
|
184
|
-
`export AWSSESSIONTOKEN=${shellQuote
|
|
185
|
-
`export AWS_ACCESS_KEY_ID=${shellQuote
|
|
186
|
-
`export AWS_SECRET_ACCESS_KEY=${shellQuote
|
|
187
|
-
`export AWS_SESSION_TOKEN=${shellQuote
|
|
692
|
+
`export AWSACCESSKEYID=${shellQuote(config.accessKeyId)}`,
|
|
693
|
+
`export AWSSECRETACCESSKEY=${shellQuote(config.secretAccessKey)}`,
|
|
694
|
+
`export AWSSESSIONTOKEN=${shellQuote(config.sessionToken)}`,
|
|
695
|
+
`export AWS_ACCESS_KEY_ID=${shellQuote(config.accessKeyId)}`,
|
|
696
|
+
`export AWS_SECRET_ACCESS_KEY=${shellQuote(config.secretAccessKey)}`,
|
|
697
|
+
`export AWS_SESSION_TOKEN=${shellQuote(config.sessionToken)}`
|
|
188
698
|
].join("\n") : `${config.accessKeyId}:${config.secretAccessKey}`;
|
|
189
699
|
await sandbox.commands.run(`sudo rm -f ${credentialsPath}`);
|
|
190
700
|
try {
|
|
@@ -212,8 +722,8 @@ Error details: ${installResult.stderr || installResult.stdout}`);
|
|
|
212
722
|
mountOptions.push("ro");
|
|
213
723
|
logger.debug(`${LOG_PREFIX} Mounting as read-only`);
|
|
214
724
|
}
|
|
215
|
-
const s3fsCmd = `s3fs ${shellQuote
|
|
216
|
-
const mountCmd = useSessionToken ? `sudo sh -c ${shellQuote
|
|
725
|
+
const s3fsCmd = `s3fs ${shellQuote(bucketArg)} ${shellQuote(mountPath)} -o ${mountOptions.join(" -o ")}`;
|
|
726
|
+
const mountCmd = useSessionToken ? `sudo sh -c ${shellQuote(`. ${credentialsPath} && exec ${s3fsCmd}`)}` : `sudo ${s3fsCmd}`;
|
|
217
727
|
logger.debug(`${LOG_PREFIX} Mounting S3:`, hasCredentials ? mountCmd.replace(credentialsPath, "***") : mountCmd);
|
|
218
728
|
try {
|
|
219
729
|
const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
|
|
@@ -236,7 +746,7 @@ Error details: ${installResult.stderr || installResult.stdout}`);
|
|
|
236
746
|
} finally {
|
|
237
747
|
if (useSessionToken) await removeSessionCredentials();
|
|
238
748
|
}
|
|
239
|
-
if ((await sandbox.commands.run(`mountpoint -q ${shellQuote
|
|
749
|
+
if ((await sandbox.commands.run(`mountpoint -q ${shellQuote(mountPath)}`)).exitCode !== 0) throw new Error(`s3fs returned exit 0 but ${mountPath} is not a mountpoint. The s3fs daemon likely failed during FUSE init (common causes: region mismatch, invalid credentials, or an S3-compatible endpoint that rejects the signature). Re-run inside the sandbox with '-f -o dbglevel=info' to see the underlying error.`);
|
|
240
750
|
}
|
|
241
751
|
//#endregion
|
|
242
752
|
//#region src/sandbox/mounts/gcs.ts
|
|
@@ -256,7 +766,7 @@ async function mountGCS(mountPath, config, ctx) {
|
|
|
256
766
|
}
|
|
257
767
|
const [uid, gid] = (await sandbox.commands.run("id -u && id -g")).stdout.trim().split("\n");
|
|
258
768
|
const uidGidFlags = uid && gid ? `--uid=${uid} --gid=${gid}` : "";
|
|
259
|
-
const onlyDirFlag = config.prefix ? ` --only-dir=${shellQuote
|
|
769
|
+
const onlyDirFlag = config.prefix ? ` --only-dir=${shellQuote(validatePrefix(config.prefix))}` : "";
|
|
260
770
|
const hasCredentials = !!config.serviceAccountKey;
|
|
261
771
|
let mountCmd;
|
|
262
772
|
if (hasCredentials) {
|
|
@@ -464,9 +974,9 @@ Error details: ${verifyResult.stderr || verifyResult.stdout || installResult?.st
|
|
|
464
974
|
await sandbox.commands.run(`sudo rm -f ${configPath}`);
|
|
465
975
|
await sandbox.files.write(configPath, yaml);
|
|
466
976
|
await sandbox.commands.run(`sudo chown root:root ${configPath} && sudo chmod 600 ${configPath}`);
|
|
467
|
-
await sandbox.commands.run(`sudo rm -rf ${shellQuote
|
|
468
|
-
const prefixFlags = prefix ? ` --virtual-directory=true --subdirectory=${shellQuote
|
|
469
|
-
const mountCmd = `sudo blobfuse2 mount ${shellQuote
|
|
977
|
+
await sandbox.commands.run(`sudo rm -rf ${shellQuote(cachePath)} && sudo mkdir -p ${shellQuote(cachePath)}`);
|
|
978
|
+
const prefixFlags = prefix ? ` --virtual-directory=true --subdirectory=${shellQuote(prefix)}` : "";
|
|
979
|
+
const mountCmd = `sudo blobfuse2 mount ${shellQuote(mountPath)} --config-file=${shellQuote(configPath)}${prefixFlags}`;
|
|
470
980
|
logger.debug(`${LOG_PREFIX} Mounting Azure Blob:`, mountCmd);
|
|
471
981
|
try {
|
|
472
982
|
const result = await sandbox.commands.run(mountCmd, { timeoutMs: 6e4 });
|
|
@@ -705,6 +1215,7 @@ var E2BSandbox = class E2BSandbox extends MastraSandbox {
|
|
|
705
1215
|
_sandbox = null;
|
|
706
1216
|
_createdAt = null;
|
|
707
1217
|
_isRetrying = false;
|
|
1218
|
+
_filesystem;
|
|
708
1219
|
timeout;
|
|
709
1220
|
templateSpec;
|
|
710
1221
|
metadata;
|
|
@@ -977,6 +1488,15 @@ var E2BSandbox = class E2BSandbox extends MastraSandbox {
|
|
|
977
1488
|
};
|
|
978
1489
|
}
|
|
979
1490
|
/**
|
|
1491
|
+
* The sandbox's disk through E2B's files API: one request per operation
|
|
1492
|
+
* instead of a shell command. Paths are absolute inside the sandbox and are
|
|
1493
|
+
* not confined to any root.
|
|
1494
|
+
*/
|
|
1495
|
+
get filesystem() {
|
|
1496
|
+
this._filesystem ??= new E2BSandboxFilesystem(this);
|
|
1497
|
+
return this._filesystem;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
980
1500
|
* Bulk-write files into the sandbox filesystem via the SDK's native upload.
|
|
981
1501
|
*
|
|
982
1502
|
* Per-file permission modes are not supported; an explicit `mode` is
|
|
@@ -1607,82 +2127,6 @@ var E2BSandbox = class E2BSandbox extends MastraSandbox {
|
|
|
1607
2127
|
}
|
|
1608
2128
|
};
|
|
1609
2129
|
//#endregion
|
|
1610
|
-
//#region ../../packages/_internals/workspace/dist/index.js
|
|
1611
|
-
/** Directory, relative to the template's build cwd, that holds every marker below. */
|
|
1612
|
-
const SETUP_MARKER_DIR = ".mastra-sandbox";
|
|
1613
|
-
/**
|
|
1614
|
-
* Setup completion marker shared by repo templates and their consumers.
|
|
1615
|
-
*
|
|
1616
|
-
* A repo template writes this file beside the checkout as its last build
|
|
1617
|
-
* step, so it exists only in images where every setup command succeeded. Its
|
|
1618
|
-
* content is a digest of the setup commands the image ran, letting a sandbox
|
|
1619
|
-
* booted from the image tell whether the setup it is about to run already
|
|
1620
|
-
* happened. Relative to the template's build cwd, which is also the runtime
|
|
1621
|
-
* working directory the repo was cloned into.
|
|
1622
|
-
*
|
|
1623
|
-
* This is the single-repository marker. Templates built from a repository
|
|
1624
|
-
* list write one marker per repository instead ({@link repoSetupMarkerPath}).
|
|
1625
|
-
*/
|
|
1626
|
-
const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
|
|
1627
|
-
/**
|
|
1628
|
-
* Marker for the workspace-level setup of a multi-repository template: the
|
|
1629
|
-
* commands that ran at the working directory after every repository was set
|
|
1630
|
-
* up. Written even when there were no such commands, so a consumer can tell
|
|
1631
|
-
* "ran nothing" from "never built".
|
|
1632
|
-
*/
|
|
1633
|
-
const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
|
|
1634
|
-
/**
|
|
1635
|
-
* One line per repository directory whose setup failed while the build was
|
|
1636
|
-
* told to continue. Absent when nothing failed. Consumers check this before
|
|
1637
|
-
* trusting a per-repository marker: that marker means every step ran, not
|
|
1638
|
-
* that every step succeeded.
|
|
1639
|
-
*/
|
|
1640
|
-
const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
|
|
1641
|
-
/** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
|
|
1642
|
-
function repoSetupMarkerPath(repoDir) {
|
|
1643
|
-
return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
|
|
1644
|
-
}
|
|
1645
|
-
/** Blank entries never become build steps, so they never count toward the digest either. */
|
|
1646
|
-
function normalizeSetupCommands$1(setupCommand) {
|
|
1647
|
-
return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
|
|
1648
|
-
}
|
|
1649
|
-
/** The marker content for a setup command list: `sha256:<hex>` over the commands joined by newlines. */
|
|
1650
|
-
function setupMarkerContent(setupCommand) {
|
|
1651
|
-
return `sha256:${createHash("sha256").update(normalizeSetupCommands$1(setupCommand).join("\n")).digest("hex")}`;
|
|
1652
|
-
}
|
|
1653
|
-
/** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
|
|
1654
|
-
function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
|
|
1655
|
-
return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
|
|
1656
|
-
}
|
|
1657
|
-
/** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
|
|
1658
|
-
const REPO_DIR_PATTERN = /^[\w.-]+$/;
|
|
1659
|
-
/**
|
|
1660
|
-
* Shell step that runs one setup command inside a repository clone.
|
|
1661
|
-
*
|
|
1662
|
-
* Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
|
|
1663
|
-
* the `cd` sits inside the parenthesized group so that the failure branch
|
|
1664
|
-
* runs back at the build cwd and the failure list lands at the workspace
|
|
1665
|
-
* level, never inside the repository.
|
|
1666
|
-
*/
|
|
1667
|
-
function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
|
|
1668
|
-
if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
|
|
1669
|
-
if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
|
|
1670
|
-
return `( cd "${repoDir}" && sh -c ${shellQuote$1(command)} ) || { mkdir -p "${SETUP_MARKER_DIR}" && grep -qxF -- '${repoDir}' "${SETUP_FAILED_MARKER_PATH}" 2>/dev/null || printf '%s\\n' '${repoDir}' >> "${SETUP_FAILED_MARKER_PATH}"; }`;
|
|
1671
|
-
}
|
|
1672
|
-
function shellQuote$1(value) {
|
|
1673
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1674
|
-
}
|
|
1675
|
-
function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
|
|
1676
|
-
return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : ""}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;
|
|
1677
|
-
}
|
|
1678
|
-
/** Per-invocation auth header; `-c` config never reaches `.git/config`. */
|
|
1679
|
-
function gitAuthFlag$1(tokenEnv) {
|
|
1680
|
-
return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
|
|
1681
|
-
}
|
|
1682
|
-
function shellQuote(value) {
|
|
1683
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1684
|
-
}
|
|
1685
|
-
//#endregion
|
|
1686
2130
|
//#region src/utils/repo-template.ts
|
|
1687
2131
|
/**
|
|
1688
2132
|
* Sha-tagged repo templates.
|
|
@@ -2586,6 +3030,6 @@ var E2BCodeModeTransport = class {
|
|
|
2586
3030
|
}
|
|
2587
3031
|
};
|
|
2588
3032
|
//#endregion
|
|
2589
|
-
export { DEFAULT_NODE_VERSION, E2BCodeModeTransport, E2BFactorySandbox, E2BProcessManager, E2BSandbox, createDefaultMountableTemplate, createRepoTemplate, e2bSandboxProvider, isDeferredNamedTemplateSpec, isNamedTemplateSpec, refreshRepoTemplate, repoTemplateRef };
|
|
3033
|
+
export { DEFAULT_NODE_VERSION, E2BCodeModeTransport, E2BFactorySandbox, E2BProcessManager, E2BSandbox, E2BSandboxFilesystem, createDefaultMountableTemplate, createRepoTemplate, e2bSandboxProvider, isDeferredNamedTemplateSpec, isNamedTemplateSpec, refreshRepoTemplate, repoTemplateRef };
|
|
2590
3034
|
|
|
2591
3035
|
//# sourceMappingURL=index.js.map
|