@mastra/deployer-sandbox 0.1.2 → 0.1.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/CHANGELOG.md +16 -0
- package/dist/client/index.cjs +162 -147
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.js +161 -145
- package/dist/client/index.js.map +1 -1
- package/dist/index.cjs +348 -338
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +346 -334
- package/dist/index.js.map +1 -1
- package/dist/shared-C-piQAcU.js +115 -0
- package/dist/{chunk-CSXC6CZL.cjs.map → shared-C-piQAcU.js.map} +1 -1
- package/dist/shared-H9GioaPM.cjs +192 -0
- package/dist/shared-H9GioaPM.cjs.map +1 -0
- package/dist/studio/assets/{core-CWmLb1rY.js → core-CZ7lrnPw.js} +1 -1
- package/dist/studio/assets/{index-CbYta8Tj.js → index-sToavS1V.js} +2 -2
- package/dist/studio/assets/{main-CdyP_WdO.js → main-n0iDoxTR.js} +243 -243
- package/dist/studio/index.html +1 -1
- package/dist/studio/routes-manifest.json +1 -1
- package/package.json +9 -9
- package/dist/chunk-CSXC6CZL.cjs +0 -115
- package/dist/chunk-Z636GNUQ.js +0 -101
- package/dist/chunk-Z636GNUQ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,289 +1,304 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdtemp, readFile, rm, writeFile
|
|
3
|
-
import {
|
|
4
|
-
import { fileURLToPath } from
|
|
5
|
-
import { Deployer } from
|
|
6
|
-
import { copy } from
|
|
7
|
-
import { execFile } from
|
|
8
|
-
import { createHash } from
|
|
9
|
-
import { existsSync } from
|
|
10
|
-
import { tmpdir } from
|
|
11
|
-
import { promisify } from
|
|
12
|
-
import { supportsNetworking } from
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { a as SERVER_SCRIPT, c as launchServer, d as shellQuote, f as tailServerLog, i as SERVER_PIDFILE, l as resolveRemoteDir, n as INSTALL_MARKER, o as getInfoSafe, p as waitForHealthy, r as SERVER_LOGFILE, s as killPreviousServer, t as DEFAULT_PORT, u as runInSandbox } from "./shared-C-piQAcU.js";
|
|
2
|
+
import { access, mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { Deployer } from "@mastra/deployer";
|
|
6
|
+
import { copy } from "fs-extra/esm";
|
|
7
|
+
import { execFile } from "child_process";
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { tmpdir } from "os";
|
|
11
|
+
import { promisify } from "util";
|
|
12
|
+
import { supportsNetworking } from "@mastra/core/workspace";
|
|
13
|
+
//#region src/alias.ts
|
|
14
|
+
/**
|
|
15
|
+
* Upsert a Vercel Edge Config item so a stable key always points at the
|
|
16
|
+
* current sandbox URL. Used for Tier 3 routing: apps read the key from Edge
|
|
17
|
+
* Config (e.g. in middleware) instead of hardcoding the rotating sandbox URL.
|
|
18
|
+
*/
|
|
15
19
|
async function updateEdgeConfigAlias(options) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!res.ok) {
|
|
38
|
-
const body = await res.text().catch(() => "");
|
|
39
|
-
throw new Error(`Failed to update Edge Config alias "${options.key}" (${res.status}): ${body}`);
|
|
40
|
-
}
|
|
20
|
+
const { token, teamId } = options;
|
|
21
|
+
if (!token) throw new Error("Updating the Edge Config alias requires a Vercel API token. Pass `alias.token`.");
|
|
22
|
+
const endpoint = new URL(`https://api.vercel.com/v1/edge-config/${options.edgeConfigId}/items`);
|
|
23
|
+
if (teamId) endpoint.searchParams.set("teamId", teamId);
|
|
24
|
+
const res = await fetch(endpoint, {
|
|
25
|
+
method: "PATCH",
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${token}`,
|
|
28
|
+
"Content-Type": "application/json"
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ items: [{
|
|
31
|
+
operation: "upsert",
|
|
32
|
+
key: options.key,
|
|
33
|
+
value: options.url
|
|
34
|
+
}] }),
|
|
35
|
+
signal: AbortSignal.timeout(3e4)
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
const body = await res.text().catch(() => "");
|
|
39
|
+
throw new Error(`Failed to update Edge Config alias "${options.key}" (${res.status}): ${body}`);
|
|
40
|
+
}
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
error: () => {
|
|
51
|
-
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/engine.ts
|
|
44
|
+
const execFileAsync = promisify(execFile);
|
|
45
|
+
const noopLogger = {
|
|
46
|
+
debug: () => {},
|
|
47
|
+
info: () => {},
|
|
48
|
+
warn: () => {},
|
|
49
|
+
error: () => {}
|
|
52
50
|
};
|
|
53
|
-
|
|
51
|
+
/** Max shell-command payload per chunk for the base64 upload fallback. */
|
|
52
|
+
const UPLOAD_CHUNK_SIZE = 96e3;
|
|
53
|
+
/**
|
|
54
|
+
* Deploy a prebuilt Mastra server directory into any workspace sandbox that
|
|
55
|
+
* supports networking. Provider-agnostic: only uses the core WorkspaceSandbox
|
|
56
|
+
* contract (`executeCommand` + `networking`, with `writeFiles` / `processes`
|
|
57
|
+
* as fast paths).
|
|
58
|
+
*/
|
|
54
59
|
async function deployToSandbox(options) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (installHash) {
|
|
117
|
-
await runInSandbox(sandbox, `printf '%s' ${shellQuote(installHash)} > ${shellQuote(marker)}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
const launchScript = buildLaunchScript({ remoteDir, port, env: mergedEnv });
|
|
121
|
-
await uploadFile(sandbox, `${remoteDir}/${SERVER_SCRIPT}`, Buffer.from(launchScript));
|
|
122
|
-
await runInSandbox(sandbox, `chmod 700 ${shellQuote(`${remoteDir}/${SERVER_SCRIPT}`)}`);
|
|
123
|
-
logger.info("Starting Mastra server...");
|
|
124
|
-
await launchServer(sandbox, remoteDir);
|
|
125
|
-
const healthy = await waitForHealthy(url, {
|
|
126
|
-
path: healthCheckPath,
|
|
127
|
-
timeoutMs: healthCheckTimeoutMs,
|
|
128
|
-
intervalMs: healthCheckIntervalMs
|
|
129
|
-
});
|
|
130
|
-
if (!healthy) {
|
|
131
|
-
const log = await tailServerLog(sandbox, remoteDir).catch(() => "");
|
|
132
|
-
throw new Error(
|
|
133
|
-
`Mastra server did not become healthy at ${url}${healthCheckPath} within ${healthCheckTimeoutMs}ms.` + (log ? `
|
|
134
|
-
|
|
135
|
-
Server log:
|
|
136
|
-
${log}` : "\n\n(no server log output captured)")
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
const info = await getInfoSafe(sandbox);
|
|
140
|
-
return {
|
|
141
|
-
url,
|
|
142
|
-
sandboxId: info?.id ?? sandbox.id,
|
|
143
|
-
expiresAt: info?.timeoutAt,
|
|
144
|
-
stop: async () => {
|
|
145
|
-
await sandbox.stop?.();
|
|
146
|
-
},
|
|
147
|
-
destroy: async () => {
|
|
148
|
-
await sandbox.destroy?.();
|
|
149
|
-
},
|
|
150
|
-
logs: (lines) => tailServerLog(sandbox, remoteDir, lines)
|
|
151
|
-
};
|
|
60
|
+
const { sandbox, dir, port = DEFAULT_PORT, env = {}, studio = false, healthCheckPath = "/api", healthCheckTimeoutMs = 6e4, healthCheckIntervalMs = 1e3, installCommand = "npm install --omit=dev", logger = noopLogger } = options;
|
|
61
|
+
if (!existsSync(join(dir, "index.mjs"))) throw new Error(`No index.mjs found in "${dir}" — did the build succeed?`);
|
|
62
|
+
logger.info(`Starting ${sandbox.provider} sandbox...`);
|
|
63
|
+
await sandbox.start?.();
|
|
64
|
+
if (!supportsNetworking(sandbox)) throw new Error(`Sandbox provider "${sandbox.provider}" does not support networking (public port URLs), which is required for sandbox deploys.`);
|
|
65
|
+
if (!sandbox.executeCommand) throw new Error(`Sandbox provider "${sandbox.provider}" does not support executeCommand, which is required for sandbox deploys.`);
|
|
66
|
+
const url = await sandbox.networking.getPortUrl(port);
|
|
67
|
+
if (!url) throw new Error(`Sandbox provider "${sandbox.provider}" did not expose a public URL for port ${port}. Make sure the port is declared when constructing the sandbox (e.g. \`ports: [${port}]\`).`);
|
|
68
|
+
const remoteDir = await resolveRemoteDir(sandbox, options.remoteDir);
|
|
69
|
+
const mergedEnv = { ...env };
|
|
70
|
+
if (studio && mergedEnv.MASTRA_STUDIO_PATH === void 0) mergedEnv.MASTRA_STUDIO_PATH = `${remoteDir}/studio`;
|
|
71
|
+
logger.info(`Uploading build output from ${dir}...`);
|
|
72
|
+
const tarball = await createTarball(dir);
|
|
73
|
+
logger.debug(`Tarball size: ${(tarball.length / 1024 / 1024).toFixed(2)} MB`);
|
|
74
|
+
const remoteTarball = `${remoteDir}/.deploy.tgz`;
|
|
75
|
+
await runInSandbox(sandbox, `mkdir -p ${shellQuote(remoteDir)}`);
|
|
76
|
+
await uploadFile(sandbox, remoteTarball, tarball);
|
|
77
|
+
await killPreviousServer(sandbox, remoteDir);
|
|
78
|
+
await runInSandbox(sandbox, `cd ${shellQuote(remoteDir)} && tar -xzf .deploy.tgz && rm -f .deploy.tgz`, { timeout: 12e4 });
|
|
79
|
+
const installHash = await hashInstallInputs(dir, installCommand);
|
|
80
|
+
const marker = `${remoteDir}/${INSTALL_MARKER}`;
|
|
81
|
+
const markerCheck = await runInSandbox(sandbox, `cat ${shellQuote(marker)} 2>/dev/null || true`, { allowFailure: true });
|
|
82
|
+
if (installHash && markerCheck.stdout.trim() === installHash) logger.info("Dependencies unchanged — skipping install.");
|
|
83
|
+
else {
|
|
84
|
+
logger.info(`Installing dependencies (${installCommand})...`);
|
|
85
|
+
await runInSandbox(sandbox, `cd ${shellQuote(remoteDir)} && ${installCommand}`, {
|
|
86
|
+
timeout: 6e5,
|
|
87
|
+
label: `install dependencies (${installCommand})`
|
|
88
|
+
});
|
|
89
|
+
if (installHash) await runInSandbox(sandbox, `printf '%s' ${shellQuote(installHash)} > ${shellQuote(marker)}`);
|
|
90
|
+
}
|
|
91
|
+
const launchScript = buildLaunchScript({
|
|
92
|
+
remoteDir,
|
|
93
|
+
port,
|
|
94
|
+
env: mergedEnv
|
|
95
|
+
});
|
|
96
|
+
await uploadFile(sandbox, `${remoteDir}/${SERVER_SCRIPT}`, Buffer.from(launchScript));
|
|
97
|
+
await runInSandbox(sandbox, `chmod 700 ${shellQuote(`${remoteDir}/${SERVER_SCRIPT}`)}`);
|
|
98
|
+
logger.info("Starting Mastra server...");
|
|
99
|
+
await launchServer(sandbox, remoteDir);
|
|
100
|
+
if (!await waitForHealthy(url, {
|
|
101
|
+
path: healthCheckPath,
|
|
102
|
+
timeoutMs: healthCheckTimeoutMs,
|
|
103
|
+
intervalMs: healthCheckIntervalMs
|
|
104
|
+
})) {
|
|
105
|
+
const log = await tailServerLog(sandbox, remoteDir).catch(() => "");
|
|
106
|
+
throw new Error(`Mastra server did not become healthy at ${url}${healthCheckPath} within ${healthCheckTimeoutMs}ms.` + (log ? `\n\nServer log:\n${log}` : "\n\n(no server log output captured)"));
|
|
107
|
+
}
|
|
108
|
+
const info = await getInfoSafe(sandbox);
|
|
109
|
+
return {
|
|
110
|
+
url,
|
|
111
|
+
sandboxId: info?.id ?? sandbox.id,
|
|
112
|
+
expiresAt: info?.timeoutAt,
|
|
113
|
+
stop: async () => {
|
|
114
|
+
await sandbox.stop?.();
|
|
115
|
+
},
|
|
116
|
+
destroy: async () => {
|
|
117
|
+
await sandbox.destroy?.();
|
|
118
|
+
},
|
|
119
|
+
logs: (lines) => tailServerLog(sandbox, remoteDir, lines)
|
|
120
|
+
};
|
|
152
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Build the POSIX launch script. Re-running the script restarts the server —
|
|
124
|
+
* the wake path uses this after a snapshot resume (which restores the
|
|
125
|
+
* filesystem but not processes).
|
|
126
|
+
*/
|
|
153
127
|
function buildLaunchScript(opts) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
lines.push(`exec node index.mjs >> ${shellQuote(SERVER_LOGFILE)} 2>&1`);
|
|
169
|
-
return lines.join("\n") + "\n";
|
|
128
|
+
const lines = ["#!/bin/sh", `cd ${shellQuote(opts.remoteDir)}`];
|
|
129
|
+
const env = {
|
|
130
|
+
MASTRA_AUTO_DETECT_URL: "true",
|
|
131
|
+
...opts.env,
|
|
132
|
+
PORT: String(opts.port),
|
|
133
|
+
MASTRA_HOST: "0.0.0.0"
|
|
134
|
+
};
|
|
135
|
+
for (const [key, value] of Object.entries(env)) {
|
|
136
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name: "${key}"`);
|
|
137
|
+
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
138
|
+
}
|
|
139
|
+
lines.push(`echo $$ > ${shellQuote(SERVER_PIDFILE)}`);
|
|
140
|
+
lines.push(`exec node index.mjs >> ${shellQuote(SERVER_LOGFILE)} 2>&1`);
|
|
141
|
+
return lines.join("\n") + "\n";
|
|
170
142
|
}
|
|
143
|
+
/** Create a gzipped tarball of the directory contents (excluding node_modules). */
|
|
171
144
|
async function createTarball(dir) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
145
|
+
const tmp = await mkdtemp(join(tmpdir(), "mastra-sandbox-"));
|
|
146
|
+
const tarPath = join(tmp, "deploy.tgz");
|
|
147
|
+
try {
|
|
148
|
+
await execFileAsync("tar", [
|
|
149
|
+
"-czf",
|
|
150
|
+
tarPath,
|
|
151
|
+
"--exclude=node_modules",
|
|
152
|
+
"-C",
|
|
153
|
+
dir,
|
|
154
|
+
"."
|
|
155
|
+
]);
|
|
156
|
+
return await readFile(tarPath);
|
|
157
|
+
} finally {
|
|
158
|
+
await rm(tmp, {
|
|
159
|
+
recursive: true,
|
|
160
|
+
force: true
|
|
161
|
+
});
|
|
162
|
+
}
|
|
180
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Upload a file into the sandbox. Uses the provider's native `writeFiles` fast
|
|
166
|
+
* path when available, otherwise falls back to base64 chunks over
|
|
167
|
+
* `executeCommand` — so `executeCommand` + `networking` is the minimum contract.
|
|
168
|
+
*/
|
|
181
169
|
async function uploadFile(sandbox, remotePath, content) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
await runInSandbox(
|
|
196
|
-
sandbox,
|
|
197
|
-
`base64 -d ${shellQuote(tmpPath)} > ${shellQuote(remotePath)} && rm -f ${shellQuote(tmpPath)}`,
|
|
198
|
-
{ label: `decode upload at ${remotePath}` }
|
|
199
|
-
);
|
|
170
|
+
if (sandbox.writeFiles) {
|
|
171
|
+
await sandbox.writeFiles([{
|
|
172
|
+
path: remotePath,
|
|
173
|
+
content
|
|
174
|
+
}]);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const b64 = content.toString("base64");
|
|
178
|
+
const tmpPath = `${remotePath}.b64`;
|
|
179
|
+
await runInSandbox(sandbox, `rm -f ${shellQuote(tmpPath)}`);
|
|
180
|
+
for (let i = 0; i < b64.length; i += UPLOAD_CHUNK_SIZE) await runInSandbox(sandbox, `printf '%s' ${shellQuote(b64.slice(i, i + UPLOAD_CHUNK_SIZE))} >> ${shellQuote(tmpPath)}`, { label: `upload chunk to ${remotePath}` });
|
|
181
|
+
await runInSandbox(sandbox, `base64 -d ${shellQuote(tmpPath)} > ${shellQuote(remotePath)} && rm -f ${shellQuote(tmpPath)}`, { label: `decode upload at ${remotePath}` });
|
|
200
182
|
}
|
|
201
|
-
|
|
183
|
+
/** Lockfiles that, when present in the build output, participate in the install-skip hash. */
|
|
184
|
+
const LOCKFILES = [
|
|
185
|
+
"package-lock.json",
|
|
186
|
+
"npm-shrinkwrap.json",
|
|
187
|
+
"pnpm-lock.yaml",
|
|
188
|
+
"yarn.lock",
|
|
189
|
+
"bun.lock"
|
|
190
|
+
];
|
|
191
|
+
/**
|
|
192
|
+
* Hash everything that determines the outcome of a dependency install:
|
|
193
|
+
* package.json, any bundled lockfile, and the install command itself. A
|
|
194
|
+
* matching hash means the previous `node_modules` can be reused.
|
|
195
|
+
*/
|
|
202
196
|
async function hashInstallInputs(dir, installCommand) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
197
|
+
const hash = createHash("sha256");
|
|
198
|
+
try {
|
|
199
|
+
hash.update(await readFile(join(dir, "package.json")));
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
for (const lockfile of LOCKFILES) {
|
|
204
|
+
let content;
|
|
205
|
+
try {
|
|
206
|
+
content = await readFile(join(dir, lockfile));
|
|
207
|
+
} catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
hash.update(lockfile).update(content);
|
|
211
|
+
}
|
|
212
|
+
hash.update(installCommand);
|
|
213
|
+
return hash.digest("hex");
|
|
220
214
|
}
|
|
221
|
-
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/manifest.ts
|
|
217
|
+
const MANIFEST_FILENAME = "sandbox-deployment.json";
|
|
218
|
+
/** Write `sandbox-deployment.json` into the build output directory. */
|
|
222
219
|
async function writeDeploymentManifest(outputDir, manifest) {
|
|
223
|
-
|
|
220
|
+
await writeFile(join(outputDir, MANIFEST_FILENAME), JSON.stringify(manifest, null, 2));
|
|
224
221
|
}
|
|
222
|
+
/** Read `sandbox-deployment.json` from the build output directory, or null when absent. */
|
|
225
223
|
async function readDeploymentManifest(outputDir) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
return JSON.parse(raw);
|
|
224
|
+
let raw;
|
|
225
|
+
try {
|
|
226
|
+
raw = await readFile(join(outputDir, MANIFEST_FILENAME), "utf-8");
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error.code === "ENOENT") return null;
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
return JSON.parse(raw);
|
|
236
232
|
}
|
|
237
|
-
|
|
238
|
-
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/deployer.ts
|
|
235
|
+
/**
|
|
236
|
+
* Deploy a full Mastra server into any workspace sandbox that supports
|
|
237
|
+
* networking (Vercel Sandbox, E2B, ...) and get a live public URL.
|
|
238
|
+
*
|
|
239
|
+
* Positioning: ephemeral environments — instant previews, PR/CI smoke deploys,
|
|
240
|
+
* agent-built-app verification. Not production hosting.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* import { SandboxDeployer } from '@mastra/deployer-sandbox';
|
|
245
|
+
* import { VercelSandbox } from '@mastra/vercel';
|
|
246
|
+
*
|
|
247
|
+
* export const mastra = new Mastra({
|
|
248
|
+
* deployer: new SandboxDeployer({
|
|
249
|
+
* sandbox: new VercelSandbox({ sandboxName: 'my-preview', timeout: 3_600_000, ports: [4111] }),
|
|
250
|
+
* }),
|
|
251
|
+
* });
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
239
254
|
var SandboxDeployer = class extends Deployer {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
255
|
+
/** Sandbox deploys are push-style: `mastra build` runs `deploy()` after bundling. */
|
|
256
|
+
deployOnBuild = true;
|
|
257
|
+
sandbox;
|
|
258
|
+
port;
|
|
259
|
+
studio;
|
|
260
|
+
/** Explicit remote dir, when configured. The engine defaults to `$HOME/mastra-app` inside the sandbox. */
|
|
261
|
+
remoteDir;
|
|
262
|
+
env;
|
|
263
|
+
alias;
|
|
264
|
+
healthCheckTimeoutMs;
|
|
265
|
+
constructor(options) {
|
|
266
|
+
super({ name: "SANDBOX" });
|
|
267
|
+
this.sandbox = options.sandbox;
|
|
268
|
+
this.port = options.port ?? 4111;
|
|
269
|
+
this.studio = options.studio ?? true;
|
|
270
|
+
this.remoteDir = options.remoteDir;
|
|
271
|
+
this.env = options.env ?? {};
|
|
272
|
+
this.alias = options.alias;
|
|
273
|
+
this.healthCheckTimeoutMs = options.healthCheckTimeoutMs;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Merge all existing env files instead of only the first one (base behavior).
|
|
277
|
+
* Later files win in `loadEnvVars()`, so order least → most specific: a
|
|
278
|
+
* `.env.local` written by `vercel env pull` shouldn't shadow the `.env` that
|
|
279
|
+
* holds the app's own keys.
|
|
280
|
+
*/
|
|
281
|
+
async getEnvFiles() {
|
|
282
|
+
const candidates = [
|
|
283
|
+
".env",
|
|
284
|
+
".env.production",
|
|
285
|
+
".env.local"
|
|
286
|
+
];
|
|
287
|
+
const existing = [];
|
|
288
|
+
for (const file of candidates) try {
|
|
289
|
+
await access(file);
|
|
290
|
+
existing.push(file);
|
|
291
|
+
} catch {}
|
|
292
|
+
return existing;
|
|
293
|
+
}
|
|
294
|
+
async getUserBundlerOptions(mastraEntryFile, outputDirectory) {
|
|
295
|
+
return {
|
|
296
|
+
...await super.getUserBundlerOptions(mastraEntryFile, outputDirectory),
|
|
297
|
+
externals: true
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
getEntry() {
|
|
301
|
+
return `
|
|
287
302
|
// @ts-expect-error
|
|
288
303
|
import { scoreTracesWorkflow } from '@mastra/core/evals/scoreTraces';
|
|
289
304
|
import { mastra } from '#mastra';
|
|
@@ -301,72 +316,69 @@ var SandboxDeployer = class extends Deployer {
|
|
|
301
316
|
mastra.__registerInternalWorkflow(scoreTracesWorkflow);
|
|
302
317
|
}
|
|
303
318
|
`;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this.logger.warn(`Sandbox expires at ${deployment.expiresAt.toISOString()} (provider runtime cap).`);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
319
|
+
}
|
|
320
|
+
async prepare(outputDirectory) {
|
|
321
|
+
await super.prepare(outputDirectory);
|
|
322
|
+
if (this.studio) {
|
|
323
|
+
const studioSource = join(dirname(dirname(fileURLToPath(import.meta.url))), "dist", "studio");
|
|
324
|
+
const studioServePath = join(outputDirectory, this.outputDir, "studio");
|
|
325
|
+
try {
|
|
326
|
+
await copy(studioSource, studioServePath, { overwrite: true });
|
|
327
|
+
} catch (err) {
|
|
328
|
+
throw new Error(`Failed to copy studio assets from "${studioSource}" to "${studioServePath}": ${err instanceof Error ? err.message : err}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async bundle(entryFile, outputDirectory, { toolsPaths, projectRoot }) {
|
|
333
|
+
return this._bundle(this.getEntry(), entryFile, {
|
|
334
|
+
outputDirectory,
|
|
335
|
+
projectRoot
|
|
336
|
+
}, toolsPaths);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Deploy the built output into the sandbox and wait for the server to come
|
|
340
|
+
* up on its public URL. Writes `sandbox-deployment.json` into the output
|
|
341
|
+
* directory and updates the Edge Config alias when configured.
|
|
342
|
+
*/
|
|
343
|
+
async deploy(outputDirectory) {
|
|
344
|
+
const dir = join(outputDirectory, this.outputDir);
|
|
345
|
+
const envVars = await this.loadEnvVars();
|
|
346
|
+
const env = {
|
|
347
|
+
...Object.fromEntries(envVars),
|
|
348
|
+
...this.env
|
|
349
|
+
};
|
|
350
|
+
if (envVars.size > 0) this.logger.warn("Environment variables from your .env file are injected into the remote sandbox. Anyone with access to the sandbox can read them.");
|
|
351
|
+
const deployment = await deployToSandbox({
|
|
352
|
+
sandbox: this.sandbox,
|
|
353
|
+
dir,
|
|
354
|
+
port: this.port,
|
|
355
|
+
env,
|
|
356
|
+
studio: this.studio,
|
|
357
|
+
remoteDir: this.remoteDir,
|
|
358
|
+
healthCheckTimeoutMs: this.healthCheckTimeoutMs,
|
|
359
|
+
logger: this.logger
|
|
360
|
+
});
|
|
361
|
+
await writeDeploymentManifest(dir, {
|
|
362
|
+
provider: this.sandbox.provider,
|
|
363
|
+
sandboxId: deployment.sandboxId,
|
|
364
|
+
url: deployment.url,
|
|
365
|
+
port: this.port,
|
|
366
|
+
deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
367
|
+
expiresAt: deployment.expiresAt?.toISOString()
|
|
368
|
+
});
|
|
369
|
+
if (this.alias) {
|
|
370
|
+
await updateEdgeConfigAlias({
|
|
371
|
+
...this.alias,
|
|
372
|
+
url: deployment.url
|
|
373
|
+
});
|
|
374
|
+
this.logger.info(`Edge Config alias "${this.alias.key}" now points at ${deployment.url}`);
|
|
375
|
+
}
|
|
376
|
+
this.logger.info(`Mastra server deployed: ${deployment.url}/api`);
|
|
377
|
+
if (this.studio) this.logger.info(`Studio: ${deployment.url}`);
|
|
378
|
+
if (deployment.expiresAt) this.logger.warn(`Sandbox expires at ${deployment.expiresAt.toISOString()} (provider runtime cap).`);
|
|
379
|
+
}
|
|
368
380
|
};
|
|
369
|
-
|
|
381
|
+
//#endregion
|
|
370
382
|
export { MANIFEST_FILENAME, SandboxDeployer, buildLaunchScript, deployToSandbox, readDeploymentManifest, updateEdgeConfigAlias, writeDeploymentManifest };
|
|
371
|
-
|
|
383
|
+
|
|
372
384
|
//# sourceMappingURL=index.js.map
|