@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,20 +11,18 @@ 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
+ import { initProcessManager } from './process-manager';
24
+ import { detectVersionMismatch, formatVersionMismatchWarning } from '../../utils/version-mismatch';
25
+
28
26
  import { ErrorCode } from '../../errors';
29
27
 
30
28
  const DEFAULT_PORT = 3500;
@@ -35,17 +33,13 @@ const MAX_PORT = 65535;
35
33
  interface ProcessLike {
36
34
  kill: (signal?: number | NodeJS.Signals) => void;
37
35
  exitCode: number | null;
36
+ pid?: number;
38
37
  stdout?: AsyncIterable<Uint8Array>;
39
38
  stderr?: AsyncIterable<Uint8Array>;
40
39
  }
41
40
 
42
41
  interface ServerLike {
43
- close: () => void;
44
- }
45
-
46
- interface BunServer {
47
- stop: (closeActiveConnections?: boolean) => void;
48
- port: number;
42
+ close: () => void | Promise<void>;
49
43
  }
50
44
 
51
45
  /**
@@ -83,90 +77,21 @@ async function killLingeringGravityProcesses(logger: {
83
77
  }
84
78
 
85
79
  /**
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).
80
+ * Kill the Bun backend subprocess if one is running.
89
81
  */
90
- async function stopBunServer(
91
- port: number,
92
- logger: { debug: (msg: string, ...args: unknown[]) => void }
93
- ): Promise<void> {
82
+ function killBunSubprocess(logger: { debug: (msg: string, ...args: unknown[]) => void }): void {
94
83
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
84
  const globalAny = globalThis as any;
96
-
97
- // Check for subprocess first (used when debugger flags are enabled)
98
85
  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
- }
86
+ if (!bunSubprocess) return;
143
87
 
144
88
  try {
145
- logger.debug('Stopping Bun server...');
146
- server.stop(true); // Close active connections immediately
147
- logger.debug('Bun server stop() called');
89
+ bunSubprocess.kill('SIGTERM');
90
+ logger.debug('Bun subprocess killed');
148
91
  } catch (err) {
149
- logger.debug('Error stopping Bun server: %s', err);
92
+ logger.debug('Error killing Bun subprocess: %s', err);
150
93
  }
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;
94
+ globalAny.__AGENTUITY_BUN_SUBPROCESS__ = undefined;
170
95
  }
171
96
 
172
97
  const getDefaultPort = (): number => {
@@ -227,22 +152,12 @@ export const command = createCommand({
227
152
  .boolean()
228
153
  .optional()
229
154
  .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
- ),
155
+
236
156
  noTypecheck: z
237
157
  .boolean()
238
158
  .optional()
239
159
  .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
- ),
160
+
246
161
  resume: z.string().optional().describe('Resume a paused Hub session by ID'),
247
162
  }),
248
163
  },
@@ -425,27 +340,12 @@ export const command = createCommand({
425
340
  );
426
341
  }
427
342
 
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 });
343
+ // Check for version mismatches (v1 vs v2 SDK packages)
344
+ const versionMismatch = detectVersionMismatch(rootDir, logger);
345
+ if (versionMismatch.hasV1Packages || versionMismatch.hasMajorMismatches) {
346
+ tui.newline();
347
+ tui.warning(formatVersionMismatchWarning(versionMismatch));
348
+ tui.newline();
449
349
  }
450
350
 
451
351
  try {
@@ -539,12 +439,12 @@ export const command = createCommand({
539
439
  }
540
440
  }
541
441
 
542
- // Get workbench info from config (new Vite approach)
543
- const { loadAgentuityConfig, getWorkbenchConfig } = await import(
442
+ // Get workbench info from createApp() in app.ts (v2 approach)
443
+ const { getWorkbenchConfig, loadRuntimeConfig } = await import(
544
444
  '../build/vite/config-loader'
545
445
  );
546
- const agentuityConfig = await loadAgentuityConfig(rootDir, ctx.logger);
547
- const workbenchConfigData = getWorkbenchConfig(agentuityConfig, true); // dev mode
446
+ const runtimeConfig = await loadRuntimeConfig(rootDir, logger);
447
+ const workbenchConfigData = getWorkbenchConfig(true, runtimeConfig); // dev mode
548
448
  const workbench = {
549
449
  hasWorkbench: workbenchConfigData.enabled,
550
450
  config: workbenchConfigData.enabled
@@ -585,157 +485,170 @@ export const command = createCommand({
585
485
  centerTitle: false,
586
486
  });
587
487
 
588
- // Start Vite asset server ONCE before restart loop
589
- // Vite handles frontend HMR independently and stays running across backend restarts
488
+ // Detect user route mount paths for Vite proxy configuration
489
+ // This is a quick AST scan of app.ts runs before Vite starts
490
+ let routePaths: string[] = ['/api']; // Default fallback
491
+ try {
492
+ const { detectExplicitRouter } = await import('../build/app-router-detector');
493
+ const detection = await detectExplicitRouter(rootDir, logger);
494
+ if (detection.detected && detection.mounts.length > 0) {
495
+ routePaths = detection.mounts.map((m) => m.path);
496
+ logger.debug('Detected route mount paths: %s', routePaths.join(', '));
497
+ }
498
+ } catch (err) {
499
+ logger.debug('Route detection failed, using default /api: %s', err);
500
+ }
501
+
502
+ // Pick internal ports (neither is user-facing — the front-door proxy is)
503
+ const bunBackendPort = opts.port + 1;
504
+ const viteInternalPort = opts.port + 2;
505
+
506
+ // No-bundle dev mode guard: ensure stale bundled app artifact cannot be executed.
507
+ // We keep other .agentuity artifacts (metadata/workbench files) intact.
508
+ try {
509
+ const staleBundlePath = join(rootDir, '.agentuity', 'app.js');
510
+ if (existsSync(staleBundlePath)) {
511
+ await Bun.file(staleBundlePath).delete();
512
+ logger.debug('Removed stale dev bundle artifact: %s', staleBundlePath);
513
+ }
514
+ } catch (err) {
515
+ logger.debug('Failed to remove stale dev bundle artifact: %s', err);
516
+ }
517
+
518
+ // Debug trace: locate unexpected legacy credential warnings.
519
+ // Enable with AGENTUITY_TRACE_CREDENTIAL_WARNINGS=true.
520
+ if (process.env.AGENTUITY_TRACE_CREDENTIAL_WARNINGS === 'true') {
521
+ const originalConsoleError = console.error.bind(console);
522
+ console.error = (...args: unknown[]) => {
523
+ try {
524
+ const first = typeof args[0] === 'string' ? args[0] : '';
525
+ if (first.includes('No credentials found for this AI provider')) {
526
+ const stack = new Error('Credential warning trace').stack;
527
+ originalConsoleError('[TRACE] Credential warning origin stack:');
528
+ if (stack) originalConsoleError(stack);
529
+ }
530
+ } catch {
531
+ // ignore tracing errors
532
+ }
533
+ originalConsoleError(...args);
534
+ };
535
+ }
536
+
537
+ // Start Vite dev server on an internal port.
538
+ // The user-facing port is handled by the front-door TCP proxy (ws-proxy)
539
+ // which routes WS upgrades to Bun and everything else to Vite.
590
540
  let viteServer: ServerLike | null = null;
591
541
  let vitePort: number;
592
542
 
543
+ // Initialize process manager to track all servers/processes
544
+ const procManager = initProcessManager(logger);
545
+
593
546
  try {
594
- logger.debug('Starting Vite asset server...');
547
+ logger.debug('Starting Vite dev server (internal port %d)...', viteInternalPort);
595
548
  const viteResult = await startViteAssetServer({
596
549
  rootDir,
597
550
  logger,
598
551
  workbenchPath: workbench.config?.route,
552
+ port: viteInternalPort,
553
+ backendPort: bunBackendPort,
554
+ routePaths,
599
555
  });
600
556
  viteServer = viteResult.server;
601
557
  vitePort = viteResult.port;
602
558
 
559
+ // Register Vite server with process manager
560
+ procManager.registerServer({
561
+ id: 'vite',
562
+ server: viteServer,
563
+ description: 'Vite dev server (frontend assets)',
564
+ port: vitePort,
565
+ });
566
+
603
567
  // Update dev lock with actual Vite port
604
568
  await devLock.updatePorts({ vite: vitePort });
605
569
 
606
570
  logger.debug(
607
- `Vite asset server running on port ${vitePort} (stays running across backend restarts)`
571
+ `Vite dev server running on port ${vitePort} (internal, proxying backend on port ${bunBackendPort})`
608
572
  );
609
573
  } catch (error) {
610
- tui.error(`Failed to start Vite asset server: ${error}`);
574
+ tui.error(`Failed to start Vite dev server: ${error}`);
575
+ await procManager.cleanup('vite startup failure');
611
576
  await devLock.release();
612
577
  originalExit(1);
613
578
  return;
614
579
  }
615
580
 
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;
619
- let gravityProcess: ProcessLike | null = null;
620
- 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
- };
581
+ // Start the front-door TCP proxy on the user-facing port.
582
+ // Routes WebSocket upgrades (for /api/*, /_agentuity/*) directly to Bun
583
+ // and everything else (HTTP, HMR WebSocket) to Vite.
584
+ // This works around Bun's broken node:http upgrade socket implementation.
585
+ let frontDoorServer: import('node:net').Server | null = null;
586
+ try {
587
+ const { startWsProxy } = await import('../build/vite/ws-proxy');
588
+ frontDoorServer = await startWsProxy({
589
+ port: opts.port,
590
+ vitePort,
591
+ backendPort: bunBackendPort,
592
+ routePaths,
593
+ logger,
594
+ });
630
595
 
631
- // Create file watcher for backend hot reload
632
- const fileWatcher = createFileWatcher({
633
- rootDir,
634
- logger,
635
- onRestart: restartServer,
636
- });
596
+ // Register front-door proxy with process manager
597
+ procManager.registerServer({
598
+ id: 'front-door-proxy',
599
+ server: {
600
+ close: () => {
601
+ frontDoorServer?.close();
602
+ },
603
+ },
604
+ description: 'Front-door TCP proxy (WS routing)',
605
+ port: opts.port,
606
+ });
637
607
 
638
- // Start file watcher (will be paused during builds)
639
- fileWatcher.start();
608
+ logger.debug(
609
+ `Front-door proxy on port ${opts.port} (Vite:${vitePort}, Bun:${bunBackendPort})`
610
+ );
611
+ } catch (error) {
612
+ tui.error(`Failed to start front-door proxy: ${error}`);
613
+ await procManager.cleanup('front-door proxy startup failure');
614
+ await devLock.release();
615
+ originalExit(1);
616
+ return;
617
+ }
640
618
 
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
619
+ // --- State for long-running processes ---
620
+ let gravityProcess: ProcessLike | null = null;
621
+ let gravityHeartbeatInterval: ReturnType<typeof setInterval> | null = null;
622
+ let stdinListenerRegistered = false;
646
623
  let stdinDataHandler: ((data: Buffer | string) => void) | null = null;
624
+ let shutdownRequested = false;
647
625
 
648
626
  /**
649
627
  * 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
628
+ * Uses the process manager for tracked servers/processes.
654
629
  */
655
630
  const cleanup = async (exitAfter = false, exitCode = 0, silent = false) => {
656
- if (cleaningUp) return;
657
- cleaningUp = true;
631
+ if (shutdownRequested) return;
632
+ shutdownRequested = true;
658
633
 
659
634
  if (!silent) {
660
635
  tui.info('Shutting down...');
661
636
  }
662
637
 
663
- // Stop file watcher first to prevent restart triggers during cleanup
664
- try {
665
- fileWatcher.stop();
666
- } catch (err) {
667
- logger.debug('Error stopping file watcher: %s', err);
668
- }
669
-
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
- }
676
-
677
- // Stop gravity heartbeat interval
638
+ // Stop gravity heartbeat interval first
678
639
  if (gravityHeartbeatInterval) {
679
640
  clearInterval(gravityHeartbeatInterval);
680
641
  gravityHeartbeatInterval = null;
681
642
  }
682
643
 
683
- // Kill gravity client with SIGTERM first, then SIGKILL as fallback
684
- if (gravityProcess) {
685
- logger.debug('Killing gravity process...');
686
- try {
687
- gravityProcess.kill('SIGTERM');
688
- // Give it a moment to gracefully shutdown
689
- await new Promise((resolve) => setTimeout(resolve, 150));
690
- if (gravityProcess.exitCode === null) {
691
- gravityProcess.kill('SIGKILL');
692
- }
693
- logger.debug('Gravity process killed');
694
- } catch (err) {
695
- logger.debug('Error killing gravity process: %s', err);
696
- } finally {
697
- gravityProcess = null;
698
- }
699
- }
700
-
701
- // Close Vite asset server with timeout to prevent hanging
702
- if (viteServer) {
703
- logger.debug('Closing Vite server...');
704
- try {
705
- // Use Promise.race with timeout to prevent hanging
706
- 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
- });
713
- await Promise.race([closePromise, timeoutPromise]);
714
- logger.debug('Vite server closed');
715
- } catch (err) {
716
- logger.debug('Error closing Vite server: %s', err);
717
- } finally {
718
- viteServer = null;
719
- }
720
- }
721
-
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
- }
644
+ // Use process manager for tracked cleanup
645
+ await procManager.cleanup('shutdown');
730
646
 
647
+ // Additional cleanup for non-tracked resources
648
+ await devLock.release();
731
649
  await killLingeringGravityProcesses(logger);
732
650
 
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
651
+ if (exitAfter) {
739
652
  if (stdinListenerRegistered && process.stdin.isTTY) {
740
653
  try {
741
654
  if (stdinDataHandler) {
@@ -746,699 +659,403 @@ export const command = createCommand({
746
659
  process.stdin.pause();
747
660
  process.stdin.unref();
748
661
  } catch {
749
- // Ignore errors during final cleanup
662
+ // Ignore
750
663
  }
751
664
  }
752
- logger.debug('Exiting with code %d', exitCode);
753
665
  originalExit(exitCode);
754
666
  }
755
667
  };
756
668
 
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;
669
+ // Signal handlers
794
670
  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
- }
671
+ const safeExit = (code: number, reason?: string) => {
672
+ if (exitingFromSignal) return;
673
+ exitingFromSignal = true;
674
+ if (reason) logger.debug('DevMode terminating (%d): %s', code, reason);
675
+ shutdownRequested = true;
676
+ cleanup(true, code).catch(() => originalExit(1));
677
+ };
843
678
 
844
- // Ensure resources are always cleaned up on exit (synchronous fallback)
679
+ process.on('SIGINT', () => safeExit(0, 'SIGINT'));
680
+ process.on('SIGTERM', () => safeExit(0, 'SIGTERM'));
681
+ process.on('SIGHUP', () => safeExit(0, 'SIGHUP'));
682
+ process.on('uncaughtException', (err) => {
683
+ tui.error(
684
+ `Uncaught exception: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`
685
+ );
686
+ void safeExit(1, 'uncaughtException');
687
+ });
688
+ process.on('unhandledRejection', (reason) => {
689
+ logger.warn(
690
+ 'Unhandled promise rejection: %s',
691
+ reason instanceof Error ? (reason.stack ?? reason.message) : String(reason)
692
+ );
693
+ });
845
694
  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) {
695
+ if (gravityProcess?.exitCode === null) {
862
696
  try {
863
697
  gravityProcess.kill('SIGKILL');
864
698
  } catch {
865
- // Ignore errors during exit cleanup
699
+ // Ignore
866
700
  }
867
701
  }
868
-
869
- // Close Vite server synchronously if possible
870
702
  if (viteServer) {
871
703
  try {
872
704
  viteServer.close();
873
705
  } catch {
874
- // Ignore errors during exit cleanup
706
+ // Ignore
875
707
  }
876
708
  }
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
709
+ killBunSubprocess(logger);
890
710
  releaseLockSync(rootDir);
891
711
  });
892
712
 
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
- );
940
-
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
- }
989
-
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
- }
1005
-
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
- );
1011
-
1012
- const promises: Promise<void>[] = [];
1013
-
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
- );
1022
-
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
- });
1033
-
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,
1051
- });
1052
-
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;
713
+ // ================================================================
714
+ // Step 1: Prepare dev server (once)
715
+ // ================================================================
716
+
717
+ await tui.spinner({
718
+ message: 'Preparing dev server',
719
+ callback: async () => {
720
+ // Typecheck (skip with --no-typecheck)
721
+ if (!opts.noTypecheck) {
722
+ const typeResult = await typecheck(rootDir);
723
+ if (!typeResult.success) {
724
+ // Non-fatal in dev: log errors and continue
725
+ console.log('');
726
+ console.log(typeResult.output);
727
+ console.log('');
1076
728
  }
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
729
  }
1101
- continue;
1102
- }
1103
730
 
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
- }
1119
- }
1120
-
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;
731
+ // Generate workbench files if enabled
732
+ if (workbenchConfigData.enabled) {
733
+ const { generateWorkbenchFiles } = await import(
734
+ '../build/vite/workbench-generator'
735
+ );
736
+ await generateWorkbenchFiles(
737
+ rootDir,
738
+ project?.projectId ?? '',
739
+ workbenchConfigData,
740
+ logger
741
+ );
1128
742
  }
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}`;
1133
743
 
1134
- if (opts.resume) {
1135
- process.env.AGENTUITY_CODER_RESUME_SESSION = opts.resume;
1136
- }
744
+ // Discover agents and routes in parallel
745
+ const srcDir = join(rootDir, 'src');
746
+ const { discoverAgents } = await import('../build/vite/agent-discovery');
747
+ const { discoverRoutes } = await import('../build/vite/route-discovery');
1137
748
 
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
- }
749
+ const [agentMetadata, { routes }] = await Promise.all([
750
+ discoverAgents(srcDir, project?.projectId ?? '', deploymentId, logger),
751
+ discoverRoutes(srcDir, project?.projectId ?? '', deploymentId, logger),
752
+ ]);
1152
753
 
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
- }
754
+ // Generate metadata file
755
+ const { generateMetadata, writeMetadataFile } = await import(
756
+ '../build/vite/metadata-generator'
757
+ );
1158
758
 
1159
- // Set Vite port for asset proxying in bundled app
1160
- process.env.VITE_PORT = String(vitePort);
759
+ const promises: Promise<void>[] = [];
1161
760
 
1162
- logger.debug('Set VITE_PORT=%s for asset proxying', process.env.VITE_PORT);
761
+ // Generate prompt files (non-blocking)
762
+ promises.push(
763
+ import('../build/vite/prompt-generator')
764
+ .then(({ generatePromptFiles }) => generatePromptFiles(srcDir, logger))
765
+ .catch((err) =>
766
+ logger.warn('Failed to generate prompt files: %s', err.message)
767
+ )
768
+ );
1163
769
 
1164
- // Start Bun dev server (Vite already running, just start backend)
1165
- await startBunDevServer({
770
+ const metadata = await generateMetadata({
1166
771
  rootDir,
1167
- port: opts.port,
1168
- projectId: project?.projectId,
1169
- orgId: project?.orgId,
772
+ projectId: project?.projectId ?? '',
773
+ orgId: project?.orgId ?? '',
1170
774
  deploymentId,
775
+ agents: agentMetadata,
776
+ routes,
777
+ dev: true,
1171
778
  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
779
  });
1178
780
 
1179
- // Check if shutdown was requested during startup
1180
- if (shutdownRequested) {
1181
- break;
781
+ writeMetadataFile(rootDir, metadata, true, logger);
782
+
783
+ // Sync metadata with backend
784
+ if (syncService && project?.projectId) {
785
+ promises.push(
786
+ syncService.sync(metadata, previousMetadata, project.projectId, deploymentId)
787
+ );
788
+ previousMetadata = metadata;
1182
789
  }
1183
- } catch (error) {
1184
- tui.error(`Failed to start dev server: ${error}`);
1185
- tui.warning('Waiting for file changes to retry...');
790
+ await Promise.all(promises);
791
+ },
792
+ clearOnSuccess: true,
793
+ });
1186
794
 
1187
- // Clean up any partially started server resources
1188
- await cleanupForRestart();
795
+ // ================================================================
796
+ // Step 2: Set environment variables
797
+ // ================================================================
798
+
799
+ if (!process.env.AGENTUITY_SDK_KEY) {
800
+ const sdkKey = await loadProjectSDKKey(logger, rootDir);
801
+ if (sdkKey) {
802
+ process.env.AGENTUITY_SDK_KEY = sdkKey;
803
+ } else if (project) {
804
+ tui.warning(
805
+ 'AGENTUITY_SDK_KEY not found in .env file. Numerous features will be unavailable.'
806
+ );
807
+ tui.bullet(
808
+ `Run "${getCommand('cloud env pull')}" to sync your SDK key, or add AGENTUITY_SDK_KEY to your .env file.`
809
+ );
810
+ }
811
+ }
1189
812
 
1190
- // Resume watcher to detect changes for retry
1191
- fileWatcher.resume();
813
+ process.env.AGENTUITY_SDK_DEV_MODE = 'true';
814
+ process.env.AGENTUITY_RUNTIME = 'yes';
815
+ process.env.AGENTUITY_ENV = 'development';
816
+ process.env.NODE_ENV = 'development';
817
+ process.env.AGENTUITY_PROJECT_DIR = rootDir;
818
+ if (project?.region) {
819
+ process.env.AGENTUITY_REGION = project.region;
820
+ }
821
+ process.env.PORT = String(bunBackendPort);
822
+ process.env.AGENTUITY_PORT = String(bunBackendPort);
823
+ process.env.AGENTUITY_BASE_URL =
824
+ process.env.AGENTUITY_BASE_URL || `http://localhost:${vitePort}`;
825
+ process.env.AGENTUITY_NO_BUNDLE = 'true';
826
+
827
+ if (opts.resume) {
828
+ process.env.AGENTUITY_CODER_RESUME_SESSION = opts.resume;
829
+ }
1192
830
 
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);
831
+ if (project) {
832
+ const serviceUrls = getServiceUrls(project.region);
833
+ process.env.AGENTUITY_TRANSPORT_URL = serviceUrls.catalyst;
834
+ process.env.AGENTUITY_CATALYST_URL = serviceUrls.catalyst;
835
+ process.env.AGENTUITY_VECTOR_URL = serviceUrls.vector;
836
+ process.env.AGENTUITY_KEYVALUE_URL = serviceUrls.keyvalue;
837
+ process.env.AGENTUITY_SANDBOX_URL = serviceUrls.sandbox;
838
+ process.env.AGENTUITY_STREAM_URL = serviceUrls.stream;
839
+ process.env.AGENTUITY_CLOUD_ORG_ID = project.orgId;
840
+ process.env.AGENTUITY_CLOUD_PROJECT_ID = project.projectId;
841
+ process.env.AGENTUITY_CLOUD_DEPLOYMENT_ID = deploymentId;
842
+ }
843
+
844
+ if (devmode?.hostname) {
845
+ process.env.AGENTUITY_DEVMODE_URL = `https://${devmode.hostname}`;
846
+ } else {
847
+ process.env.AGENTUITY_DEVMODE_URL = `http://localhost:${vitePort}`;
848
+ }
849
+
850
+ // ================================================================
851
+ // Step 3: Start Bun backend with --hot (handles its own HMR)
852
+ // ================================================================
853
+
854
+ try {
855
+ await startBunDevServer({
856
+ rootDir,
857
+ port: bunBackendPort,
858
+ logger,
859
+ vitePort,
860
+ inspect: opts.inspect,
861
+ inspectWait: opts.inspectWait,
862
+ inspectBrk: opts.inspectBrk,
863
+ });
864
+
865
+ // Register Bun subprocess with process manager
866
+ // The subprocess is stored in globalThis.__AGENTUITY_BUN_SUBPROCESS__
867
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
868
+ const bunSubprocess = (globalThis as any).__AGENTUITY_BUN_SUBPROCESS__ as ProcessLike;
869
+ if (bunSubprocess) {
870
+ procManager.registerProcess({
871
+ id: 'bun-backend',
872
+ process: bunSubprocess,
873
+ description: 'Bun backend server (--hot)',
874
+ port: bunBackendPort,
875
+ critical: true,
1201
876
  });
1202
- if (shutdownRequested) {
1203
- break;
1204
- }
1205
- continue;
1206
877
  }
878
+ } catch (error) {
879
+ tui.error(`Failed to start Bun backend server: ${error}`);
880
+ await cleanup(true, 1, true);
881
+ return;
882
+ }
883
+
884
+ // ================================================================
885
+ // Step 4: Start gravity tunnel (if public URL enabled)
886
+ // ================================================================
1207
887
 
1208
- // Exit early if shutdown was requested
1209
- if (shutdownRequested) {
1210
- break;
888
+ if (gravityBin && gravityURL && devmode && project) {
889
+ const privateKeyPEM = devmode.privateKey ?? savedPrivateKey;
890
+ if (!privateKeyPEM) {
891
+ tui.error(
892
+ 'No private key available for gravity connection. Please re-run to generate a new key.'
893
+ );
894
+ await cleanup(true, 1, true);
895
+ return;
1211
896
  }
1212
897
 
1213
898
  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)',
899
+ gravityProcess = Bun.spawn(
900
+ [
1218
901
  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
- );
902
+ '--endpoint-id',
903
+ devmode.id,
904
+ '--port',
905
+ vitePort.toString(),
906
+ '--url',
907
+ gravityURL,
908
+ '--log-level',
909
+ process.env.AGENTUITY_GRAVITY_LOG_LEVEL ?? 'error',
910
+ '--org-id',
911
+ project.orgId,
912
+ '--project-id',
913
+ project.projectId,
914
+ '--private-key',
915
+ Buffer.from(privateKeyPEM).toString('base64'),
916
+ '--health-check',
917
+ ],
918
+ {
919
+ cwd: rootDir,
920
+ stdout: 'pipe',
921
+ stderr: 'pipe',
922
+ detached: false,
1227
923
  }
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
- );
924
+ );
1254
925
 
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
- }
926
+ const gravityPid = (gravityProcess as { pid?: number }).pid;
927
+ if (gravityPid) {
928
+ await devLock.registerChild({
929
+ pid: gravityPid,
930
+ type: 'gravity',
931
+ description: 'Gravity public URL tunnel',
932
+ });
1264
933
 
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);
1301
- }
1302
- } else if (trimmed) {
1303
- logger.debug('[gravity] %s', trimmed);
934
+ // Register with process manager
935
+ procManager.registerProcess({
936
+ id: 'gravity',
937
+ process: gravityProcess,
938
+ description: 'Gravity public URL tunnel',
939
+ critical: false,
940
+ });
941
+ }
942
+
943
+ // Log gravity output and detect heartbeat port
944
+ (async () => {
945
+ try {
946
+ if (gravityProcess?.stdout) {
947
+ for await (const chunk of gravityProcess.stdout) {
948
+ const text = new TextDecoder().decode(chunk);
949
+ const trimmed = text.trim();
950
+
951
+ const match = trimmed.match(/^HEARTBEAT_PORT=(\d+)$/m);
952
+ if (match?.[1]) {
953
+ const heartbeatPort = parseInt(match[1], 10);
954
+ logger.debug('Gravity heartbeat port: %d', heartbeatPort);
955
+
956
+ if (!gravityHeartbeatInterval) {
957
+ const sendHeartbeat = async () => {
958
+ try {
959
+ await fetch(`http://127.0.0.1:${heartbeatPort}/heartbeat`, {
960
+ method: 'POST',
961
+ signal: AbortSignal.timeout(2000),
962
+ });
963
+ } catch {
964
+ // Ignore heartbeat failures
965
+ }
966
+ };
967
+ sendHeartbeat();
968
+ gravityHeartbeatInterval = setInterval(sendHeartbeat, 5000);
1304
969
  }
970
+ } else if (trimmed) {
971
+ logger.debug('[gravity] %s', trimmed);
1305
972
  }
1306
973
  }
1307
- } catch (err) {
1308
- logger.error('Error reading gravity stdout: %s', err);
1309
974
  }
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');
1326
- }
975
+ } catch (err) {
976
+ logger.error('Error reading gravity stdout: %s', err);
977
+ }
978
+ })();
1327
979
 
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;
980
+ (async () => {
981
+ try {
982
+ if (gravityProcess?.stderr) {
983
+ for await (const chunk of gravityProcess.stderr) {
984
+ logger.warn('[gravity] %s', new TextDecoder().decode(chunk).trim());
1357
985
  }
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
986
  }
987
+ } catch (err) {
988
+ logger.error('Error reading gravity stderr: %s', err);
989
+ }
990
+ })();
991
+ } catch (error) {
992
+ tui.error(`Failed to start gravity tunnel: ${error}`);
993
+ await cleanup(true, 1, true);
994
+ return;
995
+ }
996
+ }
1366
997
 
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;
1383
- }
1384
- };
1385
- process.stdin.on('data', stdinDataHandler);
1386
- }
1387
-
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
- });
998
+ // ================================================================
999
+ // Step 5: Keyboard shortcuts + wait for shutdown
1000
+ // ================================================================
1001
+
1002
+ if (interactive && process.stdin.isTTY && process.stdout.isTTY) {
1003
+ stdinListenerRegistered = true;
1004
+ process.stdin.setRawMode(true);
1005
+ process.stdin.resume();
1006
+ process.stdin.setEncoding('utf8');
1007
+
1008
+ const showHelp = () => {
1009
+ console.log('\n' + tui.bold('Keyboard Shortcuts:'));
1010
+ console.log(tui.muted(' h') + ' - show this help');
1011
+ console.log(tui.muted(' c') + ' - clear console');
1012
+ console.log(tui.muted(' q') + ' - quit\n');
1013
+ };
1402
1014
 
1403
- // Exit loop if shutdown was requested
1404
- if (shutdownRequested) {
1405
- break;
1015
+ stdinDataHandler = (data) => {
1016
+ const key = data.toString();
1017
+ if (key === '\u0003' || key === 'q') {
1018
+ if (stdinDataHandler) {
1019
+ process.stdin.removeListener('data', stdinDataHandler);
1020
+ stdinDataHandler = null;
1021
+ }
1022
+ shutdownRequested = true;
1023
+ cleanup(true, 0).catch(() => originalExit(1));
1024
+ return;
1406
1025
  }
1026
+ switch (key) {
1027
+ case 'h':
1028
+ showHelp();
1029
+ break;
1030
+ case 'c':
1031
+ console.clear();
1032
+ tui.banner('⨺ Agentuity DevMode', devmodebody, {
1033
+ padding: 2,
1034
+ topSpacer: false,
1035
+ bottomSpacer: false,
1036
+ centerTitle: false,
1037
+ });
1038
+ break;
1039
+ default:
1040
+ process.stdout.write(data);
1041
+ break;
1042
+ }
1043
+ };
1044
+ process.stdin.on('data', stdinDataHandler);
1045
+ }
1407
1046
 
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();
1047
+ logger.info('DevMode ready 🚀');
1422
1048
 
1423
- // Exit if shutdown was requested during error handling
1049
+ // Block until shutdown bun --hot handles backend HMR,
1050
+ // Vite handles frontend HMR. Nothing to restart.
1051
+ await new Promise<void>((resolve) => {
1052
+ const check = setInterval(() => {
1424
1053
  if (shutdownRequested) {
1425
- break;
1054
+ clearInterval(check);
1055
+ resolve();
1426
1056
  }
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
- }
1057
+ }, 200);
1058
+ });
1442
1059
  } finally {
1443
1060
  /* brute force clean up */
1444
1061
  await devLock.release();