@noego/app 0.0.3 → 0.0.4

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.
@@ -0,0 +1,624 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { spawn } from 'node:child_process';
4
+ import { createBuildContext } from '../build/context.js';
5
+ import { findConfigFile } from '../runtime/index.js';
6
+ import { loadConfig } from '../runtime/config-loader.js';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ export async function runDev(config) {
11
+ const context = createBuildContext(config);
12
+ const { logger } = context;
13
+
14
+ // Find config file
15
+ const configFilePath = await findConfigFile(config.rootDir, config.sources?.configFile);
16
+ const cliRoot = config.sources?.rootDir;
17
+
18
+ // Load config to get dev settings
19
+ const { config: yamlConfig, root: yamlRoot } = await loadConfig(configFilePath, cliRoot);
20
+
21
+ logger.info(`Loading config from ${path.relative(cliRoot || config.rootDir, configFilePath)}`);
22
+
23
+ // Create requireFromRoot using YAML root (user's project root)
24
+ const { createRequire } = await import('node:module');
25
+ const requireFromRoot = createRequire(path.join(yamlRoot, 'package.json'));
26
+
27
+ // Resolve tsx runner
28
+ let tsxExecutable;
29
+ let tsxArgs = [];
30
+ try {
31
+ // Try to find tsx binary in node_modules/.bin first
32
+ const fs = await import('node:fs/promises');
33
+ const tsxBin = path.join(yamlRoot, 'node_modules', '.bin', 'tsx');
34
+ if (await fs.access(tsxBin).then(() => true).catch(() => false)) {
35
+ tsxExecutable = tsxBin;
36
+ } else {
37
+ // Fallback to resolving tsx package.json to get package root, then find CLI
38
+ const tsxPackageJson = requireFromRoot.resolve('tsx/package.json');
39
+ const tsxPackageDir = path.dirname(tsxPackageJson);
40
+ const cliMjs = path.join(tsxPackageDir, 'dist', 'cli.mjs');
41
+ const cliJs = path.join(tsxPackageDir, 'dist', 'cli.js');
42
+ if (await fs.access(cliMjs).then(() => true).catch(() => false)) {
43
+ tsxExecutable = process.execPath;
44
+ tsxArgs = [cliMjs];
45
+ } else if (await fs.access(cliJs).then(() => true).catch(() => false)) {
46
+ tsxExecutable = process.execPath;
47
+ tsxArgs = [cliJs];
48
+ } else {
49
+ throw new Error('tsx CLI not found');
50
+ }
51
+ }
52
+ } catch (err) {
53
+ logger.error(`Failed to resolve tsx: ${err.message}`);
54
+ throw new Error('tsx not found. Please install "tsx" as a dev dependency.');
55
+ }
56
+
57
+ // If Forge dev loader is available, attach it
58
+ // For Node.js loaders, we need to resolve the actual file path
59
+ const fs = await import('node:fs/promises');
60
+ let forgeLoaderPath;
61
+
62
+ // Try to find loader.mjs directly in node_modules
63
+ const forgeLoader = path.join(yamlRoot, 'node_modules', '@noego', 'forge', 'loader.mjs');
64
+ if (await fs.access(forgeLoader).then(() => true).catch(() => false)) {
65
+ forgeLoaderPath = forgeLoader;
66
+ } else {
67
+ // Try to find @noego/forge package and look for loader.mjs
68
+ try {
69
+ const forgePackageDir = path.dirname(requireFromRoot.resolve('@noego/forge'));
70
+ const loaderMjs = path.join(forgePackageDir, 'loader.mjs');
71
+ if (await fs.access(loaderMjs).then(() => true).catch(() => false)) {
72
+ forgeLoaderPath = loaderMjs;
73
+ }
74
+ } catch {
75
+ // Package not found or not accessible
76
+ }
77
+ }
78
+
79
+ if (forgeLoaderPath) {
80
+ tsxArgs.push('--loader', forgeLoaderPath);
81
+ logger.info(`Using @noego/forge/loader for Svelte file support: ${forgeLoaderPath}`);
82
+ } else {
83
+ // Fallback to package name resolution (tsx will resolve it)
84
+ tsxArgs.push('--loader', '@noego/forge/loader');
85
+ logger.info('Using @noego/forge/loader for Svelte file support (package name)');
86
+ }
87
+
88
+ // Runtime entry file (TypeScript)
89
+ const runtimeEntryPath = path.resolve(__dirname, 'runtime-entry.ts');
90
+ tsxArgs.push(runtimeEntryPath);
91
+
92
+ // Set environment variables
93
+ const env = {
94
+ ...process.env,
95
+ NODE_ENV: 'development',
96
+ NOEGO_CONFIG_FILE: configFilePath,
97
+ NOEGO_CLI_ROOT: cliRoot || config.rootDir
98
+ };
99
+
100
+ // Check if split-serve mode
101
+ const splitServe = yamlConfig.dev?.splitServe || false;
102
+
103
+ if (splitServe) {
104
+ // Validate configuration for split-serve
105
+ if (!yamlConfig.server?.main) {
106
+ logger.error('splitServe requires server.main to be configured');
107
+ process.exit(1);
108
+ }
109
+ if (!yamlConfig.client?.main) {
110
+ logger.error('splitServe requires client.main to be configured');
111
+ process.exit(1);
112
+ }
113
+
114
+ // Split-serve mode: run router in main process, spawn frontend and backend
115
+ logger.info('Running in split-serve mode with router architecture...');
116
+
117
+ // Port assignment: router on main port, frontend on +1, backend on +2
118
+ const routerPort = yamlConfig.dev?.port || 3000;
119
+ const frontendPort = routerPort + 1;
120
+ const backendPort = routerPort + 2;
121
+
122
+ if (yamlConfig.dev?.watch) {
123
+ // With watching: monitor files and restart appropriate process
124
+ await runSplitServeWithWatch(context, tsxExecutable, tsxArgs, env, yamlConfig, configFilePath, routerPort, frontendPort, backendPort, logger);
125
+ } else {
126
+ // Without watching: run router and spawn processes
127
+ await runSplitServeNoWatch(tsxExecutable, tsxArgs, env, routerPort, frontendPort, backendPort, config.rootDir, logger);
128
+ }
129
+ } else if (yamlConfig.dev?.watch) {
130
+ // Non-split mode with watching
131
+ const watcher = await createWatcher(context, yamlConfig, configFilePath);
132
+ await runWithRestart(context, tsxExecutable, tsxArgs, env, watcher, logger);
133
+ } else {
134
+ // No watching: run server in background (don't wait for exit)
135
+ logger.info('Starting dev server...');
136
+ logger.debug(`Running: ${tsxExecutable} ${tsxArgs.join(' ')}`);
137
+ const child = spawn(tsxExecutable, tsxArgs, {
138
+ cwd: config.rootDir,
139
+ env,
140
+ stdio: 'inherit',
141
+ detached: false // Keep attached so signals propagate correctly
142
+ });
143
+
144
+ // Handle shutdown gracefully
145
+ const shutdown = () => {
146
+ if (child && !child.killed) {
147
+ child.kill('SIGTERM');
148
+ }
149
+ process.exit(0);
150
+ };
151
+
152
+ child.on('exit', (code) => {
153
+ if (code !== null && code !== 0) {
154
+ logger.info(`Dev server exited with code ${code}`);
155
+ }
156
+ // Only exit if there was an error or explicit exit
157
+ if (code !== null) {
158
+ process.exit(code || 0);
159
+ }
160
+ });
161
+
162
+ child.on('error', (err) => {
163
+ logger.error(`Failed to start dev server: ${err.message}`);
164
+ process.exit(1);
165
+ });
166
+
167
+ // Set up signal handlers to clean up on termination
168
+ process.on('SIGINT', shutdown);
169
+ process.on('SIGTERM', shutdown);
170
+
171
+ // Keep process alive - don't return immediately
172
+ // The process will stay alive as long as child processes are running
173
+ // Event handlers above will handle cleanup
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Run split-serve without watching: spawn backend and frontend processes
179
+ */
180
+ async function runSplitServeNoWatch(tsxExecutable, tsxArgs, baseEnv, routerPort, frontendPort, backendPort, rootDir, logger) {
181
+ logger.info(`Starting router on port ${routerPort}...`);
182
+ logger.info(`Starting frontend on port ${frontendPort}...`);
183
+ logger.info(`Starting backend on port ${backendPort}...`);
184
+
185
+ console.log('[dev.js] Port assignments:');
186
+ console.log(' routerPort:', routerPort);
187
+ console.log(' frontendPort:', frontendPort);
188
+ console.log(' backendPort:', backendPort);
189
+ console.log('[dev.js] baseEnv NOEGO_PORT:', baseEnv.NOEGO_PORT);
190
+ console.log('[dev.js] baseEnv NOEGO_BACKEND_PORT:', baseEnv.NOEGO_BACKEND_PORT);
191
+
192
+ // Spawn backend process
193
+ const backendEnv = {
194
+ ...baseEnv,
195
+ NOEGO_SERVICE: 'backend',
196
+ NOEGO_PORT: String(backendPort)
197
+ };
198
+
199
+ console.log('[dev.js] Backend env NOEGO_PORT:', backendEnv.NOEGO_PORT);
200
+
201
+ const backendProc = spawn(tsxExecutable, tsxArgs, {
202
+ cwd: rootDir,
203
+ env: backendEnv,
204
+ stdio: 'inherit',
205
+ detached: false
206
+ });
207
+
208
+ // Spawn frontend process
209
+ const frontendEnv = {
210
+ ...baseEnv,
211
+ NOEGO_SERVICE: 'frontend',
212
+ NOEGO_PORT: String(frontendPort),
213
+ // Frontend doesn't proxy to backend anymore - router handles that
214
+ };
215
+
216
+ console.log('[dev.js] Frontend env NOEGO_PORT:', frontendEnv.NOEGO_PORT);
217
+
218
+ const frontendProc = spawn(tsxExecutable, tsxArgs, {
219
+ cwd: rootDir,
220
+ env: frontendEnv,
221
+ stdio: 'inherit',
222
+ detached: false
223
+ });
224
+
225
+ // Handle shutdown
226
+ const shutdown = (exitCode = 0) => {
227
+ logger.info('Shutting down split-serve processes...');
228
+ if (backendProc && !backendProc.killed) {
229
+ backendProc.kill('SIGTERM');
230
+ }
231
+ if (frontendProc && !frontendProc.killed) {
232
+ frontendProc.kill('SIGTERM');
233
+ }
234
+ process.exit(exitCode);
235
+ };
236
+
237
+ // If either spawned process crashes, kill both and exit
238
+ backendProc.on('exit', (code) => {
239
+ if (code !== null && code !== 0) {
240
+ logger.error(`Backend exited with code ${code}. Shutting down...`);
241
+ shutdown(code);
242
+ }
243
+ });
244
+
245
+ frontendProc.on('exit', (code) => {
246
+ if (code !== null && code !== 0) {
247
+ logger.error(`Frontend exited with code ${code}. Shutting down...`);
248
+ shutdown(code);
249
+ }
250
+ });
251
+
252
+ backendProc.on('error', (err) => {
253
+ logger.error(`Backend error: ${err.message}. Shutting down...`);
254
+ shutdown(1);
255
+ });
256
+
257
+ frontendProc.on('error', (err) => {
258
+ logger.error(`Frontend error: ${err.message}. Shutting down...`);
259
+ shutdown(1);
260
+ });
261
+
262
+ // Set up signal handlers to clean up on termination
263
+ process.on('SIGINT', () => shutdown(0));
264
+ process.on('SIGTERM', () => shutdown(0));
265
+
266
+ // Wait a moment for spawned processes to start
267
+ await new Promise(resolve => setTimeout(resolve, 2000));
268
+
269
+ // Now run the router in the main process
270
+ // Set environment for router
271
+ const routerEnv = {
272
+ ...baseEnv,
273
+ NOEGO_SERVICE: 'router',
274
+ NOEGO_PORT: String(routerPort),
275
+ NOEGO_FRONTEND_PORT: String(frontendPort),
276
+ NOEGO_BACKEND_PORT: String(backendPort)
277
+ };
278
+
279
+ // Merge router env into process.env
280
+ Object.assign(process.env, routerEnv);
281
+
282
+ // Execute the router runtime in the current process
283
+ logger.info('Starting router service in main process...');
284
+ const runtimeEntryPath = tsxArgs[tsxArgs.length - 1];
285
+ // Use dynamic import instead of require for ES module compatibility
286
+ await import(runtimeEntryPath);
287
+ }
288
+
289
+ /**
290
+ * Run split-serve with watching: spawn backend and frontend processes, restart on file changes
291
+ */
292
+ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv, yamlConfig, configFilePath, routerPort, frontendPort, backendPort, logger) {
293
+ const { createRequire } = await import('node:module');
294
+ const root = yamlConfig.root || path.dirname(configFilePath);
295
+ const requireFromRoot = createRequire(path.join(root, 'package.json'));
296
+ const chokidar = requireFromRoot('chokidar');
297
+ const picomatch = (await import('picomatch')).default;
298
+
299
+ const resolvePattern = (pattern) => {
300
+ if (path.isAbsolute(pattern)) return pattern;
301
+ return path.resolve(root, pattern);
302
+ };
303
+
304
+ // Collect watch patterns
305
+ const backendPatterns = [];
306
+ const frontendPatterns = [];
307
+ const sharedPatterns = [];
308
+
309
+ // App watch patterns (restart both)
310
+ if (yamlConfig.app?.watch) {
311
+ for (const pattern of yamlConfig.app.watch) {
312
+ sharedPatterns.push(resolvePattern(pattern));
313
+ }
314
+ }
315
+
316
+ // Server watch patterns (restart backend only)
317
+ if (yamlConfig.server?.watch) {
318
+ for (const pattern of yamlConfig.server.watch) {
319
+ backendPatterns.push(resolvePattern(pattern));
320
+ }
321
+ }
322
+
323
+ // Client watch patterns (restart frontend only)
324
+ if (yamlConfig.client?.watch) {
325
+ for (const pattern of yamlConfig.client.watch) {
326
+ frontendPatterns.push(resolvePattern(pattern));
327
+ }
328
+ }
329
+
330
+ // Combine all patterns for chokidar
331
+ const allPatterns = [...sharedPatterns, ...backendPatterns, ...frontendPatterns];
332
+
333
+ if (allPatterns.length === 0) {
334
+ logger.warn('No watch patterns configured. Watching disabled.');
335
+ return await runSplitServeNoWatch(tsxExecutable, tsxArgs, baseEnv, routerPort, frontendPort, backendPort, context.config.rootDir, logger);
336
+ }
337
+
338
+ logger.info('Watching for changes...');
339
+ logger.info(` Shared: ${sharedPatterns.length} patterns`);
340
+ logger.info(` Backend: ${backendPatterns.length} patterns`);
341
+ logger.info(` Frontend: ${frontendPatterns.length} patterns`);
342
+
343
+ // Create matchers
344
+ const sharedMatcher = sharedPatterns.length > 0 ? picomatch(sharedPatterns) : null;
345
+ const backendMatcher = backendPatterns.length > 0 ? picomatch(backendPatterns) : null;
346
+ const frontendMatcher = frontendPatterns.length > 0 ? picomatch(frontendPatterns) : null;
347
+
348
+ let backendProc = null;
349
+ let frontendProc = null;
350
+ let pending = false;
351
+
352
+ const startBackend = () => {
353
+ logger.info(`Starting backend on port ${backendPort}...`);
354
+ const backendEnv = {
355
+ ...baseEnv,
356
+ NOEGO_SERVICE: 'backend',
357
+ NOEGO_PORT: String(backendPort)
358
+ };
359
+
360
+ backendProc = spawn(tsxExecutable, tsxArgs, {
361
+ cwd: context.config.rootDir,
362
+ env: backendEnv,
363
+ stdio: 'inherit',
364
+ detached: false
365
+ });
366
+
367
+ backendProc.on('exit', (code) => {
368
+ if (code !== null && code !== 0) {
369
+ logger.error(`Backend exited with code ${code}`);
370
+ }
371
+ });
372
+ };
373
+
374
+ const startFrontend = () => {
375
+ logger.info(`Starting frontend on port ${frontendPort}...`);
376
+ const frontendEnv = {
377
+ ...baseEnv,
378
+ NOEGO_SERVICE: 'frontend',
379
+ NOEGO_PORT: String(frontendPort)
380
+ };
381
+
382
+ frontendProc = spawn(tsxExecutable, tsxArgs, {
383
+ cwd: context.config.rootDir,
384
+ env: frontendEnv,
385
+ stdio: 'inherit',
386
+ detached: false
387
+ });
388
+
389
+ frontendProc.on('exit', (code) => {
390
+ if (code !== null && code !== 0) {
391
+ logger.error(`Frontend exited with code ${code}`);
392
+ }
393
+ });
394
+ };
395
+
396
+ const stopBackend = () =>
397
+ new Promise((resolve) => {
398
+ if (!backendProc) return resolve();
399
+ const to = setTimeout(resolve, 2000);
400
+ backendProc.once('exit', () => {
401
+ clearTimeout(to);
402
+ resolve();
403
+ });
404
+ try {
405
+ backendProc.kill('SIGTERM');
406
+ } catch {
407
+ resolve();
408
+ }
409
+ });
410
+
411
+ const stopFrontend = () =>
412
+ new Promise((resolve) => {
413
+ if (!frontendProc) return resolve();
414
+ const to = setTimeout(resolve, 2000);
415
+ frontendProc.once('exit', () => {
416
+ clearTimeout(to);
417
+ resolve();
418
+ });
419
+ try {
420
+ frontendProc.kill('SIGTERM');
421
+ } catch {
422
+ resolve();
423
+ }
424
+ });
425
+
426
+ const handleFileChange = async (reason, file) => {
427
+ if (pending) return;
428
+ pending = true;
429
+
430
+ const absPath = path.isAbsolute(file) ? file : path.resolve(root, file);
431
+
432
+ // Determine which service(s) to restart
433
+ let restartBackend = false;
434
+ let restartFrontend = false;
435
+
436
+ if (sharedMatcher && sharedMatcher(absPath)) {
437
+ // Shared file changed - restart both
438
+ logger.info(`Shared file changed (${reason}): ${file}`);
439
+ restartBackend = true;
440
+ restartFrontend = true;
441
+ } else if (backendMatcher && backendMatcher(absPath)) {
442
+ // Backend file changed
443
+ logger.info(`Backend file changed (${reason}): ${file}`);
444
+ restartBackend = true;
445
+ } else if (frontendMatcher && frontendMatcher(absPath)) {
446
+ // Frontend file changed
447
+ logger.info(`Frontend file changed (${reason}): ${file}`);
448
+ restartFrontend = true;
449
+ }
450
+
451
+ // Restart appropriate service(s)
452
+ if (restartBackend) {
453
+ await stopBackend();
454
+ startBackend();
455
+ }
456
+ if (restartFrontend) {
457
+ await stopFrontend();
458
+ startFrontend();
459
+ }
460
+
461
+ pending = false;
462
+ };
463
+
464
+ // Create watcher
465
+ const watcher = chokidar.watch(allPatterns, {
466
+ ignoreInitial: true,
467
+ cwd: root
468
+ });
469
+
470
+ watcher
471
+ .on('add', (p) => handleFileChange('add', p))
472
+ .on('change', (p) => handleFileChange('change', p))
473
+ .on('unlink', (p) => handleFileChange('unlink', p));
474
+
475
+ // Start initial processes
476
+ startBackend();
477
+ startFrontend();
478
+
479
+ // Wait a moment for spawned processes to start
480
+ await new Promise(resolve => setTimeout(resolve, 2000));
481
+
482
+ // Now run the router in the main process
483
+ const routerEnv = {
484
+ ...baseEnv,
485
+ NOEGO_SERVICE: 'router',
486
+ NOEGO_PORT: String(routerPort),
487
+ NOEGO_FRONTEND_PORT: String(frontendPort),
488
+ NOEGO_BACKEND_PORT: String(backendPort)
489
+ };
490
+
491
+ // Merge router env into process.env
492
+ Object.assign(process.env, routerEnv);
493
+
494
+ // Execute the router runtime in the current process
495
+ logger.info('Starting router service in main process...');
496
+ const runtimeEntryPath = tsxArgs[tsxArgs.length - 1];
497
+ // Use dynamic import instead of require for ES module compatibility
498
+ await import(runtimeEntryPath);
499
+
500
+ // Handle shutdown
501
+ const shutdown = async () => {
502
+ logger.info('Shutting down split-serve processes...');
503
+ await stopBackend();
504
+ await stopFrontend();
505
+ await watcher.close();
506
+ process.exit(0);
507
+ };
508
+
509
+ process.on('SIGINT', shutdown);
510
+ process.on('SIGTERM', shutdown);
511
+ }
512
+
513
+ async function createWatcher(context, yamlConfig, configFilePath) {
514
+ const { logger } = context;
515
+ const { createRequire } = await import('node:module');
516
+ const root = yamlConfig.root || path.dirname(configFilePath);
517
+ const requireFromRoot = createRequire(path.join(root, 'package.json'));
518
+ const chokidar = requireFromRoot('chokidar');
519
+
520
+ const patterns = new Set();
521
+
522
+ const resolvePattern = (pattern) => {
523
+ if (path.isAbsolute(pattern)) return pattern;
524
+ return path.resolve(root, pattern);
525
+ };
526
+
527
+ // User-supplied watch paths
528
+ if (yamlConfig.dev?.watchPaths) {
529
+ for (const pattern of yamlConfig.dev.watchPaths) {
530
+ patterns.add(resolvePattern(pattern));
531
+ }
532
+ }
533
+
534
+ // Default watch patterns from config
535
+ if (yamlConfig.server?.controllers) {
536
+ patterns.add(resolvePattern(`${yamlConfig.server.controllers}/**/*.{ts,js}`));
537
+ }
538
+ if (yamlConfig.server?.middleware) {
539
+ patterns.add(resolvePattern(`${yamlConfig.server.middleware}/**/*.{ts,js}`));
540
+ }
541
+ if (yamlConfig.server?.openapi) {
542
+ patterns.add(resolvePattern(yamlConfig.server.openapi));
543
+ const openapiDir = path.dirname(resolvePattern(yamlConfig.server.openapi));
544
+ patterns.add(path.join(openapiDir, 'openapi/**/*.yaml'));
545
+ }
546
+ if (yamlConfig.client?.openapi) {
547
+ patterns.add(resolvePattern(yamlConfig.client.openapi));
548
+ const openapiDir = path.dirname(resolvePattern(yamlConfig.client.openapi));
549
+ patterns.add(path.join(openapiDir, 'openapi/**/*.yaml'));
550
+ }
551
+ if (yamlConfig.assets) {
552
+ for (const asset of yamlConfig.assets) {
553
+ if (asset.includes('*.sql')) {
554
+ patterns.add(resolvePattern(asset));
555
+ }
556
+ }
557
+ }
558
+
559
+ logger.info('Watching for changes to restart server...');
560
+ const watcher = chokidar.watch(Array.from(patterns), {
561
+ ignoreInitial: true,
562
+ cwd: root
563
+ });
564
+ return watcher;
565
+ }
566
+
567
+ async function runWithRestart(context, tsxExecutable, tsxArgs, env, watcher, logger) {
568
+ let child = null;
569
+
570
+ const start = () => {
571
+ child = spawn(tsxExecutable, tsxArgs, {
572
+ cwd: context.config.rootDir,
573
+ env,
574
+ stdio: 'inherit'
575
+ });
576
+ child.on('exit', (code) => {
577
+ if (code !== null && code !== 0) {
578
+ logger.info(`Dev server exited with code ${code}`);
579
+ }
580
+ });
581
+ };
582
+
583
+ const stop = () =>
584
+ new Promise((resolve) => {
585
+ if (!child) return resolve();
586
+ const to = setTimeout(resolve, 2000);
587
+ child.once('exit', () => {
588
+ clearTimeout(to);
589
+ resolve();
590
+ });
591
+ try {
592
+ child.kill('SIGTERM');
593
+ } catch {
594
+ resolve();
595
+ }
596
+ });
597
+
598
+ let pending = false;
599
+ const scheduleRestart = async (reason, file) => {
600
+ if (pending) return;
601
+ pending = true;
602
+ logger.info(`Change detected (${reason}): ${file}. Restarting...`);
603
+ await stop();
604
+ start();
605
+ pending = false;
606
+ };
607
+
608
+ watcher
609
+ .on('add', (p) => scheduleRestart('add', p))
610
+ .on('change', (p) => scheduleRestart('change', p))
611
+ .on('unlink', (p) => scheduleRestart('unlink', p));
612
+
613
+ // Start initial server
614
+ start();
615
+
616
+ // Keep process alive until SIGINT/SIGTERM
617
+ const shutdown = async () => {
618
+ await stop();
619
+ await watcher.close();
620
+ process.exit(0);
621
+ };
622
+ process.on('SIGINT', shutdown);
623
+ process.on('SIGTERM', shutdown);
624
+ }
@@ -0,0 +1,16 @@
1
+ import { runRuntime } from '../runtime/runtime.js';
2
+
3
+ // This file is run via tsx, so it can import TypeScript files
4
+ const configFilePath = process.env.NOEGO_CONFIG_FILE;
5
+ const cliRoot = process.env.NOEGO_CLI_ROOT;
6
+
7
+ if (!configFilePath) {
8
+ console.error('NOEGO_CONFIG_FILE environment variable is required');
9
+ process.exit(1);
10
+ }
11
+
12
+ runRuntime(configFilePath, cliRoot || undefined).catch((error) => {
13
+ console.error('Runtime error:', error);
14
+ process.exit(1);
15
+ });
16
+