@mastra/platform-workspace 1.5.0-alpha.1 → 1.5.0-alpha.3

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
@@ -1989,38 +1989,71 @@ const CLONE_URL_SEGMENT_PATTERN = /^[\w.-]+$/;
1989
1989
  * pins repositories to their current default-branch commit. Private repository
1990
1990
  * credentials are used for head resolution and sent to the provider as
1991
1991
  * transient build envs; they never enter the serialized definition. If the
1992
- * head cannot be resolved, the resolver returns undefined so PlatformSandbox
1993
- * boots from the provider default and the caller's runtime setup materializes
1994
- * the checkout instead.
1992
+ * repository or its head cannot be resolved, the resolver keeps `cpuCount` and
1993
+ * `memoryMB` in a resources-only template so the sandbox still boots at the
1994
+ * requested size, and the caller's runtime setup materializes the checkout.
1995
1995
  */
1996
1996
  function createRepoTemplate(options) {
1997
1997
  const getRepositoryAccess = options.getRepositoryAccess;
1998
- if (!getRepositoryAccess) return void 0;
1998
+ const resourcesOnly = () => {
1999
+ if (options.cpuCount === void 0 && options.memoryMB === void 0) return void 0;
2000
+ return withResources(Template(), options);
2001
+ };
2002
+ if (!getRepositoryAccess) {
2003
+ const template = resourcesOnly();
2004
+ return template ? async () => template : void 0;
2005
+ }
1999
2006
  const resolveHead = options.resolveHead ?? resolveDefaultBranchHead;
2000
2007
  return async () => {
2001
- const access = await getRepositoryAccess().catch(() => void 0);
2002
- if (!access?.cloneUrl) return void 0;
2008
+ let accessError;
2009
+ const access = await getRepositoryAccess().catch((error) => {
2010
+ accessError = error;
2011
+ });
2012
+ if (!access?.cloneUrl) {
2013
+ console.warn("[platform-workspace] repo template skipped: repository access unavailable", { error: redactSecrets(accessError) });
2014
+ return resourcesOnly();
2015
+ }
2003
2016
  const cloneUrl = normalizeCloneUrl(access.cloneUrl);
2004
- if (!isValidCloneUrl(cloneUrl)) return void 0;
2017
+ if (!isValidCloneUrl(cloneUrl)) {
2018
+ console.warn("[platform-workspace] repo template skipped: clone URL failed validation", { cloneUrl: redactSecrets(cloneUrl) });
2019
+ return resourcesOnly();
2020
+ }
2005
2021
  const token = access.authorization?.token;
2006
- const sha = await (token ? resolveHead(cloneUrl, token) : resolveHead(cloneUrl)).catch(() => void 0);
2007
- if (!sha || !SHA_PATTERN.test(sha)) return void 0;
2008
- const workdir = defaultWorkdir(cloneUrl);
2022
+ let headError;
2023
+ const sha = await (token ? resolveHead(cloneUrl, token) : resolveHead(cloneUrl)).catch((error) => {
2024
+ headError = error;
2025
+ });
2026
+ if (!sha || !SHA_PATTERN.test(sha)) {
2027
+ console.warn("[platform-workspace] repo template skipped: could not resolve default-branch head", {
2028
+ cloneUrl,
2029
+ sha,
2030
+ error: redactSecrets(headError)
2031
+ });
2032
+ return resourcesOnly();
2033
+ }
2034
+ const workingDirectory = options.workingDirectory === void 0 ? void 0 : trimTrailingSlashes(assertWorkingDirectory(options.workingDirectory));
2035
+ const repoDir = repoDirName(cloneUrl);
2009
2036
  const auth = token ? `${gitAuthFlag()} ` : "";
2010
- const steps = [
2011
- `git ${auth}clone ${cloneUrl} "${workdir}"`,
2012
- `git -C "${workdir}" ${auth}fetch origin ${sha}`,
2013
- `git -C "${workdir}" checkout ${sha}`,
2014
- ...options.setupCommand ? [`cd "${workdir}" && ${options.setupCommand}`] : []
2015
- ];
2016
- const family = `repo:${cloneUrl}:${workdir}`;
2037
+ const setupCommands = (options.setupCommand === void 0 ? [] : Array.isArray(options.setupCommand) ? options.setupCommand : [options.setupCommand]).filter((command) => command.trim() !== "");
2038
+ const family = `repo:${cloneUrl}:${workingDirectory ?? ""}/${repoDir}`;
2017
2039
  let template = Template();
2018
- if (token) template = template.setEnvs({ [BUILD_TOKEN_ENV]: token }, { ephemeral: true });
2019
- if (options.cpuCount !== void 0) template = template.cpuCount(options.cpuCount);
2020
- if (options.memoryMB !== void 0) template = template.memoryMB(options.memoryMB);
2021
- return template.runCmd(steps).withFamily(family);
2040
+ const buildEnv = {
2041
+ ...options.buildEnv,
2042
+ ...token ? { [BUILD_TOKEN_ENV]: token } : {}
2043
+ };
2044
+ if (Object.keys(buildEnv).length > 0) template = template.setEnvs(buildEnv, { ephemeral: true });
2045
+ template = withResources(template, options);
2046
+ if (workingDirectory) template = template.runCmd(`mkdir -p "${workingDirectory}"`).setWorkdir(workingDirectory);
2047
+ template = template.runCmd(`git ${auth}clone ${cloneUrl} "${repoDir}"`).runCmd(`git -C "${repoDir}" ${auth}fetch origin ${sha}`).runCmd(`git -C "${repoDir}" checkout ${sha}`);
2048
+ for (const command of setupCommands) template = template.runCmd(`cd "${repoDir}" && ${command}`);
2049
+ return template.withFamily(family);
2022
2050
  };
2023
2051
  }
2052
+ function withResources(template, options) {
2053
+ if (options.cpuCount !== void 0) template = template.cpuCount(options.cpuCount);
2054
+ if (options.memoryMB !== void 0) template = template.memoryMB(options.memoryMB);
2055
+ return template;
2056
+ }
2024
2057
  function isValidCloneUrl(cloneUrl) {
2025
2058
  if (cloneUrl.length > 2048 || !CLONE_URL_ALLOWED_CHARS.test(cloneUrl)) return false;
2026
2059
  let url;
@@ -2046,13 +2079,74 @@ function normalizeCloneUrl(cloneUrl) {
2046
2079
  return `${scheme.toLowerCase()}${host.toLowerCase()}`;
2047
2080
  });
2048
2081
  }
2049
- function defaultWorkdir(cloneUrl) {
2050
- return `$HOME/${(normalizeCloneUrl(cloneUrl).split("/").at(-1) ?? "").replace(/[^\w.-]/g, "-").replace(/^\.+/, "") || "repo"}`;
2082
+ function repoDirName(cloneUrl) {
2083
+ return (normalizeCloneUrl(cloneUrl).split("/").at(-1) ?? "").replace(/[^\w.-]/g, "-").replace(/^\.+/, "") || "repo";
2084
+ }
2085
+ function trimTrailingSlashes(path) {
2086
+ let end = path.length;
2087
+ while (end > 1 && path[end - 1] === "/") end--;
2088
+ return path.slice(0, end);
2089
+ }
2090
+ /** Validate a literal absolute path before embedding it in shell build steps. */
2091
+ function assertWorkingDirectory(dir) {
2092
+ if (!(/^\/[A-Za-z0-9._/-]*$/.test(dir) && !dir.split("/").includes(".."))) throw new Error(`Repo template workingDirectory must be an absolute path of plain path characters (got ${JSON.stringify(dir)}); ~ and $HOME are not expanded.`);
2093
+ return dir;
2094
+ }
2095
+ /**
2096
+ * Render a caught value for a warning without leaking credentials: URL
2097
+ * userinfo, HTTP authorization values, and GitHub token shapes are masked.
2098
+ */
2099
+ function redactSecrets(value) {
2100
+ if (value === void 0 || value === null) return void 0;
2101
+ 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***");
2051
2102
  }
2052
2103
  function gitAuthFlag() {
2053
2104
  return `-c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$${BUILD_TOKEN_ENV}" | base64 -w0)"`;
2054
2105
  }
2055
- async function resolveDefaultBranchHead(cloneUrl, token, execute = execFileAsync) {
2106
+ /**
2107
+ * `owner/repo` for a github.com clone URL, else undefined. Only the public
2108
+ * host is API-resolvable: GitHub Enterprise and other forges keep the git
2109
+ * path below.
2110
+ */
2111
+ function parseGithubRepo(cloneUrl) {
2112
+ let url;
2113
+ try {
2114
+ url = new URL(cloneUrl);
2115
+ } catch {
2116
+ return;
2117
+ }
2118
+ if (url.hostname.toLowerCase() !== "github.com") return void 0;
2119
+ const [owner, repo, ...rest] = url.pathname.split("/").filter(Boolean);
2120
+ if (!owner || !repo || rest.length > 0) return void 0;
2121
+ return {
2122
+ owner,
2123
+ repo: repo.replace(/\.git$/i, "")
2124
+ };
2125
+ }
2126
+ /**
2127
+ * Resolve the default-branch head. github.com repositories go through the
2128
+ * REST API so resolution works wherever the host runs, including deployed
2129
+ * images without a git binary; other hosts shell out to `git ls-remote`.
2130
+ * Throws with a redaction-safe message when the head cannot be resolved.
2131
+ */
2132
+ async function resolveDefaultBranchHead(cloneUrl, token, execute = execFileAsync, fetchImpl = fetch) {
2133
+ const github = parseGithubRepo(cloneUrl);
2134
+ if (github) {
2135
+ const response = await fetchImpl(`https://api.github.com/repos/${github.owner}/${github.repo}/commits/HEAD`, {
2136
+ headers: {
2137
+ Accept: "application/vnd.github.sha",
2138
+ "X-GitHub-Api-Version": "2022-11-28",
2139
+ "User-Agent": "mastra-platform-workspace",
2140
+ ...token ? { Authorization: `Bearer ${token}` } : {}
2141
+ },
2142
+ signal: AbortSignal.timeout(1e4)
2143
+ }).catch((error) => {
2144
+ throw new Error(`GitHub head lookup failed: ${error instanceof Error ? error.message : String(error)}`);
2145
+ });
2146
+ if (!response.ok) throw new Error(`GitHub head lookup failed: ${response.status} ${response.statusText}`.trim());
2147
+ const sha = (await response.text()).trim();
2148
+ return SHA_PATTERN.test(sha) ? sha : void 0;
2149
+ }
2056
2150
  try {
2057
2151
  const env = {
2058
2152
  ...process.env,
@@ -2075,8 +2169,10 @@ async function resolveDefaultBranchHead(cloneUrl, token, execute = execFileAsync
2075
2169
  });
2076
2170
  const sha = stdout.trim().split(/\s+/, 1)[0];
2077
2171
  return sha && SHA_PATTERN.test(sha) ? sha : void 0;
2078
- } catch {
2079
- return;
2172
+ } catch (error) {
2173
+ const stderr = error.stderr;
2174
+ const detail = typeof stderr === "string" && stderr.trim() ? stderr.trim() : String(error);
2175
+ throw new Error(`git ls-remote failed: ${detail}`);
2080
2176
  }
2081
2177
  }
2082
2178
  //#endregion