@mastra/platform-workspace 1.6.2-alpha.0 → 1.7.0-alpha.0

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/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { PlatformClient, PlatformApiError, type PlatformClientOptions, type Plat
2
2
  export { PlatformFilesystem, type PlatformFilesystemOptions } from './filesystem.js';
3
3
  export { Template, type AptInstallOptions, type JsonPrimitive, type JsonValue, type NpmInstallOptions, type PipInstallOptions, type SandboxTemplateBuildOptions, type SandboxTemplateBuildResult, type SandboxTemplateBuilder, type SandboxTemplateMethod, type SetEnvsOptions, type SandboxTemplateOperation, type SerializedSandboxTemplate, } from './template.js';
4
4
  export { PlatformSandbox, SandboxExecTransportError, SandboxDestroyedError, type PlatformSandboxOptions, type PlatformSandboxNetworkIsolation, type PlatformSandboxTemplate, type SandboxAddressRegistry, type SandboxTemplatePending, } from './sandbox.js';
5
- export { createRepoTemplate, type PlatformRepoTemplateOptions, type PlatformRepoTemplateResolver, } from './repo-template.js';
5
+ export { createRepoTemplate, type PlatformRepoTemplateOptions, type PlatformRepoTemplateRepository, type PlatformRepoTemplateResolver, } from './repo-template.js';
6
6
  export { platformFilesystemProvider, platformSandboxProvider } from './provider.js';
7
7
  export { execViaPrivateNetwork, PrivateNetExecHttpError, type PrivateNetExecOptions, type PrivateNetExecResult, type PrivateNetFetch, } from './private-net-exec.js';
8
8
  export { InProcessSandboxAddressRegistry } from './address-registry.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,EACL,QAAQ,EACR,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,qBAAqB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,+BAA+B,EACpC,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,GAClC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,+BAA+B,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,EACL,QAAQ,EACR,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,eAAe,EACf,yBAAyB,EACzB,qBAAqB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,+BAA+B,EACpC,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,KAAK,2BAA2B,EAChC,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,GAClC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,+BAA+B,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import nodePath from "path";
3
3
  import { FileExistsError, FileNotFoundError, MastraFilesystem, MastraSandbox, ProcessHandle, SandboxNotReadyError, SandboxProcessManager, UnsupportedStdinCloseError, WorkspaceReadOnlyError } from "@mastra/core/workspace";
4
4
  import { CommandExitError, Sandbox, TimeoutError } from "e2b";
5
5
  import { execFile } from "child_process";
6
- import { promisify } from "util";
7
6
  import { createHash } from "crypto";
7
+ import { promisify } from "util";
8
8
  //#region src/client.ts
9
9
  const DEFAULT_PROXY_URL = "https://workspaces.mastra.ai";
10
10
  const REGIONAL_PROXY_URLS = {
@@ -1052,9 +1052,9 @@ var SandboxDestroyedError = class extends Error {
1052
1052
  * sandbox.
1053
1053
  */
1054
1054
  function buildCommand(command, args) {
1055
- return args?.length ? `${command} ${args.map(shellQuote$1).join(" ")}` : command;
1055
+ return args?.length ? `${command} ${args.map(shellQuote$2).join(" ")}` : command;
1056
1056
  }
1057
- function shellQuote$1(arg) {
1057
+ function shellQuote$2(arg) {
1058
1058
  if (/^[a-zA-Z0-9._\-/=:@]+$/.test(arg)) return arg;
1059
1059
  return `'${arg.replace(/'/g, `'\\''`)}'`;
1060
1060
  }
@@ -1972,6 +1972,8 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1972
1972
  };
1973
1973
  //#endregion
1974
1974
  //#region ../../packages/_internals/workspace/dist/index.js
1975
+ /** Directory, relative to the template's build cwd, that holds every marker below. */
1976
+ const SETUP_MARKER_DIR = ".mastra-sandbox";
1975
1977
  /**
1976
1978
  * Setup completion marker shared by repo templates and their consumers.
1977
1979
  *
@@ -1981,8 +1983,29 @@ var PlatformSandbox = class PlatformSandbox extends MastraSandbox {
1981
1983
  * booted from the image tell whether the setup it is about to run already
1982
1984
  * happened. Relative to the template's build cwd, which is also the runtime
1983
1985
  * working directory the repo was cloned into.
1986
+ *
1987
+ * This is the single-repository marker. Templates built from a repository
1988
+ * list write one marker per repository instead ({@link repoSetupMarkerPath}).
1984
1989
  */
1985
1990
  const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
1991
+ /**
1992
+ * Marker for the workspace-level setup of a multi-repository template: the
1993
+ * commands that ran at the working directory after every repository was set
1994
+ * up. Written even when there were no such commands, so a consumer can tell
1995
+ * "ran nothing" from "never built".
1996
+ */
1997
+ const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
1998
+ /**
1999
+ * One line per repository directory whose setup failed while the build was
2000
+ * told to continue. Absent when nothing failed. Consumers check this before
2001
+ * trusting a per-repository marker: that marker means every step ran, not
2002
+ * that every step succeeded.
2003
+ */
2004
+ const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
2005
+ /** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
2006
+ function repoSetupMarkerPath(repoDir) {
2007
+ return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
2008
+ }
1986
2009
  /** Blank entries never become build steps, so they never count toward the digest either. */
1987
2010
  function normalizeSetupCommands(setupCommand) {
1988
2011
  return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
@@ -1992,8 +2015,26 @@ function setupMarkerContent(setupCommand) {
1992
2015
  return `sha256:${createHash("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
1993
2016
  }
1994
2017
  /** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
1995
- function setupMarkerCommand(content) {
1996
- return `mkdir -p "$(dirname "${SETUP_MARKER_PATH}")" && printf '%s' '${content}' > "${SETUP_MARKER_PATH}"`;
2018
+ function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
2019
+ return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
2020
+ }
2021
+ /** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
2022
+ const REPO_DIR_PATTERN = /^[\w.-]+$/;
2023
+ /**
2024
+ * Shell step that runs one setup command inside a repository clone.
2025
+ *
2026
+ * Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
2027
+ * the `cd` sits inside the parenthesized group so that the failure branch
2028
+ * runs back at the build cwd and the failure list lands at the workspace
2029
+ * level, never inside the repository.
2030
+ */
2031
+ function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
2032
+ if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
2033
+ if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
2034
+ 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}"; }`;
2035
+ }
2036
+ function shellQuote$1(value) {
2037
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1997
2038
  }
1998
2039
  function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
1999
2040
  return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : ""}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;
@@ -2046,77 +2087,126 @@ function rememberHead(cloneUrl, sha) {
2046
2087
  * requested size, and the caller's runtime setup materializes the checkout.
2047
2088
  */
2048
2089
  function createRepoTemplate(options) {
2049
- const getRepositoryAccess = options.getRepositoryAccess;
2090
+ if (options.getRepositoryAccess && options.repos) throw new TypeError("createRepoTemplate: pass either getRepositoryAccess or repos, not both");
2091
+ if (!options.repos && (options.workspaceSetupCommand !== void 0 || options.continueOnSetupFailure !== void 0)) throw new TypeError("createRepoTemplate: workspaceSetupCommand and continueOnSetupFailure require repos");
2092
+ const isList = options.repos !== void 0;
2093
+ const entries = options.repos ? options.repos : options.getRepositoryAccess ? [{
2094
+ getRepositoryAccess: options.getRepositoryAccess,
2095
+ setupCommand: options.setupCommand
2096
+ }] : [];
2050
2097
  const resourcesOnly = () => {
2051
2098
  if (options.cpuCount === void 0 && options.memoryMB === void 0) return void 0;
2052
2099
  return withResources(Template(), options);
2053
2100
  };
2054
- if (!getRepositoryAccess) {
2101
+ if (entries.length === 0) {
2055
2102
  const template = resourcesOnly();
2056
2103
  return template ? async () => template : void 0;
2057
2104
  }
2058
2105
  const resolveHead = options.resolveHead ?? resolveDefaultBranchHead;
2106
+ const continueOnFailure = options.continueOnSetupFailure ?? false;
2059
2107
  return async () => {
2060
- let accessError;
2061
- const access = await getRepositoryAccess().catch((error) => {
2062
- accessError = error;
2063
- });
2064
- if (!access?.cloneUrl) {
2065
- console.warn("[platform-workspace] repo template skipped: repository access unavailable", { error: redactSecrets(accessError) });
2066
- return resourcesOnly();
2067
- }
2068
- const cloneUrl = normalizeCloneUrl(access.cloneUrl);
2069
- if (!isValidCloneUrl(cloneUrl)) {
2070
- console.warn("[platform-workspace] repo template skipped: clone URL failed validation", { cloneUrl: redactSecrets(cloneUrl) });
2071
- return resourcesOnly();
2072
- }
2073
- const token = access.authorization?.token;
2074
- let headError;
2075
- const resolved = await (token ? resolveHead(cloneUrl, token) : resolveHead(cloneUrl)).catch((error) => {
2076
- headError = error;
2077
- });
2078
- let sha;
2079
- if (resolved && SHA_PATTERN.test(resolved)) {
2080
- sha = resolved;
2081
- rememberHead(cloneUrl, sha);
2082
- } else {
2083
- const lastKnown = lastKnownHeads.get(cloneUrl);
2084
- if (!lastKnown) {
2085
- console.warn("[platform-workspace] repo template skipped: could not resolve default-branch head", {
2108
+ const repos = [];
2109
+ for (const [index, entry] of entries.entries()) {
2110
+ let accessError;
2111
+ const access = await entry.getRepositoryAccess().catch((error) => {
2112
+ accessError = error;
2113
+ });
2114
+ if (!access?.cloneUrl) {
2115
+ console.warn("[platform-workspace] repo template skipped: repository access unavailable", {
2116
+ ...isList ? { index } : {},
2117
+ error: redactSecrets(accessError)
2118
+ });
2119
+ return resourcesOnly();
2120
+ }
2121
+ const cloneUrl = normalizeCloneUrl(access.cloneUrl);
2122
+ if (!isValidCloneUrl(cloneUrl)) {
2123
+ console.warn("[platform-workspace] repo template skipped: clone URL failed validation", {
2124
+ ...isList ? { index } : {},
2125
+ cloneUrl: redactSecrets(cloneUrl)
2126
+ });
2127
+ return resourcesOnly();
2128
+ }
2129
+ const token = access.authorization?.token;
2130
+ let headError;
2131
+ const resolved = await (token ? resolveHead(cloneUrl, token) : resolveHead(cloneUrl)).catch((error) => {
2132
+ headError = error;
2133
+ });
2134
+ let sha;
2135
+ if (resolved && SHA_PATTERN.test(resolved)) {
2136
+ sha = resolved;
2137
+ rememberHead(cloneUrl, sha);
2138
+ } else {
2139
+ const lastKnown = lastKnownHeads.get(cloneUrl);
2140
+ if (lastKnown) {
2141
+ console.warn("[platform-workspace] repo template pinned to the last known default-branch head", {
2142
+ cloneUrl,
2143
+ sha: lastKnown,
2144
+ resolved,
2145
+ error: redactSecrets(headError)
2146
+ });
2147
+ sha = lastKnown;
2148
+ } else if (isList) console.warn("[platform-workspace] repo template clones unpinned: could not resolve default-branch head", {
2086
2149
  cloneUrl,
2087
2150
  sha: resolved,
2088
2151
  error: redactSecrets(headError)
2089
2152
  });
2090
- return resourcesOnly();
2153
+ else {
2154
+ console.warn("[platform-workspace] repo template skipped: could not resolve default-branch head", {
2155
+ cloneUrl,
2156
+ sha: resolved,
2157
+ error: redactSecrets(headError)
2158
+ });
2159
+ return resourcesOnly();
2160
+ }
2091
2161
  }
2092
- console.warn("[platform-workspace] repo template pinned to the last known default-branch head", {
2162
+ const repoDir = repoDirName(cloneUrl);
2163
+ repos.push({
2164
+ index,
2093
2165
  cloneUrl,
2094
- sha: lastKnown,
2095
- resolved,
2096
- error: redactSecrets(headError)
2166
+ repoDir,
2167
+ token,
2168
+ tokenEnv: token ? isList ? `${BUILD_TOKEN_ENV}_${index}` : BUILD_TOKEN_ENV : void 0,
2169
+ sha,
2170
+ setupCommands: normalizeSetupCommands(entry.setupCommand)
2097
2171
  });
2098
- sha = lastKnown;
2172
+ }
2173
+ const seenDirs = /* @__PURE__ */ new Map();
2174
+ for (const repo of repos) {
2175
+ const other = seenDirs.get(repo.repoDir);
2176
+ if (other) throw new Error(`createRepoTemplate: repositories ${other} and ${repo.cloneUrl} would both clone into "${repo.repoDir}"`);
2177
+ seenDirs.set(repo.repoDir, repo.cloneUrl);
2099
2178
  }
2100
2179
  const workingDirectory = options.workingDirectory === void 0 ? void 0 : trimTrailingSlashes(assertWorkingDirectory(options.workingDirectory));
2101
- const repoDir = repoDirName(cloneUrl);
2102
- const auth = token ? `${gitAuthFlag()} ` : "";
2103
- const setupCommands = (options.setupCommand === void 0 ? [] : Array.isArray(options.setupCommand) ? options.setupCommand : [options.setupCommand]).filter((command) => command.trim() !== "");
2104
- const family = `repo:${cloneUrl}:${workingDirectory ?? ""}/${repoDir}`;
2180
+ const family = isList ? `repos:${createHash("sha256").update(JSON.stringify([workingDirectory ?? "", repos.map((repo) => [repo.cloneUrl, repo.repoDir])])).digest("hex").slice(0, 32)}` : `repo:${repos[0].cloneUrl}:${workingDirectory ?? ""}/${repos[0].repoDir}`;
2105
2181
  let template = Template();
2106
- const buildEnv = {
2107
- ...options.buildEnv,
2108
- ...token ? { [BUILD_TOKEN_ENV]: token } : {}
2109
- };
2182
+ const buildEnv = { ...options.buildEnv };
2183
+ for (const repo of repos) if (repo.tokenEnv && repo.token) buildEnv[repo.tokenEnv] = repo.token;
2110
2184
  if (Object.keys(buildEnv).length > 0) template = template.setEnvs(buildEnv, { ephemeral: true });
2111
2185
  template = withResources(template, options);
2112
2186
  if (workingDirectory) template = template.runCmd(`mkdir -p "${workingDirectory}"`).setWorkdir(workingDirectory);
2113
- template = template.runCmd(repoCloneCommand({
2114
- cloneUrl,
2115
- destination: repoDir,
2116
- ...token ? { tokenEnv: BUILD_TOKEN_ENV } : {}
2117
- })).runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
2118
- for (const command of setupCommands) template = template.runCmd(`cd "${repoDir}" && ${command}`);
2119
- template = template.runCmd(setupMarkerCommand(setupMarkerContent(setupCommands)));
2187
+ const ordered = [...repos.filter((repo) => !repo.token), ...repos.filter((repo) => repo.token)];
2188
+ for (const repo of ordered) {
2189
+ const { repoDir, sha, tokenEnv, setupCommands } = repo;
2190
+ const auth = tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : "";
2191
+ template = template.runCmd(repoCloneCommand({
2192
+ cloneUrl: repo.cloneUrl,
2193
+ destination: repoDir,
2194
+ ...tokenEnv ? { tokenEnv } : {}
2195
+ }));
2196
+ if (sha) template = template.runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
2197
+ for (const command of setupCommands) template = template.runCmd(guardedSetupCommand({
2198
+ repoDir,
2199
+ command,
2200
+ continueOnFailure
2201
+ }));
2202
+ const content = setupMarkerContent(setupCommands);
2203
+ template = template.runCmd(isList ? setupMarkerCommand(content, repoSetupMarkerPath(repoDir)) : setupMarkerCommand(content));
2204
+ }
2205
+ if (isList) {
2206
+ const workspaceCommands = normalizeSetupCommands(options.workspaceSetupCommand);
2207
+ for (const command of workspaceCommands) template = template.runCmd(command);
2208
+ template = template.runCmd(setupMarkerCommand(setupMarkerContent(workspaceCommands), WORKSPACE_SETUP_MARKER_PATH));
2209
+ }
2120
2210
  return template.withFamily(family);
2121
2211
  };
2122
2212
  }
@@ -2171,8 +2261,8 @@ function redactSecrets(value) {
2171
2261
  if (value === void 0 || value === null) return void 0;
2172
2262
  return (value instanceof Error ? value.message : typeof value === "string" ? value : String(value)).replace(/\/\/[^/@\s]+@/g, "//***@").replace(/\b(bearer|basic)\s+[^\s"']+/gi, "$1 ***").replace(/\b(gh[pousr]_|github_pat_)[A-Za-z0-9_]+/g, "$1***");
2173
2263
  }
2174
- function gitAuthFlag() {
2175
- return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${BUILD_TOKEN_ENV}" | base64 -w0)"`;
2264
+ function gitAuthFlag(tokenEnv) {
2265
+ return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
2176
2266
  }
2177
2267
  /**
2178
2268
  * `owner/repo` for a github.com clone URL, else undefined. Only the public