@chat-js/cli 0.3.0 → 0.6.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.
Files changed (136) hide show
  1. package/dist/index.js +1173 -964
  2. package/package.json +1 -1
  3. package/templates/chat-app/app/(auth)/device-login/page.tsx +37 -0
  4. package/templates/chat-app/app/(auth)/login/page.tsx +26 -2
  5. package/templates/chat-app/app/(auth)/register/page.tsx +0 -12
  6. package/templates/chat-app/app/(chat)/api/chat/filter-reasoning-parts.ts +1 -1
  7. package/templates/chat-app/app/(chat)/api/chat/prepare/route.ts +94 -0
  8. package/templates/chat-app/app/(chat)/api/chat/route.ts +107 -16
  9. package/templates/chat-app/app/(chat)/layout.tsx +4 -1
  10. package/templates/chat-app/app/api/trpc/[trpc]/route.ts +1 -0
  11. package/templates/chat-app/app/globals.css +9 -9
  12. package/templates/chat-app/app/layout.tsx +4 -2
  13. package/templates/chat-app/biome.jsonc +3 -3
  14. package/templates/chat-app/chat.config.ts +32 -12
  15. package/templates/chat-app/components/ai-elements/prompt-input.tsx +1 -1
  16. package/templates/chat-app/components/anonymous-session-init.tsx +10 -6
  17. package/templates/chat-app/components/artifact-actions.tsx +81 -18
  18. package/templates/chat-app/components/artifact-panel.tsx +142 -41
  19. package/templates/chat-app/components/attachment-list.tsx +1 -1
  20. package/templates/chat-app/components/{social-auth-providers.tsx → auth-providers.tsx} +49 -4
  21. package/templates/chat-app/components/chat/chat-welcome.tsx +3 -3
  22. package/templates/chat-app/components/chat-menu-items.tsx +1 -1
  23. package/templates/chat-app/components/chat-sync.tsx +9 -11
  24. package/templates/chat-app/components/console.tsx +9 -9
  25. package/templates/chat-app/components/context-usage.tsx +2 -2
  26. package/templates/chat-app/components/create-artifact.tsx +15 -5
  27. package/templates/chat-app/components/data-stream-handler.tsx +57 -16
  28. package/templates/chat-app/components/device-login-page.tsx +191 -0
  29. package/templates/chat-app/components/diffview.tsx +8 -2
  30. package/templates/chat-app/components/electron-auth-handler.tsx +184 -0
  31. package/templates/chat-app/components/electron-auth-ui.tsx +121 -0
  32. package/templates/chat-app/components/favicon-group.tsx +1 -1
  33. package/templates/chat-app/components/feedback-actions.tsx +7 -3
  34. package/templates/chat-app/components/greeting.tsx +1 -1
  35. package/templates/chat-app/components/interactive-chart-impl.tsx +3 -4
  36. package/templates/chat-app/components/interactive-charts.tsx +1 -1
  37. package/templates/chat-app/components/login-form.tsx +52 -10
  38. package/templates/chat-app/components/message-editor.tsx +7 -3
  39. package/templates/chat-app/components/message-siblings.tsx +14 -1
  40. package/templates/chat-app/components/model-selector.tsx +295 -27
  41. package/templates/chat-app/components/multimodal-input.tsx +259 -22
  42. package/templates/chat-app/components/parallel-response-cards.tsx +175 -0
  43. package/templates/chat-app/components/part/code-execution.tsx +8 -2
  44. package/templates/chat-app/components/part/document-common.tsx +1 -1
  45. package/templates/chat-app/components/part/document-preview.tsx +5 -5
  46. package/templates/chat-app/components/part/retrieve-url.tsx +12 -12
  47. package/templates/chat-app/components/part/text-message-part.tsx +9 -1
  48. package/templates/chat-app/components/project-chat-item.tsx +1 -1
  49. package/templates/chat-app/components/project-menu-items.tsx +1 -1
  50. package/templates/chat-app/components/research-task.tsx +1 -1
  51. package/templates/chat-app/components/research-tasks.tsx +1 -1
  52. package/templates/chat-app/components/retry-button.tsx +25 -8
  53. package/templates/chat-app/components/sandbox.tsx +1 -1
  54. package/templates/chat-app/components/sheet-editor.tsx +7 -7
  55. package/templates/chat-app/components/sidebar-chats-list.tsx +1 -1
  56. package/templates/chat-app/components/sidebar-toggle.tsx +15 -2
  57. package/templates/chat-app/components/sidebar-top-row.tsx +27 -12
  58. package/templates/chat-app/components/sidebar-user-nav.tsx +10 -1
  59. package/templates/chat-app/components/signup-form.tsx +49 -10
  60. package/templates/chat-app/components/sources.tsx +4 -4
  61. package/templates/chat-app/components/text-editor.tsx +5 -2
  62. package/templates/chat-app/components/toolbar.tsx +3 -3
  63. package/templates/chat-app/components/ui/sidebar.tsx +0 -1
  64. package/templates/chat-app/components/upgrade-cta/limit-display.tsx +1 -1
  65. package/templates/chat-app/components/user-message.tsx +14 -2
  66. package/templates/chat-app/electron.d.ts +41 -0
  67. package/templates/chat-app/evals/my-eval.eval.ts +3 -1
  68. package/templates/chat-app/hooks/chat-sync-hooks.ts +11 -0
  69. package/templates/chat-app/hooks/use-artifact.tsx +13 -13
  70. package/templates/chat-app/hooks/use-navigate-to-message.ts +39 -0
  71. package/templates/chat-app/lib/ai/gateways/provider-types.ts +19 -10
  72. package/templates/chat-app/lib/ai/stream-errors.test.ts +72 -0
  73. package/templates/chat-app/lib/ai/stream-errors.ts +94 -0
  74. package/templates/chat-app/lib/ai/tools/code-execution.javascript.ts +171 -0
  75. package/templates/chat-app/lib/ai/tools/code-execution.python.ts +336 -0
  76. package/templates/chat-app/lib/ai/tools/code-execution.shared.test.ts +71 -0
  77. package/templates/chat-app/lib/ai/tools/code-execution.shared.ts +59 -0
  78. package/templates/chat-app/lib/ai/tools/code-execution.ts +62 -391
  79. package/templates/chat-app/lib/ai/tools/code-execution.types.ts +24 -0
  80. package/templates/chat-app/lib/ai/tools/steps/multi-query-web-search.ts +3 -2
  81. package/templates/chat-app/lib/ai/types.ts +74 -3
  82. package/templates/chat-app/lib/anonymous-session-client.ts +0 -3
  83. package/templates/chat-app/lib/artifacts/code/client.tsx +35 -5
  84. package/templates/chat-app/lib/artifacts/sheet/client.tsx +11 -3
  85. package/templates/chat-app/lib/auth-client.ts +23 -1
  86. package/templates/chat-app/lib/auth.ts +18 -1
  87. package/templates/chat-app/lib/blob.ts +1 -1
  88. package/templates/chat-app/lib/clone-messages.ts +1 -1
  89. package/templates/chat-app/lib/config-schema.ts +18 -1
  90. package/templates/chat-app/lib/constants.ts +3 -4
  91. package/templates/chat-app/lib/db/migrations/0044_gray_red_shift.sql +5 -0
  92. package/templates/chat-app/lib/db/migrations/meta/0044_snapshot.json +1480 -0
  93. package/templates/chat-app/lib/db/migrations/meta/_journal.json +7 -0
  94. package/templates/chat-app/lib/db/queries.ts +84 -4
  95. package/templates/chat-app/lib/db/schema.ts +4 -1
  96. package/templates/chat-app/lib/editor/config.ts +4 -4
  97. package/templates/chat-app/lib/electron-auth.ts +96 -0
  98. package/templates/chat-app/lib/env-schema.ts +33 -4
  99. package/templates/chat-app/lib/message-conversion.ts +14 -2
  100. package/templates/chat-app/lib/playwright-test-environment.ts +18 -0
  101. package/templates/chat-app/lib/social-auth.ts +5 -0
  102. package/templates/chat-app/lib/stores/hooks-threads.ts +38 -1
  103. package/templates/chat-app/lib/stores/with-threads.test.ts +137 -0
  104. package/templates/chat-app/lib/stores/with-threads.ts +159 -7
  105. package/templates/chat-app/lib/stores/with-tracing.ts +1 -1
  106. package/templates/chat-app/lib/thread-utils.ts +22 -3
  107. package/templates/chat-app/lib/utils/download-assets.ts +6 -7
  108. package/templates/chat-app/lib/utils/rate-limit.ts +9 -3
  109. package/templates/chat-app/package.json +20 -18
  110. package/templates/chat-app/playwright.config.ts +0 -19
  111. package/templates/chat-app/providers/chat-input-provider.tsx +40 -2
  112. package/templates/chat-app/proxy.ts +28 -3
  113. package/templates/chat-app/scripts/check-env.ts +10 -0
  114. package/templates/chat-app/scripts/db-branch-delete.sh +7 -1
  115. package/templates/chat-app/scripts/db-branch-use.sh +7 -1
  116. package/templates/chat-app/scripts/with-db.sh +7 -1
  117. package/templates/chat-app/trpc/server.tsx +7 -2
  118. package/templates/chat-app/tsconfig.json +2 -1
  119. package/templates/chat-app/vercel.json +0 -10
  120. package/templates/chat-app/vitest.config.ts +2 -0
  121. package/templates/electron/CHANGELOG.md +7 -0
  122. package/templates/electron/README.md +54 -0
  123. package/templates/electron/entitlements.mac.plist +10 -0
  124. package/templates/electron/forge.config.ts +157 -0
  125. package/templates/electron/icon.png +0 -0
  126. package/templates/electron/package.json +53 -0
  127. package/templates/electron/scripts/generate-icons.test.js +37 -0
  128. package/templates/electron/scripts/generate-icons.ts +29 -0
  129. package/templates/electron/scripts/run-forge.cjs +28 -0
  130. package/templates/electron/scripts/write-branding.ts +18 -0
  131. package/templates/electron/src/config.ts +16 -0
  132. package/templates/electron/src/lib/auth-client.ts +64 -0
  133. package/templates/electron/src/main.ts +670 -0
  134. package/templates/electron/src/preload.d.ts +27 -0
  135. package/templates/electron/src/preload.ts +25 -0
  136. package/templates/electron/tsconfig.json +18 -0
@@ -0,0 +1,336 @@
1
+ import type { Sandbox } from "@vercel/sandbox";
2
+ import type {
3
+ CodeExecutionContext,
4
+ CodeExecutionResult,
5
+ } from "./code-execution.types";
6
+
7
+ const WHITESPACE_REGEX = /\s+/;
8
+ const PACKAGE_SPEC_SPLIT_RE = /[=<>![\s]/;
9
+ const CHART_JSON_PREFIX = "__CHART_JSON__:";
10
+
11
+ function packageName(spec: string): string {
12
+ return spec.split(PACKAGE_SPEC_SPLIT_RE)[0].toLowerCase();
13
+ }
14
+
15
+ async function installBasePackages(
16
+ sandbox: Sandbox,
17
+ basePackages: readonly string[],
18
+ requestId: string,
19
+ log: CodeExecutionContext["log"]
20
+ ): Promise<{
21
+ success: boolean;
22
+ result?: CodeExecutionResult;
23
+ }> {
24
+ const installStep = await sandbox.runCommand({
25
+ cmd: "pip",
26
+ args: ["install", ...basePackages],
27
+ });
28
+ if (installStep.exitCode !== 0) {
29
+ const installStderr = await installStep.stderr();
30
+ log.error(
31
+ { requestId, stderr: installStderr },
32
+ "base package installation failed"
33
+ );
34
+ return {
35
+ success: false,
36
+ result: {
37
+ message: `Failed to install base packages: ${installStderr}`,
38
+ chart: "",
39
+ },
40
+ };
41
+ }
42
+ log.info({ requestId }, "base packages installed");
43
+ return { success: true };
44
+ }
45
+
46
+ async function processExtraPackages(
47
+ code: string,
48
+ basePackages: readonly string[],
49
+ sandbox: Sandbox,
50
+ requestId: string,
51
+ log: CodeExecutionContext["log"]
52
+ ): Promise<{
53
+ codeToRun: string;
54
+ installResult: {
55
+ success: boolean;
56
+ result?: CodeExecutionResult;
57
+ };
58
+ }> {
59
+ const basePackageNames = new Set(basePackages.map((p) => p.toLowerCase()));
60
+ const lines = code.split("\n");
61
+ const pipLines = lines.filter((line) =>
62
+ line.trim().startsWith("!pip install ")
63
+ );
64
+ const extraPackages = pipLines
65
+ .flatMap((line) =>
66
+ line
67
+ .trim()
68
+ .slice("!pip install ".length)
69
+ .split(WHITESPACE_REGEX)
70
+ .filter(Boolean)
71
+ )
72
+ .filter((spec) => !basePackageNames.has(packageName(spec)));
73
+
74
+ const codeWithoutPipLines = lines
75
+ .filter((line) => !line.trim().startsWith("!pip install "))
76
+ .join("\n");
77
+
78
+ if (extraPackages.length === 0) {
79
+ return { codeToRun: codeWithoutPipLines, installResult: { success: true } };
80
+ }
81
+
82
+ log.info({ requestId, extraPackages }, "installing extra packages");
83
+ const dynamicInstall = await sandbox.runCommand({
84
+ cmd: "pip",
85
+ args: ["install", ...extraPackages],
86
+ });
87
+ if (dynamicInstall.exitCode !== 0) {
88
+ const stderr = await dynamicInstall.stderr();
89
+ log.error({ requestId, stderr }, "dynamic package installation failed");
90
+ return {
91
+ codeToRun: code,
92
+ installResult: {
93
+ success: false,
94
+ result: {
95
+ message: `Failed to install packages: ${stderr}`,
96
+ chart: "",
97
+ },
98
+ },
99
+ };
100
+ }
101
+
102
+ return {
103
+ codeToRun: codeWithoutPipLines,
104
+ installResult: { success: true },
105
+ };
106
+ }
107
+
108
+ function createWrappedCode(codeToRun: string, chartPath: string): string {
109
+ return `
110
+ import sys
111
+ import json
112
+ import traceback
113
+
114
+ try:
115
+ import matplotlib.pyplot as _plt_module
116
+ _orig_savefig = _plt_module.savefig
117
+ def _intercepted_savefig(*args, **kwargs):
118
+ _orig_savefig('${chartPath}', format='png', bbox_inches='tight', dpi=100)
119
+ _user_target = args[0] if args else kwargs.get('fname')
120
+ if _user_target not in (None, '${chartPath}'):
121
+ return _orig_savefig(*args, **kwargs)
122
+ _plt_module.savefig = _intercepted_savefig
123
+ except ImportError:
124
+ pass
125
+
126
+ try:
127
+ exec(${JSON.stringify(codeToRun)})
128
+ try:
129
+ _locals = locals()
130
+ _globals = globals()
131
+ _chart_var = _locals.get("chart") or _globals.get("chart")
132
+ if (isinstance(_chart_var, dict)
133
+ and isinstance(_chart_var.get("type"), str)
134
+ and isinstance(_chart_var.get("elements"), list)):
135
+ print("__CHART_JSON__:" + json.dumps(_chart_var))
136
+ else:
137
+ if "result" in _locals:
138
+ print(_locals["result"])
139
+ elif "result" in _globals:
140
+ print(_globals["result"])
141
+ elif "results" in _locals:
142
+ print(_locals["results"])
143
+ elif "results" in _globals:
144
+ print(_globals["results"])
145
+ except Exception:
146
+ pass
147
+ try:
148
+ import matplotlib.pyplot as plt
149
+ if plt.get_fignums():
150
+ plt.savefig('${chartPath}', format='png', bbox_inches='tight', dpi=100)
151
+ plt.close('all')
152
+ except ImportError:
153
+ pass
154
+ print(json.dumps({"success": True}))
155
+ except Exception as e:
156
+ error_info = {"success": False, "error": {"name": type(e).__name__, "value": str(e), "traceback": traceback.format_exc()}}
157
+ print(json.dumps(error_info))
158
+ sys.exit(1)
159
+ `;
160
+ }
161
+
162
+ async function parseExecutionOutput(execResult: {
163
+ stdout: () => Promise<string>;
164
+ exitCode: number;
165
+ }): Promise<{
166
+ outputText: string;
167
+ chartData: Record<string, unknown> | null;
168
+ execInfo: {
169
+ success: boolean;
170
+ error?: { name: string; value: string; traceback: string };
171
+ };
172
+ }> {
173
+ const stdout = await execResult.stdout();
174
+ let execInfo: {
175
+ success: boolean;
176
+ error?: { name: string; value: string; traceback: string };
177
+ } = { success: true };
178
+ let outputText = "";
179
+ let chartData: Record<string, unknown> | null = null;
180
+
181
+ try {
182
+ const outLines = (stdout ?? "").trim().split("\n");
183
+ const lastLine = outLines.at(-1);
184
+ execInfo = JSON.parse(lastLine ?? "{}");
185
+ outLines.pop();
186
+
187
+ const chartLineIdx = outLines.findIndex((line) =>
188
+ line.startsWith(CHART_JSON_PREFIX)
189
+ );
190
+ if (chartLineIdx !== -1) {
191
+ const raw = outLines[chartLineIdx].slice(CHART_JSON_PREFIX.length);
192
+ try {
193
+ chartData = JSON.parse(raw) as Record<string, unknown>;
194
+ } catch {
195
+ // Ignore malformed chart JSON from the sandboxed snippet.
196
+ }
197
+ outLines.splice(chartLineIdx, 1);
198
+ }
199
+
200
+ outputText = outLines.join("\n");
201
+ } catch {
202
+ outputText = stdout ?? "";
203
+ if (execResult.exitCode !== 0) {
204
+ execInfo = {
205
+ success: false,
206
+ error: {
207
+ name: "SandboxExecutionError",
208
+ value: "Execution completed without a parsable status trailer",
209
+ traceback: "",
210
+ },
211
+ };
212
+ }
213
+ }
214
+
215
+ return { outputText, chartData, execInfo };
216
+ }
217
+
218
+ async function checkForChart(
219
+ sandbox: Sandbox,
220
+ chartPath: string,
221
+ requestId: string,
222
+ log: CodeExecutionContext["log"]
223
+ ): Promise<{ base64: string; format: string } | undefined> {
224
+ const chartCheck = await sandbox.runCommand({
225
+ cmd: "test",
226
+ args: ["-f", chartPath],
227
+ });
228
+ if (chartCheck.exitCode === 0) {
229
+ const b64 = await (
230
+ await sandbox.runCommand({
231
+ cmd: "base64",
232
+ args: ["-w", "0", chartPath],
233
+ })
234
+ ).stdout();
235
+ log.info({ requestId }, "chart generated");
236
+ return { base64: (b64 ?? "").trim(), format: "png" };
237
+ }
238
+ return;
239
+ }
240
+
241
+ function buildResponseMessage({
242
+ outputText,
243
+ stderr,
244
+ execInfo,
245
+ log,
246
+ requestId,
247
+ }: {
248
+ outputText: string;
249
+ stderr: string;
250
+ execInfo: {
251
+ success: boolean;
252
+ error?: { name: string; value: string; traceback: string };
253
+ };
254
+ log: CodeExecutionContext["log"];
255
+ requestId: string;
256
+ }): string {
257
+ let message = "";
258
+
259
+ if (outputText) {
260
+ message += `${outputText}\n`;
261
+ }
262
+ if (stderr && stderr.trim().length > 0) {
263
+ message += `${stderr}\n`;
264
+ }
265
+ if (execInfo.error) {
266
+ message += `Error: ${execInfo.error.name}: ${execInfo.error.value}\n`;
267
+ log.error({ requestId, error: execInfo.error }, "python execution error");
268
+ }
269
+
270
+ return message;
271
+ }
272
+
273
+ export async function executePythonInSandbox({
274
+ sandbox,
275
+ code,
276
+ log,
277
+ requestId,
278
+ }: CodeExecutionContext): Promise<CodeExecutionResult> {
279
+ const basePackages = [
280
+ "matplotlib",
281
+ "pandas",
282
+ "numpy",
283
+ "sympy",
284
+ "yfinance",
285
+ ] as const;
286
+ const chartPath = "/tmp/chart.png";
287
+
288
+ const baseInstallResult = await installBasePackages(
289
+ sandbox,
290
+ basePackages,
291
+ requestId,
292
+ log
293
+ );
294
+ if (!baseInstallResult.success) {
295
+ return baseInstallResult.result ?? { message: "Unknown error", chart: "" };
296
+ }
297
+
298
+ const { codeToRun, installResult } = await processExtraPackages(
299
+ code,
300
+ basePackages,
301
+ sandbox,
302
+ requestId,
303
+ log
304
+ );
305
+ if (!installResult.success) {
306
+ return installResult.result ?? { message: "Unknown error", chart: "" };
307
+ }
308
+
309
+ const wrappedCode = createWrappedCode(codeToRun, chartPath);
310
+ const execResult = await sandbox.runCommand({
311
+ cmd: "python3",
312
+ args: ["-c", wrappedCode],
313
+ });
314
+
315
+ const { outputText, chartData, execInfo } =
316
+ await parseExecutionOutput(execResult);
317
+
318
+ const message = buildResponseMessage({
319
+ outputText,
320
+ stderr: await execResult.stderr(),
321
+ execInfo,
322
+ log,
323
+ requestId,
324
+ });
325
+
326
+ if (chartData) {
327
+ log.info({ requestId }, "interactive chart data returned");
328
+ return { message: message.trim(), chart: chartData };
329
+ }
330
+
331
+ const chartOut = await checkForChart(sandbox, chartPath, requestId, log);
332
+ return {
333
+ message: message.trim(),
334
+ chart: chartOut ?? "",
335
+ };
336
+ }
@@ -0,0 +1,71 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const envMock: {
4
+ VERCEL_SANDBOX_RUNTIME: string | undefined;
5
+ VERCEL_SANDBOX_RUNTIME_PYTHON: string | undefined;
6
+ VERCEL_SANDBOX_RUNTIME_JAVASCRIPT: string | undefined;
7
+ } = {
8
+ VERCEL_SANDBOX_RUNTIME: undefined,
9
+ VERCEL_SANDBOX_RUNTIME_PYTHON: undefined,
10
+ VERCEL_SANDBOX_RUNTIME_JAVASCRIPT: undefined,
11
+ };
12
+
13
+ vi.mock("@/lib/env", () => ({
14
+ env: envMock,
15
+ }));
16
+
17
+ describe("getSandboxRuntime", () => {
18
+ beforeEach(() => {
19
+ envMock.VERCEL_SANDBOX_RUNTIME = undefined;
20
+ envMock.VERCEL_SANDBOX_RUNTIME_PYTHON = undefined;
21
+ envMock.VERCEL_SANDBOX_RUNTIME_JAVASCRIPT = undefined;
22
+ });
23
+
24
+ it("uses Python defaults when no override is set", async () => {
25
+ const { getSandboxRuntime } = await import("./code-execution.shared");
26
+
27
+ expect(getSandboxRuntime("python")).toBe("python3.13");
28
+ });
29
+
30
+ it("uses JavaScript defaults when no override is set", async () => {
31
+ const { getSandboxRuntime } = await import("./code-execution.shared");
32
+
33
+ expect(getSandboxRuntime("javascript")).toBe("node22");
34
+ });
35
+
36
+ it("honors VERCEL_SANDBOX_RUNTIME_PYTHON override for python", async () => {
37
+ envMock.VERCEL_SANDBOX_RUNTIME_PYTHON = "python3.12";
38
+ const { getSandboxRuntime } = await import("./code-execution.shared");
39
+
40
+ expect(getSandboxRuntime("python")).toBe("python3.12");
41
+ });
42
+
43
+ it("honors VERCEL_SANDBOX_RUNTIME_JAVASCRIPT override for javascript", async () => {
44
+ envMock.VERCEL_SANDBOX_RUNTIME_JAVASCRIPT = "node20";
45
+ const { getSandboxRuntime } = await import("./code-execution.shared");
46
+
47
+ expect(getSandboxRuntime("javascript")).toBe("node20");
48
+ });
49
+
50
+ it("falls back to legacy VERCEL_SANDBOX_RUNTIME for python", async () => {
51
+ envMock.VERCEL_SANDBOX_RUNTIME = "python3.11";
52
+ const { getSandboxRuntime } = await import("./code-execution.shared");
53
+
54
+ expect(getSandboxRuntime("python")).toBe("python3.11");
55
+ });
56
+
57
+ it("prefers VERCEL_SANDBOX_RUNTIME_PYTHON over legacy VERCEL_SANDBOX_RUNTIME", async () => {
58
+ envMock.VERCEL_SANDBOX_RUNTIME_PYTHON = "python3.12";
59
+ envMock.VERCEL_SANDBOX_RUNTIME = "python3.11";
60
+ const { getSandboxRuntime } = await import("./code-execution.shared");
61
+
62
+ expect(getSandboxRuntime("python")).toBe("python3.12");
63
+ });
64
+
65
+ it("does not use legacy VERCEL_SANDBOX_RUNTIME for javascript", async () => {
66
+ envMock.VERCEL_SANDBOX_RUNTIME = "python3.11";
67
+ const { getSandboxRuntime } = await import("./code-execution.shared");
68
+
69
+ expect(getSandboxRuntime("javascript")).toBe("node22");
70
+ });
71
+ });
@@ -0,0 +1,59 @@
1
+ import { Sandbox } from "@vercel/sandbox";
2
+ import { env } from "@/lib/env";
3
+ import type { createModuleLogger } from "@/lib/logger";
4
+ import type { SupportedExecutionLanguage } from "./code-execution.types";
5
+
6
+ export function getTokenAuth(): Record<string, string> {
7
+ const { VERCEL_TEAM_ID, VERCEL_PROJECT_ID, VERCEL_TOKEN } = env;
8
+ if (VERCEL_TEAM_ID && VERCEL_PROJECT_ID && VERCEL_TOKEN) {
9
+ return {
10
+ teamId: VERCEL_TEAM_ID,
11
+ projectId: VERCEL_PROJECT_ID,
12
+ token: VERCEL_TOKEN,
13
+ };
14
+ }
15
+ return {};
16
+ }
17
+
18
+ export function getSandboxRuntime(
19
+ language: SupportedExecutionLanguage
20
+ ): string {
21
+ if (language === "javascript") {
22
+ return env.VERCEL_SANDBOX_RUNTIME_JAVASCRIPT ?? "node22";
23
+ }
24
+
25
+ return (
26
+ env.VERCEL_SANDBOX_RUNTIME_PYTHON ??
27
+ env.VERCEL_SANDBOX_RUNTIME ??
28
+ "python3.13"
29
+ );
30
+ }
31
+
32
+ export function createSandbox(runtime: string): Promise<Sandbox> {
33
+ return Sandbox.create({
34
+ runtime,
35
+ timeout: 5 * 60 * 1000,
36
+ resources: { vcpus: 2 },
37
+ ...getTokenAuth(),
38
+ });
39
+ }
40
+
41
+ export async function cleanupSandbox(
42
+ sandbox: Sandbox | undefined,
43
+ log: ReturnType<typeof createModuleLogger>,
44
+ requestId: string
45
+ ): Promise<void> {
46
+ if (!sandbox) {
47
+ return;
48
+ }
49
+ try {
50
+ await sandbox.stop();
51
+ log.info({ requestId }, "sandbox closed");
52
+ } catch (closeErr) {
53
+ log.warn({ requestId, closeErr }, "failed to close sandbox");
54
+ }
55
+ }
56
+
57
+ export function getErrorMessage(err: unknown): string {
58
+ return err instanceof Error ? err.message : "Unknown error";
59
+ }