nebula-notebook 0.1.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 (97) hide show
  1. package/README.md +222 -0
  2. package/bin/nebula-notebook.js +33 -0
  3. package/dist/assets/index-C1h_sArD.css +32 -0
  4. package/dist/assets/index-CDSTBon8.js +658 -0
  5. package/dist/favicon.svg +11 -0
  6. package/dist/index.html +73 -0
  7. package/node-server/dist/app.d.ts +5 -0
  8. package/node-server/dist/app.js +38 -0
  9. package/node-server/dist/auth/auth-middleware.d.ts +24 -0
  10. package/node-server/dist/auth/auth-middleware.js +276 -0
  11. package/node-server/dist/auth/auth-service.d.ts +84 -0
  12. package/node-server/dist/auth/auth-service.js +265 -0
  13. package/node-server/dist/auth/index.d.ts +3 -0
  14. package/node-server/dist/auth/index.js +8 -0
  15. package/node-server/dist/cluster/client-registration.d.ts +43 -0
  16. package/node-server/dist/cluster/client-registration.js +217 -0
  17. package/node-server/dist/cluster/cluster-secret.d.ts +11 -0
  18. package/node-server/dist/cluster/cluster-secret.js +90 -0
  19. package/node-server/dist/cluster/kernel-proxy.d.ts +100 -0
  20. package/node-server/dist/cluster/kernel-proxy.js +361 -0
  21. package/node-server/dist/cluster/server-registry.d.ts +109 -0
  22. package/node-server/dist/cluster/server-registry.js +217 -0
  23. package/node-server/dist/config/output-limits.d.ts +7 -0
  24. package/node-server/dist/config/output-limits.js +10 -0
  25. package/node-server/dist/discovery/discovery-service.d.ts +198 -0
  26. package/node-server/dist/discovery/discovery-service.js +811 -0
  27. package/node-server/dist/discovery/index.d.ts +5 -0
  28. package/node-server/dist/discovery/index.js +21 -0
  29. package/node-server/dist/discovery/types.d.ts +48 -0
  30. package/node-server/dist/discovery/types.js +24 -0
  31. package/node-server/dist/fs/fs-service.d.ts +218 -0
  32. package/node-server/dist/fs/fs-service.js +1422 -0
  33. package/node-server/dist/fs/index.d.ts +5 -0
  34. package/node-server/dist/fs/index.js +21 -0
  35. package/node-server/dist/fs/types.d.ts +132 -0
  36. package/node-server/dist/fs/types.js +5 -0
  37. package/node-server/dist/index.d.ts +13 -0
  38. package/node-server/dist/index.js +556 -0
  39. package/node-server/dist/kernel/default-kernel.d.ts +5 -0
  40. package/node-server/dist/kernel/default-kernel.js +138 -0
  41. package/node-server/dist/kernel/index.d.ts +7 -0
  42. package/node-server/dist/kernel/index.js +23 -0
  43. package/node-server/dist/kernel/kernel-service.d.ts +290 -0
  44. package/node-server/dist/kernel/kernel-service.js +1714 -0
  45. package/node-server/dist/kernel/kernelspec.d.ts +29 -0
  46. package/node-server/dist/kernel/kernelspec.js +210 -0
  47. package/node-server/dist/kernel/session-store.d.ts +87 -0
  48. package/node-server/dist/kernel/session-store.js +303 -0
  49. package/node-server/dist/kernel/types.d.ts +143 -0
  50. package/node-server/dist/kernel/types.js +17 -0
  51. package/node-server/dist/llm/index.d.ts +5 -0
  52. package/node-server/dist/llm/index.js +21 -0
  53. package/node-server/dist/llm/llm-service.d.ts +77 -0
  54. package/node-server/dist/llm/llm-service.js +454 -0
  55. package/node-server/dist/llm/types.d.ts +40 -0
  56. package/node-server/dist/llm/types.js +15 -0
  57. package/node-server/dist/notebook/cell-metadata.d.ts +27 -0
  58. package/node-server/dist/notebook/cell-metadata.js +76 -0
  59. package/node-server/dist/notebook/headless-handler.d.ts +127 -0
  60. package/node-server/dist/notebook/headless-handler.js +1530 -0
  61. package/node-server/dist/notebook/notebook-websocket.d.ts +12 -0
  62. package/node-server/dist/notebook/notebook-websocket.js +103 -0
  63. package/node-server/dist/notebook/operation-router.d.ts +115 -0
  64. package/node-server/dist/notebook/operation-router.js +641 -0
  65. package/node-server/dist/notebook/undoRedoManager.d.ts +194 -0
  66. package/node-server/dist/notebook/undoRedoManager.js +558 -0
  67. package/node-server/dist/output/display-data.d.ts +14 -0
  68. package/node-server/dist/output/display-data.js +134 -0
  69. package/node-server/dist/resources/resource-service.d.ts +69 -0
  70. package/node-server/dist/resources/resource-service.js +363 -0
  71. package/node-server/dist/routes/auth.d.ts +5 -0
  72. package/node-server/dist/routes/auth.js +61 -0
  73. package/node-server/dist/routes/cluster.d.ts +7 -0
  74. package/node-server/dist/routes/cluster.js +94 -0
  75. package/node-server/dist/routes/fs.d.ts +7 -0
  76. package/node-server/dist/routes/fs.js +392 -0
  77. package/node-server/dist/routes/kernel.d.ts +13 -0
  78. package/node-server/dist/routes/kernel.js +637 -0
  79. package/node-server/dist/routes/llm.d.ts +8 -0
  80. package/node-server/dist/routes/llm.js +105 -0
  81. package/node-server/dist/routes/notebook.d.ts +10 -0
  82. package/node-server/dist/routes/notebook.js +335 -0
  83. package/node-server/dist/routes/python.d.ts +8 -0
  84. package/node-server/dist/routes/python.js +187 -0
  85. package/node-server/dist/routes/resources.d.ts +7 -0
  86. package/node-server/dist/routes/resources.js +77 -0
  87. package/node-server/dist/scripts/show-auth-qr.d.ts +1 -0
  88. package/node-server/dist/scripts/show-auth-qr.js +81 -0
  89. package/node-server/dist/terminal/pty-manager.d.ts +100 -0
  90. package/node-server/dist/terminal/pty-manager.js +246 -0
  91. package/node-server/dist/terminal/server.d.ts +19 -0
  92. package/node-server/dist/terminal/server.js +254 -0
  93. package/node-server/dist/terminal/types.d.ts +50 -0
  94. package/node-server/dist/terminal/types.js +9 -0
  95. package/node-server/package.json +45 -0
  96. package/package.json +99 -0
  97. package/scripts/postinstall.cjs +26 -0
@@ -0,0 +1,556 @@
1
+ "use strict";
2
+ /**
3
+ * Nebula Node Server - Main Entry Point
4
+ *
5
+ * Unified Node.js server for:
6
+ * - Jupyter kernel management
7
+ * - LLM providers (Google, OpenAI, Anthropic)
8
+ * - Filesystem operations
9
+ * - Python environment discovery
10
+ * - Terminal PTY management
11
+ *
12
+ * Uses Fastify with HTTP/2 support.
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ var __importDefault = (this && this.__importDefault) || function (mod) {
48
+ return (mod && mod.__esModule) ? mod : { "default": mod };
49
+ };
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ const fastify_1 = __importDefault(require("fastify"));
52
+ const cors_1 = __importDefault(require("@fastify/cors"));
53
+ const multipart_1 = __importDefault(require("@fastify/multipart"));
54
+ const static_1 = __importDefault(require("@fastify/static"));
55
+ const ws_1 = require("ws");
56
+ const url_1 = require("url");
57
+ const path = __importStar(require("path"));
58
+ const fs = __importStar(require("fs"));
59
+ const os = __importStar(require("os"));
60
+ const crypto = __importStar(require("crypto"));
61
+ const crypto_1 = require("crypto");
62
+ // Import routes
63
+ const kernel_1 = __importStar(require("./routes/kernel"));
64
+ const llm_1 = __importStar(require("./routes/llm"));
65
+ const fs_1 = __importDefault(require("./routes/fs"));
66
+ const notebook_1 = __importDefault(require("./routes/notebook"));
67
+ const python_1 = __importDefault(require("./routes/python"));
68
+ const auth_1 = __importDefault(require("./routes/auth"));
69
+ const cluster_1 = __importDefault(require("./routes/cluster"));
70
+ const resources_1 = __importDefault(require("./routes/resources"));
71
+ // Import cluster
72
+ const server_registry_1 = require("./cluster/server-registry");
73
+ const cluster_secret_1 = require("./cluster/cluster-secret");
74
+ const client_registration_1 = require("./cluster/client-registration");
75
+ // Import auth
76
+ const auth_2 = require("./auth");
77
+ const fs_service_1 = require("./fs/fs-service");
78
+ // Import terminal routes (existing)
79
+ const server_1 = require("./terminal/server");
80
+ // Import notebook WebSocket
81
+ const notebook_websocket_1 = require("./notebook/notebook-websocket");
82
+ const PORT = process.env.PORT || process.env.NODE_SERVER_PORT || 3000;
83
+ const DEV_MODE = process.env.DEV_MODE === 'true' || process.argv.includes('--dev');
84
+ const BODY_LIMIT = process.env.NEBULA_BODY_LIMIT ||
85
+ process.env.NEBULA_MAX_BODY_SIZE ||
86
+ '1gb';
87
+ const CLIENT_MODE = process.argv.includes('--client') ||
88
+ process.argv.includes('--client-mode') ||
89
+ process.env.NEBULA_CLIENT === 'true' ||
90
+ process.env.NEBULA_CLIENT_MODE === 'true' ||
91
+ process.env.npm_config_client === 'true' ||
92
+ process.env.npm_config_client === '1';
93
+ const hasCliFlag = (names) => names.some(name => process.argv.includes(name));
94
+ const parseOptionalBoolean = (value) => {
95
+ if (value === undefined)
96
+ return null;
97
+ const normalized = value.trim().toLowerCase();
98
+ if (['true', '1', 'yes', 'on'].includes(normalized))
99
+ return true;
100
+ if (['false', '0', 'no', 'off'].includes(normalized))
101
+ return false;
102
+ return null;
103
+ };
104
+ const resolveBooleanFlag = (enabledCliFlags, disabledCliFlags, envNames, npmConfigNames, defaultValue) => {
105
+ if (hasCliFlag(disabledCliFlags))
106
+ return false;
107
+ if (hasCliFlag(enabledCliFlags))
108
+ return true;
109
+ for (const envName of [...envNames, ...npmConfigNames]) {
110
+ const resolved = parseOptionalBoolean(process.env[envName]);
111
+ if (resolved !== null) {
112
+ return resolved;
113
+ }
114
+ }
115
+ return defaultValue;
116
+ };
117
+ const getArgValue = (name) => {
118
+ const idx = process.argv.findIndex(arg => arg === name);
119
+ if (idx !== -1 && process.argv[idx + 1]) {
120
+ return process.argv[idx + 1];
121
+ }
122
+ const prefix = `${name}=`;
123
+ const found = process.argv.find(arg => arg.startsWith(prefix));
124
+ if (found) {
125
+ return found.slice(prefix.length);
126
+ }
127
+ return null;
128
+ };
129
+ const WORKDIR = getArgValue('--workdir') || process.env.NEBULA_WORKDIR || process.env.npm_config_workdir;
130
+ const PRESERVE_KERNELS = resolveBooleanFlag(['--preserve-kernels', '--preserve-kernel'], ['--no-preserve-kernels', '--no-preserve-kernel'], ['NEBULA_PRESERVE_KERNELS'], ['npm_config_preserve_kernels'], DEV_MODE);
131
+ const REATTACH_KERNELS = resolveBooleanFlag(['--reattach-kernels', '--reattach-kernel'], ['--no-reattach-kernels', '--no-reattach-kernel'], ['NEBULA_REATTACH_KERNELS'], ['npm_config_reattach_kernels'], DEV_MODE);
132
+ // Log kernel preservation settings
133
+ if (PRESERVE_KERNELS)
134
+ console.log('[Server] Kernel preservation ENABLED');
135
+ if (REATTACH_KERNELS)
136
+ console.log('[Server] Kernel reattachment ENABLED');
137
+ /**
138
+ * Parse a human-readable body limit string (e.g., '200mb', '1gb') to bytes.
139
+ */
140
+ function parseBodyLimit(limit) {
141
+ const match = limit.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb|tb)?$/i);
142
+ if (!match) {
143
+ return 1024 * 1024 * 1024; // default 1gb
144
+ }
145
+ const value = parseFloat(match[1]);
146
+ const unit = (match[2] || 'b').toLowerCase();
147
+ const multipliers = {
148
+ b: 1,
149
+ kb: 1024,
150
+ mb: 1024 * 1024,
151
+ gb: 1024 * 1024 * 1024,
152
+ tb: 1024 * 1024 * 1024 * 1024,
153
+ };
154
+ return Math.floor(value * (multipliers[unit] || 1));
155
+ }
156
+ // HTTP/1.1 only for now — HTTP/2 streaming requires a separate HTTPS server.
157
+ // Returning from a function (vs a literal null const) keeps the TLS branch
158
+ // typechecking so the build stays green while the feature is parked.
159
+ function loadTlsCert() {
160
+ return null;
161
+ }
162
+ /**
163
+ * Generate or load self-signed TLS certificate for HTTP/2
164
+ */
165
+ function getOrCreateTlsCert() {
166
+ const tlsDir = path.join(os.homedir(), '.nebula', 'tls');
167
+ const keyPath = path.join(tlsDir, 'server.key');
168
+ const certPath = path.join(tlsDir, 'server.cert');
169
+ try {
170
+ // Check if cert already exists and is still valid
171
+ if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
172
+ const key = fs.readFileSync(keyPath);
173
+ const cert = fs.readFileSync(certPath);
174
+ return { key, cert };
175
+ }
176
+ // Generate self-signed certificate using Node.js crypto
177
+ // Only available in Node.js 15+
178
+ if (!('generateKeyPairSync' in crypto)) {
179
+ console.log('[TLS] crypto.generateKeyPairSync not available, falling back to HTTP/1.1');
180
+ return null;
181
+ }
182
+ console.log('[TLS] Generating self-signed certificate for HTTP/2...');
183
+ // Create TLS directory
184
+ fs.mkdirSync(tlsDir, { recursive: true, mode: 0o700 });
185
+ // Use child_process to generate cert with openssl (most portable approach)
186
+ const { execSync } = require('child_process');
187
+ try {
188
+ execSync(`openssl req -x509 -newkey rsa:2048 -keyout "${keyPath}" -out "${certPath}" -days 365 -nodes -subj "/CN=localhost"`, { stdio: 'pipe' });
189
+ fs.chmodSync(keyPath, 0o600);
190
+ fs.chmodSync(certPath, 0o600);
191
+ const key = fs.readFileSync(keyPath);
192
+ const cert = fs.readFileSync(certPath);
193
+ console.log('[TLS] Self-signed certificate generated and cached at ~/.nebula/tls/');
194
+ return { key, cert };
195
+ }
196
+ catch (err) {
197
+ console.log('[TLS] openssl not available, falling back to HTTP/1.1');
198
+ return null;
199
+ }
200
+ }
201
+ catch (err) {
202
+ console.log('[TLS] Failed to generate certificate, falling back to HTTP/1.1');
203
+ return null;
204
+ }
205
+ }
206
+ /**
207
+ * Create and configure Fastify app
208
+ */
209
+ async function createApp() {
210
+ const bodyLimitBytes = parseBodyLimit(BODY_LIMIT);
211
+ const tlsCert = loadTlsCert();
212
+ let fastify;
213
+ if (tlsCert) {
214
+ // Cast to FastifyInstance to unify the type with the HTTP/1 branch.
215
+ // The HTTP/2 Fastify instance is a superset but TypeScript infers a
216
+ // different generic specialisation; the cast is safe because we only
217
+ // use the common API surface.
218
+ fastify = (0, fastify_1.default)({
219
+ http2: true,
220
+ https: {
221
+ key: tlsCert.key,
222
+ cert: tlsCert.cert,
223
+ allowHTTP1: true,
224
+ },
225
+ bodyLimit: bodyLimitBytes,
226
+ });
227
+ console.log('[Server] HTTP/2 with TLS enabled');
228
+ }
229
+ else {
230
+ fastify = (0, fastify_1.default)({
231
+ bodyLimit: bodyLimitBytes,
232
+ });
233
+ console.log('[Server] HTTP/1.1 mode (no TLS)');
234
+ }
235
+ // Request timing hook
236
+ fastify.addHook('onResponse', (request, reply, done) => {
237
+ const duration = reply.elapsedTime;
238
+ if (duration > 1000) {
239
+ console.log(`[Slow Request] ${request.method} ${request.url} - ${Math.round(duration)}ms`);
240
+ }
241
+ done();
242
+ });
243
+ // Register CORS
244
+ await fastify.register(cors_1.default, {
245
+ origin: '*',
246
+ credentials: true,
247
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
248
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-API-Provider'],
249
+ });
250
+ // Register multipart support (replaces multer)
251
+ await fastify.register(multipart_1.default, {
252
+ limits: {
253
+ fileSize: bodyLimitBytes,
254
+ },
255
+ });
256
+ // Allow empty body with Content-Type: application/json.
257
+ // MCP clients and some tools send DELETE/GET with an empty JSON body.
258
+ // Fastify rejects this by default (FST_ERR_CTP_EMPTY_JSON_BODY).
259
+ fastify.addContentTypeParser('application/json', { parseAs: 'string' }, (req, body, done) => {
260
+ if (!body || (typeof body === 'string' && body.trim() === '')) {
261
+ done(null, undefined);
262
+ return;
263
+ }
264
+ try {
265
+ done(null, JSON.parse(body));
266
+ }
267
+ catch (err) {
268
+ done(err, undefined);
269
+ }
270
+ });
271
+ // Health check endpoints (public - no auth required)
272
+ fastify.get('/api/health', async (_request, reply) => {
273
+ // Use configured root directory (from .nebula-config.json or fallback to cwd)
274
+ const rootDir = fs_service_1.fsService.normalizePath('~');
275
+ return reply.send({
276
+ status: 'ok',
277
+ version: '1.0.0',
278
+ ready: kernel_1.kernelService.isReady,
279
+ llm_providers: Object.keys(llm_1.llmService.getAvailableProviders()),
280
+ cwd: rootDir,
281
+ });
282
+ });
283
+ fastify.get('/api/ready', async (_request, reply) => {
284
+ if (!kernel_1.kernelService.isReady) {
285
+ return reply.code(503).send({
286
+ detail: 'Service initializing, kernel discovery in progress',
287
+ });
288
+ }
289
+ return reply.send({ status: 'ready' });
290
+ });
291
+ // Auth routes (public - no auth required)
292
+ await fastify.register(auth_1.default, { prefix: '/api' });
293
+ // Auth middleware - protect all other API routes
294
+ // Applied as an onRequest hook for /api/* routes (excluding public ones)
295
+ fastify.addHook('onRequest', async (request, reply) => {
296
+ const pathname = request.url.split('?')[0];
297
+ // Skip health, ready, and auth routes (they are public)
298
+ if (pathname === '/api/health' ||
299
+ pathname === '/api/ready' ||
300
+ pathname.startsWith('/api/auth/')) {
301
+ return;
302
+ }
303
+ // Skip non-API routes (static files etc)
304
+ if (!pathname.startsWith('/api/')) {
305
+ return;
306
+ }
307
+ // Apply auth middleware
308
+ await (0, auth_2.authMiddleware)(request, reply);
309
+ });
310
+ // API routes (protected)
311
+ await fastify.register(kernel_1.default, { prefix: '/api' });
312
+ await fastify.register(llm_1.default, { prefix: '/api' });
313
+ await fastify.register(fs_1.default, { prefix: '/api' });
314
+ await fastify.register(notebook_1.default, { prefix: '/api' });
315
+ await fastify.register(python_1.default, { prefix: '/api' });
316
+ await fastify.register(cluster_1.default, { prefix: '/api' });
317
+ await fastify.register(resources_1.default, { prefix: '/api/resources' });
318
+ // Terminal routes (registered directly on the app, not under /api prefix)
319
+ await fastify.register(server_1.setupTerminalRoutes);
320
+ return fastify;
321
+ }
322
+ /**
323
+ * Setup WebSocket routing (kernel, terminal, notebook)
324
+ * Uses the raw Node.js HTTP(S) server from Fastify for upgrade handling
325
+ */
326
+ function setupWebSockets(server) {
327
+ // Create WebSocket server with noServer mode for path-based routing
328
+ // Disable per-message deflate to avoid compression issues with proxies/browsers
329
+ const wss = new ws_1.WebSocketServer({
330
+ noServer: true,
331
+ perMessageDeflate: false,
332
+ });
333
+ // Handle upgrade requests
334
+ server.on('upgrade', (request, socket, head) => {
335
+ const pathname = (0, url_1.parse)(request.url || '').pathname || '';
336
+ // Route to kernel WebSocket
337
+ if (pathname.match(/^\/api\/kernels\/[^/]+\/ws$/)) {
338
+ // Authenticate WebSocket connection
339
+ if (!(0, auth_2.authWebSocketMiddleware)(request)) {
340
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
341
+ socket.destroy();
342
+ return;
343
+ }
344
+ wss.handleUpgrade(request, socket, head, (ws) => {
345
+ wss.emit('connection', ws, request);
346
+ });
347
+ }
348
+ // Terminal WebSocket (/ws) is handled by setupTerminalWebSocket
349
+ // Don't destroy socket for other paths - let other handlers deal with them
350
+ });
351
+ // Setup kernel WebSocket handler
352
+ (0, kernel_1.setupKernelWebSocket)(wss);
353
+ }
354
+ /**
355
+ * Setup static file serving for frontend
356
+ */
357
+ async function setupStaticServing(fastify) {
358
+ const distDir = path.join(__dirname, '../../dist');
359
+ if (DEV_MODE) {
360
+ console.log('[Server] Development mode - frontend should run separately with npm run dev');
361
+ return;
362
+ }
363
+ // Production mode: serve from dist
364
+ if (fs.existsSync(distDir)) {
365
+ // Serve static files
366
+ await fastify.register(static_1.default, {
367
+ root: distDir,
368
+ prefix: '/',
369
+ });
370
+ // SPA fallback - serve index.html for all non-API routes
371
+ fastify.setNotFoundHandler(async (request, reply) => {
372
+ const pathname = request.url.split('?')[0];
373
+ if (pathname.startsWith('/api/')) {
374
+ return reply.code(404).send({ detail: 'Not found' });
375
+ }
376
+ if (pathname.startsWith('/assets/') || path.extname(pathname)) {
377
+ return reply.code(404).type('text/plain').send('Not found');
378
+ }
379
+ return reply.sendFile('index.html');
380
+ });
381
+ console.log(`[Server] Serving frontend from ${distDir}`);
382
+ }
383
+ else {
384
+ console.log(`[Server] Warning: dist directory not found at ${distDir}`);
385
+ console.log('[Server] Run "npm run build" to create the production build');
386
+ }
387
+ }
388
+ /**
389
+ * Main entry point
390
+ */
391
+ async function main() {
392
+ console.log('[Server] Starting Nebula Node Server...');
393
+ const mainServerUrl = process.env.NEBULA_MAIN_SERVER;
394
+ if (CLIENT_MODE && !mainServerUrl) {
395
+ console.error('[Cluster] Client mode requested but NEBULA_MAIN_SERVER is not set.');
396
+ console.error('[Cluster] Set NEBULA_MAIN_SERVER or remove --client.');
397
+ process.exit(1);
398
+ }
399
+ process.env.NEBULA_CLIENT_MODE = CLIENT_MODE ? 'true' : 'false';
400
+ if (!process.env.NEBULA_CLUSTER_SECRET) {
401
+ const allowCreate = !CLIENT_MODE;
402
+ const secret = (0, cluster_secret_1.getOrCreateClusterSecret)({ allowCreate });
403
+ if (secret) {
404
+ process.env.NEBULA_CLUSTER_SECRET = secret;
405
+ if (allowCreate) {
406
+ console.log('[Cluster] Generated cluster secret (stored at ~/.nebula/cluster.json)');
407
+ }
408
+ }
409
+ else if (CLIENT_MODE) {
410
+ console.error('[Cluster] No NEBULA_CLUSTER_SECRET set and no ~/.nebula/cluster.json found.');
411
+ console.error('[Cluster] Set NEBULA_CLUSTER_SECRET or copy ~/.nebula/cluster.json from the main server.');
412
+ process.exit(1);
413
+ }
414
+ }
415
+ const authDisabled = process.argv.includes('--noauth') ||
416
+ process.argv.includes('--no-auth') ||
417
+ process.env.NO_AUTH === 'true' ||
418
+ process.env.NEBULA_NO_AUTH === 'true' ||
419
+ process.env.npm_config_noauth === 'true' ||
420
+ process.env.npm_config_noauth === '1' ||
421
+ process.env.npm_config_no_auth === 'true' ||
422
+ process.env.npm_config_no_auth === '1' ||
423
+ CLIENT_MODE;
424
+ if (authDisabled) {
425
+ auth_2.authService.disableAuth();
426
+ console.log(`[Auth] Disabled (${CLIENT_MODE ? 'client mode' : '--noauth'})`);
427
+ }
428
+ if (WORKDIR) {
429
+ try {
430
+ const updated = fs_service_1.fsService.setRootDirectory(WORKDIR);
431
+ console.log(`[Server] Root directory set to ${updated}`);
432
+ }
433
+ catch (err) {
434
+ const message = err instanceof Error ? err.message : String(err);
435
+ console.warn(`[Server] Failed to set root directory: ${message}`);
436
+ }
437
+ }
438
+ if (PRESERVE_KERNELS) {
439
+ console.log('[Kernel] Preserve kernels enabled');
440
+ }
441
+ if (REATTACH_KERNELS) {
442
+ console.log('[Kernel] Reattach kernels on startup enabled');
443
+ }
444
+ // Initialize authentication
445
+ const setupNeeded = await auth_2.authService.initialize();
446
+ if (setupNeeded) {
447
+ auth_2.authService.printSetupInstructions();
448
+ }
449
+ else {
450
+ console.log('[Auth] 2FA configured and ready');
451
+ }
452
+ // Set local server ID from hostname
453
+ const localServerId = `${os.hostname()}:${PORT}`;
454
+ server_registry_1.serverRegistry.setLocalServerId(localServerId);
455
+ console.log(`[Cluster] Local server ID: ${localServerId}`);
456
+ const serverInstanceId = (0, crypto_1.randomUUID)();
457
+ process.env.NEBULA_SERVER_ID = localServerId;
458
+ process.env.NEBULA_SERVER_INSTANCE_ID = serverInstanceId;
459
+ kernel_1.kernelService.setServerIdentity(localServerId, serverInstanceId);
460
+ // Create Fastify app (HTTP — always works, no cert warnings)
461
+ const fastify = await createApp();
462
+ // Setup static file serving
463
+ await setupStaticServing(fastify);
464
+ // Start HTTP server on main port
465
+ await fastify.listen({ port: Number(PORT), host: '0.0.0.0' });
466
+ // Get the raw Node.js server for WebSocket handling
467
+ const server = fastify.server;
468
+ // Setup WebSocket routing (kernel)
469
+ setupWebSockets(server);
470
+ // Setup terminal WebSocket (now using noServer mode)
471
+ (0, server_1.setupTerminalWebSocket)(server);
472
+ // Setup notebook operations WebSocket
473
+ (0, notebook_websocket_1.setupNotebookWebSocket)(server);
474
+ if (REATTACH_KERNELS) {
475
+ try {
476
+ const result = await kernel_1.kernelService.reattachOrphanedSessions();
477
+ if (result.attempted > 0) {
478
+ console.log(`[Kernel] Reattach summary: ${result.reattached} reattached, ${result.failed} failed, ${result.skipped} skipped`);
479
+ }
480
+ }
481
+ catch (err) {
482
+ const message = err instanceof Error ? err.message : String(err);
483
+ console.warn(`[Kernel] Reattach failed: ${message}`);
484
+ }
485
+ }
486
+ // Graceful shutdown
487
+ let shuttingDown = false;
488
+ const shutdown = async () => {
489
+ if (shuttingDown)
490
+ return; // ignore repeat signals (tsx watch can send several)
491
+ shuttingDown = true;
492
+ console.log('\n[Server] Shutting down...');
493
+ console.log(`[Server] PRESERVE_KERNELS=${PRESERVE_KERNELS}`);
494
+ // Safety net: shutdown must never hang the process. fastify.close() blocks
495
+ // until every open connection drains, and Nebula holds long-lived WebSockets
496
+ // (kernel/notebook/terminal), so without this a SIGTERM from `tsx watch` would
497
+ // leave the old process alive — and backend code changes would silently never
498
+ // reload. Force-exit if cleanup stalls.
499
+ const forceExit = setTimeout(() => {
500
+ console.warn('[Server] Shutdown exceeded 3s — forcing exit');
501
+ process.exit(0);
502
+ }, 3000);
503
+ forceExit.unref();
504
+ // Cleanup terminals
505
+ (0, server_1.cleanupTerminals)();
506
+ // Cleanup kernel sessions (fast when preserving; awaited so kernel state is saved)
507
+ try {
508
+ await kernel_1.kernelService.shutdown({ preserveKernels: PRESERVE_KERNELS });
509
+ }
510
+ catch (err) {
511
+ console.error('[Server] Error during kernel cleanup:', err);
512
+ }
513
+ // Cleanup cluster registration
514
+ try {
515
+ await client_registration_1.clientRegistration.shutdown();
516
+ server_registry_1.serverRegistry.shutdown();
517
+ }
518
+ catch (err) {
519
+ console.error('[Server] Error during cluster cleanup:', err);
520
+ }
521
+ // Close the HTTP/WS server, but don't wait forever on open WebSocket clients —
522
+ // race the close against a short timeout so dev restarts stay snappy.
523
+ try {
524
+ await Promise.race([
525
+ fastify.close(),
526
+ new Promise((resolve) => setTimeout(resolve, 750)),
527
+ ]);
528
+ }
529
+ catch (err) {
530
+ console.error('[Server] Error during server close:', err);
531
+ }
532
+ console.log('[Server] Server closed');
533
+ clearTimeout(forceExit);
534
+ process.exit(0);
535
+ };
536
+ process.on('SIGINT', shutdown);
537
+ process.on('SIGTERM', shutdown);
538
+ const protocol = fastify.server.constructor.name.includes('Secure') ? 'https' : 'http';
539
+ const wsProtocol = protocol === 'https' ? 'wss' : 'ws';
540
+ const mode = DEV_MODE ? 'development' : 'production';
541
+ console.log(`[Server] Nebula running on ${protocol}://localhost:${PORT} (${mode} mode)`);
542
+ console.log(`[Server] API endpoints: ${protocol}://localhost:${PORT}/api/*`);
543
+ console.log(`[Server] Kernel WebSocket: ${wsProtocol}://localhost:${PORT}/api/kernels/{session_id}/ws`);
544
+ console.log(`[Server] Notebook WebSocket: ${wsProtocol}://localhost:${PORT}/api/notebook/{path}/ws`);
545
+ console.log(`[Server] Terminal WebSocket: ${wsProtocol}://localhost:${PORT}/ws?id={terminal_id}`);
546
+ console.log(`[Server] Root directory: ${fs_service_1.fsService.getRootDirectory()} (change with --workdir)`);
547
+ // Initialize client registration (explicit client mode only)
548
+ if (CLIENT_MODE) {
549
+ client_registration_1.clientRegistration.initFromEnv(Number(PORT));
550
+ }
551
+ }
552
+ // Run
553
+ main().catch((err) => {
554
+ console.error('[Server] Fatal error:', err);
555
+ process.exit(1);
556
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Default kernel resolution based on the Python executable in the server env.
3
+ */
4
+ export declare function getDefaultKernelName(): Promise<string | null>;
5
+ export declare function invalidateDefaultKernelName(): void;