@cloudflare/sandbox 0.0.0-bb855ca → 0.0.0-c39674b

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 (95) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/Dockerfile +107 -66
  3. package/README.md +88 -710
  4. package/dist/chunk-BFVUNTP4.js +104 -0
  5. package/dist/chunk-BFVUNTP4.js.map +1 -0
  6. package/dist/chunk-EKSWCBCA.js +86 -0
  7. package/dist/chunk-EKSWCBCA.js.map +1 -0
  8. package/dist/chunk-JXZMAU2C.js +559 -0
  9. package/dist/chunk-JXZMAU2C.js.map +1 -0
  10. package/dist/chunk-UZQBJBJF.js +7 -0
  11. package/dist/chunk-UZQBJBJF.js.map +1 -0
  12. package/dist/chunk-YEZBBFK7.js +2420 -0
  13. package/dist/chunk-YEZBBFK7.js.map +1 -0
  14. package/dist/chunk-Z532A7QC.js +78 -0
  15. package/dist/chunk-Z532A7QC.js.map +1 -0
  16. package/dist/file-stream.d.ts +43 -0
  17. package/dist/file-stream.js +9 -0
  18. package/dist/file-stream.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.js +67 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/interpreter.d.ts +33 -0
  23. package/dist/interpreter.js +8 -0
  24. package/dist/interpreter.js.map +1 -0
  25. package/dist/request-handler.d.ts +18 -0
  26. package/dist/request-handler.js +13 -0
  27. package/dist/request-handler.js.map +1 -0
  28. package/dist/sandbox-DMlNr93l.d.ts +596 -0
  29. package/dist/sandbox.d.ts +4 -0
  30. package/dist/sandbox.js +13 -0
  31. package/dist/sandbox.js.map +1 -0
  32. package/dist/security.d.ts +31 -0
  33. package/dist/security.js +13 -0
  34. package/dist/security.js.map +1 -0
  35. package/dist/sse-parser.d.ts +28 -0
  36. package/dist/sse-parser.js +11 -0
  37. package/dist/sse-parser.js.map +1 -0
  38. package/dist/version.d.ts +8 -0
  39. package/dist/version.js +7 -0
  40. package/dist/version.js.map +1 -0
  41. package/package.json +13 -5
  42. package/src/clients/base-client.ts +280 -0
  43. package/src/clients/command-client.ts +115 -0
  44. package/src/clients/file-client.ts +269 -0
  45. package/src/clients/git-client.ts +92 -0
  46. package/src/clients/index.ts +64 -0
  47. package/src/clients/interpreter-client.ts +329 -0
  48. package/src/clients/port-client.ts +105 -0
  49. package/src/clients/process-client.ts +177 -0
  50. package/src/clients/sandbox-client.ts +41 -0
  51. package/src/clients/types.ts +84 -0
  52. package/src/clients/utility-client.ts +119 -0
  53. package/src/errors/adapter.ts +180 -0
  54. package/src/errors/classes.ts +469 -0
  55. package/src/errors/index.ts +105 -0
  56. package/src/file-stream.ts +164 -0
  57. package/src/index.ts +85 -21
  58. package/src/interpreter.ts +22 -13
  59. package/src/request-handler.ts +69 -43
  60. package/src/sandbox.ts +663 -444
  61. package/src/security.ts +14 -23
  62. package/src/sse-parser.ts +4 -8
  63. package/src/version.ts +6 -0
  64. package/startup.sh +3 -0
  65. package/tests/base-client.test.ts +328 -0
  66. package/tests/command-client.test.ts +407 -0
  67. package/tests/file-client.test.ts +643 -0
  68. package/tests/file-stream.test.ts +306 -0
  69. package/tests/get-sandbox.test.ts +110 -0
  70. package/tests/git-client.test.ts +328 -0
  71. package/tests/port-client.test.ts +301 -0
  72. package/tests/process-client.test.ts +658 -0
  73. package/tests/sandbox.test.ts +465 -0
  74. package/tests/sse-parser.test.ts +290 -0
  75. package/tests/utility-client.test.ts +332 -0
  76. package/tests/version.test.ts +16 -0
  77. package/tests/wrangler.jsonc +35 -0
  78. package/tsconfig.json +9 -1
  79. package/vitest.config.ts +31 -0
  80. package/container_src/bun.lock +0 -122
  81. package/container_src/handler/exec.ts +0 -340
  82. package/container_src/handler/file.ts +0 -844
  83. package/container_src/handler/git.ts +0 -182
  84. package/container_src/handler/ports.ts +0 -314
  85. package/container_src/handler/process.ts +0 -640
  86. package/container_src/index.ts +0 -531
  87. package/container_src/jupyter-server.ts +0 -336
  88. package/container_src/mime-processor.ts +0 -255
  89. package/container_src/package.json +0 -18
  90. package/container_src/startup.sh +0 -52
  91. package/container_src/types.ts +0 -108
  92. package/src/client.ts +0 -1021
  93. package/src/interpreter-types.ts +0 -383
  94. package/src/jupyter-client.ts +0 -266
  95. package/src/types.ts +0 -401
@@ -1,336 +0,0 @@
1
- import { type Kernel, KernelManager, ServerConnection } from "@jupyterlab/services";
2
- import type {
3
- IDisplayDataMsg,
4
- IErrorMsg,
5
- IExecuteResultMsg,
6
- IIOPubMessage,
7
- IStreamMsg
8
- } from "@jupyterlab/services/lib/kernel/messages";
9
- import {
10
- isDisplayDataMsg,
11
- isErrorMsg,
12
- isExecuteResultMsg,
13
- isStreamMsg
14
- } from "@jupyterlab/services/lib/kernel/messages";
15
- import { v4 as uuidv4 } from "uuid";
16
- import type { ExecutionResult } from "./mime-processor";
17
- import { processJupyterMessage } from "./mime-processor";
18
-
19
- export interface JupyterContext {
20
- id: string;
21
- language: string;
22
- connection: Kernel.IKernelConnection;
23
- cwd: string;
24
- createdAt: Date;
25
- lastUsed: Date;
26
- }
27
-
28
- export interface CreateContextRequest {
29
- language?: string;
30
- cwd?: string;
31
- envVars?: Record<string, string>;
32
- }
33
-
34
- export interface ExecuteCodeRequest {
35
- context_id?: string;
36
- code: string;
37
- language?: string;
38
- env_vars?: Record<string, string>;
39
- }
40
-
41
- export class JupyterServer {
42
- private kernelManager: KernelManager;
43
- private contexts = new Map<string, JupyterContext>();
44
- private defaultContexts = new Map<string, string>(); // language -> context_id
45
-
46
- constructor() {
47
- // Configure connection to local Jupyter server
48
- const serverSettings = ServerConnection.makeSettings({
49
- baseUrl: "http://localhost:8888",
50
- token: "",
51
- appUrl: "",
52
- wsUrl: "ws://localhost:8888",
53
- appendToken: false,
54
- init: {
55
- headers: {
56
- 'Content-Type': 'application/json'
57
- }
58
- }
59
- });
60
-
61
- this.kernelManager = new KernelManager({ serverSettings });
62
- }
63
-
64
- async initialize() {
65
- await this.kernelManager.ready;
66
- console.log("[JupyterServer] Kernel manager initialized");
67
-
68
- // Create default Python context
69
- const pythonContext = await this.createContext({ language: "python" });
70
- this.defaultContexts.set("python", pythonContext.id);
71
- console.log(
72
- "[JupyterServer] Default Python context created:",
73
- pythonContext.id
74
- );
75
- }
76
-
77
- async createContext(req: CreateContextRequest): Promise<JupyterContext> {
78
- const language = req.language || "python";
79
- const cwd = req.cwd || "/workspace";
80
-
81
- const kernelModel = await this.kernelManager.startNew({
82
- name: this.getKernelName(language),
83
- });
84
-
85
- const connection = this.kernelManager.connectTo({ model: kernelModel });
86
-
87
- const context: JupyterContext = {
88
- id: uuidv4(),
89
- language,
90
- connection,
91
- cwd,
92
- createdAt: new Date(),
93
- lastUsed: new Date(),
94
- };
95
-
96
- this.contexts.set(context.id, context);
97
-
98
- // Set working directory
99
- if (cwd !== "/workspace") {
100
- await this.changeWorkingDirectory(context, cwd);
101
- }
102
-
103
- // Set environment variables if provided
104
- if (req.envVars) {
105
- await this.setEnvironmentVariables(context, req.envVars);
106
- }
107
-
108
- return context;
109
- }
110
-
111
- async executeCode(
112
- contextId: string | undefined,
113
- code: string,
114
- language?: string
115
- ): Promise<Response> {
116
- let context: JupyterContext | undefined;
117
-
118
- if (contextId) {
119
- context = this.contexts.get(contextId);
120
- if (!context) {
121
- return new Response(
122
- JSON.stringify({ error: `Context ${contextId} not found` }),
123
- {
124
- status: 404,
125
- headers: { "Content-Type": "application/json" },
126
- }
127
- );
128
- }
129
- } else if (language) {
130
- // Use default context for the language
131
- const defaultContextId = this.defaultContexts.get(language);
132
- if (defaultContextId) {
133
- context = this.contexts.get(defaultContextId);
134
- }
135
-
136
- // Create new default context if needed
137
- if (!context) {
138
- context = await this.createContext({ language });
139
- this.defaultContexts.set(language, context.id);
140
- }
141
- } else {
142
- // Use default Python context
143
- const pythonContextId = this.defaultContexts.get("python");
144
- context = pythonContextId
145
- ? this.contexts.get(pythonContextId)
146
- : undefined;
147
- }
148
-
149
- if (!context) {
150
- return new Response(JSON.stringify({ error: "No context available" }), {
151
- status: 400,
152
- headers: { "Content-Type": "application/json" },
153
- });
154
- }
155
-
156
- // Update last used
157
- context.lastUsed = new Date();
158
-
159
- // Execute with streaming
160
- return this.streamExecution(context.connection, code);
161
- }
162
-
163
- private async streamExecution(
164
- connection: Kernel.IKernelConnection,
165
- code: string
166
- ): Promise<Response> {
167
- const stream = new ReadableStream({
168
- async start(controller) {
169
- const future = connection.requestExecute({
170
- code,
171
- stop_on_error: false,
172
- store_history: true,
173
- silent: false,
174
- allow_stdin: false,
175
- });
176
-
177
- // Handle different message types
178
- future.onIOPub = (msg: IIOPubMessage) => {
179
- const result = processJupyterMessage(msg);
180
- if (result) {
181
- controller.enqueue(
182
- new TextEncoder().encode(`${JSON.stringify(result)}\n`)
183
- );
184
- }
185
- };
186
-
187
- future.onReply = (msg: any) => {
188
- if (msg.content.status === "ok") {
189
- controller.enqueue(
190
- new TextEncoder().encode(
191
- `${JSON.stringify({
192
- type: "execution_complete",
193
- execution_count: msg.content.execution_count,
194
- })}\n`
195
- )
196
- );
197
- } else if (msg.content.status === "error") {
198
- controller.enqueue(
199
- new TextEncoder().encode(
200
- `${JSON.stringify({
201
- type: "error",
202
- ename: msg.content.ename,
203
- evalue: msg.content.evalue,
204
- traceback: msg.content.traceback,
205
- })}\n`
206
- )
207
- );
208
- }
209
- controller.close();
210
- };
211
-
212
- future.onStdin = (msg: any) => {
213
- // We don't support stdin for now
214
- console.warn("[JupyterServer] Stdin requested but not supported");
215
- };
216
- },
217
- });
218
-
219
- return new Response(stream, {
220
- headers: {
221
- "Content-Type": "text/event-stream",
222
- "Cache-Control": "no-cache",
223
- Connection: "keep-alive",
224
- },
225
- });
226
- }
227
-
228
- private getKernelName(language: string): string {
229
- const kernelMap: Record<string, string> = {
230
- python: "python3",
231
- javascript: "javascript",
232
- typescript: "javascript",
233
- js: "javascript",
234
- ts: "javascript",
235
- };
236
- return kernelMap[language.toLowerCase()] || "python3";
237
- }
238
-
239
- private async changeWorkingDirectory(context: JupyterContext, cwd: string) {
240
- const code =
241
- context.language === "python"
242
- ? `import os; os.chdir('${cwd}')`
243
- : `process.chdir('${cwd}')`;
244
-
245
- const future = context.connection.requestExecute({
246
- code,
247
- silent: true,
248
- store_history: false,
249
- });
250
-
251
- return future.done;
252
- }
253
-
254
- private async setEnvironmentVariables(
255
- context: JupyterContext,
256
- envVars: Record<string, string>
257
- ) {
258
- const commands: string[] = [];
259
-
260
- for (const [key, value] of Object.entries(envVars)) {
261
- if (context.language === "python") {
262
- commands.push(`import os; os.environ['${key}'] = '${value}'`);
263
- } else if (
264
- context.language === "javascript" ||
265
- context.language === "typescript"
266
- ) {
267
- commands.push(`process.env['${key}'] = '${value}'`);
268
- }
269
- }
270
-
271
- if (commands.length > 0) {
272
- const code = commands.join("\n");
273
- const future = context.connection.requestExecute({
274
- code,
275
- silent: true,
276
- store_history: false,
277
- });
278
-
279
- return future.done;
280
- }
281
- }
282
-
283
- async listContexts(): Promise<
284
- Array<{
285
- id: string;
286
- language: string;
287
- cwd: string;
288
- createdAt: Date;
289
- lastUsed: Date;
290
- }>
291
- > {
292
- return Array.from(this.contexts.values()).map((ctx) => ({
293
- id: ctx.id,
294
- language: ctx.language,
295
- cwd: ctx.cwd,
296
- createdAt: ctx.createdAt,
297
- lastUsed: ctx.lastUsed,
298
- }));
299
- }
300
-
301
- async deleteContext(contextId: string): Promise<void> {
302
- const context = this.contexts.get(contextId);
303
- if (!context) {
304
- throw new Error(`Context ${contextId} not found`);
305
- }
306
-
307
- // Shutdown the kernel
308
- await context.connection.shutdown();
309
-
310
- // Remove from maps
311
- this.contexts.delete(contextId);
312
-
313
- // Remove from default contexts if it was a default
314
- for (const [lang, id] of this.defaultContexts.entries()) {
315
- if (id === contextId) {
316
- this.defaultContexts.delete(lang);
317
- break;
318
- }
319
- }
320
- }
321
-
322
- async shutdown() {
323
- // Shutdown all kernels
324
- for (const context of this.contexts.values()) {
325
- try {
326
- await context.connection.shutdown();
327
- } catch (error) {
328
- console.error("[JupyterServer] Error shutting down kernel:", error);
329
- }
330
- }
331
-
332
- this.contexts.clear();
333
- this.defaultContexts.clear();
334
- }
335
- }
336
-
@@ -1,255 +0,0 @@
1
- export interface ExecutionResult {
2
- type: 'result' | 'stdout' | 'stderr' | 'error' | 'execution_complete';
3
- text?: string;
4
- html?: string;
5
- png?: string; // base64
6
- jpeg?: string; // base64
7
- svg?: string;
8
- latex?: string;
9
- markdown?: string;
10
- javascript?: string;
11
- json?: any;
12
- chart?: ChartData;
13
- data?: any;
14
- metadata?: any;
15
- execution_count?: number;
16
- ename?: string;
17
- evalue?: string;
18
- traceback?: string[];
19
- timestamp: number;
20
- }
21
-
22
- export interface ChartData {
23
- type: 'line' | 'bar' | 'scatter' | 'pie' | 'histogram' | 'heatmap' | 'unknown';
24
- title?: string;
25
- data: any;
26
- layout?: any;
27
- config?: any;
28
- library?: 'matplotlib' | 'plotly' | 'altair' | 'seaborn' | 'unknown';
29
- }
30
-
31
- export function processJupyterMessage(msg: any): ExecutionResult | null {
32
- const msgType = msg.header?.msg_type || msg.msg_type;
33
-
34
- switch (msgType) {
35
- case 'execute_result':
36
- case 'display_data':
37
- return processDisplayData(msg.content.data, msg.content.metadata);
38
-
39
- case 'stream':
40
- return {
41
- type: msg.content.name === 'stdout' ? 'stdout' : 'stderr',
42
- text: msg.content.text,
43
- timestamp: Date.now()
44
- };
45
-
46
- case 'error':
47
- return {
48
- type: 'error',
49
- ename: msg.content.ename,
50
- evalue: msg.content.evalue,
51
- traceback: msg.content.traceback,
52
- timestamp: Date.now()
53
- };
54
-
55
- default:
56
- return null;
57
- }
58
- }
59
-
60
- function processDisplayData(data: any, metadata?: any): ExecutionResult {
61
- const result: ExecutionResult = {
62
- type: 'result',
63
- timestamp: Date.now(),
64
- metadata
65
- };
66
-
67
- // Process different MIME types in order of preference
68
-
69
- // Interactive/Rich formats
70
- if (data['application/vnd.plotly.v1+json']) {
71
- result.chart = extractPlotlyChart(data['application/vnd.plotly.v1+json']);
72
- result.json = data['application/vnd.plotly.v1+json'];
73
- }
74
-
75
- if (data['application/vnd.vega.v5+json']) {
76
- result.chart = extractVegaChart(data['application/vnd.vega.v5+json'], 'vega');
77
- result.json = data['application/vnd.vega.v5+json'];
78
- }
79
-
80
- if (data['application/vnd.vegalite.v4+json'] || data['application/vnd.vegalite.v5+json']) {
81
- const vegaData = data['application/vnd.vegalite.v4+json'] || data['application/vnd.vegalite.v5+json'];
82
- result.chart = extractVegaChart(vegaData, 'vega-lite');
83
- result.json = vegaData;
84
- }
85
-
86
- // HTML content (tables, formatted output)
87
- if (data['text/html']) {
88
- result.html = data['text/html'];
89
-
90
- // Check if it's a pandas DataFrame
91
- if (isPandasDataFrame(data['text/html'])) {
92
- result.data = { type: 'dataframe', html: data['text/html'] };
93
- }
94
- }
95
-
96
- // Images
97
- if (data['image/png']) {
98
- result.png = data['image/png'];
99
-
100
- // Try to detect if it's a chart
101
- if (isLikelyChart(data, metadata)) {
102
- result.chart = {
103
- type: 'unknown',
104
- library: 'matplotlib',
105
- data: { image: data['image/png'] }
106
- };
107
- }
108
- }
109
-
110
- if (data['image/jpeg']) {
111
- result.jpeg = data['image/jpeg'];
112
- }
113
-
114
- if (data['image/svg+xml']) {
115
- result.svg = data['image/svg+xml'];
116
- }
117
-
118
- // Mathematical content
119
- if (data['text/latex']) {
120
- result.latex = data['text/latex'];
121
- }
122
-
123
- // Code
124
- if (data['application/javascript']) {
125
- result.javascript = data['application/javascript'];
126
- }
127
-
128
- // Structured data
129
- if (data['application/json']) {
130
- result.json = data['application/json'];
131
- }
132
-
133
- // Markdown
134
- if (data['text/markdown']) {
135
- result.markdown = data['text/markdown'];
136
- }
137
-
138
- // Plain text (fallback)
139
- if (data['text/plain']) {
140
- result.text = data['text/plain'];
141
- }
142
-
143
- return result;
144
- }
145
-
146
- function extractPlotlyChart(plotlyData: any): ChartData {
147
- const data = plotlyData.data || plotlyData;
148
- const layout = plotlyData.layout || {};
149
-
150
- // Try to detect chart type from traces
151
- let chartType: ChartData['type'] = 'unknown';
152
- if (data && data.length > 0) {
153
- const firstTrace = data[0];
154
- if (firstTrace.type === 'scatter') {
155
- chartType = firstTrace.mode?.includes('lines') ? 'line' : 'scatter';
156
- } else if (firstTrace.type === 'bar') {
157
- chartType = 'bar';
158
- } else if (firstTrace.type === 'pie') {
159
- chartType = 'pie';
160
- } else if (firstTrace.type === 'histogram') {
161
- chartType = 'histogram';
162
- } else if (firstTrace.type === 'heatmap') {
163
- chartType = 'heatmap';
164
- }
165
- }
166
-
167
- return {
168
- type: chartType,
169
- title: layout.title?.text || layout.title,
170
- data: data,
171
- layout: layout,
172
- config: plotlyData.config,
173
- library: 'plotly'
174
- };
175
- }
176
-
177
- function extractVegaChart(vegaData: any, format: 'vega' | 'vega-lite'): ChartData {
178
- // Try to detect chart type from mark or encoding
179
- let chartType: ChartData['type'] = 'unknown';
180
-
181
- if (format === 'vega-lite' && vegaData.mark) {
182
- const mark = typeof vegaData.mark === 'string' ? vegaData.mark : vegaData.mark.type;
183
- switch (mark) {
184
- case 'line':
185
- chartType = 'line';
186
- break;
187
- case 'bar':
188
- chartType = 'bar';
189
- break;
190
- case 'point':
191
- case 'circle':
192
- chartType = 'scatter';
193
- break;
194
- case 'arc':
195
- chartType = 'pie';
196
- break;
197
- case 'rect':
198
- if (vegaData.encoding?.color) {
199
- chartType = 'heatmap';
200
- }
201
- break;
202
- }
203
- }
204
-
205
- return {
206
- type: chartType,
207
- title: vegaData.title,
208
- data: vegaData,
209
- library: 'altair' // Altair outputs Vega-Lite
210
- };
211
- }
212
-
213
- function isPandasDataFrame(html: string): boolean {
214
- // Simple heuristic to detect pandas DataFrame HTML
215
- return html.includes('dataframe') ||
216
- (html.includes('<table') && html.includes('<thead') && html.includes('<tbody'));
217
- }
218
-
219
- function isLikelyChart(data: any, metadata?: any): boolean {
220
- // Check metadata for hints
221
- if (metadata?.needs?.includes('matplotlib')) {
222
- return true;
223
- }
224
-
225
- // Check if other chart formats are present
226
- if (data['application/vnd.plotly.v1+json'] ||
227
- data['application/vnd.vega.v5+json'] ||
228
- data['application/vnd.vegalite.v4+json']) {
229
- return true;
230
- }
231
-
232
- // If only image output without text, likely a chart
233
- if ((data['image/png'] || data['image/svg+xml']) && !data['text/plain']) {
234
- return true;
235
- }
236
-
237
- return false;
238
- }
239
-
240
- export function extractFormats(result: ExecutionResult): string[] {
241
- const formats: string[] = [];
242
-
243
- if (result.text) formats.push('text');
244
- if (result.html) formats.push('html');
245
- if (result.png) formats.push('png');
246
- if (result.jpeg) formats.push('jpeg');
247
- if (result.svg) formats.push('svg');
248
- if (result.latex) formats.push('latex');
249
- if (result.markdown) formats.push('markdown');
250
- if (result.javascript) formats.push('javascript');
251
- if (result.json) formats.push('json');
252
- if (result.chart) formats.push('chart');
253
-
254
- return formats;
255
- }
@@ -1,18 +0,0 @@
1
- {
2
- "name": "sandbox-server",
3
- "version": "1.0.0",
4
- "description": "A server for the sandbox package",
5
- "main": "index.ts",
6
- "scripts": {
7
- "start": "bun run index.ts"
8
- },
9
- "dependencies": {
10
- "@jupyterlab/services": "^7.0.0",
11
- "ws": "^8.16.0",
12
- "uuid": "^9.0.1"
13
- },
14
- "devDependencies": {
15
- "@types/ws": "^8.5.10",
16
- "@types/uuid": "^9.0.7"
17
- }
18
- }
@@ -1,52 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Start Jupyter notebook server in background
4
- echo "[Startup] Starting Jupyter server..."
5
- jupyter notebook \
6
- --ip=0.0.0.0 \
7
- --port=8888 \
8
- --no-browser \
9
- --allow-root \
10
- --NotebookApp.token='' \
11
- --NotebookApp.password='' \
12
- --NotebookApp.allow_origin='*' \
13
- --NotebookApp.disable_check_xsrf=True \
14
- --NotebookApp.allow_remote_access=True \
15
- --NotebookApp.allow_credentials=True \
16
- > /tmp/jupyter.log 2>&1 &
17
-
18
- JUPYTER_PID=$!
19
-
20
- # Wait for Jupyter to be ready
21
- echo "[Startup] Waiting for Jupyter to become ready..."
22
- MAX_ATTEMPTS=30
23
- ATTEMPT=0
24
-
25
- while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
26
- if curl -s http://localhost:8888/api > /dev/null 2>&1; then
27
- echo "[Startup] Jupyter server is ready!"
28
- break
29
- fi
30
-
31
- # Check if Jupyter process is still running
32
- if ! kill -0 $JUPYTER_PID 2>/dev/null; then
33
- echo "[Startup] ERROR: Jupyter process died. Check /tmp/jupyter.log for details"
34
- cat /tmp/jupyter.log
35
- exit 1
36
- fi
37
-
38
- ATTEMPT=$((ATTEMPT + 1))
39
- echo "[Startup] Waiting for Jupyter... (attempt $ATTEMPT/$MAX_ATTEMPTS)"
40
- sleep 1
41
- done
42
-
43
- if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
44
- echo "[Startup] ERROR: Jupyter failed to start within 30 seconds"
45
- echo "[Startup] Jupyter logs:"
46
- cat /tmp/jupyter.log
47
- exit 1
48
- fi
49
-
50
- # Start the main Bun server
51
- echo "[Startup] Starting Bun server..."
52
- exec bun index.ts