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