@agentuity/cli 2.0.11 → 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 (220) 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/bun-dev-server.d.ts +20 -0
  5. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
  6. package/dist/cmd/build/vite/bun-dev-server.js +62 -4
  7. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  8. package/dist/cmd/build/vite/index.d.ts +0 -1
  9. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  10. package/dist/cmd/build/vite/index.js +0 -1
  11. package/dist/cmd/build/vite/index.js.map +1 -1
  12. package/dist/cmd/build/vite/static-renderer.d.ts +17 -0
  13. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  14. package/dist/cmd/build/vite/static-renderer.js +18 -6
  15. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  16. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  17. package/dist/cmd/build/vite/vite-asset-server-config.js +34 -27
  18. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  19. package/dist/cmd/build/vite/vite-asset-server.d.ts +9 -0
  20. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  21. package/dist/cmd/build/vite/vite-asset-server.js +5 -1
  22. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  23. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  24. package/dist/cmd/build/vite/vite-builder.js +12 -1
  25. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  26. package/dist/cmd/build/vite/ws-proxy.d.ts +15 -1
  27. package/dist/cmd/build/vite/ws-proxy.d.ts.map +1 -1
  28. package/dist/cmd/build/vite/ws-proxy.js +33 -0
  29. package/dist/cmd/build/vite/ws-proxy.js.map +1 -1
  30. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  31. package/dist/cmd/cloud/deploy.js +98 -39
  32. package/dist/cmd/cloud/deploy.js.map +1 -1
  33. package/dist/cmd/cloud/sandbox/checkpoint/create.d.ts.map +1 -1
  34. package/dist/cmd/cloud/sandbox/checkpoint/create.js +3 -4
  35. package/dist/cmd/cloud/sandbox/checkpoint/create.js.map +1 -1
  36. package/dist/cmd/cloud/sandbox/checkpoint/delete.d.ts.map +1 -1
  37. package/dist/cmd/cloud/sandbox/checkpoint/delete.js +3 -4
  38. package/dist/cmd/cloud/sandbox/checkpoint/delete.js.map +1 -1
  39. package/dist/cmd/cloud/sandbox/checkpoint/list.d.ts.map +1 -1
  40. package/dist/cmd/cloud/sandbox/checkpoint/list.js +3 -4
  41. package/dist/cmd/cloud/sandbox/checkpoint/list.js.map +1 -1
  42. package/dist/cmd/cloud/sandbox/checkpoint/restore.d.ts.map +1 -1
  43. package/dist/cmd/cloud/sandbox/checkpoint/restore.js +3 -4
  44. package/dist/cmd/cloud/sandbox/checkpoint/restore.js.map +1 -1
  45. package/dist/cmd/cloud/sandbox/create.d.ts.map +1 -1
  46. package/dist/cmd/cloud/sandbox/create.js +13 -4
  47. package/dist/cmd/cloud/sandbox/create.js.map +1 -1
  48. package/dist/cmd/cloud/sandbox/delete.d.ts.map +1 -1
  49. package/dist/cmd/cloud/sandbox/delete.js +3 -4
  50. package/dist/cmd/cloud/sandbox/delete.js.map +1 -1
  51. package/dist/cmd/cloud/sandbox/env.d.ts.map +1 -1
  52. package/dist/cmd/cloud/sandbox/env.js +3 -5
  53. package/dist/cmd/cloud/sandbox/env.js.map +1 -1
  54. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  55. package/dist/cmd/cloud/sandbox/exec.js +114 -41
  56. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  57. package/dist/cmd/cloud/sandbox/execution/list.d.ts.map +1 -1
  58. package/dist/cmd/cloud/sandbox/execution/list.js +3 -5
  59. package/dist/cmd/cloud/sandbox/execution/list.js.map +1 -1
  60. package/dist/cmd/cloud/sandbox/fs/cp.d.ts.map +1 -1
  61. package/dist/cmd/cloud/sandbox/fs/cp.js +61 -113
  62. package/dist/cmd/cloud/sandbox/fs/cp.js.map +1 -1
  63. package/dist/cmd/cloud/sandbox/fs/download.d.ts.map +1 -1
  64. package/dist/cmd/cloud/sandbox/fs/download.js +11 -22
  65. package/dist/cmd/cloud/sandbox/fs/download.js.map +1 -1
  66. package/dist/cmd/cloud/sandbox/fs/ls.d.ts.map +1 -1
  67. package/dist/cmd/cloud/sandbox/fs/ls.js +3 -5
  68. package/dist/cmd/cloud/sandbox/fs/ls.js.map +1 -1
  69. package/dist/cmd/cloud/sandbox/fs/mkdir.d.ts.map +1 -1
  70. package/dist/cmd/cloud/sandbox/fs/mkdir.js +3 -5
  71. package/dist/cmd/cloud/sandbox/fs/mkdir.js.map +1 -1
  72. package/dist/cmd/cloud/sandbox/fs/rm.d.ts.map +1 -1
  73. package/dist/cmd/cloud/sandbox/fs/rm.js +3 -5
  74. package/dist/cmd/cloud/sandbox/fs/rm.js.map +1 -1
  75. package/dist/cmd/cloud/sandbox/fs/rmdir.d.ts.map +1 -1
  76. package/dist/cmd/cloud/sandbox/fs/rmdir.js +3 -5
  77. package/dist/cmd/cloud/sandbox/fs/rmdir.js.map +1 -1
  78. package/dist/cmd/cloud/sandbox/fs/upload.d.ts.map +1 -1
  79. package/dist/cmd/cloud/sandbox/fs/upload.js +7 -8
  80. package/dist/cmd/cloud/sandbox/fs/upload.js.map +1 -1
  81. package/dist/cmd/cloud/sandbox/get.d.ts.map +1 -1
  82. package/dist/cmd/cloud/sandbox/get.js +21 -7
  83. package/dist/cmd/cloud/sandbox/get.js.map +1 -1
  84. package/dist/cmd/cloud/sandbox/job/create.d.ts.map +1 -1
  85. package/dist/cmd/cloud/sandbox/job/create.js +3 -4
  86. package/dist/cmd/cloud/sandbox/job/create.js.map +1 -1
  87. package/dist/cmd/cloud/sandbox/job/destroy.d.ts.map +1 -1
  88. package/dist/cmd/cloud/sandbox/job/destroy.js +3 -4
  89. package/dist/cmd/cloud/sandbox/job/destroy.js.map +1 -1
  90. package/dist/cmd/cloud/sandbox/job/get.d.ts.map +1 -1
  91. package/dist/cmd/cloud/sandbox/job/get.js +3 -4
  92. package/dist/cmd/cloud/sandbox/job/get.js.map +1 -1
  93. package/dist/cmd/cloud/sandbox/job/list.d.ts.map +1 -1
  94. package/dist/cmd/cloud/sandbox/job/list.js +3 -4
  95. package/dist/cmd/cloud/sandbox/job/list.js.map +1 -1
  96. package/dist/cmd/cloud/sandbox/job/logs.d.ts.map +1 -1
  97. package/dist/cmd/cloud/sandbox/job/logs.js +4 -4
  98. package/dist/cmd/cloud/sandbox/job/logs.js.map +1 -1
  99. package/dist/cmd/cloud/sandbox/pause.d.ts.map +1 -1
  100. package/dist/cmd/cloud/sandbox/pause.js +21 -5
  101. package/dist/cmd/cloud/sandbox/pause.js.map +1 -1
  102. package/dist/cmd/cloud/sandbox/resume.d.ts.map +1 -1
  103. package/dist/cmd/cloud/sandbox/resume.js +3 -4
  104. package/dist/cmd/cloud/sandbox/resume.js.map +1 -1
  105. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  106. package/dist/cmd/cloud/sandbox/run.js +36 -7
  107. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  108. package/dist/cmd/cloud/sandbox/util.d.ts +19 -0
  109. package/dist/cmd/cloud/sandbox/util.d.ts.map +1 -1
  110. package/dist/cmd/cloud/sandbox/util.js +40 -2
  111. package/dist/cmd/cloud/sandbox/util.js.map +1 -1
  112. package/dist/cmd/coder/create.js +7 -7
  113. package/dist/cmd/coder/create.js.map +1 -1
  114. package/dist/cmd/coder/start.d.ts.map +1 -1
  115. package/dist/cmd/coder/start.js +3 -0
  116. package/dist/cmd/coder/start.js.map +1 -1
  117. package/dist/cmd/coder/tui-init.js +1 -1
  118. package/dist/cmd/coder/tui-init.js.map +1 -1
  119. package/dist/cmd/coder/update.js +8 -8
  120. package/dist/cmd/coder/update.js.map +1 -1
  121. package/dist/cmd/coder/workspace/create.d.ts.map +1 -1
  122. package/dist/cmd/coder/workspace/create.js +49 -21
  123. package/dist/cmd/coder/workspace/create.js.map +1 -1
  124. package/dist/cmd/coder/workspace/index.d.ts.map +1 -1
  125. package/dist/cmd/coder/workspace/index.js +1 -1
  126. package/dist/cmd/coder/workspace/index.js.map +1 -1
  127. package/dist/cmd/dev/dev-lock.d.ts.map +1 -1
  128. package/dist/cmd/dev/dev-lock.js +43 -17
  129. package/dist/cmd/dev/dev-lock.js.map +1 -1
  130. package/dist/cmd/dev/index.d.ts.map +1 -1
  131. package/dist/cmd/dev/index.js +211 -125
  132. package/dist/cmd/dev/index.js.map +1 -1
  133. package/dist/cmd/dev/process-manager.d.ts +41 -1
  134. package/dist/cmd/dev/process-manager.d.ts.map +1 -1
  135. package/dist/cmd/dev/process-manager.js +160 -31
  136. package/dist/cmd/dev/process-manager.js.map +1 -1
  137. package/dist/cmd/project/create.d.ts.map +1 -1
  138. package/dist/cmd/project/create.js +0 -2
  139. package/dist/cmd/project/create.js.map +1 -1
  140. package/dist/cmd/project/index.d.ts.map +1 -1
  141. package/dist/cmd/project/index.js +0 -3
  142. package/dist/cmd/project/index.js.map +1 -1
  143. package/dist/cmd/project/template-flow.d.ts +0 -1
  144. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  145. package/dist/cmd/project/template-flow.js +1 -124
  146. package/dist/cmd/project/template-flow.js.map +1 -1
  147. package/package.json +7 -7
  148. package/src/cache/resource-region.ts +68 -44
  149. package/src/cmd/ai/prompt/web.md +43 -17
  150. package/src/cmd/build/vite/bun-dev-server.ts +92 -6
  151. package/src/cmd/build/vite/index.ts +0 -1
  152. package/src/cmd/build/vite/static-renderer.ts +18 -7
  153. package/src/cmd/build/vite/vite-asset-server-config.ts +37 -27
  154. package/src/cmd/build/vite/vite-asset-server.ts +5 -1
  155. package/src/cmd/build/vite/vite-builder.ts +12 -1
  156. package/src/cmd/build/vite/ws-proxy.ts +52 -3
  157. package/src/cmd/cloud/deploy.ts +117 -49
  158. package/src/cmd/cloud/sandbox/checkpoint/create.ts +10 -4
  159. package/src/cmd/cloud/sandbox/checkpoint/delete.ts +10 -4
  160. package/src/cmd/cloud/sandbox/checkpoint/list.ts +10 -4
  161. package/src/cmd/cloud/sandbox/checkpoint/restore.ts +10 -4
  162. package/src/cmd/cloud/sandbox/create.ts +14 -4
  163. package/src/cmd/cloud/sandbox/delete.ts +10 -4
  164. package/src/cmd/cloud/sandbox/env.ts +10 -5
  165. package/src/cmd/cloud/sandbox/exec.ts +157 -42
  166. package/src/cmd/cloud/sandbox/execution/list.ts +10 -5
  167. package/src/cmd/cloud/sandbox/fs/cp.ts +94 -126
  168. package/src/cmd/cloud/sandbox/fs/download.ts +18 -25
  169. package/src/cmd/cloud/sandbox/fs/ls.ts +10 -5
  170. package/src/cmd/cloud/sandbox/fs/mkdir.ts +10 -5
  171. package/src/cmd/cloud/sandbox/fs/rm.ts +10 -5
  172. package/src/cmd/cloud/sandbox/fs/rmdir.ts +10 -5
  173. package/src/cmd/cloud/sandbox/fs/upload.ts +14 -8
  174. package/src/cmd/cloud/sandbox/get.ts +28 -7
  175. package/src/cmd/cloud/sandbox/job/create.ts +10 -4
  176. package/src/cmd/cloud/sandbox/job/destroy.ts +10 -4
  177. package/src/cmd/cloud/sandbox/job/get.ts +10 -4
  178. package/src/cmd/cloud/sandbox/job/list.ts +10 -4
  179. package/src/cmd/cloud/sandbox/job/logs.ts +11 -4
  180. package/src/cmd/cloud/sandbox/pause.ts +31 -5
  181. package/src/cmd/cloud/sandbox/resume.ts +10 -4
  182. package/src/cmd/cloud/sandbox/run.ts +49 -11
  183. package/src/cmd/cloud/sandbox/util.ts +63 -2
  184. package/src/cmd/coder/create.ts +8 -8
  185. package/src/cmd/coder/start.ts +3 -0
  186. package/src/cmd/coder/tui-init.ts +1 -1
  187. package/src/cmd/coder/update.ts +7 -7
  188. package/src/cmd/coder/workspace/create.ts +77 -26
  189. package/src/cmd/coder/workspace/index.ts +3 -1
  190. package/src/cmd/dev/dev-lock.ts +50 -16
  191. package/src/cmd/dev/index.ts +249 -134
  192. package/src/cmd/dev/process-manager.ts +173 -33
  193. package/src/cmd/project/create.ts +0 -2
  194. package/src/cmd/project/index.ts +0 -3
  195. package/src/cmd/project/template-flow.ts +0 -147
  196. package/dist/cmd/build/vite/public-asset-path-plugin.d.ts +0 -45
  197. package/dist/cmd/build/vite/public-asset-path-plugin.d.ts.map +0 -1
  198. package/dist/cmd/build/vite/public-asset-path-plugin.js +0 -166
  199. package/dist/cmd/build/vite/public-asset-path-plugin.js.map +0 -1
  200. package/dist/cmd/project/auth/generate.d.ts +0 -5
  201. package/dist/cmd/project/auth/generate.d.ts.map +0 -1
  202. package/dist/cmd/project/auth/generate.js +0 -102
  203. package/dist/cmd/project/auth/generate.js.map +0 -1
  204. package/dist/cmd/project/auth/index.d.ts +0 -2
  205. package/dist/cmd/project/auth/index.d.ts.map +0 -1
  206. package/dist/cmd/project/auth/index.js +0 -21
  207. package/dist/cmd/project/auth/index.js.map +0 -1
  208. package/dist/cmd/project/auth/init.d.ts +0 -2
  209. package/dist/cmd/project/auth/init.d.ts.map +0 -1
  210. package/dist/cmd/project/auth/init.js +0 -213
  211. package/dist/cmd/project/auth/init.js.map +0 -1
  212. package/dist/cmd/project/auth/shared.d.ts +0 -93
  213. package/dist/cmd/project/auth/shared.d.ts.map +0 -1
  214. package/dist/cmd/project/auth/shared.js +0 -475
  215. package/dist/cmd/project/auth/shared.js.map +0 -1
  216. package/src/cmd/build/vite/public-asset-path-plugin.ts +0 -209
  217. package/src/cmd/project/auth/generate.ts +0 -116
  218. package/src/cmd/project/auth/index.ts +0 -21
  219. package/src/cmd/project/auth/init.ts +0 -256
  220. package/src/cmd/project/auth/shared.ts +0 -591
@@ -22,6 +22,37 @@ export interface GenerateAssetServerConfigOptions {
22
22
  liveHostname?: string;
23
23
  }
24
24
 
25
+ /**
26
+ * Shared proxy configuration for backend routes.
27
+ *
28
+ * Includes `configure` callback that gracefully handles ECONNREFUSED errors
29
+ * when the Bun backend isn't ready yet (startup race condition or brief
30
+ * disconnect during --hot reload). Instead of logging noisy errors, the
31
+ * proxy returns 503 Service Unavailable with a retry hint.
32
+ */
33
+ function backendProxyOptions(backendPort: number) {
34
+ return {
35
+ target: `http://127.0.0.1:${backendPort}`,
36
+ changeOrigin: true,
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ configure: (proxy: any, _options: any) => {
39
+ proxy.on('error', (err: Error & { code?: string }, _req: any, res: any) => {
40
+ if (err.code === 'ECONNREFUSED' && res && !res.writableEnded) {
41
+ res.statusCode = 503;
42
+ res.setHeader('Content-Type', 'application/json');
43
+ res.end(
44
+ JSON.stringify({
45
+ error: 'Backend unavailable',
46
+ message: 'The Bun backend is not ready yet. Retrying shortly...',
47
+ retry: true,
48
+ })
49
+ );
50
+ }
51
+ });
52
+ },
53
+ };
54
+ }
55
+
25
56
  /**
26
57
  * Vite plugin that injects analytics scripts in dev mode.
27
58
  *
@@ -95,7 +126,7 @@ function spaFallbackPlugin(rootDir: string, routePaths: string[], workbenchPath?
95
126
  const isDocumentRequest = secFetchDest === 'document' || accept.includes('text/html');
96
127
 
97
128
  // Skip file requests (have an extension)
98
- if (pathname !== '/' && /\.[a-zA-Z0-9]+$/.test(pathname)) return next();
129
+ if (pathname !== '/' && /\.[\w-]+$/.test(pathname)) return next();
99
130
 
100
131
  // For non-document requests, only allow root path fallback.
101
132
  // (e.g. don't turn module/script fetches into HTML accidentally)
@@ -241,37 +272,19 @@ export async function generateAssetServerConfig(
241
272
  proxy: {
242
273
  // User-defined route mounts (from createApp({ router }))
243
274
  ...Object.fromEntries(
244
- routePaths.map((routePath) => [
245
- routePath,
246
- {
247
- target: `http://127.0.0.1:${backendPort}`,
248
- changeOrigin: true,
249
- },
250
- ])
275
+ routePaths.map((routePath) => [routePath, backendProxyOptions(backendPort)])
251
276
  ),
252
277
  // Agentuity system routes (workbench API, health, analytics, etc.)
253
- '/_agentuity': {
254
- target: `http://127.0.0.1:${backendPort}`,
255
- changeOrigin: true,
256
- },
278
+ '/_agentuity': backendProxyOptions(backendPort),
257
279
  // Workbench UI route (served by Bun, references /@fs/* paths handled by Vite)
258
280
  ...(workbenchPath
259
281
  ? {
260
- [workbenchPath]: {
261
- target: `http://127.0.0.1:${backendPort}`,
262
- changeOrigin: true,
263
- },
282
+ [workbenchPath]: backendProxyOptions(backendPort),
264
283
  }
265
284
  : {}),
266
285
  // Legacy health check routes
267
- '/_health': {
268
- target: `http://127.0.0.1:${backendPort}`,
269
- changeOrigin: true,
270
- },
271
- '/_idle': {
272
- target: `http://127.0.0.1:${backendPort}`,
273
- changeOrigin: true,
274
- },
286
+ '/_health': backendProxyOptions(backendPort),
287
+ '/_idle': backendProxyOptions(backendPort),
275
288
  },
276
289
 
277
290
  // HMR works natively — Vite is the primary server, no proxy needed
@@ -296,13 +309,10 @@ export async function generateAssetServerConfig(
296
309
  // Agentuity-specific plugins (Vite loads user plugins from vite.config.ts automatically)
297
310
  plugins: await (async () => {
298
311
  const { browserEnvPlugin } = await import('./browser-env-plugin');
299
- const { publicAssetPathPlugin } = await import('./public-asset-path-plugin');
300
312
 
301
313
  return [
302
314
  // Browser env plugin to map process.env to import.meta.env
303
315
  browserEnvPlugin(),
304
- // Warn about incorrect public asset paths in dev mode
305
- publicAssetPathPlugin({ warnInDev: true }),
306
316
  // Inject analytics scripts in dev HTML
307
317
  devAnalyticsPlugin(),
308
318
  // SPA fallback: serve src/web/index.html for navigation requests
@@ -50,8 +50,12 @@ function isPortAvailable(port: number, host: string): Promise<boolean> {
50
50
  /**
51
51
  * Find an available port starting from the preferred port.
52
52
  * Tries incrementing ports up to maxAttempts times.
53
+ *
54
+ * Exported so the dev command can pre-resolve the Vite port before
55
+ * starting the Bun backend (env vars like AGENTUITY_BASE_URL need
56
+ * the real port before Bun initializes CORS).
53
57
  */
54
- async function findAvailablePort(
58
+ export async function findAvailablePort(
55
59
  preferredPort: number,
56
60
  host: string = '127.0.0.1',
57
61
  maxAttempts: number = 20
@@ -227,6 +227,7 @@ import { join } from 'node:path';
227
227
  export default defineConfig({
228
228
  plugins: [react()],
229
229
  root: '.',
230
+ publicDir: 'src/web/public',
230
231
  build: {
231
232
  rollupOptions: {
232
233
  input: join(__dirname, 'src/web/index.html'),
@@ -244,17 +245,27 @@ export default defineConfig({
244
245
  ? `https://${options.region === 'local' ? 'localstack-static-assets.t3.storageapi.dev' : 'cdn.agentuity.com'}/${options.deploymentId}/client/`
245
246
  : undefined;
246
247
 
248
+ // Pass the user's vite.config.ts directly to the subprocess. We used to
249
+ // wrap it in an auto-generated `.agentuity/vite.client.config.ts` to
250
+ // merge a lint plugin in, but that wrapper added complexity (plugin-path
251
+ // resolution, a junk file in the deploy bundle) for a warning-only
252
+ // linter. If we need to inject plugins again, expose them as a published
253
+ // CLI export and have users add them in their own vite.config.ts.
247
254
  const args = [
248
255
  'bun',
249
256
  'x',
250
257
  'vite',
251
258
  'build',
259
+ '--config',
260
+ viteConfigPath,
252
261
  '--mode',
253
262
  buildMode,
254
263
  '--outDir',
255
264
  clientOutDir,
265
+ // `warn` surfaces Vite warnings (e.g. large-chunk notices) without
266
+ // adding its own info-level chatter.
256
267
  '--logLevel',
257
- 'error',
268
+ 'warn',
258
269
  '--clearScreen',
259
270
  'false',
260
271
  ];
@@ -33,7 +33,7 @@
33
33
  * ```
34
34
  */
35
35
 
36
- import { createServer, connect, type Server } from 'node:net';
36
+ import { createServer, connect, type Server, type Socket } from 'node:net';
37
37
  import type { Logger } from '../../../types';
38
38
 
39
39
  export interface WsProxyOptions {
@@ -48,18 +48,44 @@ export interface WsProxyOptions {
48
48
  logger: Logger;
49
49
  }
50
50
 
51
+ /**
52
+ * Front-door TCP proxy server.
53
+ *
54
+ * Extends `net.Server` with a `closeAll()` method that destroys all live
55
+ * client + upstream sockets and waits for the listening socket to close.
56
+ * The native `Server.close()` only stops accepting new connections — long-
57
+ * lived piped sockets (Vite HMR WebSocket, backend WS) keep the listener
58
+ * bound until they close on their own. During dev-mode shutdown we want
59
+ * the user-facing port released immediately, so cleanup paths should
60
+ * prefer `closeAll()` over `close()`.
61
+ */
62
+ export interface WsProxyServer extends Server {
63
+ closeAll(): Promise<void>;
64
+ }
65
+
51
66
  /**
52
67
  * Start a front-door TCP proxy that routes WebSocket upgrades to the Bun
53
68
  * backend and everything else to Vite. Returns the `net.Server` instance.
54
69
  */
55
- export function startWsProxy(options: WsProxyOptions): Promise<Server> {
70
+ export function startWsProxy(options: WsProxyOptions): Promise<WsProxyServer> {
56
71
  const { port, vitePort, backendPort, routePaths, logger } = options;
57
72
 
58
73
  // Prefixes whose WebSocket upgrades go to Bun instead of Vite
59
74
  const wsPathPrefixes = ['/_agentuity', ...routePaths];
60
75
 
76
+ // Track every live socket pair so shutdown can drop them. Without this,
77
+ // `server.close()` waits for active connections to terminate by themselves
78
+ // (e.g. browser HMR WebSockets), which can keep the user-facing port bound
79
+ // for many seconds after dev mode exits.
80
+ const liveSockets = new Set<Socket>();
81
+ const trackSocket = (sock: Socket) => {
82
+ liveSockets.add(sock);
83
+ sock.once('close', () => liveSockets.delete(sock));
84
+ };
85
+
61
86
  return new Promise((resolve, reject) => {
62
87
  const server = createServer((socket) => {
88
+ trackSocket(socket);
63
89
  let handled = false;
64
90
 
65
91
  // Peek at the first chunk to decide where to route
@@ -87,6 +113,7 @@ export function startWsProxy(options: WsProxyOptions): Promise<Server> {
87
113
  }
88
114
 
89
115
  const target = connect(targetPort, '127.0.0.1');
116
+ trackSocket(target);
90
117
 
91
118
  target.on('connect', () => {
92
119
  target.write(firstChunk);
@@ -109,7 +136,29 @@ export function startWsProxy(options: WsProxyOptions): Promise<Server> {
109
136
  socket.on('error', () => {
110
137
  if (!handled) socket.destroy();
111
138
  });
112
- });
139
+ }) as WsProxyServer;
140
+
141
+ // Async close that destroys live sockets first, then waits for the
142
+ // listener to close. Idempotent: safe to call after the server has
143
+ // already been closed by other means.
144
+ server.closeAll = () => {
145
+ return new Promise<void>((resolveClose) => {
146
+ for (const sock of liveSockets) {
147
+ try {
148
+ if (!sock.destroyed) sock.destroy();
149
+ } catch {
150
+ // Best effort
151
+ }
152
+ }
153
+ liveSockets.clear();
154
+
155
+ if (!server.listening) {
156
+ resolveClose();
157
+ return;
158
+ }
159
+ server.close(() => resolveClose());
160
+ });
161
+ };
113
162
 
114
163
  server.on('error', reject);
115
164
 
@@ -95,6 +95,21 @@ const DeployResponseSchema = z.object({
95
95
  .describe('Deployment URLs'),
96
96
  });
97
97
 
98
+ /**
99
+ * Render the final "Uploaded N assets ..." line after a successful CDN
100
+ * upload pass. Reports both the original and on-the-wire totals when gzip
101
+ * compressed at least one file, so the user can see the real transfer cost.
102
+ */
103
+ function formatUploadSummary(count: number, rawBytes: number, transferredBytes: number): string {
104
+ const noun = tui.plural(count, 'asset', 'assets');
105
+ // When no compression happened (icons, fonts, binaries), rawBytes ===
106
+ // transferredBytes and the extra detail would just be noise.
107
+ if (transferredBytes === rawBytes) {
108
+ return `✓ Uploaded ${count} ${noun} (${tui.formatBytes(rawBytes)}) to CDN`;
109
+ }
110
+ return `✓ Uploaded ${count} ${noun} (${tui.formatBytes(transferredBytes)} on wire, ${tui.formatBytes(rawBytes)} raw) to CDN`;
111
+ }
112
+
98
113
  export const deploySubcommand = createSubcommand({
99
114
  name: 'deploy',
100
115
  description: 'Deploy project to the Agentuity Cloud',
@@ -848,8 +863,12 @@ export const deploySubcommand = createSubcommand({
848
863
  }
849
864
 
850
865
  progress(80);
851
- let bytes = 0;
852
- if (build?.assets) {
866
+ // Track both the raw on-disk size and the actual bytes we put on
867
+ // the wire. For gzipped assets these differ significantly; reporting
868
+ // the transferred total gives the user an honest view of CDN cost.
869
+ let rawBytes = 0;
870
+ let transferredBytes = 0;
871
+ if (build?.assets && build.assets.length > 0) {
853
872
  // Start CDN upload diagnostic
854
873
  const endCdnUploadDiagnostic = collector.startDiagnostic('cdn-upload');
855
874
  ctx.logger.trace(`Uploading ${build.assets.length} assets`);
@@ -863,31 +882,64 @@ export const deploySubcommand = createSubcommand({
863
882
  return stepError(errorMsg);
864
883
  }
865
884
 
866
- // Process assets in batches with limited concurrency
867
- const concurrency = Math.min(4, build.assets.length);
868
- for (let i = 0; i < build.assets.length; i += concurrency) {
869
- const batch = build.assets.slice(i, i + concurrency);
870
- const promises: Promise<Response>[] = [];
871
-
872
- for (const asset of batch) {
873
- const assetUrl = instructions.assets[asset.filename];
874
- if (!assetUrl) {
875
- return stepError(
876
- `server did not provide upload URL for asset "${asset.filename}"; upload aborted`
877
- );
885
+ // Pre-flight: every asset the build emitted must have a signed
886
+ // PUT URL from the backend. Failing up-front gives a single
887
+ // clear error instead of aborting mid-batch with partial uploads.
888
+ for (const asset of build.assets) {
889
+ if (!instructions.assets[asset.filename]) {
890
+ const errorMsg = `server did not provide upload URL for asset "${asset.filename}"; upload aborted`;
891
+ collector.addGeneralError('deploy', errorMsg, 'DEPLOY006');
892
+ if (opts.reportFile) {
893
+ await collector.forceWrite();
878
894
  }
895
+ return stepError(errorMsg);
896
+ }
897
+ rawBytes += asset.size;
898
+ }
879
899
 
880
- // Asset filename already includes the subdirectory (e.g., "client/assets/main-abc123.js")
900
+ // Track every temp gzip file we create so we can clean them up
901
+ // even if the deploy is aborted mid-upload (e.g. Ctrl+C).
902
+ const tempFiles = new Set<string>();
903
+ const cleanupTempFiles = () => {
904
+ for (const p of tempFiles) {
905
+ try {
906
+ unlinkSync(p);
907
+ } catch {
908
+ // ignore — may already be gone
909
+ }
910
+ }
911
+ tempFiles.clear();
912
+ };
913
+
914
+ // Hoist narrowed locals for use inside the async upload closure;
915
+ // TS doesn't propagate narrowings of `build` / `instructions`
916
+ // across async callbacks, and we've already null-checked both
917
+ // above.
918
+ const assets = build.assets;
919
+ const assetUrls = instructions.assets;
920
+
921
+ try {
922
+ // Upload each asset with bounded concurrency. gzip compression
923
+ // runs inside the per-asset task, so compressible files
924
+ // compress in parallel (up to `concurrency`) rather than
925
+ // serially — a meaningful win for builds with many JS/CSS
926
+ // chunks since gzip is single-threaded per call.
927
+ const uploadOne = async (
928
+ asset: (typeof assets)[number]
929
+ ): Promise<void> => {
930
+ const assetUrl = assetUrls[asset.filename]!;
931
+ // Asset filename already includes the subdirectory
932
+ // (e.g., "client/assets/main-abc123.js").
881
933
  const filePath = join(projectDir, '.agentuity', asset.filename);
882
934
 
883
935
  const headers: Record<string, string> = {
884
936
  'Content-Type': asset.contentType,
885
937
  };
886
938
 
887
- bytes += asset.size;
888
-
889
939
  let body: Blob;
890
940
  let gzTempPath: string | undefined;
941
+ let onWireSize = asset.size;
942
+
891
943
  if (asset.contentEncoding === 'gzip') {
892
944
  // Gzip to a temp file so Bun.file() can provide
893
945
  // Content-Length to S3 (streaming bodies use chunked
@@ -896,6 +948,7 @@ export const deploySubcommand = createSubcommand({
896
948
  tmpdir(),
897
949
  `agentuity-asset-${deployment.id}-${Date.now()}-${asset.filename.replace(/\//g, '_')}.gz`
898
950
  );
951
+ tempFiles.add(gzTempPath);
899
952
  await pipeline(
900
953
  createReadStream(filePath),
901
954
  createGzip(),
@@ -903,57 +956,72 @@ export const deploySubcommand = createSubcommand({
903
956
  );
904
957
  headers['Content-Encoding'] = 'gzip';
905
958
  body = Bun.file(gzTempPath);
906
- const compressedSize = body.size;
959
+ onWireSize = body.size;
907
960
  ctx.logger.trace(
908
- `Gzip compressed ${asset.filename} (${asset.size} -> ${compressedSize} bytes)`
961
+ `Gzip compressed ${asset.filename} (${asset.size} -> ${onWireSize} bytes)`
909
962
  );
910
963
  } else {
911
964
  body = Bun.file(filePath);
912
965
  }
913
966
 
914
- const assetGzTempPath = gzTempPath;
915
- promises.push(
916
- fetch(assetUrl, {
917
- method: 'PUT',
918
- headers,
919
- body,
920
- signal: stepCtx.signal,
921
- }).then((response) => {
922
- // Clean up temp gzip file after upload completes
923
- if (assetGzTempPath) {
924
- try {
925
- unlinkSync(assetGzTempPath);
926
- } catch {
927
- // ignore — file may already be cleaned up
928
- }
929
- }
930
- return response;
931
- })
932
- );
933
- }
967
+ const response = await fetch(assetUrl, {
968
+ method: 'PUT',
969
+ headers,
970
+ body,
971
+ signal: stepCtx.signal,
972
+ });
934
973
 
935
- const resps = await Promise.all(promises);
936
- for (const r of resps) {
937
- if (!r.ok) {
938
- const errorMsg = `error uploading asset: ${await r.text()}`;
939
- collector.addGeneralError('deploy', errorMsg, 'DEPLOY006');
940
- if (opts.reportFile) {
941
- await collector.forceWrite();
974
+ if (gzTempPath) {
975
+ try {
976
+ unlinkSync(gzTempPath);
977
+ } catch {
978
+ // ignore
942
979
  }
943
- return stepError(errorMsg);
980
+ tempFiles.delete(gzTempPath);
944
981
  }
982
+
983
+ if (!response.ok) {
984
+ throw new Error(
985
+ `asset "${asset.filename}" upload failed: ${response.status} ${await response.text()}`
986
+ );
987
+ }
988
+
989
+ transferredBytes += onWireSize;
990
+ };
991
+
992
+ const concurrency = Math.min(4, assets.length);
993
+ for (let i = 0; i < assets.length; i += concurrency) {
994
+ const batch = assets.slice(i, i + concurrency);
995
+ await Promise.all(batch.map(uploadOne));
945
996
  }
997
+ } catch (error) {
998
+ cleanupTempFiles();
999
+ const errorMsg = error instanceof Error ? error.message : String(error);
1000
+ collector.addGeneralError('deploy', errorMsg, 'DEPLOY006');
1001
+ if (opts.reportFile) {
1002
+ await collector.forceWrite();
1003
+ }
1004
+ return stepError(errorMsg);
946
1005
  }
947
- ctx.logger.trace('Asset uploads complete');
1006
+
1007
+ ctx.logger.trace(
1008
+ `Asset uploads complete: ${build.assets.length} files, raw=${rawBytes}B, on-wire=${transferredBytes}B`
1009
+ );
948
1010
  endCdnUploadDiagnostic();
949
1011
  progress(95);
1012
+ } else {
1013
+ ctx.logger.debug('No assets to upload to CDN');
950
1014
  }
951
1015
 
952
1016
  progress(100);
953
1017
  const output = build?.assets.length
954
1018
  ? [
955
1019
  tui.muted(
956
- `✓ Uploaded ${build.assets.length} ${tui.plural(build.assets.length, 'asset', 'assets')} (${tui.formatBytes(bytes)}) to CDN`
1020
+ formatUploadSummary(
1021
+ build.assets.length,
1022
+ rawBytes,
1023
+ transferredBytes
1024
+ )
957
1025
  ),
958
1026
  ]
959
1027
  : undefined;
@@ -1,9 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { createCommand } from '../../../../types';
3
3
  import * as tui from '../../../../tui';
4
- import { createSandboxClient } from '../util';
4
+ import { createSandboxClient, resolveSandboxTarget } from '../util';
5
5
  import { getCommand } from '../../../../command-prefix';
6
- import { sandboxResolve, diskCheckpointCreate } from '@agentuity/server';
6
+ import { diskCheckpointCreate } from '@agentuity/server';
7
7
 
8
8
  const CheckpointCreateResponseSchema = z.object({
9
9
  success: z.boolean().describe('Whether the operation succeeded'),
@@ -42,8 +42,14 @@ export const createSubcommand = createCommand({
42
42
 
43
43
  const started = Date.now();
44
44
 
45
- // Resolve sandbox to get region and orgId using CLI API
46
- const sandboxInfo = await sandboxResolve(apiClient, args.sandboxId);
45
+ const sandboxInfo = await resolveSandboxTarget(
46
+ logger,
47
+ auth,
48
+ apiClient,
49
+ args.sandboxId,
50
+ ctx.config?.name ?? 'production',
51
+ ctx.config
52
+ );
47
53
 
48
54
  const client = createSandboxClient(logger, auth, sandboxInfo.region);
49
55
 
@@ -1,9 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { createCommand } from '../../../../types';
3
3
  import * as tui from '../../../../tui';
4
- import { createSandboxClient } from '../util';
4
+ import { createSandboxClient, resolveSandboxTarget } from '../util';
5
5
  import { getCommand } from '../../../../command-prefix';
6
- import { sandboxResolve, diskCheckpointDelete } from '@agentuity/server';
6
+ import { diskCheckpointDelete } from '@agentuity/server';
7
7
 
8
8
  const CheckpointDeleteResponseSchema = z.object({
9
9
  success: z.boolean().describe('Whether the operation succeeded'),
@@ -63,8 +63,14 @@ export const deleteSubcommand = createCommand({
63
63
 
64
64
  const started = Date.now();
65
65
 
66
- // Resolve sandbox to get region and orgId using CLI API
67
- const sandboxInfo = await sandboxResolve(apiClient, args.sandboxId);
66
+ const sandboxInfo = await resolveSandboxTarget(
67
+ logger,
68
+ auth,
69
+ apiClient,
70
+ args.sandboxId,
71
+ ctx.config?.name ?? 'production',
72
+ ctx.config
73
+ );
68
74
 
69
75
  const client = createSandboxClient(logger, auth, sandboxInfo.region);
70
76
 
@@ -1,9 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { createCommand } from '../../../../types';
3
3
  import * as tui from '../../../../tui';
4
- import { createSandboxClient } from '../util';
4
+ import { createSandboxClient, resolveSandboxTarget } from '../util';
5
5
  import { getCommand } from '../../../../command-prefix';
6
- import { sandboxResolve, diskCheckpointList } from '@agentuity/server';
6
+ import { diskCheckpointList } from '@agentuity/server';
7
7
 
8
8
  const CheckpointInfoSchema = z.object({
9
9
  id: z.string(),
@@ -40,8 +40,14 @@ export const listSubcommand = createCommand({
40
40
  async handler(ctx) {
41
41
  const { args, options, auth, logger, apiClient } = ctx;
42
42
 
43
- // Resolve sandbox to get region and orgId using CLI API
44
- const sandboxInfo = await sandboxResolve(apiClient, args.sandboxId);
43
+ const sandboxInfo = await resolveSandboxTarget(
44
+ logger,
45
+ auth,
46
+ apiClient,
47
+ args.sandboxId,
48
+ ctx.config?.name ?? 'production',
49
+ ctx.config
50
+ );
45
51
 
46
52
  const client = createSandboxClient(logger, auth, sandboxInfo.region);
47
53
 
@@ -1,9 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { createCommand } from '../../../../types';
3
3
  import * as tui from '../../../../tui';
4
- import { createSandboxClient } from '../util';
4
+ import { createSandboxClient, resolveSandboxTarget } from '../util';
5
5
  import { getCommand } from '../../../../command-prefix';
6
- import { sandboxResolve, diskCheckpointRestore } from '@agentuity/server';
6
+ import { diskCheckpointRestore } from '@agentuity/server';
7
7
 
8
8
  const CheckpointRestoreResponseSchema = z.object({
9
9
  success: z.boolean().describe('Whether the operation succeeded'),
@@ -42,8 +42,14 @@ export const restoreSubcommand = createCommand({
42
42
 
43
43
  const started = Date.now();
44
44
 
45
- // Resolve sandbox to get region and orgId using CLI API
46
- const sandboxInfo = await sandboxResolve(apiClient, args.sandboxId);
45
+ const sandboxInfo = await resolveSandboxTarget(
46
+ logger,
47
+ auth,
48
+ apiClient,
49
+ args.sandboxId,
50
+ ctx.config?.name ?? 'production',
51
+ ctx.config
52
+ );
47
53
 
48
54
  const client = createSandboxClient(logger, auth, sandboxInfo.region);
49
55
 
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { createCommand } from '../../../types';
3
3
  import * as tui from '../../../tui';
4
- import { createSandboxClient, parseFileArgs, cacheSandboxRegion } from './util';
4
+ import { createSandboxClient, parseFileArgs, cacheSandboxTarget } from './util';
5
5
  import { getCommand } from '../../../command-prefix';
6
6
  import { sandboxCreate } from '@agentuity/server';
7
7
  import { StructuredError } from '@agentuity/core';
@@ -72,6 +72,10 @@ export const createSubcommand = createCommand({
72
72
  .string()
73
73
  .optional()
74
74
  .describe('Idle timeout before sandbox is reaped (e.g., "10m", "1h")'),
75
+ pausedTimeout: z
76
+ .string()
77
+ .optional()
78
+ .describe('Maximum time sandbox can remain paused before termination (e.g., "24h")'),
75
79
  env: z.array(z.string()).optional().describe('Environment variables (KEY=VALUE)'),
76
80
  file: z
77
81
  .array(z.string())
@@ -195,7 +199,13 @@ export const createSubcommand = createCommand({
195
199
  opts.network || opts.port
196
200
  ? { enabled: opts.network || opts.port !== undefined, port: opts.port }
197
201
  : undefined,
198
- timeout: opts.idleTimeout ? { idle: opts.idleTimeout } : undefined,
202
+ timeout:
203
+ opts.idleTimeout || opts.pausedTimeout
204
+ ? {
205
+ idle: opts.idleTimeout,
206
+ paused: opts.pausedTimeout,
207
+ }
208
+ : undefined,
199
209
  env: Object.keys(envMap).length > 0 ? envMap : undefined,
200
210
  files: hasFiles ? files : undefined,
201
211
  snapshot: opts.snapshot,
@@ -206,8 +216,8 @@ export const createSubcommand = createCommand({
206
216
  orgId,
207
217
  });
208
218
 
209
- // Cache the region for future lookups
210
- await cacheSandboxRegion(config?.name, result.sandboxId, region);
219
+ // Cache routing context for future sandbox commands.
220
+ await cacheSandboxTarget(config?.name, result.sandboxId, region, orgId);
211
221
 
212
222
  if (!options.json) {
213
223
  const duration = Date.now() - started;
@@ -1,9 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { createCommand } from '../../../types';
3
3
  import * as tui from '../../../tui';
4
- import { createSandboxClient, clearSandboxRegionCache } from './util';
4
+ import { createSandboxClient, clearSandboxRegionCache, resolveSandboxTarget } from './util';
5
5
  import { getCommand } from '../../../command-prefix';
6
- import { sandboxDestroy, sandboxResolve } from '@agentuity/server';
6
+ import { sandboxDestroy } from '@agentuity/server';
7
7
 
8
8
  const SandboxDeleteResponseSchema = z.object({
9
9
  success: z.boolean().describe('Whether the operation succeeded'),
@@ -57,8 +57,14 @@ export const deleteSubcommand = createCommand({
57
57
 
58
58
  const started = Date.now();
59
59
 
60
- // Resolve sandbox to get region and orgId using CLI API
61
- const sandboxInfo = await sandboxResolve(apiClient, args.sandboxId);
60
+ const sandboxInfo = await resolveSandboxTarget(
61
+ logger,
62
+ auth,
63
+ apiClient,
64
+ args.sandboxId,
65
+ config?.name ?? 'production',
66
+ config
67
+ );
62
68
 
63
69
  const client = createSandboxClient(logger, auth, sandboxInfo.region);
64
70