@agentuity/cli 1.0.48 → 2.0.0-beta.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 (124) hide show
  1. package/dist/cmd/build/app-router-detector.d.ts +2 -5
  2. package/dist/cmd/build/app-router-detector.d.ts.map +1 -1
  3. package/dist/cmd/build/app-router-detector.js +130 -154
  4. package/dist/cmd/build/app-router-detector.js.map +1 -1
  5. package/dist/cmd/build/ids.d.ts +11 -0
  6. package/dist/cmd/build/ids.d.ts.map +1 -0
  7. package/dist/cmd/build/ids.js +18 -0
  8. package/dist/cmd/build/ids.js.map +1 -0
  9. package/dist/cmd/build/vite/agent-discovery.d.ts +8 -4
  10. package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
  11. package/dist/cmd/build/vite/agent-discovery.js +166 -487
  12. package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
  13. package/dist/cmd/build/vite/bun-dev-server.d.ts +10 -16
  14. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
  15. package/dist/cmd/build/vite/bun-dev-server.js +67 -134
  16. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  17. package/dist/cmd/build/vite/docs-generator.d.ts.map +1 -1
  18. package/dist/cmd/build/vite/docs-generator.js +0 -2
  19. package/dist/cmd/build/vite/docs-generator.js.map +1 -1
  20. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  21. package/dist/cmd/build/vite/index.js +0 -36
  22. package/dist/cmd/build/vite/index.js.map +1 -1
  23. package/dist/cmd/build/vite/lifecycle-generator.d.ts +10 -2
  24. package/dist/cmd/build/vite/lifecycle-generator.d.ts.map +1 -1
  25. package/dist/cmd/build/vite/lifecycle-generator.js +302 -23
  26. package/dist/cmd/build/vite/lifecycle-generator.js.map +1 -1
  27. package/dist/cmd/build/vite/route-discovery.d.ts +11 -38
  28. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  29. package/dist/cmd/build/vite/route-discovery.js +97 -177
  30. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  31. package/dist/cmd/build/vite/server-bundler.js +1 -1
  32. package/dist/cmd/build/vite/server-bundler.js.map +1 -1
  33. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  34. package/dist/cmd/build/vite/static-renderer.js +1 -9
  35. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  36. package/dist/cmd/build/vite/vite-asset-server-config.d.ts +6 -3
  37. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  38. package/dist/cmd/build/vite/vite-asset-server-config.js +171 -21
  39. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  40. package/dist/cmd/build/vite/vite-asset-server.d.ts +8 -3
  41. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  42. package/dist/cmd/build/vite/vite-asset-server.js +14 -13
  43. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  44. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  45. package/dist/cmd/build/vite/vite-builder.js +6 -36
  46. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  47. package/dist/cmd/build/vite/ws-proxy.d.ts +53 -0
  48. package/dist/cmd/build/vite/ws-proxy.d.ts.map +1 -0
  49. package/dist/cmd/build/vite/ws-proxy.js +95 -0
  50. package/dist/cmd/build/vite/ws-proxy.js.map +1 -0
  51. package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
  52. package/dist/cmd/build/vite-bundler.js +0 -3
  53. package/dist/cmd/build/vite-bundler.js.map +1 -1
  54. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  55. package/dist/cmd/cloud/deploy.js +0 -1
  56. package/dist/cmd/cloud/deploy.js.map +1 -1
  57. package/dist/cmd/dev/file-watcher.d.ts.map +1 -1
  58. package/dist/cmd/dev/file-watcher.js +2 -8
  59. package/dist/cmd/dev/file-watcher.js.map +1 -1
  60. package/dist/cmd/dev/index.d.ts.map +1 -1
  61. package/dist/cmd/dev/index.js +369 -720
  62. package/dist/cmd/dev/index.js.map +1 -1
  63. package/package.json +6 -8
  64. package/src/cmd/ai/prompt/agent.md +0 -1
  65. package/src/cmd/ai/prompt/api.md +0 -7
  66. package/src/cmd/ai/prompt/web.md +51 -213
  67. package/src/cmd/build/app-router-detector.ts +152 -182
  68. package/src/cmd/build/ids.ts +19 -0
  69. package/src/cmd/build/vite/agent-discovery.ts +208 -679
  70. package/src/cmd/build/vite/bun-dev-server.ts +78 -154
  71. package/src/cmd/build/vite/docs-generator.ts +0 -2
  72. package/src/cmd/build/vite/index.ts +1 -42
  73. package/src/cmd/build/vite/lifecycle-generator.ts +345 -21
  74. package/src/cmd/build/vite/route-discovery.ts +116 -274
  75. package/src/cmd/build/vite/server-bundler.ts +1 -1
  76. package/src/cmd/build/vite/static-renderer.ts +1 -11
  77. package/src/cmd/build/vite/vite-asset-server-config.ts +196 -23
  78. package/src/cmd/build/vite/vite-asset-server.ts +25 -15
  79. package/src/cmd/build/vite/vite-builder.ts +6 -53
  80. package/src/cmd/build/vite/ws-proxy.ts +126 -0
  81. package/src/cmd/build/vite-bundler.ts +0 -4
  82. package/src/cmd/cloud/deploy.ts +0 -1
  83. package/src/cmd/dev/file-watcher.ts +2 -9
  84. package/src/cmd/dev/index.ts +409 -832
  85. package/dist/cmd/build/ast.d.ts +0 -78
  86. package/dist/cmd/build/ast.d.ts.map +0 -1
  87. package/dist/cmd/build/ast.js +0 -2703
  88. package/dist/cmd/build/ast.js.map +0 -1
  89. package/dist/cmd/build/entry-generator.d.ts +0 -25
  90. package/dist/cmd/build/entry-generator.d.ts.map +0 -1
  91. package/dist/cmd/build/entry-generator.js +0 -695
  92. package/dist/cmd/build/entry-generator.js.map +0 -1
  93. package/dist/cmd/build/vite/api-mount-path.d.ts +0 -61
  94. package/dist/cmd/build/vite/api-mount-path.d.ts.map +0 -1
  95. package/dist/cmd/build/vite/api-mount-path.js +0 -83
  96. package/dist/cmd/build/vite/api-mount-path.js.map +0 -1
  97. package/dist/cmd/build/vite/registry-generator.d.ts +0 -19
  98. package/dist/cmd/build/vite/registry-generator.d.ts.map +0 -1
  99. package/dist/cmd/build/vite/registry-generator.js +0 -1108
  100. package/dist/cmd/build/vite/registry-generator.js.map +0 -1
  101. package/dist/cmd/build/vite/tailwind-source-plugin.d.ts +0 -13
  102. package/dist/cmd/build/vite/tailwind-source-plugin.d.ts.map +0 -1
  103. package/dist/cmd/build/vite/tailwind-source-plugin.js +0 -44
  104. package/dist/cmd/build/vite/tailwind-source-plugin.js.map +0 -1
  105. package/dist/cmd/build/webanalytics-generator.d.ts +0 -16
  106. package/dist/cmd/build/webanalytics-generator.d.ts.map +0 -1
  107. package/dist/cmd/build/webanalytics-generator.js +0 -178
  108. package/dist/cmd/build/webanalytics-generator.js.map +0 -1
  109. package/dist/cmd/build/workbench.d.ts +0 -7
  110. package/dist/cmd/build/workbench.d.ts.map +0 -1
  111. package/dist/cmd/build/workbench.js +0 -55
  112. package/dist/cmd/build/workbench.js.map +0 -1
  113. package/dist/utils/route-migration.d.ts +0 -62
  114. package/dist/utils/route-migration.d.ts.map +0 -1
  115. package/dist/utils/route-migration.js +0 -630
  116. package/dist/utils/route-migration.js.map +0 -1
  117. package/src/cmd/build/ast.ts +0 -3529
  118. package/src/cmd/build/entry-generator.ts +0 -760
  119. package/src/cmd/build/vite/api-mount-path.ts +0 -87
  120. package/src/cmd/build/vite/registry-generator.ts +0 -1267
  121. package/src/cmd/build/vite/tailwind-source-plugin.ts +0 -54
  122. package/src/cmd/build/webanalytics-generator.ts +0 -197
  123. package/src/cmd/build/workbench.ts +0 -58
  124. package/src/utils/route-migration.ts +0 -757
@@ -11,15 +11,13 @@ import { generateEndpoint } from './api';
11
11
  import { APIClient, getAPIBaseURL, getAppBaseURL, getGravityDevModeURL } from '../../api';
12
12
  import { download } from './download';
13
13
  import { createDevmodeSyncService } from './sync';
14
- import { getDevmodeDeploymentId } from '../build/ast';
14
+ import { getDevmodeDeploymentId } from '../build/ids';
15
15
  import { getDefaultConfigDir, saveConfig, loadProjectSDKKey, getAuth } from '../../config';
16
16
  import { typecheck } from '../build/typecheck';
17
17
  import { validateGravityRequiresUpgrade } from '../../runtime';
18
18
  import { isTTY, hasLoggedInBefore } from '../../auth';
19
- import { createFileWatcher } from './file-watcher';
20
19
  import { prepareDevLock, releaseLockSync } from './dev-lock';
21
20
  import { checkAndUpgradeDependencies } from '../../utils/dependency-checker';
22
- import { promptRouteMigration, performMigration, checkMigrationEligibility, } from '../../utils/route-migration';
23
21
  import { ErrorCode } from '../../errors';
24
22
  const DEFAULT_PORT = 3500;
25
23
  const MIN_PORT = 1024;
@@ -56,85 +54,22 @@ async function killLingeringGravityProcesses(logger) {
56
54
  }
57
55
  }
58
56
  /**
59
- * Stop the existing Bun server if one is running.
60
- * Waits for the port to become available before returning (with timeout).
61
- * Handles both in-process server and subprocess (when debugger is enabled).
57
+ * Kill the Bun backend subprocess if one is running.
62
58
  */
63
- async function stopBunServer(port, logger) {
59
+ function killBunSubprocess(logger) {
64
60
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
61
  const globalAny = globalThis;
66
- // Check for subprocess first (used when debugger flags are enabled)
67
62
  const bunSubprocess = globalAny.__AGENTUITY_BUN_SUBPROCESS__;
68
- if (bunSubprocess) {
69
- logger.debug('Stopping Bun subprocess...');
70
- try {
71
- bunSubprocess.kill('SIGTERM');
72
- // After SIGTERM, wait and check multiple times before giving up
73
- let attempts = 0;
74
- while (bunSubprocess.exitCode === null && attempts < 3) {
75
- await new Promise((resolve) => setTimeout(resolve, 100));
76
- attempts++;
77
- }
78
- if (bunSubprocess.exitCode === null) {
79
- bunSubprocess.kill('SIGKILL');
80
- }
81
- logger.debug('Bun subprocess killed');
82
- }
83
- catch (err) {
84
- logger.debug('Error killing Bun subprocess: %s', err);
85
- }
86
- globalAny.__AGENTUITY_BUN_SUBPROCESS__ = undefined;
87
- // Wait for port to become available
88
- const MAX_WAIT_ITERATIONS = 10;
89
- for (let i = 0; i < MAX_WAIT_ITERATIONS; i++) {
90
- try {
91
- await fetch(`http://127.0.0.1:${port}/`, {
92
- method: 'HEAD',
93
- signal: AbortSignal.timeout(150),
94
- });
95
- // Still responding, wait a bit more
96
- await new Promise((r) => setTimeout(r, 50));
97
- }
98
- catch {
99
- // Connection refused or timeout => server is down
100
- logger.debug('Bun subprocess stopped');
101
- break;
102
- }
103
- }
63
+ if (!bunSubprocess)
104
64
  return;
105
- }
106
- // Handle in-process server
107
- const server = globalAny.__AGENTUITY_SERVER__;
108
- if (!server) {
109
- logger.debug('No Bun server to stop');
110
- return;
111
- }
112
65
  try {
113
- logger.debug('Stopping Bun server...');
114
- server.stop(true); // Close active connections immediately
115
- logger.debug('Bun server stop() called');
66
+ bunSubprocess.kill('SIGTERM');
67
+ logger.debug('Bun subprocess killed');
116
68
  }
117
69
  catch (err) {
118
- logger.debug('Error stopping Bun server: %s', err);
119
- }
120
- // Wait for socket to close (max 2 seconds to avoid hanging on shutdown)
121
- const MAX_WAIT_ITERATIONS = 10;
122
- for (let i = 0; i < MAX_WAIT_ITERATIONS; i++) {
123
- try {
124
- await fetch(`http://127.0.0.1:${port}/`, {
125
- method: 'HEAD',
126
- signal: AbortSignal.timeout(150),
127
- });
128
- // Still responding, wait a bit more
129
- await new Promise((r) => setTimeout(r, 50));
130
- }
131
- catch {
132
- // Connection refused or timeout => server is down
133
- logger.debug('Bun server stopped');
134
- break;
135
- }
70
+ logger.debug('Error killing Bun subprocess: %s', err);
136
71
  }
137
- globalAny.__AGENTUITY_SERVER__ = undefined;
72
+ globalAny.__AGENTUITY_BUN_SUBPROCESS__ = undefined;
138
73
  }
139
74
  const getDefaultPort = () => {
140
75
  const envPort = process.env.PORT;
@@ -192,18 +127,10 @@ export const command = createCommand({
192
127
  .boolean()
193
128
  .optional()
194
129
  .describe('Enable bun debugger with breakpoint at first line'),
195
- experimentalNoBundle: z
196
- .boolean()
197
- .optional()
198
- .describe('[Experimental] Skip Bun.build in dev mode — run generated entry file directly'),
199
130
  noTypecheck: z
200
131
  .boolean()
201
132
  .optional()
202
133
  .describe('Skip TypeScript type checking on startup and restarts'),
203
- migrateRoutes: z
204
- .boolean()
205
- .optional()
206
- .describe('Migrate file-based routes to explicit routing (src/api/index.ts root router)'),
207
134
  resume: z.string().optional().describe('Resume a paused Hub session by ID'),
208
135
  }),
209
136
  },
@@ -357,31 +284,6 @@ export const command = createCommand({
357
284
  devLock.release();
358
285
  tui.fatal(`Failed to upgrade dependencies: ${upgradeResult.failed.join(', ')}`, ErrorCode.BUILD_FAILED);
359
286
  }
360
- // Check if project can migrate to explicit routing
361
- if (opts.migrateRoutes) {
362
- const eligibility = checkMigrationEligibility(rootDir);
363
- if (eligibility.available) {
364
- const result = performMigration(rootDir, eligibility.routeFiles);
365
- if (result.success) {
366
- tui.success(result.message);
367
- if (result.filesCreated.length > 0) {
368
- tui.info(`Created: ${result.filesCreated.map((f) => tui.muted(f)).join(', ')}`);
369
- }
370
- tui.newline();
371
- }
372
- else {
373
- tui.warning(result.message);
374
- tui.newline();
375
- }
376
- }
377
- else {
378
- tui.info('No migration needed — already using explicit routing.');
379
- tui.newline();
380
- }
381
- }
382
- else {
383
- await promptRouteMigration(rootDir, logger, { interactive });
384
- }
385
287
  try {
386
288
  // Setup devmode and gravity (if using public URL)
387
289
  const useMockService = process.env.DEVMODE_SYNC_SERVICE_MOCK === 'true';
@@ -491,61 +393,113 @@ export const command = createCommand({
491
393
  bottomSpacer: false,
492
394
  centerTitle: false,
493
395
  });
494
- // Start Vite asset server ONCE before restart loop
495
- // Vite handles frontend HMR independently and stays running across backend restarts
396
+ // Detect user route mount paths for Vite proxy configuration
397
+ // This is a quick AST scan of app.ts runs before Vite starts
398
+ let routePaths = ['/api']; // Default fallback
399
+ try {
400
+ const { detectExplicitRouter } = await import('../build/app-router-detector');
401
+ const detection = await detectExplicitRouter(rootDir, logger);
402
+ if (detection.detected && detection.mounts.length > 0) {
403
+ routePaths = detection.mounts.map((m) => m.path);
404
+ logger.debug('Detected route mount paths: %s', routePaths.join(', '));
405
+ }
406
+ }
407
+ catch (err) {
408
+ logger.debug('Route detection failed, using default /api: %s', err);
409
+ }
410
+ // Pick internal ports (neither is user-facing — the front-door proxy is)
411
+ const bunBackendPort = opts.port + 1;
412
+ const viteInternalPort = opts.port + 2;
413
+ // No-bundle dev mode guard: ensure stale bundled app artifact cannot be executed.
414
+ // We keep other .agentuity artifacts (metadata/workbench files) intact.
415
+ try {
416
+ const staleBundlePath = join(rootDir, '.agentuity', 'app.js');
417
+ if (existsSync(staleBundlePath)) {
418
+ await Bun.file(staleBundlePath).delete();
419
+ logger.debug('Removed stale dev bundle artifact: %s', staleBundlePath);
420
+ }
421
+ }
422
+ catch (err) {
423
+ logger.debug('Failed to remove stale dev bundle artifact: %s', err);
424
+ }
425
+ // Debug trace: locate unexpected legacy credential warnings.
426
+ // Enable with AGENTUITY_TRACE_CREDENTIAL_WARNINGS=true.
427
+ if (process.env.AGENTUITY_TRACE_CREDENTIAL_WARNINGS === 'true') {
428
+ const originalConsoleError = console.error.bind(console);
429
+ console.error = (...args) => {
430
+ try {
431
+ const first = typeof args[0] === 'string' ? args[0] : '';
432
+ if (first.includes('No credentials found for this AI provider')) {
433
+ const stack = new Error('Credential warning trace').stack;
434
+ originalConsoleError('[TRACE] Credential warning origin stack:');
435
+ if (stack)
436
+ originalConsoleError(stack);
437
+ }
438
+ }
439
+ catch {
440
+ // ignore tracing errors
441
+ }
442
+ originalConsoleError(...args);
443
+ };
444
+ }
445
+ // Start Vite dev server on an internal port.
446
+ // The user-facing port is handled by the front-door TCP proxy (ws-proxy)
447
+ // which routes WS upgrades to Bun and everything else to Vite.
496
448
  let viteServer = null;
497
449
  let vitePort;
498
450
  try {
499
- logger.debug('Starting Vite asset server...');
451
+ logger.debug('Starting Vite dev server (internal port %d)...', viteInternalPort);
500
452
  const viteResult = await startViteAssetServer({
501
453
  rootDir,
502
454
  logger,
503
455
  workbenchPath: workbench.config?.route,
456
+ port: viteInternalPort,
457
+ backendPort: bunBackendPort,
458
+ routePaths,
504
459
  });
505
460
  viteServer = viteResult.server;
506
461
  vitePort = viteResult.port;
507
462
  // Update dev lock with actual Vite port
508
463
  await devLock.updatePorts({ vite: vitePort });
509
- logger.debug(`Vite asset server running on port ${vitePort} (stays running across backend restarts)`);
464
+ logger.debug(`Vite dev server running on port ${vitePort} (internal, proxying backend on port ${bunBackendPort})`);
465
+ }
466
+ catch (error) {
467
+ tui.error(`Failed to start Vite dev server: ${error}`);
468
+ await devLock.release();
469
+ originalExit(1);
470
+ return;
471
+ }
472
+ // Start the front-door TCP proxy on the user-facing port.
473
+ // Routes WebSocket upgrades (for /api/*, /_agentuity/*) directly to Bun
474
+ // and everything else (HTTP, HMR WebSocket) to Vite.
475
+ // This works around Bun's broken node:http upgrade socket implementation.
476
+ let frontDoorServer = null;
477
+ try {
478
+ const { startWsProxy } = await import('../build/vite/ws-proxy');
479
+ frontDoorServer = await startWsProxy({
480
+ port: opts.port,
481
+ vitePort,
482
+ backendPort: bunBackendPort,
483
+ routePaths,
484
+ logger,
485
+ });
486
+ logger.debug(`Front-door proxy on port ${opts.port} (Vite:${vitePort}, Bun:${bunBackendPort})`);
510
487
  }
511
488
  catch (error) {
512
- tui.error(`Failed to start Vite asset server: ${error}`);
489
+ tui.error(`Failed to start front-door proxy: ${error}`);
513
490
  await devLock.release();
514
491
  originalExit(1);
515
492
  return;
516
493
  }
517
- // Restart loop - allows BACKEND server to restart on file changes
518
- // Vite stays running and handles frontend changes via HMR
519
- let shouldRestart = false;
494
+ // --- State for long-running processes ---
520
495
  let gravityProcess = null;
521
496
  let gravityHeartbeatInterval = null;
522
- let stdinListenerRegistered = false; // Track if stdin listener is already registered
523
- const restartServer = () => {
524
- shouldRestart = true;
525
- };
526
- const showWelcome = () => {
527
- logger.info('DevMode ready 🚀');
528
- };
529
- // Create file watcher for backend hot reload
530
- const fileWatcher = createFileWatcher({
531
- rootDir,
532
- logger,
533
- onRestart: restartServer,
534
- });
535
- // Start file watcher (will be paused during builds)
536
- fileWatcher.start();
537
- // Track if cleanup is in progress to avoid duplicate cleanup
538
- let cleaningUp = false;
539
- // Track if shutdown was requested (SIGINT/SIGTERM) to break the main loop
540
- let shutdownRequested = false;
541
- // Store stdin data handler reference for cleanup
497
+ let stdinListenerRegistered = false;
542
498
  let stdinDataHandler = null;
499
+ let shutdownRequested = false;
500
+ let cleaningUp = false;
543
501
  /**
544
502
  * Centralized cleanup function for all resources.
545
- * Called on restart, shutdown, and fatal errors.
546
- * @param exitAfter - If true, exit the process after cleanup
547
- * @param exitCode - Exit code to use if exitAfter is true
548
- * @param silent - If true, don't show "Shutting down" message
549
503
  */
550
504
  const cleanup = async (exitAfter = false, exitCode = 0, silent = false) => {
551
505
  if (cleaningUp)
@@ -554,83 +508,53 @@ export const command = createCommand({
554
508
  if (!silent) {
555
509
  tui.info('Shutting down...');
556
510
  }
557
- // Stop file watcher first to prevent restart triggers during cleanup
558
- try {
559
- fileWatcher.stop();
560
- }
561
- catch (err) {
562
- logger.debug('Error stopping file watcher: %s', err);
563
- }
564
- // Stop Bun server
511
+ // Stop front-door proxy
565
512
  try {
566
- await stopBunServer(opts.port, logger);
513
+ frontDoorServer?.close();
567
514
  }
568
515
  catch (err) {
569
- logger.debug('Error stopping Bun server during cleanup: %s', err);
516
+ logger.debug('Error stopping front-door proxy: %s', err);
570
517
  }
518
+ // Kill Bun subprocess
519
+ killBunSubprocess(logger);
571
520
  // Stop gravity heartbeat interval
572
521
  if (gravityHeartbeatInterval) {
573
522
  clearInterval(gravityHeartbeatInterval);
574
523
  gravityHeartbeatInterval = null;
575
524
  }
576
- // Kill gravity client with SIGTERM first, then SIGKILL as fallback
525
+ // Kill gravity client
577
526
  if (gravityProcess) {
578
- logger.debug('Killing gravity process...');
579
527
  try {
580
528
  gravityProcess.kill('SIGTERM');
581
- // Give it a moment to gracefully shutdown
582
529
  await new Promise((resolve) => setTimeout(resolve, 150));
583
530
  if (gravityProcess.exitCode === null) {
584
531
  gravityProcess.kill('SIGKILL');
585
532
  }
586
- logger.debug('Gravity process killed');
587
533
  }
588
534
  catch (err) {
589
- logger.debug('Error killing gravity process: %s', err);
535
+ logger.debug('Error killing gravity: %s', err);
590
536
  }
591
537
  finally {
592
538
  gravityProcess = null;
593
539
  }
594
540
  }
595
- // Close Vite asset server with timeout to prevent hanging
541
+ // Close Vite
596
542
  if (viteServer) {
597
- logger.debug('Closing Vite server...');
598
543
  try {
599
- // Use Promise.race with timeout to prevent hanging
600
544
  const closePromise = viteServer.close();
601
- const timeoutPromise = new Promise((resolve) => {
602
- setTimeout(() => {
603
- logger.debug('Vite server close timed out, continuing...');
604
- resolve();
605
- }, 2000);
606
- });
545
+ const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 2000));
607
546
  await Promise.race([closePromise, timeoutPromise]);
608
- logger.debug('Vite server closed');
609
547
  }
610
548
  catch (err) {
611
- logger.debug('Error closing Vite server: %s', err);
549
+ logger.debug('Error closing Vite: %s', err);
612
550
  }
613
551
  finally {
614
552
  viteServer = null;
615
553
  }
616
554
  }
617
- // Release the dev lockfile
618
- logger.debug('Releasing dev lock...');
619
- try {
620
- await devLock.release();
621
- logger.debug('Dev lock released');
622
- }
623
- catch (err) {
624
- logger.debug('Error releasing dev lock: %s', err);
625
- }
555
+ await devLock.release();
626
556
  await killLingeringGravityProcesses(logger);
627
- // Reset cleanup flag if not exiting (allows restart)
628
- if (!exitAfter) {
629
- cleaningUp = false;
630
- }
631
- else {
632
- // Clean up stdin keyboard handler right before exiting
633
- // This must happen AFTER all async cleanup to keep event loop alive
557
+ if (exitAfter) {
634
558
  if (stdinListenerRegistered && process.stdin.isTTY) {
635
559
  try {
636
560
  if (stdinDataHandler) {
@@ -642,588 +566,313 @@ export const command = createCommand({
642
566
  process.stdin.unref();
643
567
  }
644
568
  catch {
645
- // Ignore errors during final cleanup
569
+ // Ignore
646
570
  }
647
571
  }
648
- logger.debug('Exiting with code %d', exitCode);
649
572
  originalExit(exitCode);
650
573
  }
651
574
  };
652
- /**
653
- * Cleanup for restart: stops Bun server and Gravity, keeps Vite running
654
- */
655
- const cleanupForRestart = async () => {
656
- logger.debug('Cleaning up for restart...');
657
- // Stop Bun server
658
- try {
659
- await stopBunServer(opts.port, logger);
660
- }
661
- catch (err) {
662
- logger.debug('Error stopping Bun server for restart: %s', err);
663
- }
664
- // Stop gravity heartbeat interval
665
- if (gravityHeartbeatInterval) {
666
- clearInterval(gravityHeartbeatInterval);
667
- gravityHeartbeatInterval = null;
668
- }
669
- // Kill gravity client
670
- if (gravityProcess) {
671
- try {
672
- gravityProcess.kill('SIGTERM');
673
- await new Promise((resolve) => setTimeout(resolve, 150));
674
- if (gravityProcess.exitCode === null) {
675
- gravityProcess.kill('SIGKILL');
676
- }
677
- }
678
- catch (err) {
679
- logger.debug('Error killing gravity process for restart: %s', err);
680
- }
681
- finally {
682
- gravityProcess = null;
683
- }
684
- }
685
- };
686
- // SIGINT/SIGTERM: coordinate shutdown between bundle and dev resources
687
- let signalHandlersRegistered = false;
575
+ // Signal handlers
688
576
  let exitingFromSignal = false;
689
- if (!signalHandlersRegistered) {
690
- signalHandlersRegistered = true;
691
- const safeExit = (code, reason) => {
692
- // Prevent multiple signal handlers from racing
693
- if (exitingFromSignal)
694
- return;
695
- exitingFromSignal = true;
696
- if (reason) {
697
- logger.debug('DevMode terminating (%d) due to: %s', code, reason);
698
- }
699
- shutdownRequested = true;
700
- // Run cleanup and ensure we wait for it to complete before exiting
701
- cleanup(true, code).catch((err) => {
702
- logger.debug('Cleanup error: %s', err);
703
- originalExit(1);
704
- });
705
- };
706
- process.on('SIGINT', () => {
707
- safeExit(0, 'SIGINT');
708
- });
709
- process.on('SIGTERM', () => {
710
- safeExit(0, 'SIGTERM');
711
- });
712
- // Handle SIGHUP (terminal closed) - same as SIGINT
713
- process.on('SIGHUP', () => {
714
- safeExit(0, 'SIGHUP');
715
- });
716
- // Handle uncaught exceptions - clean up and exit rather than limping on
717
- process.on('uncaughtException', (err) => {
718
- tui.error(`Uncaught exception: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
719
- void safeExit(1, 'uncaughtException');
720
- });
721
- // Handle unhandled rejections - log but don't exit (usually recoverable)
722
- process.on('unhandledRejection', (reason) => {
723
- logger.warn('Unhandled promise rejection: %s', reason instanceof Error ? (reason.stack ?? reason.message) : String(reason));
724
- });
725
- }
726
- // Ensure resources are always cleaned up on exit (synchronous fallback)
577
+ const safeExit = (code, reason) => {
578
+ if (exitingFromSignal)
579
+ return;
580
+ exitingFromSignal = true;
581
+ if (reason)
582
+ logger.debug('DevMode terminating (%d): %s', code, reason);
583
+ shutdownRequested = true;
584
+ cleanup(true, code).catch(() => originalExit(1));
585
+ };
586
+ process.on('SIGINT', () => safeExit(0, 'SIGINT'));
587
+ process.on('SIGTERM', () => safeExit(0, 'SIGTERM'));
588
+ process.on('SIGHUP', () => safeExit(0, 'SIGHUP'));
589
+ process.on('uncaughtException', (err) => {
590
+ tui.error(`Uncaught exception: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
591
+ void safeExit(1, 'uncaughtException');
592
+ });
593
+ process.on('unhandledRejection', (reason) => {
594
+ logger.warn('Unhandled promise rejection: %s', reason instanceof Error ? (reason.stack ?? reason.message) : String(reason));
595
+ });
727
596
  process.on('exit', () => {
728
- // Clean up stdin keyboard handler
729
- if (stdinListenerRegistered && process.stdin.isTTY) {
730
- try {
731
- if (stdinDataHandler) {
732
- process.stdin.removeListener('data', stdinDataHandler);
733
- }
734
- process.stdin.setRawMode(false);
735
- process.stdin.pause();
736
- process.stdin.unref();
737
- }
738
- catch {
739
- // Ignore errors during exit cleanup
740
- }
741
- }
742
- // Kill gravity client with SIGKILL for immediate termination
743
- if (gravityProcess && gravityProcess.exitCode === null) {
597
+ if (gravityProcess?.exitCode === null) {
744
598
  try {
745
599
  gravityProcess.kill('SIGKILL');
746
600
  }
747
601
  catch {
748
- // Ignore errors during exit cleanup
602
+ // Ignore
749
603
  }
750
604
  }
751
- // Close Vite server synchronously if possible
752
605
  if (viteServer) {
753
606
  try {
754
607
  viteServer.close();
755
608
  }
756
609
  catch {
757
- // Ignore errors during exit cleanup
758
- }
759
- }
760
- // Stop Bun server synchronously (best effort)
761
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
762
- const server = globalThis.__AGENTUITY_SERVER__;
763
- if (server?.stop) {
764
- try {
765
- server.stop(true);
766
- }
767
- catch {
768
- // Ignore errors during exit cleanup
610
+ // Ignore
769
611
  }
770
612
  }
771
- // Release the dev lockfile synchronously
613
+ killBunSubprocess(logger);
772
614
  releaseLockSync(rootDir);
773
615
  });
774
- while (!shutdownRequested) {
775
- shouldRestart = false;
776
- // Pause file watcher during build to avoid loops
777
- fileWatcher.pause();
778
- try {
779
- let typeCheckErrors;
780
- // Generate entry file and bundle for dev server (with LLM patches)
781
- await tui.spinner({
782
- message: opts.experimentalNoBundle
783
- ? 'Preparing dev server'
784
- : 'Building dev bundle',
785
- callback: async () => {
786
- // Step 0: typecheck (skip with --no-typecheck)
787
- typeCheckErrors = undefined;
788
- if (!opts.noTypecheck) {
789
- const typeResult = await typecheck(rootDir);
790
- if (!typeResult.success) {
791
- typeCheckErrors = typeResult.output;
792
- return;
793
- }
794
- }
795
- // Step 1: Generate workbench files if enabled (must be done before entry generation)
796
- if (workbenchConfigData.enabled) {
797
- logger.debug('Workbench enabled, generating source files before bundle...');
798
- const { generateWorkbenchFiles } = await import('../build/vite/workbench-generator');
799
- await generateWorkbenchFiles(rootDir, project?.projectId ?? '', workbenchConfigData, logger);
800
- }
801
- // Step 2: Discover agents and routes in parallel
802
- const srcDir = join(rootDir, 'src');
803
- const { discoverAgents } = await import('../build/vite/agent-discovery');
804
- const { discoverRoutes } = await import('../build/vite/route-discovery');
805
- const { generateAgentRegistry, generateRouteRegistry } = await import('../build/vite/registry-generator');
806
- const [agentMetadata, { routes, routeInfoList }] = await Promise.all([
807
- discoverAgents(srcDir, project?.projectId ?? '', deploymentId, logger),
808
- discoverRoutes(srcDir, project?.projectId ?? '', deploymentId, logger),
809
- ]);
810
- // Step 2.5: Compute a hash of discovery results to skip codegen when unchanged
811
- // This avoids rewriting identical files on every restart
812
- const discoveryFingerprint = Bun.hash(JSON.stringify({
813
- agents: agentMetadata.map((a) => a.id + a.filename),
814
- routes: routeInfoList.map((r) => r.method + r.path + r.filename),
815
- })).toString(36);
816
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
817
- const prevFingerprint = globalThis
818
- .__AGENTUITY_DISCOVERY_FINGERPRINT__;
819
- const discoveryChanged = discoveryFingerprint !== prevFingerprint;
820
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
821
- globalThis.__AGENTUITY_DISCOVERY_FINGERPRINT__ = discoveryFingerprint;
822
- if (discoveryChanged) {
823
- // Generate agent and route registries for type augmentation
824
- // (TypeScript needs these files to exist for proper type inference)
825
- generateAgentRegistry(srcDir, agentMetadata);
826
- generateRouteRegistry(srcDir, routeInfoList);
827
- logger.debug('Agent and route registries generated for dev mode');
828
- // Step 3: Generate entry file with workbench and analytics config
829
- // Pass pre-discovered routes to avoid redundant route discovery
830
- const { generateEntryFile } = await import('../build/entry-generator');
831
- await generateEntryFile({
832
- rootDir,
833
- projectId: project?.projectId ?? '',
834
- deploymentId,
835
- logger,
836
- mode: 'dev',
837
- workbench: workbenchConfigData.enabled ? workbenchConfigData : undefined,
838
- analytics: agentuityConfig?.analytics,
839
- noBundle: opts.experimentalNoBundle,
840
- preDiscoveredRoutes: routeInfoList,
841
- });
842
- }
843
- else {
844
- logger.debug('Discovery unchanged (fingerprint: %s), skipping codegen', discoveryFingerprint);
845
- }
846
- // Step 4: Bundle the app with LLM patches (skip in --experimental-no-bundle mode)
847
- if (!opts.experimentalNoBundle) {
848
- // This produces .agentuity/app.js with AI Gateway routing patches applied
849
- // Must re-bundle even if discovery unchanged (user code may have changed)
850
- const { installExternalsAndBuild } = await import('../build/vite/server-bundler');
851
- await installExternalsAndBuild({
852
- rootDir,
853
- dev: true,
854
- logger,
855
- });
856
- }
857
- else {
858
- logger.debug('Skipping Bun.build (--experimental-no-bundle mode)');
859
- }
860
- // Generate metadata file (needed for eval ID lookup at runtime)
861
- // Reuse agentMetadata and routes from Step 2
862
- const { generateMetadata, writeMetadataFile } = await import('../build/vite/metadata-generator');
863
- const promises = [];
864
- // Generate/update prompt files (non-blocking)
865
- promises.push(import('../build/vite/prompt-generator')
866
- .then(({ generatePromptFiles }) => generatePromptFiles(srcDir, logger))
867
- .catch((err) => logger.warn('Failed to generate prompt files: %s', err.message)));
868
- const metadata = await generateMetadata({
869
- rootDir,
870
- projectId: project?.projectId ?? '',
871
- orgId: project?.orgId ?? '',
872
- deploymentId,
873
- agents: agentMetadata,
874
- routes,
875
- dev: true,
876
- logger,
877
- });
878
- writeMetadataFile(rootDir, metadata, true, logger);
879
- // Sync metadata with backend (creates agents and evals in the database)
880
- if (syncService && project?.projectId) {
881
- promises.push(syncService.sync(metadata, previousMetadata, project.projectId, deploymentId));
882
- previousMetadata = metadata;
883
- }
884
- await Promise.all(promises);
885
- },
886
- clearOnSuccess: true,
887
- });
888
- if (typeCheckErrors) {
889
- console.log('');
890
- console.log(typeCheckErrors);
891
- console.log('');
892
- fileWatcher.resume();
893
- // wait for a file change or shutdown to trigger a recompile
894
- while (!shutdownRequested && !shouldRestart) {
895
- await tui.spinner({
896
- message: 'Waiting for changes...',
897
- clearOnSuccess: true,
898
- callback: async () => {
899
- // Check more frequently so CTRL+C is responsive
900
- for (let i = 0; i < 10; i++) {
901
- if (shutdownRequested || shouldRestart) {
902
- return;
903
- }
904
- await Bun.sleep(100);
905
- }
906
- },
907
- });
908
- }
909
- if (shutdownRequested) {
910
- return;
616
+ // ================================================================
617
+ // Step 1: Prepare dev server (once)
618
+ // ================================================================
619
+ await tui.spinner({
620
+ message: 'Preparing dev server',
621
+ callback: async () => {
622
+ // Typecheck (skip with --no-typecheck)
623
+ if (!opts.noTypecheck) {
624
+ const typeResult = await typecheck(rootDir);
625
+ if (!typeResult.success) {
626
+ // Non-fatal in dev: log errors and continue
627
+ console.log('');
628
+ console.log(typeResult.output);
629
+ console.log('');
911
630
  }
912
- // Re-enter the main loop to re-typecheck and rebuild
913
- // Without this, the code falls through and tries to start the server
914
- // with the old/stale bundle instead of rebuilding first
915
- continue;
916
- }
917
- }
918
- catch (error) {
919
- tui.error(`Failed to build dev bundle: ${error}`);
920
- tui.warning('Waiting for file changes to retry...');
921
- // Resume watcher to detect changes for retry
922
- fileWatcher.resume();
923
- // Wait for next restart trigger or shutdown
924
- await new Promise((resolve) => {
925
- const checkRestart = setInterval(() => {
926
- if (shouldRestart || shutdownRequested) {
927
- clearInterval(checkRestart);
928
- resolve();
929
- }
930
- }, 100);
931
- });
932
- if (shutdownRequested) {
933
- break;
934
631
  }
935
- continue;
936
- }
937
- try {
938
- // Load SDK key from project .env files for AI Gateway routing
939
- // This must be set so the bundled AI SDK patches can inject the API key
940
- if (!process.env.AGENTUITY_SDK_KEY) {
941
- const sdkKey = await loadProjectSDKKey(logger, rootDir);
942
- if (sdkKey) {
943
- process.env.AGENTUITY_SDK_KEY = sdkKey;
944
- }
945
- else if (project) {
946
- tui.warning('AGENTUITY_SDK_KEY not found in .env file. Numerous features will be unavailable.');
947
- tui.bullet(`Run "${getCommand('cloud env pull')}" to sync your SDK key, or add AGENTUITY_SDK_KEY to your .env file.`);
948
- }
949
- }
950
- process.env.AGENTUITY_SDK_DEV_MODE = 'true';
951
- process.env.AGENTUITY_RUNTIME = 'yes';
952
- process.env.AGENTUITY_ENV = 'development';
953
- process.env.NODE_ENV = 'development';
954
- process.env.AGENTUITY_PROJECT_DIR = rootDir;
955
- if (project?.region) {
956
- process.env.AGENTUITY_REGION = project.region;
957
- }
958
- process.env.PORT = String(opts.port);
959
- process.env.AGENTUITY_PORT = process.env.PORT;
960
- process.env.AGENTUITY_BASE_URL =
961
- process.env.AGENTUITY_BASE_URL || `http://localhost:${opts.port}`;
962
- if (opts.resume) {
963
- process.env.AGENTUITY_CODER_RESUME_SESSION = opts.resume;
964
- }
965
- if (project) {
966
- // Set environment variables for LLM provider patches
967
- // These must be set so the bundled patches can route LLM calls through AI Gateway
968
- const serviceUrls = getServiceUrls(project.region);
969
- process.env.AGENTUITY_TRANSPORT_URL = serviceUrls.catalyst;
970
- process.env.AGENTUITY_CATALYST_URL = serviceUrls.catalyst;
971
- process.env.AGENTUITY_VECTOR_URL = serviceUrls.vector;
972
- process.env.AGENTUITY_KEYVALUE_URL = serviceUrls.keyvalue;
973
- process.env.AGENTUITY_SANDBOX_URL = serviceUrls.sandbox;
974
- process.env.AGENTUITY_STREAM_URL = serviceUrls.stream;
975
- process.env.AGENTUITY_CLOUD_ORG_ID = project.orgId;
976
- process.env.AGENTUITY_CLOUD_PROJECT_ID = project.projectId;
977
- process.env.AGENTUITY_CLOUD_DEPLOYMENT_ID = deploymentId;
978
- }
979
- if (devmode?.hostname) {
980
- process.env.AGENTUITY_DEVMODE_URL = `https://${devmode.hostname}`;
981
- }
982
- else {
983
- process.env.AGENTUITY_DEVMODE_URL = `http://localhost:${opts.port}`;
984
- }
985
- // Set Vite port for asset proxying in bundled app
986
- process.env.VITE_PORT = String(vitePort);
987
- logger.debug('Set VITE_PORT=%s for asset proxying', process.env.VITE_PORT);
988
- // Start Bun dev server (Vite already running, just start backend)
989
- await startBunDevServer({
632
+ // Generate workbench files if enabled
633
+ if (workbenchConfigData.enabled) {
634
+ const { generateWorkbenchFiles } = await import('../build/vite/workbench-generator');
635
+ await generateWorkbenchFiles(rootDir, project?.projectId ?? '', workbenchConfigData, logger);
636
+ }
637
+ // Discover agents and routes in parallel
638
+ const srcDir = join(rootDir, 'src');
639
+ const { discoverAgents } = await import('../build/vite/agent-discovery');
640
+ const { discoverRoutes } = await import('../build/vite/route-discovery');
641
+ const [agentMetadata, { routes }] = await Promise.all([
642
+ discoverAgents(srcDir, project?.projectId ?? '', deploymentId, logger),
643
+ discoverRoutes(srcDir, project?.projectId ?? '', deploymentId, logger),
644
+ ]);
645
+ // Generate metadata file
646
+ const { generateMetadata, writeMetadataFile } = await import('../build/vite/metadata-generator');
647
+ const promises = [];
648
+ // Generate prompt files (non-blocking)
649
+ promises.push(import('../build/vite/prompt-generator')
650
+ .then(({ generatePromptFiles }) => generatePromptFiles(srcDir, logger))
651
+ .catch((err) => logger.warn('Failed to generate prompt files: %s', err.message)));
652
+ const metadata = await generateMetadata({
990
653
  rootDir,
991
- port: opts.port,
992
- projectId: project?.projectId,
993
- orgId: project?.orgId,
654
+ projectId: project?.projectId ?? '',
655
+ orgId: project?.orgId ?? '',
994
656
  deploymentId,
657
+ agents: agentMetadata,
658
+ routes,
659
+ dev: true,
995
660
  logger,
996
- vitePort, // Pass port of already-running Vite server
997
- inspect: opts.inspect,
998
- inspectWait: opts.inspectWait,
999
- inspectBrk: opts.inspectBrk,
1000
- noBundle: opts.experimentalNoBundle,
1001
661
  });
1002
- // Check if shutdown was requested during startup
1003
- if (shutdownRequested) {
1004
- break;
1005
- }
662
+ writeMetadataFile(rootDir, metadata, true, logger);
663
+ // Sync metadata with backend
664
+ if (syncService && project?.projectId) {
665
+ promises.push(syncService.sync(metadata, previousMetadata, project.projectId, deploymentId));
666
+ previousMetadata = metadata;
667
+ }
668
+ await Promise.all(promises);
669
+ },
670
+ clearOnSuccess: true,
671
+ });
672
+ // ================================================================
673
+ // Step 2: Set environment variables
674
+ // ================================================================
675
+ if (!process.env.AGENTUITY_SDK_KEY) {
676
+ const sdkKey = await loadProjectSDKKey(logger, rootDir);
677
+ if (sdkKey) {
678
+ process.env.AGENTUITY_SDK_KEY = sdkKey;
679
+ }
680
+ else if (project) {
681
+ tui.warning('AGENTUITY_SDK_KEY not found in .env file. Numerous features will be unavailable.');
682
+ tui.bullet(`Run "${getCommand('cloud env pull')}" to sync your SDK key, or add AGENTUITY_SDK_KEY to your .env file.`);
1006
683
  }
1007
- catch (error) {
1008
- tui.error(`Failed to start dev server: ${error}`);
1009
- tui.warning('Waiting for file changes to retry...');
1010
- // Clean up any partially started server resources
1011
- await cleanupForRestart();
1012
- // Resume watcher to detect changes for retry
1013
- fileWatcher.resume();
1014
- // Wait for next restart trigger or shutdown
1015
- await new Promise((resolve) => {
1016
- const checkRestart = setInterval(() => {
1017
- if (shouldRestart || shutdownRequested) {
1018
- clearInterval(checkRestart);
1019
- resolve();
1020
- }
1021
- }, 100);
684
+ }
685
+ process.env.AGENTUITY_SDK_DEV_MODE = 'true';
686
+ process.env.AGENTUITY_RUNTIME = 'yes';
687
+ process.env.AGENTUITY_ENV = 'development';
688
+ process.env.NODE_ENV = 'development';
689
+ process.env.AGENTUITY_PROJECT_DIR = rootDir;
690
+ if (project?.region) {
691
+ process.env.AGENTUITY_REGION = project.region;
692
+ }
693
+ process.env.PORT = String(bunBackendPort);
694
+ process.env.AGENTUITY_PORT = String(bunBackendPort);
695
+ process.env.AGENTUITY_BASE_URL =
696
+ process.env.AGENTUITY_BASE_URL || `http://localhost:${vitePort}`;
697
+ process.env.AGENTUITY_NO_BUNDLE = 'true';
698
+ if (opts.resume) {
699
+ process.env.AGENTUITY_CODER_RESUME_SESSION = opts.resume;
700
+ }
701
+ if (project) {
702
+ const serviceUrls = getServiceUrls(project.region);
703
+ process.env.AGENTUITY_TRANSPORT_URL = serviceUrls.catalyst;
704
+ process.env.AGENTUITY_CATALYST_URL = serviceUrls.catalyst;
705
+ process.env.AGENTUITY_VECTOR_URL = serviceUrls.vector;
706
+ process.env.AGENTUITY_KEYVALUE_URL = serviceUrls.keyvalue;
707
+ process.env.AGENTUITY_SANDBOX_URL = serviceUrls.sandbox;
708
+ process.env.AGENTUITY_STREAM_URL = serviceUrls.stream;
709
+ process.env.AGENTUITY_CLOUD_ORG_ID = project.orgId;
710
+ process.env.AGENTUITY_CLOUD_PROJECT_ID = project.projectId;
711
+ process.env.AGENTUITY_CLOUD_DEPLOYMENT_ID = deploymentId;
712
+ }
713
+ if (devmode?.hostname) {
714
+ process.env.AGENTUITY_DEVMODE_URL = `https://${devmode.hostname}`;
715
+ }
716
+ else {
717
+ process.env.AGENTUITY_DEVMODE_URL = `http://localhost:${vitePort}`;
718
+ }
719
+ // ================================================================
720
+ // Step 3: Start Bun backend with --hot (handles its own HMR)
721
+ // ================================================================
722
+ await startBunDevServer({
723
+ rootDir,
724
+ port: bunBackendPort,
725
+ logger,
726
+ vitePort,
727
+ inspect: opts.inspect,
728
+ inspectWait: opts.inspectWait,
729
+ inspectBrk: opts.inspectBrk,
730
+ });
731
+ // ================================================================
732
+ // Step 4: Start gravity tunnel (if public URL enabled)
733
+ // ================================================================
734
+ if (gravityBin && gravityURL && devmode && project) {
735
+ const privateKeyPEM = devmode.privateKey ?? savedPrivateKey;
736
+ if (!privateKeyPEM) {
737
+ throw new Error('No private key available for gravity connection. Please re-run to generate a new key.');
738
+ }
739
+ gravityProcess = Bun.spawn([
740
+ gravityBin,
741
+ '--endpoint-id',
742
+ devmode.id,
743
+ '--port',
744
+ vitePort.toString(),
745
+ '--url',
746
+ gravityURL,
747
+ '--log-level',
748
+ process.env.AGENTUITY_GRAVITY_LOG_LEVEL ?? 'error',
749
+ '--org-id',
750
+ project.orgId,
751
+ '--project-id',
752
+ project.projectId,
753
+ '--private-key',
754
+ Buffer.from(privateKeyPEM).toString('base64'),
755
+ '--health-check',
756
+ ], {
757
+ cwd: rootDir,
758
+ stdout: 'pipe',
759
+ stderr: 'pipe',
760
+ detached: false,
761
+ });
762
+ const gravityPid = gravityProcess.pid;
763
+ if (gravityPid) {
764
+ await devLock.registerChild({
765
+ pid: gravityPid,
766
+ type: 'gravity',
767
+ description: 'Gravity public URL tunnel',
1022
768
  });
1023
- if (shutdownRequested) {
1024
- break;
1025
- }
1026
- continue;
1027
- }
1028
- // Exit early if shutdown was requested
1029
- if (shutdownRequested) {
1030
- break;
1031
769
  }
1032
- try {
1033
- // Start gravity client if we have devmode
1034
- if (gravityBin && gravityURL && devmode && project) {
1035
- logger.trace('Starting gravity client: %s (cwd: %s, id: %s)', gravityBin, rootDir, devmode.id);
1036
- const privateKeyPEM = devmode.privateKey ?? savedPrivateKey;
1037
- if (!privateKeyPEM) {
1038
- throw new Error('No private key available for gravity connection. Please re-run to generate a new key.');
1039
- }
1040
- gravityProcess = Bun.spawn([
1041
- gravityBin,
1042
- '--endpoint-id',
1043
- devmode.id,
1044
- '--port',
1045
- opts.port.toString(),
1046
- '--url',
1047
- gravityURL,
1048
- '--log-level',
1049
- process.env.AGENTUITY_GRAVITY_LOG_LEVEL ?? 'error',
1050
- '--org-id',
1051
- project.orgId,
1052
- '--project-id',
1053
- project.projectId,
1054
- '--private-key',
1055
- Buffer.from(privateKeyPEM).toString('base64'),
1056
- '--health-check',
1057
- ], {
1058
- cwd: rootDir,
1059
- stdout: 'pipe',
1060
- stderr: 'pipe',
1061
- detached: false, // Ensure gravity dies with parent process
1062
- });
1063
- // Register gravity process in dev lock for cleanup tracking
1064
- const gravityPid = gravityProcess.pid;
1065
- if (gravityPid) {
1066
- await devLock.registerChild({
1067
- pid: gravityPid,
1068
- type: 'gravity',
1069
- description: 'Gravity public URL tunnel',
1070
- });
1071
- }
1072
- // Log gravity output and detect heartbeat port
1073
- (async () => {
1074
- try {
1075
- if (gravityProcess?.stdout) {
1076
- for await (const chunk of gravityProcess.stdout) {
1077
- const text = new TextDecoder().decode(chunk);
1078
- const trimmed = text.trim();
1079
- // Check for heartbeat port announcement
1080
- const match = trimmed.match(/^HEARTBEAT_PORT=(\d+)$/m);
1081
- if (match?.[1]) {
1082
- const heartbeatPort = parseInt(match[1], 10);
1083
- logger.debug('Gravity heartbeat port detected: %d', heartbeatPort);
1084
- // Start sending heartbeats every 5 seconds
1085
- if (!gravityHeartbeatInterval) {
1086
- const sendHeartbeat = async () => {
1087
- try {
1088
- await fetch(`http://127.0.0.1:${heartbeatPort}/heartbeat`, {
1089
- method: 'POST',
1090
- signal: AbortSignal.timeout(2000),
1091
- });
1092
- logger.trace('Gravity heartbeat sent');
1093
- }
1094
- catch (err) {
1095
- logger.trace('Gravity heartbeat failed: %s', err);
1096
- }
1097
- };
1098
- // Send initial heartbeat immediately
1099
- sendHeartbeat();
1100
- // Then send every 5 seconds
1101
- gravityHeartbeatInterval = setInterval(sendHeartbeat, 5000);
770
+ // Log gravity output and detect heartbeat port
771
+ (async () => {
772
+ try {
773
+ if (gravityProcess?.stdout) {
774
+ for await (const chunk of gravityProcess.stdout) {
775
+ const text = new TextDecoder().decode(chunk);
776
+ const trimmed = text.trim();
777
+ const match = trimmed.match(/^HEARTBEAT_PORT=(\d+)$/m);
778
+ if (match?.[1]) {
779
+ const heartbeatPort = parseInt(match[1], 10);
780
+ logger.debug('Gravity heartbeat port: %d', heartbeatPort);
781
+ if (!gravityHeartbeatInterval) {
782
+ const sendHeartbeat = async () => {
783
+ try {
784
+ await fetch(`http://127.0.0.1:${heartbeatPort}/heartbeat`, {
785
+ method: 'POST',
786
+ signal: AbortSignal.timeout(2000),
787
+ });
1102
788
  }
1103
- }
1104
- else if (trimmed) {
1105
- logger.debug('[gravity] %s', trimmed);
1106
- }
789
+ catch {
790
+ // Ignore heartbeat failures
791
+ }
792
+ };
793
+ sendHeartbeat();
794
+ gravityHeartbeatInterval = setInterval(sendHeartbeat, 5000);
1107
795
  }
1108
796
  }
1109
- }
1110
- catch (err) {
1111
- logger.error('Error reading gravity stdout: %s', err);
1112
- }
1113
- })();
1114
- (async () => {
1115
- try {
1116
- if (gravityProcess?.stderr) {
1117
- for await (const chunk of gravityProcess.stderr) {
1118
- const text = new TextDecoder().decode(chunk);
1119
- logger.warn('[gravity] %s', text.trim());
1120
- }
797
+ else if (trimmed) {
798
+ logger.debug('[gravity] %s', trimmed);
1121
799
  }
1122
800
  }
1123
- catch (err) {
1124
- logger.error('Error reading gravity stderr: %s', err);
1125
- }
1126
- })();
1127
- logger.debug('Gravity client started');
801
+ }
1128
802
  }
1129
- // Handle keyboard shortcuts - only register listener once
1130
- if (interactive &&
1131
- process.stdin.isTTY &&
1132
- process.stdout.isTTY &&
1133
- !stdinListenerRegistered) {
1134
- stdinListenerRegistered = true;
1135
- process.stdin.setRawMode(true);
1136
- process.stdin.resume();
1137
- process.stdin.setEncoding('utf8');
1138
- const showHelp = () => {
1139
- console.log('\n' + tui.bold('Keyboard Shortcuts:'));
1140
- console.log(tui.muted(' h') + ' - show this help');
1141
- console.log(tui.muted(' c') + ' - clear console');
1142
- console.log(tui.muted(' q') + ' - quit\n');
1143
- };
1144
- // Store handler reference for cleanup
1145
- stdinDataHandler = (data) => {
1146
- const key = data.toString();
1147
- // Handle Ctrl+C or q - trigger graceful shutdown
1148
- if (key === '\u0003' || key === 'q') {
1149
- // Remove stdin listener immediately to prevent re-entrancy
1150
- if (stdinDataHandler) {
1151
- process.stdin.removeListener('data', stdinDataHandler);
1152
- stdinDataHandler = null;
1153
- }
1154
- // Set shutdown flag and trigger cleanup directly
1155
- shutdownRequested = true;
1156
- cleanup(true, 0).catch((err) => {
1157
- logger.debug('Cleanup error: %s', err);
1158
- originalExit(1);
1159
- });
1160
- return;
1161
- }
1162
- switch (key) {
1163
- case 'h':
1164
- showHelp();
1165
- break;
1166
- case 'c':
1167
- console.clear();
1168
- tui.banner('⨺ Agentuity DevMode', devmodebody, {
1169
- padding: 2,
1170
- topSpacer: false,
1171
- bottomSpacer: false,
1172
- centerTitle: false,
1173
- });
1174
- break;
1175
- default:
1176
- process.stdout.write(data);
1177
- break;
1178
- }
1179
- };
1180
- process.stdin.on('data', stdinDataHandler);
803
+ catch (err) {
804
+ logger.error('Error reading gravity stdout: %s', err);
1181
805
  }
1182
- showWelcome();
1183
- // Start/resume file watcher now that server is ready
1184
- fileWatcher.resume();
1185
- // Wait for restart signal or shutdown
1186
- await new Promise((resolve) => {
1187
- const checkRestart = setInterval(() => {
1188
- if (shouldRestart || shutdownRequested) {
1189
- clearInterval(checkRestart);
1190
- resolve();
806
+ })();
807
+ (async () => {
808
+ try {
809
+ if (gravityProcess?.stderr) {
810
+ for await (const chunk of gravityProcess.stderr) {
811
+ logger.warn('[gravity] %s', new TextDecoder().decode(chunk).trim());
1191
812
  }
1192
- }, 100);
1193
- });
1194
- // Exit loop if shutdown was requested
1195
- if (shutdownRequested) {
1196
- break;
813
+ }
1197
814
  }
1198
- // Restart triggered - cleanup and loop (Vite stays running)
1199
- logger.debug('Restarting backend server...');
1200
- // Clean up Bun server and Gravity (Vite stays running)
1201
- await cleanupForRestart();
1202
- // Brief pause before restart
1203
- await Bun.sleep(500);
1204
- }
1205
- catch (error) {
1206
- tui.error(`Error during server operation: ${error}`);
1207
- tui.warning('Waiting for file changes to retry...');
1208
- // Cleanup on error (Vite stays running)
1209
- await cleanupForRestart();
1210
- // Exit if shutdown was requested during error handling
1211
- if (shutdownRequested) {
1212
- break;
815
+ catch (err) {
816
+ logger.error('Error reading gravity stderr: %s', err);
1213
817
  }
1214
- // Resume file watcher to detect changes for retry
1215
- fileWatcher.resume();
1216
- // Wait for next restart trigger or shutdown
1217
- await new Promise((resolve) => {
1218
- const checkRestart = setInterval(() => {
1219
- if (shouldRestart || shutdownRequested) {
1220
- clearInterval(checkRestart);
1221
- resolve();
1222
- }
1223
- }, 100);
1224
- });
1225
- }
818
+ })();
1226
819
  }
820
+ // ================================================================
821
+ // Step 5: Keyboard shortcuts + wait for shutdown
822
+ // ================================================================
823
+ if (interactive && process.stdin.isTTY && process.stdout.isTTY) {
824
+ stdinListenerRegistered = true;
825
+ process.stdin.setRawMode(true);
826
+ process.stdin.resume();
827
+ process.stdin.setEncoding('utf8');
828
+ const showHelp = () => {
829
+ console.log('\n' + tui.bold('Keyboard Shortcuts:'));
830
+ console.log(tui.muted(' h') + ' - show this help');
831
+ console.log(tui.muted(' c') + ' - clear console');
832
+ console.log(tui.muted(' q') + ' - quit\n');
833
+ };
834
+ stdinDataHandler = (data) => {
835
+ const key = data.toString();
836
+ if (key === '\u0003' || key === 'q') {
837
+ if (stdinDataHandler) {
838
+ process.stdin.removeListener('data', stdinDataHandler);
839
+ stdinDataHandler = null;
840
+ }
841
+ shutdownRequested = true;
842
+ cleanup(true, 0).catch(() => originalExit(1));
843
+ return;
844
+ }
845
+ switch (key) {
846
+ case 'h':
847
+ showHelp();
848
+ break;
849
+ case 'c':
850
+ console.clear();
851
+ tui.banner('⨺ Agentuity DevMode', devmodebody, {
852
+ padding: 2,
853
+ topSpacer: false,
854
+ bottomSpacer: false,
855
+ centerTitle: false,
856
+ });
857
+ break;
858
+ default:
859
+ process.stdout.write(data);
860
+ break;
861
+ }
862
+ };
863
+ process.stdin.on('data', stdinDataHandler);
864
+ }
865
+ logger.info('DevMode ready 🚀');
866
+ // Block until shutdown — bun --hot handles backend HMR,
867
+ // Vite handles frontend HMR. Nothing to restart.
868
+ await new Promise((resolve) => {
869
+ const check = setInterval(() => {
870
+ if (shutdownRequested) {
871
+ clearInterval(check);
872
+ resolve();
873
+ }
874
+ }, 200);
875
+ });
1227
876
  }
1228
877
  finally {
1229
878
  /* brute force clean up */