@mastra/platform-workspace 1.6.2 → 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.cjs CHANGED
@@ -27,8 +27,8 @@ path = __toESM(path, 1);
27
27
  let _mastra_core_workspace = require("@mastra/core/workspace");
28
28
  let e2b = require("e2b");
29
29
  let child_process = require("child_process");
30
- let util = require("util");
31
30
  let crypto = require("crypto");
31
+ let util = require("util");
32
32
  //#region src/client.ts
33
33
  const DEFAULT_PROXY_URL = "https://workspaces.mastra.ai";
34
34
  const REGIONAL_PROXY_URLS = {
@@ -1076,9 +1076,9 @@ var SandboxDestroyedError = class extends Error {
1076
1076
  * sandbox.
1077
1077
  */
1078
1078
  function buildCommand(command, args) {
1079
- return args?.length ? `${command} ${args.map(shellQuote$1).join(" ")}` : command;
1079
+ return args?.length ? `${command} ${args.map(shellQuote$2).join(" ")}` : command;
1080
1080
  }
1081
- function shellQuote$1(arg) {
1081
+ function shellQuote$2(arg) {
1082
1082
  if (/^[a-zA-Z0-9._\-/=:@]+$/.test(arg)) return arg;
1083
1083
  return `'${arg.replace(/'/g, `'\\''`)}'`;
1084
1084
  }
@@ -1996,6 +1996,8 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
1996
1996
  };
1997
1997
  //#endregion
1998
1998
  //#region ../../packages/_internals/workspace/dist/index.js
1999
+ /** Directory, relative to the template's build cwd, that holds every marker below. */
2000
+ const SETUP_MARKER_DIR = ".mastra-sandbox";
1999
2001
  /**
2000
2002
  * Setup completion marker shared by repo templates and their consumers.
2001
2003
  *
@@ -2005,8 +2007,29 @@ var PlatformSandbox = class PlatformSandbox extends _mastra_core_workspace.Mastr
2005
2007
  * booted from the image tell whether the setup it is about to run already
2006
2008
  * happened. Relative to the template's build cwd, which is also the runtime
2007
2009
  * working directory the repo was cloned into.
2010
+ *
2011
+ * This is the single-repository marker. Templates built from a repository
2012
+ * list write one marker per repository instead ({@link repoSetupMarkerPath}).
2008
2013
  */
2009
2014
  const SETUP_MARKER_PATH = ".mastra-sandbox/setup";
2015
+ /**
2016
+ * Marker for the workspace-level setup of a multi-repository template: the
2017
+ * commands that ran at the working directory after every repository was set
2018
+ * up. Written even when there were no such commands, so a consumer can tell
2019
+ * "ran nothing" from "never built".
2020
+ */
2021
+ const WORKSPACE_SETUP_MARKER_PATH = ".mastra-sandbox/workspace-setup";
2022
+ /**
2023
+ * One line per repository directory whose setup failed while the build was
2024
+ * told to continue. Absent when nothing failed. Consumers check this before
2025
+ * trusting a per-repository marker: that marker means every step ran, not
2026
+ * that every step succeeded.
2027
+ */
2028
+ const SETUP_FAILED_MARKER_PATH = ".mastra-sandbox/setup-failed";
2029
+ /** Per-repository marker of a multi-repository template, keyed by the clone directory name. */
2030
+ function repoSetupMarkerPath(repoDir) {
2031
+ return `${SETUP_MARKER_DIR}/repos/${repoDir}`;
2032
+ }
2010
2033
  /** Blank entries never become build steps, so they never count toward the digest either. */
2011
2034
  function normalizeSetupCommands(setupCommand) {
2012
2035
  return (setupCommand === void 0 ? [] : Array.isArray(setupCommand) ? setupCommand : [setupCommand]).filter((command) => command.trim() !== "");
@@ -2016,8 +2039,26 @@ function setupMarkerContent(setupCommand) {
2016
2039
  return `sha256:${(0, crypto.createHash)("sha256").update(normalizeSetupCommands(setupCommand).join("\n")).digest("hex")}`;
2017
2040
  }
2018
2041
  /** Shell step that writes the marker relative to the cwd. `content` is a digest, so it is shell-safe. */
2019
- function setupMarkerCommand(content) {
2020
- return `mkdir -p "$(dirname "${SETUP_MARKER_PATH}")" && printf '%s' '${content}' > "${SETUP_MARKER_PATH}"`;
2042
+ function setupMarkerCommand(content, markerPath = SETUP_MARKER_PATH) {
2043
+ return `mkdir -p "$(dirname "${markerPath}")" && printf '%s' '${content}' > "${markerPath}"`;
2044
+ }
2045
+ /** A clone directory name is one path segment of word characters, dots and dashes, so it never needs quoting. */
2046
+ const REPO_DIR_PATTERN = /^[\w.-]+$/;
2047
+ /**
2048
+ * Shell step that runs one setup command inside a repository clone.
2049
+ *
2050
+ * Without the guard this is the plain `cd "<dir>" && <cmd>` step. With it,
2051
+ * the `cd` sits inside the parenthesized group so that the failure branch
2052
+ * runs back at the build cwd and the failure list lands at the workspace
2053
+ * level, never inside the repository.
2054
+ */
2055
+ function guardedSetupCommand({ repoDir, command, continueOnFailure }) {
2056
+ if (!REPO_DIR_PATTERN.test(repoDir)) throw new TypeError(`Repository directory "${repoDir}" must match ${REPO_DIR_PATTERN}.`);
2057
+ if (!continueOnFailure) return `cd "${repoDir}" && ${command}`;
2058
+ 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}"; }`;
2059
+ }
2060
+ function shellQuote$1(value) {
2061
+ return `'${value.replace(/'/g, `'\\''`)}'`;
2021
2062
  }
2022
2063
  function repoCloneCommand({ cloneUrl, destination, branch, tokenEnv }) {
2023
2064
  return `git ${tokenEnv ? `${gitAuthFlag$1(tokenEnv)} ` : ""}clone --depth=1 --single-branch ${branch ? `--branch ${shellQuote(branch)} ` : ""}${shellQuote(cloneUrl)} ${shellQuote(destination)}`;
@@ -2070,77 +2111,126 @@ function rememberHead(cloneUrl, sha) {
2070
2111
  * requested size, and the caller's runtime setup materializes the checkout.
2071
2112
  */
2072
2113
  function createRepoTemplate(options) {
2073
- const getRepositoryAccess = options.getRepositoryAccess;
2114
+ if (options.getRepositoryAccess && options.repos) throw new TypeError("createRepoTemplate: pass either getRepositoryAccess or repos, not both");
2115
+ if (!options.repos && (options.workspaceSetupCommand !== void 0 || options.continueOnSetupFailure !== void 0)) throw new TypeError("createRepoTemplate: workspaceSetupCommand and continueOnSetupFailure require repos");
2116
+ const isList = options.repos !== void 0;
2117
+ const entries = options.repos ? options.repos : options.getRepositoryAccess ? [{
2118
+ getRepositoryAccess: options.getRepositoryAccess,
2119
+ setupCommand: options.setupCommand
2120
+ }] : [];
2074
2121
  const resourcesOnly = () => {
2075
2122
  if (options.cpuCount === void 0 && options.memoryMB === void 0) return void 0;
2076
2123
  return withResources(Template(), options);
2077
2124
  };
2078
- if (!getRepositoryAccess) {
2125
+ if (entries.length === 0) {
2079
2126
  const template = resourcesOnly();
2080
2127
  return template ? async () => template : void 0;
2081
2128
  }
2082
2129
  const resolveHead = options.resolveHead ?? resolveDefaultBranchHead;
2130
+ const continueOnFailure = options.continueOnSetupFailure ?? false;
2083
2131
  return async () => {
2084
- let accessError;
2085
- const access = await getRepositoryAccess().catch((error) => {
2086
- accessError = error;
2087
- });
2088
- if (!access?.cloneUrl) {
2089
- console.warn("[platform-workspace] repo template skipped: repository access unavailable", { error: redactSecrets(accessError) });
2090
- return resourcesOnly();
2091
- }
2092
- const cloneUrl = normalizeCloneUrl(access.cloneUrl);
2093
- if (!isValidCloneUrl(cloneUrl)) {
2094
- console.warn("[platform-workspace] repo template skipped: clone URL failed validation", { cloneUrl: redactSecrets(cloneUrl) });
2095
- return resourcesOnly();
2096
- }
2097
- const token = access.authorization?.token;
2098
- let headError;
2099
- const resolved = await (token ? resolveHead(cloneUrl, token) : resolveHead(cloneUrl)).catch((error) => {
2100
- headError = error;
2101
- });
2102
- let sha;
2103
- if (resolved && SHA_PATTERN.test(resolved)) {
2104
- sha = resolved;
2105
- rememberHead(cloneUrl, sha);
2106
- } else {
2107
- const lastKnown = lastKnownHeads.get(cloneUrl);
2108
- if (!lastKnown) {
2109
- console.warn("[platform-workspace] repo template skipped: could not resolve default-branch head", {
2132
+ const repos = [];
2133
+ for (const [index, entry] of entries.entries()) {
2134
+ let accessError;
2135
+ const access = await entry.getRepositoryAccess().catch((error) => {
2136
+ accessError = error;
2137
+ });
2138
+ if (!access?.cloneUrl) {
2139
+ console.warn("[platform-workspace] repo template skipped: repository access unavailable", {
2140
+ ...isList ? { index } : {},
2141
+ error: redactSecrets(accessError)
2142
+ });
2143
+ return resourcesOnly();
2144
+ }
2145
+ const cloneUrl = normalizeCloneUrl(access.cloneUrl);
2146
+ if (!isValidCloneUrl(cloneUrl)) {
2147
+ console.warn("[platform-workspace] repo template skipped: clone URL failed validation", {
2148
+ ...isList ? { index } : {},
2149
+ cloneUrl: redactSecrets(cloneUrl)
2150
+ });
2151
+ return resourcesOnly();
2152
+ }
2153
+ const token = access.authorization?.token;
2154
+ let headError;
2155
+ const resolved = await (token ? resolveHead(cloneUrl, token) : resolveHead(cloneUrl)).catch((error) => {
2156
+ headError = error;
2157
+ });
2158
+ let sha;
2159
+ if (resolved && SHA_PATTERN.test(resolved)) {
2160
+ sha = resolved;
2161
+ rememberHead(cloneUrl, sha);
2162
+ } else {
2163
+ const lastKnown = lastKnownHeads.get(cloneUrl);
2164
+ if (lastKnown) {
2165
+ console.warn("[platform-workspace] repo template pinned to the last known default-branch head", {
2166
+ cloneUrl,
2167
+ sha: lastKnown,
2168
+ resolved,
2169
+ error: redactSecrets(headError)
2170
+ });
2171
+ sha = lastKnown;
2172
+ } else if (isList) console.warn("[platform-workspace] repo template clones unpinned: could not resolve default-branch head", {
2110
2173
  cloneUrl,
2111
2174
  sha: resolved,
2112
2175
  error: redactSecrets(headError)
2113
2176
  });
2114
- return resourcesOnly();
2177
+ else {
2178
+ console.warn("[platform-workspace] repo template skipped: could not resolve default-branch head", {
2179
+ cloneUrl,
2180
+ sha: resolved,
2181
+ error: redactSecrets(headError)
2182
+ });
2183
+ return resourcesOnly();
2184
+ }
2115
2185
  }
2116
- console.warn("[platform-workspace] repo template pinned to the last known default-branch head", {
2186
+ const repoDir = repoDirName(cloneUrl);
2187
+ repos.push({
2188
+ index,
2117
2189
  cloneUrl,
2118
- sha: lastKnown,
2119
- resolved,
2120
- error: redactSecrets(headError)
2190
+ repoDir,
2191
+ token,
2192
+ tokenEnv: token ? isList ? `${BUILD_TOKEN_ENV}_${index}` : BUILD_TOKEN_ENV : void 0,
2193
+ sha,
2194
+ setupCommands: normalizeSetupCommands(entry.setupCommand)
2121
2195
  });
2122
- sha = lastKnown;
2196
+ }
2197
+ const seenDirs = /* @__PURE__ */ new Map();
2198
+ for (const repo of repos) {
2199
+ const other = seenDirs.get(repo.repoDir);
2200
+ if (other) throw new Error(`createRepoTemplate: repositories ${other} and ${repo.cloneUrl} would both clone into "${repo.repoDir}"`);
2201
+ seenDirs.set(repo.repoDir, repo.cloneUrl);
2123
2202
  }
2124
2203
  const workingDirectory = options.workingDirectory === void 0 ? void 0 : trimTrailingSlashes(assertWorkingDirectory(options.workingDirectory));
2125
- const repoDir = repoDirName(cloneUrl);
2126
- const auth = token ? `${gitAuthFlag()} ` : "";
2127
- const setupCommands = (options.setupCommand === void 0 ? [] : Array.isArray(options.setupCommand) ? options.setupCommand : [options.setupCommand]).filter((command) => command.trim() !== "");
2128
- const family = `repo:${cloneUrl}:${workingDirectory ?? ""}/${repoDir}`;
2204
+ const family = isList ? `repos:${(0, crypto.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}`;
2129
2205
  let template = Template();
2130
- const buildEnv = {
2131
- ...options.buildEnv,
2132
- ...token ? { [BUILD_TOKEN_ENV]: token } : {}
2133
- };
2206
+ const buildEnv = { ...options.buildEnv };
2207
+ for (const repo of repos) if (repo.tokenEnv && repo.token) buildEnv[repo.tokenEnv] = repo.token;
2134
2208
  if (Object.keys(buildEnv).length > 0) template = template.setEnvs(buildEnv, { ephemeral: true });
2135
2209
  template = withResources(template, options);
2136
2210
  if (workingDirectory) template = template.runCmd(`mkdir -p "${workingDirectory}"`).setWorkdir(workingDirectory);
2137
- template = template.runCmd(repoCloneCommand({
2138
- cloneUrl,
2139
- destination: repoDir,
2140
- ...token ? { tokenEnv: BUILD_TOKEN_ENV } : {}
2141
- })).runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
2142
- for (const command of setupCommands) template = template.runCmd(`cd "${repoDir}" && ${command}`);
2143
- template = template.runCmd(setupMarkerCommand(setupMarkerContent(setupCommands)));
2211
+ const ordered = [...repos.filter((repo) => !repo.token), ...repos.filter((repo) => repo.token)];
2212
+ for (const repo of ordered) {
2213
+ const { repoDir, sha, tokenEnv, setupCommands } = repo;
2214
+ const auth = tokenEnv ? `${gitAuthFlag(tokenEnv)} ` : "";
2215
+ template = template.runCmd(repoCloneCommand({
2216
+ cloneUrl: repo.cloneUrl,
2217
+ destination: repoDir,
2218
+ ...tokenEnv ? { tokenEnv } : {}
2219
+ }));
2220
+ if (sha) template = template.runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
2221
+ for (const command of setupCommands) template = template.runCmd(guardedSetupCommand({
2222
+ repoDir,
2223
+ command,
2224
+ continueOnFailure
2225
+ }));
2226
+ const content = setupMarkerContent(setupCommands);
2227
+ template = template.runCmd(isList ? setupMarkerCommand(content, repoSetupMarkerPath(repoDir)) : setupMarkerCommand(content));
2228
+ }
2229
+ if (isList) {
2230
+ const workspaceCommands = normalizeSetupCommands(options.workspaceSetupCommand);
2231
+ for (const command of workspaceCommands) template = template.runCmd(command);
2232
+ template = template.runCmd(setupMarkerCommand(setupMarkerContent(workspaceCommands), WORKSPACE_SETUP_MARKER_PATH));
2233
+ }
2144
2234
  return template.withFamily(family);
2145
2235
  };
2146
2236
  }
@@ -2195,8 +2285,8 @@ function redactSecrets(value) {
2195
2285
  if (value === void 0 || value === null) return void 0;
2196
2286
  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***");
2197
2287
  }
2198
- function gitAuthFlag() {
2199
- return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${BUILD_TOKEN_ENV}" | base64 -w0)"`;
2288
+ function gitAuthFlag(tokenEnv) {
2289
+ return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${tokenEnv}" | base64 -w0)"`;
2200
2290
  }
2201
2291
  /**
2202
2292
  * `owner/repo` for a github.com clone URL, else undefined. Only the public