@agentuity/cli 1.0.59 → 2.0.0-beta.1

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 (189) hide show
  1. package/bin/cli.ts +2 -3
  2. package/dist/cmd/build/app-config-extractor.d.ts +27 -0
  3. package/dist/cmd/build/app-config-extractor.d.ts.map +1 -0
  4. package/dist/cmd/build/app-config-extractor.js +152 -0
  5. package/dist/cmd/build/app-config-extractor.js.map +1 -0
  6. package/dist/cmd/build/app-router-detector.d.ts +2 -5
  7. package/dist/cmd/build/app-router-detector.d.ts.map +1 -1
  8. package/dist/cmd/build/app-router-detector.js +130 -154
  9. package/dist/cmd/build/app-router-detector.js.map +1 -1
  10. package/dist/cmd/build/ci.d.ts.map +1 -1
  11. package/dist/cmd/build/ci.js +5 -21
  12. package/dist/cmd/build/ci.js.map +1 -1
  13. package/dist/cmd/build/ids.d.ts +11 -0
  14. package/dist/cmd/build/ids.d.ts.map +1 -0
  15. package/dist/cmd/build/ids.js +18 -0
  16. package/dist/cmd/build/ids.js.map +1 -0
  17. package/dist/cmd/build/index.d.ts.map +1 -1
  18. package/dist/cmd/build/index.js +8 -0
  19. package/dist/cmd/build/index.js.map +1 -1
  20. package/dist/cmd/build/vite/agent-discovery.d.ts +8 -4
  21. package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
  22. package/dist/cmd/build/vite/agent-discovery.js +166 -487
  23. package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
  24. package/dist/cmd/build/vite/bun-dev-server.d.ts +43 -14
  25. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
  26. package/dist/cmd/build/vite/bun-dev-server.js +290 -129
  27. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  28. package/dist/cmd/build/vite/config-loader.d.ts +15 -20
  29. package/dist/cmd/build/vite/config-loader.d.ts.map +1 -1
  30. package/dist/cmd/build/vite/config-loader.js +41 -74
  31. package/dist/cmd/build/vite/config-loader.js.map +1 -1
  32. package/dist/cmd/build/vite/docs-generator.d.ts.map +1 -1
  33. package/dist/cmd/build/vite/docs-generator.js +0 -2
  34. package/dist/cmd/build/vite/docs-generator.js.map +1 -1
  35. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  36. package/dist/cmd/build/vite/index.js +0 -36
  37. package/dist/cmd/build/vite/index.js.map +1 -1
  38. package/dist/cmd/build/vite/lifecycle-generator.d.ts +10 -2
  39. package/dist/cmd/build/vite/lifecycle-generator.d.ts.map +1 -1
  40. package/dist/cmd/build/vite/lifecycle-generator.js +302 -23
  41. package/dist/cmd/build/vite/lifecycle-generator.js.map +1 -1
  42. package/dist/cmd/build/vite/route-discovery.d.ts +11 -38
  43. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  44. package/dist/cmd/build/vite/route-discovery.js +97 -177
  45. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  46. package/dist/cmd/build/vite/server-bundler.js +1 -1
  47. package/dist/cmd/build/vite/server-bundler.js.map +1 -1
  48. package/dist/cmd/build/vite/static-renderer.d.ts +0 -2
  49. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  50. package/dist/cmd/build/vite/static-renderer.js +19 -13
  51. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  52. package/dist/cmd/build/vite/vite-asset-server-config.d.ts +6 -3
  53. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  54. package/dist/cmd/build/vite/vite-asset-server-config.js +175 -69
  55. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  56. package/dist/cmd/build/vite/vite-asset-server.d.ts +8 -3
  57. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  58. package/dist/cmd/build/vite/vite-asset-server.js +14 -13
  59. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  60. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  61. package/dist/cmd/build/vite/vite-builder.js +42 -190
  62. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  63. package/dist/cmd/build/vite/ws-proxy.d.ts +53 -0
  64. package/dist/cmd/build/vite/ws-proxy.d.ts.map +1 -0
  65. package/dist/cmd/build/vite/ws-proxy.js +95 -0
  66. package/dist/cmd/build/vite/ws-proxy.js.map +1 -0
  67. package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
  68. package/dist/cmd/build/vite-bundler.js +0 -3
  69. package/dist/cmd/build/vite-bundler.js.map +1 -1
  70. package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -1
  71. package/dist/cmd/cloud/deploy-fork.js +15 -36
  72. package/dist/cmd/cloud/deploy-fork.js.map +1 -1
  73. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  74. package/dist/cmd/cloud/sandbox/exec.js +28 -86
  75. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  76. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  77. package/dist/cmd/cloud/sandbox/run.js +2 -9
  78. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  79. package/dist/cmd/cloud/sandbox/snapshot/build.js +2 -2
  80. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  81. package/dist/cmd/coder/hub-url.d.ts.map +1 -1
  82. package/dist/cmd/coder/hub-url.js +1 -3
  83. package/dist/cmd/coder/hub-url.js.map +1 -1
  84. package/dist/cmd/coder/start.js +6 -6
  85. package/dist/cmd/coder/start.js.map +1 -1
  86. package/dist/cmd/coder/tui-init.d.ts +2 -2
  87. package/dist/cmd/coder/tui-init.js +2 -2
  88. package/dist/cmd/coder/tui-init.js.map +1 -1
  89. package/dist/cmd/dev/file-watcher.d.ts.map +1 -1
  90. package/dist/cmd/dev/file-watcher.js +2 -8
  91. package/dist/cmd/dev/file-watcher.js.map +1 -1
  92. package/dist/cmd/dev/index.d.ts.map +1 -1
  93. package/dist/cmd/dev/index.js +432 -752
  94. package/dist/cmd/dev/index.js.map +1 -1
  95. package/dist/cmd/dev/process-manager.d.ts +104 -0
  96. package/dist/cmd/dev/process-manager.d.ts.map +1 -0
  97. package/dist/cmd/dev/process-manager.js +204 -0
  98. package/dist/cmd/dev/process-manager.js.map +1 -0
  99. package/dist/errors.d.ts +10 -24
  100. package/dist/errors.d.ts.map +1 -1
  101. package/dist/errors.js +12 -42
  102. package/dist/errors.js.map +1 -1
  103. package/dist/schema-generator.d.ts.map +1 -1
  104. package/dist/schema-generator.js +12 -2
  105. package/dist/schema-generator.js.map +1 -1
  106. package/dist/tui.d.ts.map +1 -1
  107. package/dist/tui.js +5 -19
  108. package/dist/tui.js.map +1 -1
  109. package/dist/utils/version-mismatch.d.ts +39 -0
  110. package/dist/utils/version-mismatch.d.ts.map +1 -0
  111. package/dist/utils/version-mismatch.js +161 -0
  112. package/dist/utils/version-mismatch.js.map +1 -0
  113. package/package.json +6 -6
  114. package/src/cmd/ai/prompt/agent.md +0 -1
  115. package/src/cmd/ai/prompt/api.md +0 -7
  116. package/src/cmd/ai/prompt/web.md +51 -213
  117. package/src/cmd/build/app-config-extractor.ts +186 -0
  118. package/src/cmd/build/app-router-detector.ts +152 -182
  119. package/src/cmd/build/ci.ts +5 -21
  120. package/src/cmd/build/ids.ts +19 -0
  121. package/src/cmd/build/index.ts +10 -0
  122. package/src/cmd/build/vite/agent-discovery.ts +208 -679
  123. package/src/cmd/build/vite/bun-dev-server.ts +383 -146
  124. package/src/cmd/build/vite/config-loader.ts +45 -77
  125. package/src/cmd/build/vite/docs-generator.ts +0 -2
  126. package/src/cmd/build/vite/index.ts +1 -42
  127. package/src/cmd/build/vite/lifecycle-generator.ts +345 -21
  128. package/src/cmd/build/vite/route-discovery.ts +116 -274
  129. package/src/cmd/build/vite/server-bundler.ts +1 -1
  130. package/src/cmd/build/vite/static-renderer.ts +23 -15
  131. package/src/cmd/build/vite/vite-asset-server-config.ts +200 -70
  132. package/src/cmd/build/vite/vite-asset-server.ts +25 -15
  133. package/src/cmd/build/vite/vite-builder.ts +49 -220
  134. package/src/cmd/build/vite/ws-proxy.ts +126 -0
  135. package/src/cmd/build/vite-bundler.ts +0 -4
  136. package/src/cmd/cloud/deploy-fork.ts +16 -39
  137. package/src/cmd/cloud/sandbox/exec.ts +23 -130
  138. package/src/cmd/cloud/sandbox/run.ts +2 -9
  139. package/src/cmd/cloud/sandbox/snapshot/build.ts +2 -2
  140. package/src/cmd/coder/hub-url.ts +1 -3
  141. package/src/cmd/coder/start.ts +6 -6
  142. package/src/cmd/coder/tui-init.ts +4 -4
  143. package/src/cmd/dev/file-watcher.ts +2 -9
  144. package/src/cmd/dev/index.ts +476 -859
  145. package/src/cmd/dev/process-manager.ts +261 -0
  146. package/src/errors.ts +12 -44
  147. package/src/schema-generator.ts +12 -2
  148. package/src/tui.ts +5 -18
  149. package/src/utils/version-mismatch.ts +204 -0
  150. package/dist/cmd/build/ast.d.ts +0 -78
  151. package/dist/cmd/build/ast.d.ts.map +0 -1
  152. package/dist/cmd/build/ast.js +0 -2703
  153. package/dist/cmd/build/ast.js.map +0 -1
  154. package/dist/cmd/build/entry-generator.d.ts +0 -25
  155. package/dist/cmd/build/entry-generator.d.ts.map +0 -1
  156. package/dist/cmd/build/entry-generator.js +0 -695
  157. package/dist/cmd/build/entry-generator.js.map +0 -1
  158. package/dist/cmd/build/vite/api-mount-path.d.ts +0 -61
  159. package/dist/cmd/build/vite/api-mount-path.d.ts.map +0 -1
  160. package/dist/cmd/build/vite/api-mount-path.js +0 -83
  161. package/dist/cmd/build/vite/api-mount-path.js.map +0 -1
  162. package/dist/cmd/build/vite/registry-generator.d.ts +0 -19
  163. package/dist/cmd/build/vite/registry-generator.d.ts.map +0 -1
  164. package/dist/cmd/build/vite/registry-generator.js +0 -1108
  165. package/dist/cmd/build/vite/registry-generator.js.map +0 -1
  166. package/dist/cmd/build/webanalytics-generator.d.ts +0 -16
  167. package/dist/cmd/build/webanalytics-generator.d.ts.map +0 -1
  168. package/dist/cmd/build/webanalytics-generator.js +0 -178
  169. package/dist/cmd/build/webanalytics-generator.js.map +0 -1
  170. package/dist/cmd/build/workbench.d.ts +0 -7
  171. package/dist/cmd/build/workbench.d.ts.map +0 -1
  172. package/dist/cmd/build/workbench.js +0 -55
  173. package/dist/cmd/build/workbench.js.map +0 -1
  174. package/dist/utils/route-migration.d.ts +0 -62
  175. package/dist/utils/route-migration.d.ts.map +0 -1
  176. package/dist/utils/route-migration.js +0 -630
  177. package/dist/utils/route-migration.js.map +0 -1
  178. package/dist/utils/stream-capture.d.ts +0 -9
  179. package/dist/utils/stream-capture.d.ts.map +0 -1
  180. package/dist/utils/stream-capture.js +0 -34
  181. package/dist/utils/stream-capture.js.map +0 -1
  182. package/src/cmd/build/ast.ts +0 -3529
  183. package/src/cmd/build/entry-generator.ts +0 -760
  184. package/src/cmd/build/vite/api-mount-path.ts +0 -87
  185. package/src/cmd/build/vite/registry-generator.ts +0 -1267
  186. package/src/cmd/build/webanalytics-generator.ts +0 -197
  187. package/src/cmd/build/workbench.ts +0 -58
  188. package/src/utils/route-migration.ts +0 -757
  189. package/src/utils/stream-capture.ts +0 -39
@@ -11,15 +11,15 @@ 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';
21
+ import { initProcessManager } from './process-manager';
22
+ import { detectVersionMismatch, formatVersionMismatchWarning } from '../../utils/version-mismatch';
23
23
  import { ErrorCode } from '../../errors';
24
24
  const DEFAULT_PORT = 3500;
25
25
  const MIN_PORT = 1024;
@@ -56,85 +56,22 @@ async function killLingeringGravityProcesses(logger) {
56
56
  }
57
57
  }
58
58
  /**
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).
59
+ * Kill the Bun backend subprocess if one is running.
62
60
  */
63
- async function stopBunServer(port, logger) {
61
+ function killBunSubprocess(logger) {
64
62
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
63
  const globalAny = globalThis;
66
- // Check for subprocess first (used when debugger flags are enabled)
67
64
  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
- }
104
- return;
105
- }
106
- // Handle in-process server
107
- const server = globalAny.__AGENTUITY_SERVER__;
108
- if (!server) {
109
- logger.debug('No Bun server to stop');
65
+ if (!bunSubprocess)
110
66
  return;
111
- }
112
67
  try {
113
- logger.debug('Stopping Bun server...');
114
- server.stop(true); // Close active connections immediately
115
- logger.debug('Bun server stop() called');
68
+ bunSubprocess.kill('SIGTERM');
69
+ logger.debug('Bun subprocess killed');
116
70
  }
117
71
  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
- }
72
+ logger.debug('Error killing Bun subprocess: %s', err);
136
73
  }
137
- globalAny.__AGENTUITY_SERVER__ = undefined;
74
+ globalAny.__AGENTUITY_BUN_SUBPROCESS__ = undefined;
138
75
  }
139
76
  const getDefaultPort = () => {
140
77
  const envPort = process.env.PORT;
@@ -192,18 +129,10 @@ export const command = createCommand({
192
129
  .boolean()
193
130
  .optional()
194
131
  .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
132
  noTypecheck: z
200
133
  .boolean()
201
134
  .optional()
202
135
  .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
136
  resume: z.string().optional().describe('Resume a paused Hub session by ID'),
208
137
  }),
209
138
  },
@@ -357,30 +286,12 @@ export const command = createCommand({
357
286
  devLock.release();
358
287
  tui.fatal(`Failed to upgrade dependencies: ${upgradeResult.failed.join(', ')}`, ErrorCode.BUILD_FAILED);
359
288
  }
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 });
289
+ // Check for version mismatches (v1 vs v2 SDK packages)
290
+ const versionMismatch = detectVersionMismatch(rootDir, logger);
291
+ if (versionMismatch.hasV1Packages || versionMismatch.hasMajorMismatches) {
292
+ tui.newline();
293
+ tui.warning(formatVersionMismatchWarning(versionMismatch));
294
+ tui.newline();
384
295
  }
385
296
  try {
386
297
  // Setup devmode and gravity (if using public URL)
@@ -454,10 +365,10 @@ export const command = createCommand({
454
365
  config = _config;
455
366
  }
456
367
  }
457
- // Get workbench info from config (new Vite approach)
458
- const { loadAgentuityConfig, getWorkbenchConfig } = await import('../build/vite/config-loader');
459
- const agentuityConfig = await loadAgentuityConfig(rootDir, ctx.logger);
460
- const workbenchConfigData = getWorkbenchConfig(agentuityConfig, true); // dev mode
368
+ // Get workbench info from createApp() in app.ts (v2 approach)
369
+ const { getWorkbenchConfig, loadRuntimeConfig } = await import('../build/vite/config-loader');
370
+ const runtimeConfig = await loadRuntimeConfig(rootDir, logger);
371
+ const workbenchConfigData = getWorkbenchConfig(true, runtimeConfig); // dev mode
461
372
  const workbench = {
462
373
  hasWorkbench: workbenchConfigData.enabled,
463
374
  config: workbenchConfigData.enabled
@@ -491,146 +402,154 @@ export const command = createCommand({
491
402
  bottomSpacer: false,
492
403
  centerTitle: false,
493
404
  });
494
- // Start Vite asset server ONCE before restart loop
495
- // Vite handles frontend HMR independently and stays running across backend restarts
405
+ // Detect user route mount paths for Vite proxy configuration
406
+ // This is a quick AST scan of app.ts runs before Vite starts
407
+ let routePaths = ['/api']; // Default fallback
408
+ try {
409
+ const { detectExplicitRouter } = await import('../build/app-router-detector');
410
+ const detection = await detectExplicitRouter(rootDir, logger);
411
+ if (detection.detected && detection.mounts.length > 0) {
412
+ routePaths = detection.mounts.map((m) => m.path);
413
+ logger.debug('Detected route mount paths: %s', routePaths.join(', '));
414
+ }
415
+ }
416
+ catch (err) {
417
+ logger.debug('Route detection failed, using default /api: %s', err);
418
+ }
419
+ // Pick internal ports (neither is user-facing — the front-door proxy is)
420
+ const bunBackendPort = opts.port + 1;
421
+ const viteInternalPort = opts.port + 2;
422
+ // No-bundle dev mode guard: ensure stale bundled app artifact cannot be executed.
423
+ // We keep other .agentuity artifacts (metadata/workbench files) intact.
424
+ try {
425
+ const staleBundlePath = join(rootDir, '.agentuity', 'app.js');
426
+ if (existsSync(staleBundlePath)) {
427
+ await Bun.file(staleBundlePath).delete();
428
+ logger.debug('Removed stale dev bundle artifact: %s', staleBundlePath);
429
+ }
430
+ }
431
+ catch (err) {
432
+ logger.debug('Failed to remove stale dev bundle artifact: %s', err);
433
+ }
434
+ // Debug trace: locate unexpected legacy credential warnings.
435
+ // Enable with AGENTUITY_TRACE_CREDENTIAL_WARNINGS=true.
436
+ if (process.env.AGENTUITY_TRACE_CREDENTIAL_WARNINGS === 'true') {
437
+ const originalConsoleError = console.error.bind(console);
438
+ console.error = (...args) => {
439
+ try {
440
+ const first = typeof args[0] === 'string' ? args[0] : '';
441
+ if (first.includes('No credentials found for this AI provider')) {
442
+ const stack = new Error('Credential warning trace').stack;
443
+ originalConsoleError('[TRACE] Credential warning origin stack:');
444
+ if (stack)
445
+ originalConsoleError(stack);
446
+ }
447
+ }
448
+ catch {
449
+ // ignore tracing errors
450
+ }
451
+ originalConsoleError(...args);
452
+ };
453
+ }
454
+ // Start Vite dev server on an internal port.
455
+ // The user-facing port is handled by the front-door TCP proxy (ws-proxy)
456
+ // which routes WS upgrades to Bun and everything else to Vite.
496
457
  let viteServer = null;
497
458
  let vitePort;
459
+ // Initialize process manager to track all servers/processes
460
+ const procManager = initProcessManager(logger);
498
461
  try {
499
- logger.debug('Starting Vite asset server...');
462
+ logger.debug('Starting Vite dev server (internal port %d)...', viteInternalPort);
500
463
  const viteResult = await startViteAssetServer({
501
464
  rootDir,
502
465
  logger,
503
466
  workbenchPath: workbench.config?.route,
467
+ port: viteInternalPort,
468
+ backendPort: bunBackendPort,
469
+ routePaths,
504
470
  });
505
471
  viteServer = viteResult.server;
506
472
  vitePort = viteResult.port;
473
+ // Register Vite server with process manager
474
+ procManager.registerServer({
475
+ id: 'vite',
476
+ server: viteServer,
477
+ description: 'Vite dev server (frontend assets)',
478
+ port: vitePort,
479
+ });
507
480
  // Update dev lock with actual Vite port
508
481
  await devLock.updatePorts({ vite: vitePort });
509
- logger.debug(`Vite asset server running on port ${vitePort} (stays running across backend restarts)`);
482
+ logger.debug(`Vite dev server running on port ${vitePort} (internal, proxying backend on port ${bunBackendPort})`);
510
483
  }
511
484
  catch (error) {
512
- tui.error(`Failed to start Vite asset server: ${error}`);
485
+ tui.error(`Failed to start Vite dev server: ${error}`);
486
+ await procManager.cleanup('vite startup failure');
513
487
  await devLock.release();
514
488
  originalExit(1);
515
489
  return;
516
490
  }
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;
491
+ // Start the front-door TCP proxy on the user-facing port.
492
+ // Routes WebSocket upgrades (for /api/*, /_agentuity/*) directly to Bun
493
+ // and everything else (HTTP, HMR WebSocket) to Vite.
494
+ // This works around Bun's broken node:http upgrade socket implementation.
495
+ let frontDoorServer = null;
496
+ try {
497
+ const { startWsProxy } = await import('../build/vite/ws-proxy');
498
+ frontDoorServer = await startWsProxy({
499
+ port: opts.port,
500
+ vitePort,
501
+ backendPort: bunBackendPort,
502
+ routePaths,
503
+ logger,
504
+ });
505
+ // Register front-door proxy with process manager
506
+ procManager.registerServer({
507
+ id: 'front-door-proxy',
508
+ server: {
509
+ close: () => {
510
+ frontDoorServer?.close();
511
+ },
512
+ },
513
+ description: 'Front-door TCP proxy (WS routing)',
514
+ port: opts.port,
515
+ });
516
+ logger.debug(`Front-door proxy on port ${opts.port} (Vite:${vitePort}, Bun:${bunBackendPort})`);
517
+ }
518
+ catch (error) {
519
+ tui.error(`Failed to start front-door proxy: ${error}`);
520
+ await procManager.cleanup('front-door proxy startup failure');
521
+ await devLock.release();
522
+ originalExit(1);
523
+ return;
524
+ }
525
+ // --- State for long-running processes ---
520
526
  let gravityProcess = null;
521
527
  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
528
+ let stdinListenerRegistered = false;
542
529
  let stdinDataHandler = null;
530
+ let shutdownRequested = false;
543
531
  /**
544
532
  * 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
533
+ * Uses the process manager for tracked servers/processes.
549
534
  */
550
535
  const cleanup = async (exitAfter = false, exitCode = 0, silent = false) => {
551
- if (cleaningUp)
536
+ if (shutdownRequested)
552
537
  return;
553
- cleaningUp = true;
538
+ shutdownRequested = true;
554
539
  if (!silent) {
555
540
  tui.info('Shutting down...');
556
541
  }
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
565
- try {
566
- await stopBunServer(opts.port, logger);
567
- }
568
- catch (err) {
569
- logger.debug('Error stopping Bun server during cleanup: %s', err);
570
- }
571
- // Stop gravity heartbeat interval
542
+ // Stop gravity heartbeat interval first
572
543
  if (gravityHeartbeatInterval) {
573
544
  clearInterval(gravityHeartbeatInterval);
574
545
  gravityHeartbeatInterval = null;
575
546
  }
576
- // Kill gravity client with SIGTERM first, then SIGKILL as fallback
577
- if (gravityProcess) {
578
- logger.debug('Killing gravity process...');
579
- try {
580
- gravityProcess.kill('SIGTERM');
581
- // Give it a moment to gracefully shutdown
582
- await new Promise((resolve) => setTimeout(resolve, 150));
583
- if (gravityProcess.exitCode === null) {
584
- gravityProcess.kill('SIGKILL');
585
- }
586
- logger.debug('Gravity process killed');
587
- }
588
- catch (err) {
589
- logger.debug('Error killing gravity process: %s', err);
590
- }
591
- finally {
592
- gravityProcess = null;
593
- }
594
- }
595
- // Close Vite asset server with timeout to prevent hanging
596
- if (viteServer) {
597
- logger.debug('Closing Vite server...');
598
- try {
599
- // Use Promise.race with timeout to prevent hanging
600
- 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
- });
607
- await Promise.race([closePromise, timeoutPromise]);
608
- logger.debug('Vite server closed');
609
- }
610
- catch (err) {
611
- logger.debug('Error closing Vite server: %s', err);
612
- }
613
- finally {
614
- viteServer = null;
615
- }
616
- }
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
- }
547
+ // Use process manager for tracked cleanup
548
+ await procManager.cleanup('shutdown');
549
+ // Additional cleanup for non-tracked resources
550
+ await devLock.release();
626
551
  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
552
+ if (exitAfter) {
634
553
  if (stdinListenerRegistered && process.stdin.isTTY) {
635
554
  try {
636
555
  if (stdinDataHandler) {
@@ -642,588 +561,349 @@ export const command = createCommand({
642
561
  process.stdin.unref();
643
562
  }
644
563
  catch {
645
- // Ignore errors during final cleanup
564
+ // Ignore
646
565
  }
647
566
  }
648
- logger.debug('Exiting with code %d', exitCode);
649
567
  originalExit(exitCode);
650
568
  }
651
569
  };
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;
570
+ // Signal handlers
688
571
  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)
572
+ const safeExit = (code, reason) => {
573
+ if (exitingFromSignal)
574
+ return;
575
+ exitingFromSignal = true;
576
+ if (reason)
577
+ logger.debug('DevMode terminating (%d): %s', code, reason);
578
+ shutdownRequested = true;
579
+ cleanup(true, code).catch(() => originalExit(1));
580
+ };
581
+ process.on('SIGINT', () => safeExit(0, 'SIGINT'));
582
+ process.on('SIGTERM', () => safeExit(0, 'SIGTERM'));
583
+ process.on('SIGHUP', () => safeExit(0, 'SIGHUP'));
584
+ process.on('uncaughtException', (err) => {
585
+ tui.error(`Uncaught exception: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
586
+ void safeExit(1, 'uncaughtException');
587
+ });
588
+ process.on('unhandledRejection', (reason) => {
589
+ logger.warn('Unhandled promise rejection: %s', reason instanceof Error ? (reason.stack ?? reason.message) : String(reason));
590
+ });
727
591
  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) {
592
+ if (gravityProcess?.exitCode === null) {
744
593
  try {
745
594
  gravityProcess.kill('SIGKILL');
746
595
  }
747
596
  catch {
748
- // Ignore errors during exit cleanup
597
+ // Ignore
749
598
  }
750
599
  }
751
- // Close Vite server synchronously if possible
752
600
  if (viteServer) {
753
601
  try {
754
602
  viteServer.close();
755
603
  }
756
604
  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
605
+ // Ignore
769
606
  }
770
607
  }
771
- // Release the dev lockfile synchronously
608
+ killBunSubprocess(logger);
772
609
  releaseLockSync(rootDir);
773
610
  });
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;
911
- }
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
- }
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.`);
611
+ // ================================================================
612
+ // Step 1: Prepare dev server (once)
613
+ // ================================================================
614
+ await tui.spinner({
615
+ message: 'Preparing dev server',
616
+ callback: async () => {
617
+ // Typecheck (skip with --no-typecheck)
618
+ if (!opts.noTypecheck) {
619
+ const typeResult = await typecheck(rootDir);
620
+ if (!typeResult.success) {
621
+ // Non-fatal in dev: log errors and continue
622
+ console.log('');
623
+ console.log(typeResult.output);
624
+ console.log('');
948
625
  }
949
626
  }
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({
627
+ // Generate workbench files if enabled
628
+ if (workbenchConfigData.enabled) {
629
+ const { generateWorkbenchFiles } = await import('../build/vite/workbench-generator');
630
+ await generateWorkbenchFiles(rootDir, project?.projectId ?? '', workbenchConfigData, logger);
631
+ }
632
+ // Discover agents and routes in parallel
633
+ const srcDir = join(rootDir, 'src');
634
+ const { discoverAgents } = await import('../build/vite/agent-discovery');
635
+ const { discoverRoutes } = await import('../build/vite/route-discovery');
636
+ const [agentMetadata, { routes }] = await Promise.all([
637
+ discoverAgents(srcDir, project?.projectId ?? '', deploymentId, logger),
638
+ discoverRoutes(srcDir, project?.projectId ?? '', deploymentId, logger),
639
+ ]);
640
+ // Generate metadata file
641
+ const { generateMetadata, writeMetadataFile } = await import('../build/vite/metadata-generator');
642
+ const promises = [];
643
+ // Generate prompt files (non-blocking)
644
+ promises.push(import('../build/vite/prompt-generator')
645
+ .then(({ generatePromptFiles }) => generatePromptFiles(srcDir, logger))
646
+ .catch((err) => logger.warn('Failed to generate prompt files: %s', err.message)));
647
+ const metadata = await generateMetadata({
990
648
  rootDir,
991
- port: opts.port,
992
- projectId: project?.projectId,
993
- orgId: project?.orgId,
649
+ projectId: project?.projectId ?? '',
650
+ orgId: project?.orgId ?? '',
994
651
  deploymentId,
652
+ agents: agentMetadata,
653
+ routes,
654
+ dev: true,
995
655
  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
656
  });
1002
- // Check if shutdown was requested during startup
1003
- if (shutdownRequested) {
1004
- break;
1005
- }
657
+ writeMetadataFile(rootDir, metadata, true, logger);
658
+ // Sync metadata with backend
659
+ if (syncService && project?.projectId) {
660
+ promises.push(syncService.sync(metadata, previousMetadata, project.projectId, deploymentId));
661
+ previousMetadata = metadata;
662
+ }
663
+ await Promise.all(promises);
664
+ },
665
+ clearOnSuccess: true,
666
+ });
667
+ // ================================================================
668
+ // Step 2: Set environment variables
669
+ // ================================================================
670
+ if (!process.env.AGENTUITY_SDK_KEY) {
671
+ const sdkKey = await loadProjectSDKKey(logger, rootDir);
672
+ if (sdkKey) {
673
+ process.env.AGENTUITY_SDK_KEY = sdkKey;
674
+ }
675
+ else if (project) {
676
+ tui.warning('AGENTUITY_SDK_KEY not found in .env file. Numerous features will be unavailable.');
677
+ tui.bullet(`Run "${getCommand('cloud env pull')}" to sync your SDK key, or add AGENTUITY_SDK_KEY to your .env file.`);
1006
678
  }
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);
679
+ }
680
+ process.env.AGENTUITY_SDK_DEV_MODE = 'true';
681
+ process.env.AGENTUITY_RUNTIME = 'yes';
682
+ process.env.AGENTUITY_ENV = 'development';
683
+ process.env.NODE_ENV = 'development';
684
+ process.env.AGENTUITY_PROJECT_DIR = rootDir;
685
+ if (project?.region) {
686
+ process.env.AGENTUITY_REGION = project.region;
687
+ }
688
+ process.env.PORT = String(bunBackendPort);
689
+ process.env.AGENTUITY_PORT = String(bunBackendPort);
690
+ process.env.AGENTUITY_BASE_URL =
691
+ process.env.AGENTUITY_BASE_URL || `http://localhost:${vitePort}`;
692
+ process.env.AGENTUITY_NO_BUNDLE = 'true';
693
+ if (opts.resume) {
694
+ process.env.AGENTUITY_CODER_RESUME_SESSION = opts.resume;
695
+ }
696
+ if (project) {
697
+ const serviceUrls = getServiceUrls(project.region);
698
+ process.env.AGENTUITY_TRANSPORT_URL = serviceUrls.catalyst;
699
+ process.env.AGENTUITY_CATALYST_URL = serviceUrls.catalyst;
700
+ process.env.AGENTUITY_VECTOR_URL = serviceUrls.vector;
701
+ process.env.AGENTUITY_KEYVALUE_URL = serviceUrls.keyvalue;
702
+ process.env.AGENTUITY_SANDBOX_URL = serviceUrls.sandbox;
703
+ process.env.AGENTUITY_STREAM_URL = serviceUrls.stream;
704
+ process.env.AGENTUITY_CLOUD_ORG_ID = project.orgId;
705
+ process.env.AGENTUITY_CLOUD_PROJECT_ID = project.projectId;
706
+ process.env.AGENTUITY_CLOUD_DEPLOYMENT_ID = deploymentId;
707
+ }
708
+ if (devmode?.hostname) {
709
+ process.env.AGENTUITY_DEVMODE_URL = `https://${devmode.hostname}`;
710
+ }
711
+ else {
712
+ process.env.AGENTUITY_DEVMODE_URL = `http://localhost:${vitePort}`;
713
+ }
714
+ // ================================================================
715
+ // Step 3: Start Bun backend with --hot (handles its own HMR)
716
+ // ================================================================
717
+ try {
718
+ await startBunDevServer({
719
+ rootDir,
720
+ port: bunBackendPort,
721
+ logger,
722
+ vitePort,
723
+ inspect: opts.inspect,
724
+ inspectWait: opts.inspectWait,
725
+ inspectBrk: opts.inspectBrk,
726
+ });
727
+ // Register Bun subprocess with process manager
728
+ // The subprocess is stored in globalThis.__AGENTUITY_BUN_SUBPROCESS__
729
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
730
+ const bunSubprocess = globalThis.__AGENTUITY_BUN_SUBPROCESS__;
731
+ if (bunSubprocess) {
732
+ procManager.registerProcess({
733
+ id: 'bun-backend',
734
+ process: bunSubprocess,
735
+ description: 'Bun backend server (--hot)',
736
+ port: bunBackendPort,
737
+ critical: true,
1022
738
  });
1023
- if (shutdownRequested) {
1024
- break;
1025
- }
1026
- continue;
1027
739
  }
1028
- // Exit early if shutdown was requested
1029
- if (shutdownRequested) {
1030
- break;
740
+ }
741
+ catch (error) {
742
+ tui.error(`Failed to start Bun backend server: ${error}`);
743
+ await cleanup(true, 1, true);
744
+ return;
745
+ }
746
+ // ================================================================
747
+ // Step 4: Start gravity tunnel (if public URL enabled)
748
+ // ================================================================
749
+ if (gravityBin && gravityURL && devmode && project) {
750
+ const privateKeyPEM = devmode.privateKey ?? savedPrivateKey;
751
+ if (!privateKeyPEM) {
752
+ tui.error('No private key available for gravity connection. Please re-run to generate a new key.');
753
+ await cleanup(true, 1, true);
754
+ return;
1031
755
  }
1032
756
  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
757
+ gravityProcess = Bun.spawn([
758
+ gravityBin,
759
+ '--endpoint-id',
760
+ devmode.id,
761
+ '--port',
762
+ vitePort.toString(),
763
+ '--url',
764
+ gravityURL,
765
+ '--log-level',
766
+ process.env.AGENTUITY_GRAVITY_LOG_LEVEL ?? 'error',
767
+ '--org-id',
768
+ project.orgId,
769
+ '--project-id',
770
+ project.projectId,
771
+ '--private-key',
772
+ Buffer.from(privateKeyPEM).toString('base64'),
773
+ '--health-check',
774
+ ], {
775
+ cwd: rootDir,
776
+ stdout: 'pipe',
777
+ stderr: 'pipe',
778
+ detached: false,
779
+ });
780
+ const gravityPid = gravityProcess.pid;
781
+ if (gravityPid) {
782
+ await devLock.registerChild({
783
+ pid: gravityPid,
784
+ type: 'gravity',
785
+ description: 'Gravity public URL tunnel',
1062
786
  });
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);
1102
- }
1103
- }
1104
- else if (trimmed) {
1105
- logger.debug('[gravity] %s', trimmed);
787
+ // Register with process manager
788
+ procManager.registerProcess({
789
+ id: 'gravity',
790
+ process: gravityProcess,
791
+ description: 'Gravity public URL tunnel',
792
+ critical: false,
793
+ });
794
+ }
795
+ // Log gravity output and detect heartbeat port
796
+ (async () => {
797
+ try {
798
+ if (gravityProcess?.stdout) {
799
+ for await (const chunk of gravityProcess.stdout) {
800
+ const text = new TextDecoder().decode(chunk);
801
+ const trimmed = text.trim();
802
+ const match = trimmed.match(/^HEARTBEAT_PORT=(\d+)$/m);
803
+ if (match?.[1]) {
804
+ const heartbeatPort = parseInt(match[1], 10);
805
+ logger.debug('Gravity heartbeat port: %d', heartbeatPort);
806
+ if (!gravityHeartbeatInterval) {
807
+ const sendHeartbeat = async () => {
808
+ try {
809
+ await fetch(`http://127.0.0.1:${heartbeatPort}/heartbeat`, {
810
+ method: 'POST',
811
+ signal: AbortSignal.timeout(2000),
812
+ });
813
+ }
814
+ catch {
815
+ // Ignore heartbeat failures
816
+ }
817
+ };
818
+ sendHeartbeat();
819
+ gravityHeartbeatInterval = setInterval(sendHeartbeat, 5000);
1106
820
  }
1107
821
  }
1108
- }
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());
822
+ else if (trimmed) {
823
+ logger.debug('[gravity] %s', trimmed);
1120
824
  }
1121
825
  }
1122
826
  }
1123
- catch (err) {
1124
- logger.error('Error reading gravity stderr: %s', err);
1125
- }
1126
- })();
1127
- logger.debug('Gravity client started');
1128
- }
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;
827
+ }
828
+ catch (err) {
829
+ logger.error('Error reading gravity stdout: %s', err);
830
+ }
831
+ })();
832
+ (async () => {
833
+ try {
834
+ if (gravityProcess?.stderr) {
835
+ for await (const chunk of gravityProcess.stderr) {
836
+ logger.warn('[gravity] %s', new TextDecoder().decode(chunk).trim());
1153
837
  }
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
838
  }
1179
- };
1180
- process.stdin.on('data', stdinDataHandler);
1181
- }
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();
1191
- }
1192
- }, 100);
1193
- });
1194
- // Exit loop if shutdown was requested
1195
- if (shutdownRequested) {
1196
- break;
1197
- }
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);
839
+ }
840
+ catch (err) {
841
+ logger.error('Error reading gravity stderr: %s', err);
842
+ }
843
+ })();
1204
844
  }
1205
845
  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;
1213
- }
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
- });
846
+ tui.error(`Failed to start gravity tunnel: ${error}`);
847
+ await cleanup(true, 1, true);
848
+ return;
1225
849
  }
1226
850
  }
851
+ // ================================================================
852
+ // Step 5: Keyboard shortcuts + wait for shutdown
853
+ // ================================================================
854
+ if (interactive && process.stdin.isTTY && process.stdout.isTTY) {
855
+ stdinListenerRegistered = true;
856
+ process.stdin.setRawMode(true);
857
+ process.stdin.resume();
858
+ process.stdin.setEncoding('utf8');
859
+ const showHelp = () => {
860
+ console.log('\n' + tui.bold('Keyboard Shortcuts:'));
861
+ console.log(tui.muted(' h') + ' - show this help');
862
+ console.log(tui.muted(' c') + ' - clear console');
863
+ console.log(tui.muted(' q') + ' - quit\n');
864
+ };
865
+ stdinDataHandler = (data) => {
866
+ const key = data.toString();
867
+ if (key === '\u0003' || key === 'q') {
868
+ if (stdinDataHandler) {
869
+ process.stdin.removeListener('data', stdinDataHandler);
870
+ stdinDataHandler = null;
871
+ }
872
+ shutdownRequested = true;
873
+ cleanup(true, 0).catch(() => originalExit(1));
874
+ return;
875
+ }
876
+ switch (key) {
877
+ case 'h':
878
+ showHelp();
879
+ break;
880
+ case 'c':
881
+ console.clear();
882
+ tui.banner('⨺ Agentuity DevMode', devmodebody, {
883
+ padding: 2,
884
+ topSpacer: false,
885
+ bottomSpacer: false,
886
+ centerTitle: false,
887
+ });
888
+ break;
889
+ default:
890
+ process.stdout.write(data);
891
+ break;
892
+ }
893
+ };
894
+ process.stdin.on('data', stdinDataHandler);
895
+ }
896
+ logger.info('DevMode ready 🚀');
897
+ // Block until shutdown — bun --hot handles backend HMR,
898
+ // Vite handles frontend HMR. Nothing to restart.
899
+ await new Promise((resolve) => {
900
+ const check = setInterval(() => {
901
+ if (shutdownRequested) {
902
+ clearInterval(check);
903
+ resolve();
904
+ }
905
+ }, 200);
906
+ });
1227
907
  }
1228
908
  finally {
1229
909
  /* brute force clean up */