@agentuity/cli 2.0.10 → 2.0.12

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 (233) hide show
  1. package/dist/cache/resource-region.d.ts.map +1 -1
  2. package/dist/cache/resource-region.js +48 -25
  3. package/dist/cache/resource-region.js.map +1 -1
  4. package/dist/cmd/build/vite/agent-discovery.js +4 -4
  5. package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
  6. package/dist/cmd/build/vite/bun-dev-server.d.ts +20 -0
  7. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
  8. package/dist/cmd/build/vite/bun-dev-server.js +62 -4
  9. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  10. package/dist/cmd/build/vite/index.d.ts +0 -1
  11. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  12. package/dist/cmd/build/vite/index.js +0 -1
  13. package/dist/cmd/build/vite/index.js.map +1 -1
  14. package/dist/cmd/build/vite/static-renderer.d.ts +17 -0
  15. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  16. package/dist/cmd/build/vite/static-renderer.js +18 -6
  17. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  18. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  19. package/dist/cmd/build/vite/vite-asset-server-config.js +34 -27
  20. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  21. package/dist/cmd/build/vite/vite-asset-server.d.ts +9 -0
  22. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  23. package/dist/cmd/build/vite/vite-asset-server.js +5 -1
  24. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  25. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  26. package/dist/cmd/build/vite/vite-builder.js +12 -1
  27. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  28. package/dist/cmd/build/vite/ws-proxy.d.ts +15 -1
  29. package/dist/cmd/build/vite/ws-proxy.d.ts.map +1 -1
  30. package/dist/cmd/build/vite/ws-proxy.js +33 -0
  31. package/dist/cmd/build/vite/ws-proxy.js.map +1 -1
  32. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  33. package/dist/cmd/cloud/deploy.js +98 -39
  34. package/dist/cmd/cloud/deploy.js.map +1 -1
  35. package/dist/cmd/cloud/sandbox/checkpoint/create.d.ts.map +1 -1
  36. package/dist/cmd/cloud/sandbox/checkpoint/create.js +3 -4
  37. package/dist/cmd/cloud/sandbox/checkpoint/create.js.map +1 -1
  38. package/dist/cmd/cloud/sandbox/checkpoint/delete.d.ts.map +1 -1
  39. package/dist/cmd/cloud/sandbox/checkpoint/delete.js +3 -4
  40. package/dist/cmd/cloud/sandbox/checkpoint/delete.js.map +1 -1
  41. package/dist/cmd/cloud/sandbox/checkpoint/list.d.ts.map +1 -1
  42. package/dist/cmd/cloud/sandbox/checkpoint/list.js +3 -4
  43. package/dist/cmd/cloud/sandbox/checkpoint/list.js.map +1 -1
  44. package/dist/cmd/cloud/sandbox/checkpoint/restore.d.ts.map +1 -1
  45. package/dist/cmd/cloud/sandbox/checkpoint/restore.js +3 -4
  46. package/dist/cmd/cloud/sandbox/checkpoint/restore.js.map +1 -1
  47. package/dist/cmd/cloud/sandbox/create.d.ts.map +1 -1
  48. package/dist/cmd/cloud/sandbox/create.js +13 -4
  49. package/dist/cmd/cloud/sandbox/create.js.map +1 -1
  50. package/dist/cmd/cloud/sandbox/delete.d.ts.map +1 -1
  51. package/dist/cmd/cloud/sandbox/delete.js +3 -4
  52. package/dist/cmd/cloud/sandbox/delete.js.map +1 -1
  53. package/dist/cmd/cloud/sandbox/env.d.ts.map +1 -1
  54. package/dist/cmd/cloud/sandbox/env.js +3 -5
  55. package/dist/cmd/cloud/sandbox/env.js.map +1 -1
  56. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  57. package/dist/cmd/cloud/sandbox/exec.js +114 -41
  58. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  59. package/dist/cmd/cloud/sandbox/execution/list.d.ts.map +1 -1
  60. package/dist/cmd/cloud/sandbox/execution/list.js +3 -5
  61. package/dist/cmd/cloud/sandbox/execution/list.js.map +1 -1
  62. package/dist/cmd/cloud/sandbox/fs/cp.d.ts.map +1 -1
  63. package/dist/cmd/cloud/sandbox/fs/cp.js +61 -113
  64. package/dist/cmd/cloud/sandbox/fs/cp.js.map +1 -1
  65. package/dist/cmd/cloud/sandbox/fs/download.d.ts.map +1 -1
  66. package/dist/cmd/cloud/sandbox/fs/download.js +11 -22
  67. package/dist/cmd/cloud/sandbox/fs/download.js.map +1 -1
  68. package/dist/cmd/cloud/sandbox/fs/ls.d.ts.map +1 -1
  69. package/dist/cmd/cloud/sandbox/fs/ls.js +3 -5
  70. package/dist/cmd/cloud/sandbox/fs/ls.js.map +1 -1
  71. package/dist/cmd/cloud/sandbox/fs/mkdir.d.ts.map +1 -1
  72. package/dist/cmd/cloud/sandbox/fs/mkdir.js +3 -5
  73. package/dist/cmd/cloud/sandbox/fs/mkdir.js.map +1 -1
  74. package/dist/cmd/cloud/sandbox/fs/rm.d.ts.map +1 -1
  75. package/dist/cmd/cloud/sandbox/fs/rm.js +3 -5
  76. package/dist/cmd/cloud/sandbox/fs/rm.js.map +1 -1
  77. package/dist/cmd/cloud/sandbox/fs/rmdir.d.ts.map +1 -1
  78. package/dist/cmd/cloud/sandbox/fs/rmdir.js +3 -5
  79. package/dist/cmd/cloud/sandbox/fs/rmdir.js.map +1 -1
  80. package/dist/cmd/cloud/sandbox/fs/upload.d.ts.map +1 -1
  81. package/dist/cmd/cloud/sandbox/fs/upload.js +7 -8
  82. package/dist/cmd/cloud/sandbox/fs/upload.js.map +1 -1
  83. package/dist/cmd/cloud/sandbox/get.d.ts.map +1 -1
  84. package/dist/cmd/cloud/sandbox/get.js +21 -7
  85. package/dist/cmd/cloud/sandbox/get.js.map +1 -1
  86. package/dist/cmd/cloud/sandbox/job/create.d.ts.map +1 -1
  87. package/dist/cmd/cloud/sandbox/job/create.js +3 -4
  88. package/dist/cmd/cloud/sandbox/job/create.js.map +1 -1
  89. package/dist/cmd/cloud/sandbox/job/destroy.d.ts.map +1 -1
  90. package/dist/cmd/cloud/sandbox/job/destroy.js +3 -4
  91. package/dist/cmd/cloud/sandbox/job/destroy.js.map +1 -1
  92. package/dist/cmd/cloud/sandbox/job/get.d.ts.map +1 -1
  93. package/dist/cmd/cloud/sandbox/job/get.js +3 -4
  94. package/dist/cmd/cloud/sandbox/job/get.js.map +1 -1
  95. package/dist/cmd/cloud/sandbox/job/list.d.ts.map +1 -1
  96. package/dist/cmd/cloud/sandbox/job/list.js +3 -4
  97. package/dist/cmd/cloud/sandbox/job/list.js.map +1 -1
  98. package/dist/cmd/cloud/sandbox/job/logs.d.ts.map +1 -1
  99. package/dist/cmd/cloud/sandbox/job/logs.js +4 -4
  100. package/dist/cmd/cloud/sandbox/job/logs.js.map +1 -1
  101. package/dist/cmd/cloud/sandbox/pause.d.ts.map +1 -1
  102. package/dist/cmd/cloud/sandbox/pause.js +21 -5
  103. package/dist/cmd/cloud/sandbox/pause.js.map +1 -1
  104. package/dist/cmd/cloud/sandbox/resume.d.ts.map +1 -1
  105. package/dist/cmd/cloud/sandbox/resume.js +3 -4
  106. package/dist/cmd/cloud/sandbox/resume.js.map +1 -1
  107. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  108. package/dist/cmd/cloud/sandbox/run.js +36 -7
  109. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  110. package/dist/cmd/cloud/sandbox/util.d.ts +19 -0
  111. package/dist/cmd/cloud/sandbox/util.d.ts.map +1 -1
  112. package/dist/cmd/cloud/sandbox/util.js +40 -2
  113. package/dist/cmd/cloud/sandbox/util.js.map +1 -1
  114. package/dist/cmd/coder/create.d.ts.map +1 -1
  115. package/dist/cmd/coder/create.js +18 -0
  116. package/dist/cmd/coder/create.js.map +1 -1
  117. package/dist/cmd/coder/index.d.ts.map +1 -1
  118. package/dist/cmd/coder/index.js +4 -0
  119. package/dist/cmd/coder/index.js.map +1 -1
  120. package/dist/cmd/coder/start.d.ts.map +1 -1
  121. package/dist/cmd/coder/start.js +52 -1
  122. package/dist/cmd/coder/start.js.map +1 -1
  123. package/dist/cmd/coder/tui-init.js +1 -1
  124. package/dist/cmd/coder/tui-init.js.map +1 -1
  125. package/dist/cmd/coder/update.d.ts.map +1 -1
  126. package/dist/cmd/coder/update.js +21 -1
  127. package/dist/cmd/coder/update.js.map +1 -1
  128. package/dist/cmd/coder/workspace/create.d.ts.map +1 -1
  129. package/dist/cmd/coder/workspace/create.js +57 -13
  130. package/dist/cmd/coder/workspace/create.js.map +1 -1
  131. package/dist/cmd/coder/workspace/index.d.ts.map +1 -1
  132. package/dist/cmd/coder/workspace/index.js +1 -1
  133. package/dist/cmd/coder/workspace/index.js.map +1 -1
  134. package/dist/cmd/coder/workspace/list.js +2 -2
  135. package/dist/cmd/coder/workspace/list.js.map +1 -1
  136. package/dist/cmd/dev/dev-lock.d.ts.map +1 -1
  137. package/dist/cmd/dev/dev-lock.js +43 -17
  138. package/dist/cmd/dev/dev-lock.js.map +1 -1
  139. package/dist/cmd/dev/index.d.ts.map +1 -1
  140. package/dist/cmd/dev/index.js +211 -125
  141. package/dist/cmd/dev/index.js.map +1 -1
  142. package/dist/cmd/dev/process-manager.d.ts +41 -1
  143. package/dist/cmd/dev/process-manager.d.ts.map +1 -1
  144. package/dist/cmd/dev/process-manager.js +160 -31
  145. package/dist/cmd/dev/process-manager.js.map +1 -1
  146. package/dist/cmd/project/create.d.ts.map +1 -1
  147. package/dist/cmd/project/create.js +0 -2
  148. package/dist/cmd/project/create.js.map +1 -1
  149. package/dist/cmd/project/index.d.ts.map +1 -1
  150. package/dist/cmd/project/index.js +0 -3
  151. package/dist/cmd/project/index.js.map +1 -1
  152. package/dist/cmd/project/template-flow.d.ts +0 -1
  153. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  154. package/dist/cmd/project/template-flow.js +1 -124
  155. package/dist/cmd/project/template-flow.js.map +1 -1
  156. package/dist/types.d.ts +1 -1
  157. package/package.json +7 -7
  158. package/src/cache/resource-region.ts +68 -44
  159. package/src/cmd/ai/prompt/web.md +43 -17
  160. package/src/cmd/build/vite/agent-discovery.ts +4 -4
  161. package/src/cmd/build/vite/bun-dev-server.ts +92 -6
  162. package/src/cmd/build/vite/index.ts +0 -1
  163. package/src/cmd/build/vite/static-renderer.ts +18 -7
  164. package/src/cmd/build/vite/vite-asset-server-config.ts +37 -27
  165. package/src/cmd/build/vite/vite-asset-server.ts +5 -1
  166. package/src/cmd/build/vite/vite-builder.ts +12 -1
  167. package/src/cmd/build/vite/ws-proxy.ts +52 -3
  168. package/src/cmd/cloud/deploy.ts +117 -49
  169. package/src/cmd/cloud/sandbox/checkpoint/create.ts +10 -4
  170. package/src/cmd/cloud/sandbox/checkpoint/delete.ts +10 -4
  171. package/src/cmd/cloud/sandbox/checkpoint/list.ts +10 -4
  172. package/src/cmd/cloud/sandbox/checkpoint/restore.ts +10 -4
  173. package/src/cmd/cloud/sandbox/create.ts +14 -4
  174. package/src/cmd/cloud/sandbox/delete.ts +10 -4
  175. package/src/cmd/cloud/sandbox/env.ts +10 -5
  176. package/src/cmd/cloud/sandbox/exec.ts +157 -42
  177. package/src/cmd/cloud/sandbox/execution/list.ts +10 -5
  178. package/src/cmd/cloud/sandbox/fs/cp.ts +94 -126
  179. package/src/cmd/cloud/sandbox/fs/download.ts +18 -25
  180. package/src/cmd/cloud/sandbox/fs/ls.ts +10 -5
  181. package/src/cmd/cloud/sandbox/fs/mkdir.ts +10 -5
  182. package/src/cmd/cloud/sandbox/fs/rm.ts +10 -5
  183. package/src/cmd/cloud/sandbox/fs/rmdir.ts +10 -5
  184. package/src/cmd/cloud/sandbox/fs/upload.ts +14 -8
  185. package/src/cmd/cloud/sandbox/get.ts +28 -7
  186. package/src/cmd/cloud/sandbox/job/create.ts +10 -4
  187. package/src/cmd/cloud/sandbox/job/destroy.ts +10 -4
  188. package/src/cmd/cloud/sandbox/job/get.ts +10 -4
  189. package/src/cmd/cloud/sandbox/job/list.ts +10 -4
  190. package/src/cmd/cloud/sandbox/job/logs.ts +11 -4
  191. package/src/cmd/cloud/sandbox/pause.ts +31 -5
  192. package/src/cmd/cloud/sandbox/resume.ts +10 -4
  193. package/src/cmd/cloud/sandbox/run.ts +49 -11
  194. package/src/cmd/cloud/sandbox/util.ts +63 -2
  195. package/src/cmd/coder/create.ts +24 -1
  196. package/src/cmd/coder/index.ts +4 -0
  197. package/src/cmd/coder/start.ts +63 -1
  198. package/src/cmd/coder/tui-init.ts +1 -1
  199. package/src/cmd/coder/update.ts +18 -1
  200. package/src/cmd/coder/workspace/create.ts +84 -15
  201. package/src/cmd/coder/workspace/index.ts +3 -1
  202. package/src/cmd/coder/workspace/list.ts +2 -2
  203. package/src/cmd/dev/dev-lock.ts +50 -16
  204. package/src/cmd/dev/index.ts +249 -134
  205. package/src/cmd/dev/process-manager.ts +173 -33
  206. package/src/cmd/project/create.ts +0 -2
  207. package/src/cmd/project/index.ts +0 -3
  208. package/src/cmd/project/template-flow.ts +0 -147
  209. package/dist/cmd/build/vite/public-asset-path-plugin.d.ts +0 -45
  210. package/dist/cmd/build/vite/public-asset-path-plugin.d.ts.map +0 -1
  211. package/dist/cmd/build/vite/public-asset-path-plugin.js +0 -166
  212. package/dist/cmd/build/vite/public-asset-path-plugin.js.map +0 -1
  213. package/dist/cmd/project/auth/generate.d.ts +0 -5
  214. package/dist/cmd/project/auth/generate.d.ts.map +0 -1
  215. package/dist/cmd/project/auth/generate.js +0 -102
  216. package/dist/cmd/project/auth/generate.js.map +0 -1
  217. package/dist/cmd/project/auth/index.d.ts +0 -2
  218. package/dist/cmd/project/auth/index.d.ts.map +0 -1
  219. package/dist/cmd/project/auth/index.js +0 -21
  220. package/dist/cmd/project/auth/index.js.map +0 -1
  221. package/dist/cmd/project/auth/init.d.ts +0 -2
  222. package/dist/cmd/project/auth/init.d.ts.map +0 -1
  223. package/dist/cmd/project/auth/init.js +0 -213
  224. package/dist/cmd/project/auth/init.js.map +0 -1
  225. package/dist/cmd/project/auth/shared.d.ts +0 -93
  226. package/dist/cmd/project/auth/shared.d.ts.map +0 -1
  227. package/dist/cmd/project/auth/shared.js +0 -475
  228. package/dist/cmd/project/auth/shared.js.map +0 -1
  229. package/src/cmd/build/vite/public-asset-path-plugin.ts +0 -209
  230. package/src/cmd/project/auth/generate.ts +0 -116
  231. package/src/cmd/project/auth/index.ts +0 -21
  232. package/src/cmd/project/auth/init.ts +0 -256
  233. package/src/cmd/project/auth/shared.ts +0 -591
@@ -45,27 +45,38 @@ interface ServerLike {
45
45
  /**
46
46
  * Kill any lingering gravity processes from previous dev sessions.
47
47
  * This is a defensive measure to clean up orphaned processes.
48
+ *
49
+ * When a projectId is provided, only kills gravity processes for that specific
50
+ * project. Otherwise falls back to killing all gravity processes (used during
51
+ * startup before project info is available).
48
52
  */
49
- async function killLingeringGravityProcesses(logger: {
50
- debug: (msg: string, ...args: unknown[]) => void;
51
- }): Promise<void> {
53
+ async function killLingeringGravityProcesses(
54
+ logger: {
55
+ debug: (msg: string, ...args: unknown[]) => void;
56
+ },
57
+ projectId?: string
58
+ ): Promise<void> {
52
59
  // Only attempt on Unix-like systems (macOS, Linux)
53
60
  if (process.platform === 'win32') {
54
61
  return;
55
62
  }
56
63
 
57
64
  try {
58
- // Use pkill to kill gravity processes owned by current user
59
- // The -f flag matches against full command line
60
- // We specifically match the gravity binary name to avoid killing unrelated processes
61
- const result = Bun.spawnSync(['pkill', '-f', 'gravity.*--endpoint-id'], {
65
+ // Scope the pkill pattern to the specific project when possible,
66
+ // avoiding killing gravity processes from other dev sessions.
67
+ const pattern = projectId ? `gravity.*--project-id.*${projectId}` : 'gravity.*--endpoint-id';
68
+
69
+ const result = Bun.spawnSync(['pkill', '-f', pattern], {
62
70
  stdout: 'ignore',
63
71
  stderr: 'ignore',
64
72
  });
65
73
 
66
74
  // Exit code 0 = processes killed, 1 = no matching processes, other = error
67
75
  if (result.exitCode === 0) {
68
- logger.debug('Killed lingering gravity processes from previous session');
76
+ logger.debug(
77
+ 'Killed lingering gravity processes%s from previous session',
78
+ projectId ? ` (project ${projectId})` : ''
79
+ );
69
80
  // Brief pause to let processes fully terminate
70
81
  await new Promise((resolve) => setTimeout(resolve, 100));
71
82
  } else if (result.exitCode === 1) {
@@ -78,19 +89,46 @@ async function killLingeringGravityProcesses(logger: {
78
89
 
79
90
  /**
80
91
  * Kill the Bun backend subprocess if one is running.
92
+ *
93
+ * @param forceKill - If true, sends SIGKILL instead of SIGTERM. Used in
94
+ * process.on('exit') handlers where there's no time for graceful shutdown.
95
+ * Also kills the entire process tree to prevent orphaned child processes.
81
96
  */
82
- function killBunSubprocess(logger: { debug: (msg: string, ...args: unknown[]) => void }): void {
97
+ function killBunSubprocess(
98
+ logger: { debug: (msg: string, ...args: unknown[]) => void },
99
+ forceKill = false
100
+ ): void {
83
101
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
102
  const globalAny = globalThis as any;
85
103
  const bunSubprocess = globalAny.__AGENTUITY_BUN_SUBPROCESS__ as ProcessLike | undefined;
86
104
  if (!bunSubprocess) return;
87
105
 
88
- try {
89
- bunSubprocess.kill('SIGTERM');
90
- logger.debug('Bun subprocess killed');
91
- } catch (err) {
92
- logger.debug('Error killing Bun subprocess: %s', err);
106
+ const signal: NodeJS.Signals = forceKill ? 'SIGKILL' : 'SIGTERM';
107
+ const pid = bunSubprocess.pid;
108
+
109
+ // Kill the entire process tree if we have a PID (guard against dangerous PIDs)
110
+ if (pid && pid > 1) {
111
+ try {
112
+ process.kill(-pid, signal);
113
+ logger.debug('Sent %s to Bun subprocess process group -%d', signal, pid);
114
+ } catch {
115
+ // Process group kill failed, fall back to direct kill
116
+ try {
117
+ bunSubprocess.kill(signal);
118
+ logger.debug('Sent %s to Bun subprocess pid %d (direct)', signal, pid);
119
+ } catch (err) {
120
+ logger.debug('Error killing Bun subprocess: %s', err);
121
+ }
122
+ }
123
+ } else {
124
+ try {
125
+ bunSubprocess.kill(signal);
126
+ logger.debug('Bun subprocess killed with %s', signal);
127
+ } catch (err) {
128
+ logger.debug('Error killing Bun subprocess: %s', err);
129
+ }
93
130
  }
131
+
94
132
  globalAny.__AGENTUITY_BUN_SUBPROCESS__ = undefined;
95
133
  }
96
134
 
@@ -326,9 +364,9 @@ export const command = createCommand({
326
364
  // and creates a new lockfile for this session
327
365
  const devLock = await prepareDevLock(rootDir, opts.port, logger);
328
366
 
329
- // Kill any lingering gravity processes from previous dev sessions
330
- // This is a fallback for cases where the lockfile was corrupted
331
- await killLingeringGravityProcesses(logger);
367
+ // Kill any lingering gravity processes from previous dev sessions.
368
+ // Scoped to this project to avoid killing gravity from other dev sessions.
369
+ await killLingeringGravityProcesses(logger, project?.projectId);
332
370
 
333
371
  // Check and upgrade @agentuity/* dependencies if needed
334
372
  const upgradeResult = await checkAndUpgradeDependencies(rootDir, logger);
@@ -534,102 +572,47 @@ export const command = createCommand({
534
572
  };
535
573
  }
536
574
 
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.
575
+ // --- State for long-running processes ---
576
+ // Declared early so signal handlers can reference them before
577
+ // servers are started.
540
578
  let viteServer: ServerLike | null = null;
541
- let vitePort: number;
579
+ let frontDoorServer: import('../build/vite/ws-proxy').WsProxyServer | null = null;
580
+ let gravityProcess: ProcessLike | null = null;
581
+ let gravityHeartbeatInterval: ReturnType<typeof setInterval> | null = null;
582
+ let stdinListenerRegistered = false;
583
+ let stdinDataHandler: ((data: Buffer | string) => void) | null = null;
584
+ let shutdownRequested = false;
542
585
 
543
586
  // Initialize process manager to track all servers/processes
544
587
  const procManager = initProcessManager(logger);
545
588
 
546
- try {
547
- logger.debug('Starting Vite dev server (internal port %d)...', viteInternalPort);
548
- const viteResult = await startViteAssetServer({
549
- rootDir,
550
- logger,
551
- workbenchPath: workbench.config?.route,
552
- port: viteInternalPort,
553
- backendPort: bunBackendPort,
554
- routePaths,
555
- liveHostname: devmode?.hostname,
556
- });
557
- viteServer = viteResult.server;
558
- vitePort = viteResult.port;
559
-
560
- // Register Vite server with process manager
561
- procManager.registerServer({
562
- id: 'vite',
563
- server: viteServer,
564
- description: 'Vite dev server (frontend assets)',
565
- port: vitePort,
566
- });
567
-
568
- // Update dev lock with actual Vite port
569
- await devLock.updatePorts({ vite: vitePort });
570
-
571
- logger.debug(
572
- `Vite dev server running on port ${vitePort} (internal, proxying backend on port ${bunBackendPort})`
573
- );
574
- } catch (error) {
575
- tui.error(`Failed to start Vite dev server: ${error}`);
576
- await procManager.cleanup('vite startup failure');
577
- await devLock.release();
578
- originalExit(1);
579
- return;
580
- }
581
-
582
- // Start the front-door TCP proxy on the user-facing port.
583
- // Routes WebSocket upgrades (for /api/*, /_agentuity/*) directly to Bun
584
- // and everything else (HTTP, HMR WebSocket) to Vite.
585
- // This works around Bun's broken node:http upgrade socket implementation.
586
- let frontDoorServer: import('node:net').Server | null = null;
587
- try {
588
- const { startWsProxy } = await import('../build/vite/ws-proxy');
589
- frontDoorServer = await startWsProxy({
590
- port: opts.port,
591
- vitePort,
592
- backendPort: bunBackendPort,
593
- routePaths,
594
- logger,
595
- });
589
+ // Resolve the actual Vite port BEFORE starting Bun so that env vars
590
+ // like AGENTUITY_BASE_URL contain the correct port. The runtime's
591
+ // CORS trusted-origins are built once at startup, so a late update
592
+ // would leave the backend with stale origins.
593
+ const { findAvailablePort } = await import('../build/vite/vite-asset-server');
594
+ const vitePort = await findAvailablePort(viteInternalPort, '127.0.0.1');
596
595
 
597
- // Register front-door proxy with process manager
598
- procManager.registerServer({
599
- id: 'front-door-proxy',
600
- server: {
601
- close: () => {
602
- frontDoorServer?.close();
603
- },
604
- },
605
- description: 'Front-door TCP proxy (WS routing)',
606
- port: opts.port,
607
- });
608
-
609
- logger.debug(
610
- `Front-door proxy on port ${opts.port} (Vite:${vitePort}, Bun:${bunBackendPort})`
596
+ if (vitePort !== viteInternalPort) {
597
+ logger.info(
598
+ `Port ${viteInternalPort} is in use, using port ${vitePort} for Vite dev server`
611
599
  );
612
- } catch (error) {
613
- tui.error(`Failed to start front-door proxy: ${error}`);
614
- await procManager.cleanup('front-door proxy startup failure');
615
- await devLock.release();
616
- originalExit(1);
617
- return;
618
600
  }
619
601
 
620
- // --- State for long-running processes ---
621
- let gravityProcess: ProcessLike | null = null;
622
- let gravityHeartbeatInterval: ReturnType<typeof setInterval> | null = null;
623
- let stdinListenerRegistered = false;
624
- let stdinDataHandler: ((data: Buffer | string) => void) | null = null;
625
- let shutdownRequested = false;
602
+ // Separate guard flags:
603
+ // - cleanupStarted: prevents double-entry into cleanup()
604
+ // - shutdownRequested: breaks the main wait loop (set inside cleanup
605
+ // AFTER the cleanupStarted guard passes, so the loop resolves only
606
+ // once cleanup has actually started running)
607
+ let cleanupStarted = false;
626
608
 
627
609
  /**
628
610
  * Centralized cleanup function for all resources.
629
611
  * Uses the process manager for tracked servers/processes.
630
612
  */
631
613
  const cleanup = async (exitAfter = false, exitCode = 0, silent = false) => {
632
- if (shutdownRequested) return;
614
+ if (cleanupStarted) return;
615
+ cleanupStarted = true;
633
616
  shutdownRequested = true;
634
617
 
635
618
  if (!silent) {
@@ -647,7 +630,7 @@ export const command = createCommand({
647
630
 
648
631
  // Additional cleanup for non-tracked resources
649
632
  await devLock.release();
650
- await killLingeringGravityProcesses(logger);
633
+ await killLingeringGravityProcesses(logger, project?.projectId);
651
634
 
652
635
  if (exitAfter) {
653
636
  if (stdinListenerRegistered && process.stdin.isTTY) {
@@ -673,7 +656,6 @@ export const command = createCommand({
673
656
  if (exitingFromSignal) return;
674
657
  exitingFromSignal = true;
675
658
  if (reason) logger.debug('DevMode terminating (%d): %s', code, reason);
676
- shutdownRequested = true;
677
659
  cleanup(true, code).catch(() => originalExit(1));
678
660
  };
679
661
 
@@ -693,21 +675,16 @@ export const command = createCommand({
693
675
  );
694
676
  });
695
677
  process.on('exit', () => {
696
- if (gravityProcess?.exitCode === null) {
697
- try {
698
- gravityProcess.kill('SIGKILL');
699
- } catch {
700
- // Ignore
701
- }
702
- }
703
- if (viteServer) {
704
- try {
705
- viteServer.close();
706
- } catch {
707
- // Ignore
708
- }
709
- }
710
- killBunSubprocess(logger);
678
+ // Last-resort synchronous cleanup. Only runs aggressive SIGKILL
679
+ // if the async cleanup() hasn't already handled everything.
680
+ // This prevents the race where both cleanup paths try to kill
681
+ // the same processes.
682
+ procManager.forceKillAllSync();
683
+
684
+ // SIGKILL the Bun subprocess tree as a final safety net.
685
+ // forceKill=true ensures we use SIGKILL (no time for graceful
686
+ // shutdown in an exit handler) and target the process group.
687
+ killBunSubprocess(logger, true);
711
688
  releaseLockSync(rootDir);
712
689
  });
713
690
 
@@ -861,6 +838,10 @@ export const command = createCommand({
861
838
  // Note: AGENTUITY_SDK_KEY, NODE_ENV, AGENTUITY_ENV, and
862
839
  // AGENTUITY_TRANSPORT_URL are already set in Step 0b (before
863
840
  // agent discovery) to support gateway env patching.
841
+ //
842
+ // vitePort is pre-resolved (via findAvailablePort) before Bun
843
+ // starts, so env vars like AGENTUITY_BASE_URL are correct when
844
+ // the runtime builds its CORS trusted-origin set.
864
845
 
865
846
  process.env.AGENTUITY_SDK_DEV_MODE = 'true';
866
847
  process.env.AGENTUITY_RUNTIME = 'yes';
@@ -910,29 +891,141 @@ export const command = createCommand({
910
891
  inspect: opts.inspect,
911
892
  inspectWait: opts.inspectWait,
912
893
  inspectBrk: opts.inspectBrk,
894
+ // Register the subprocess BEFORE the readiness wait so a SIGINT
895
+ // during startup can clean it up via procManager. Without this,
896
+ // the only safety net is the synchronous process.on('exit')
897
+ // handler, which is fragile and runs too late on some signals.
898
+ onSpawn: (proc) => {
899
+ procManager.registerProcess({
900
+ id: 'bun-backend',
901
+ process: proc,
902
+ description: 'Bun backend server (--hot)',
903
+ port: bunBackendPort,
904
+ critical: true,
905
+ });
906
+ },
913
907
  });
908
+ } catch (error) {
909
+ tui.error(`Failed to start Bun backend server: ${error}`);
910
+ await cleanup(true, 1, true);
911
+ return;
912
+ }
914
913
 
915
- // Register Bun subprocess with process manager
916
- // The subprocess is stored in globalThis.__AGENTUITY_BUN_SUBPROCESS__
917
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
918
- const bunSubprocess = (globalThis as any).__AGENTUITY_BUN_SUBPROCESS__ as ProcessLike;
919
- if (bunSubprocess) {
920
- procManager.registerProcess({
921
- id: 'bun-backend',
922
- process: bunSubprocess,
923
- description: 'Bun backend server (--hot)',
924
- port: bunBackendPort,
925
- critical: true,
926
- });
914
+ // ================================================================
915
+ // Step 4: Start Vite asset server (frontend + API proxy)
916
+ // ================================================================
917
+ // Vite starts AFTER the Bun backend so that its proxy is ready
918
+ // to forward API requests immediately — no ECONNREFUSED race.
919
+
920
+ try {
921
+ logger.debug('Starting Vite dev server (internal port %d)...', viteInternalPort);
922
+ const viteResult = await startViteAssetServer({
923
+ rootDir,
924
+ logger,
925
+ workbenchPath: workbench.config?.route,
926
+ port: vitePort,
927
+ backendPort: bunBackendPort,
928
+ routePaths,
929
+ liveHostname: devmode?.hostname,
930
+ });
931
+ viteServer = viteResult.server;
932
+
933
+ // Verify Vite used the port we pre-resolved (should always match
934
+ // since strictPort:true is set and we already confirmed availability).
935
+ if (viteResult.port !== vitePort) {
936
+ logger.warn(
937
+ `Vite started on port ${viteResult.port} instead of expected ${vitePort} — env vars may be incorrect`
938
+ );
927
939
  }
940
+
941
+ // Register Vite server with process manager.
942
+ // We wrap close() to first force-drop keep-alive HTTP/HMR sockets via
943
+ // httpServer.closeAllConnections() (Node 18.2+). Without this, idle
944
+ // keep-alive connections keep the listener bound until they time out,
945
+ // which leaves the Vite port reserved for several seconds after the
946
+ // CLI exits. We also extend the close timeout (Vite's chokidar +
947
+ // HMR teardown can exceed 1s under load).
948
+ const viteForCleanup = viteServer;
949
+ procManager.registerServer({
950
+ id: 'vite',
951
+ server: {
952
+ close: async () => {
953
+ try {
954
+ const http = (
955
+ viteForCleanup as unknown as {
956
+ httpServer?: {
957
+ closeAllConnections?: () => void;
958
+ closeIdleConnections?: () => void;
959
+ };
960
+ }
961
+ ).httpServer;
962
+ http?.closeIdleConnections?.();
963
+ http?.closeAllConnections?.();
964
+ } catch {
965
+ // Best effort — these methods are runtime-dependent.
966
+ }
967
+ await viteForCleanup.close();
968
+ },
969
+ },
970
+ description: 'Vite dev server (frontend assets)',
971
+ port: vitePort,
972
+ closeTimeoutMs: 3000,
973
+ });
974
+
975
+ // Update dev lock with actual Vite port
976
+ await devLock.updatePorts({ vite: vitePort });
977
+
978
+ logger.debug(
979
+ `Vite dev server running on port ${vitePort} (internal, proxying backend on port ${bunBackendPort})`
980
+ );
928
981
  } catch (error) {
929
- tui.error(`Failed to start Bun backend server: ${error}`);
982
+ tui.error(`Failed to start Vite dev server: ${error}`);
930
983
  await cleanup(true, 1, true);
931
984
  return;
932
985
  }
933
986
 
934
987
  // ================================================================
935
- // Step 4: Start gravity tunnel (if public URL enabled)
988
+ // Step 5: Start front-door TCP proxy (user-facing port)
989
+ // ================================================================
990
+ // Routes WebSocket upgrades (for /api/*, /_agentuity/*) directly to Bun
991
+ // and everything else (HTTP, HMR WebSocket) to Vite.
992
+ // This works around Bun's broken node:http upgrade socket implementation.
993
+
994
+ try {
995
+ const { startWsProxy } = await import('../build/vite/ws-proxy');
996
+ frontDoorServer = await startWsProxy({
997
+ port: opts.port,
998
+ vitePort,
999
+ backendPort: bunBackendPort,
1000
+ routePaths,
1001
+ logger,
1002
+ });
1003
+
1004
+ // Register front-door proxy with process manager. Use closeAll()
1005
+ // (returns a Promise) so cleanup actually waits for the listener
1006
+ // to release the user-facing port instead of fire-and-forgetting.
1007
+ // closeAll() also destroys live piped sockets (HMR WS, backend WS)
1008
+ // so the listener doesn't sit waiting for clients to disconnect.
1009
+ procManager.registerServer({
1010
+ id: 'front-door-proxy',
1011
+ server: {
1012
+ close: () => frontDoorServer?.closeAll() ?? Promise.resolve(),
1013
+ },
1014
+ description: 'Front-door TCP proxy (WS routing)',
1015
+ port: opts.port,
1016
+ });
1017
+
1018
+ logger.debug(
1019
+ `Front-door proxy on port ${opts.port} (Vite:${vitePort}, Bun:${bunBackendPort})`
1020
+ );
1021
+ } catch (error) {
1022
+ tui.error(`Failed to start front-door proxy: ${error}`);
1023
+ await cleanup(true, 1, true);
1024
+ return;
1025
+ }
1026
+
1027
+ // ================================================================
1028
+ // Step 6: Start gravity tunnel (if public URL enabled)
936
1029
  // ================================================================
937
1030
 
938
1031
  if (gravityBin && gravityURL && devmode && project) {
@@ -945,6 +1038,15 @@ export const command = createCommand({
945
1038
  return;
946
1039
  }
947
1040
 
1041
+ // Gravity must target the user-facing front-door proxy port.
1042
+ // The front-door proxy (ws-proxy) is the only server that correctly
1043
+ // routes public WebSocket upgrades to /api/* through to the Bun backend.
1044
+ // We read the actual bound port from the front-door server to avoid any
1045
+ // mismatch with opts.port (e.g. if the port was overridden or shifted).
1046
+ const frontDoorPort =
1047
+ (frontDoorServer?.address() as import('node:net').AddressInfo | null)?.port ??
1048
+ opts.port;
1049
+
948
1050
  try {
949
1051
  gravityProcess = Bun.spawn(
950
1052
  [
@@ -952,7 +1054,7 @@ export const command = createCommand({
952
1054
  '--endpoint-id',
953
1055
  devmode.id,
954
1056
  '--port',
955
- vitePort.toString(),
1057
+ frontDoorPort.toString(),
956
1058
  '--url',
957
1059
  gravityURL,
958
1060
  '--log-level',
@@ -969,10 +1071,24 @@ export const command = createCommand({
969
1071
  cwd: rootDir,
970
1072
  stdout: 'pipe',
971
1073
  stderr: 'pipe',
972
- detached: false,
1074
+ // Make the child a process-group leader so process.kill(-pid, signal)
1075
+ // from procManager.killProcessTree() actually reaches the whole tree
1076
+ // (otherwise it fails with EPERM and falls back to a direct PID kill
1077
+ // that leaves any grandchildren running). We intentionally do NOT call
1078
+ // .unref() — we still want the parent to track the child's lifecycle
1079
+ // and drive cleanup on Ctrl-C / shutdown.
1080
+ detached: true,
1081
+ // Pass a clean env without PORT to prevent the inherited
1082
+ // PORT (set to bunBackendPort) from leaking into gravity.
1083
+ env: {
1084
+ ...process.env,
1085
+ PORT: undefined,
1086
+ },
973
1087
  }
974
1088
  );
975
1089
 
1090
+ logger.debug('Gravity tunnel targeting front-door proxy on port %d', frontDoorPort);
1091
+
976
1092
  const gravityPid = (gravityProcess as { pid?: number }).pid;
977
1093
  if (gravityPid) {
978
1094
  await devLock.registerChild({
@@ -1046,7 +1162,7 @@ export const command = createCommand({
1046
1162
  }
1047
1163
 
1048
1164
  // ================================================================
1049
- // Step 5: Keyboard shortcuts + wait for shutdown
1165
+ // Step 7: Keyboard shortcuts + wait for shutdown
1050
1166
  // ================================================================
1051
1167
 
1052
1168
  if (interactive && process.stdin.isTTY && process.stdout.isTTY) {
@@ -1069,7 +1185,6 @@ export const command = createCommand({
1069
1185
  process.stdin.removeListener('data', stdinDataHandler);
1070
1186
  stdinDataHandler = null;
1071
1187
  }
1072
- shutdownRequested = true;
1073
1188
  cleanup(true, 0).catch(() => originalExit(1));
1074
1189
  return;
1075
1190
  }
@@ -1109,7 +1224,7 @@ export const command = createCommand({
1109
1224
  } finally {
1110
1225
  /* brute force clean up */
1111
1226
  await devLock.release();
1112
- await killLingeringGravityProcesses(logger);
1227
+ await killLingeringGravityProcesses(logger, project?.projectId);
1113
1228
  releaseLockSync(rootDir);
1114
1229
  }
1115
1230
  },