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