@agentuity/cli 0.0.94 → 0.0.96

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 (277) hide show
  1. package/AGENTS.md +54 -0
  2. package/dist/auth.d.ts +1 -1
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +8 -5
  5. package/dist/auth.js.map +1 -1
  6. package/dist/banner.js +1 -1
  7. package/dist/banner.js.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +190 -27
  10. package/dist/cli.js.map +1 -1
  11. package/dist/cmd/auth/signup.js +1 -1
  12. package/dist/cmd/auth/signup.js.map +1 -1
  13. package/dist/cmd/build/ast.d.ts.map +1 -1
  14. package/dist/cmd/build/ast.js +7 -0
  15. package/dist/cmd/build/ast.js.map +1 -1
  16. package/dist/cmd/build/entry-generator.d.ts +20 -0
  17. package/dist/cmd/build/entry-generator.d.ts.map +1 -0
  18. package/dist/cmd/build/entry-generator.js +366 -0
  19. package/dist/cmd/build/entry-generator.js.map +1 -0
  20. package/dist/cmd/build/index.d.ts.map +1 -1
  21. package/dist/cmd/build/index.js +5 -23
  22. package/dist/cmd/build/index.js.map +1 -1
  23. package/dist/cmd/build/vite/agent-discovery.d.ts +33 -0
  24. package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -0
  25. package/dist/cmd/build/vite/agent-discovery.js +297 -0
  26. package/dist/cmd/build/vite/agent-discovery.js.map +1 -0
  27. package/dist/cmd/build/vite/browser-env-plugin.d.ts +9 -0
  28. package/dist/cmd/build/vite/browser-env-plugin.d.ts.map +1 -0
  29. package/dist/cmd/build/vite/browser-env-plugin.js +28 -0
  30. package/dist/cmd/build/vite/browser-env-plugin.js.map +1 -0
  31. package/dist/cmd/build/vite/bun-dev-server.d.ts +29 -0
  32. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -0
  33. package/dist/cmd/build/vite/bun-dev-server.js +54 -0
  34. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -0
  35. package/dist/cmd/build/vite/config-loader.d.ts +23 -0
  36. package/dist/cmd/build/vite/config-loader.d.ts.map +1 -0
  37. package/dist/cmd/build/vite/config-loader.js +50 -0
  38. package/dist/cmd/build/vite/config-loader.js.map +1 -0
  39. package/dist/cmd/build/vite/index.d.ts +26 -0
  40. package/dist/cmd/build/vite/index.d.ts.map +1 -0
  41. package/dist/cmd/build/vite/index.js +127 -0
  42. package/dist/cmd/build/vite/index.js.map +1 -0
  43. package/dist/cmd/build/vite/lifecycle-generator.d.ts +11 -0
  44. package/dist/cmd/build/vite/lifecycle-generator.d.ts.map +1 -0
  45. package/dist/cmd/build/vite/lifecycle-generator.js +35 -0
  46. package/dist/cmd/build/vite/lifecycle-generator.js.map +1 -0
  47. package/dist/cmd/build/vite/metadata-generator.d.ts +32 -0
  48. package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -0
  49. package/dist/cmd/build/vite/metadata-generator.js +489 -0
  50. package/dist/cmd/build/vite/metadata-generator.js.map +1 -0
  51. package/dist/cmd/build/vite/patch-plugin.d.ts +21 -0
  52. package/dist/cmd/build/vite/patch-plugin.d.ts.map +1 -0
  53. package/dist/cmd/build/vite/patch-plugin.js +70 -0
  54. package/dist/cmd/build/vite/patch-plugin.js.map +1 -0
  55. package/dist/cmd/build/vite/registry-generator.d.ts +19 -0
  56. package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -0
  57. package/dist/cmd/build/{route-registry.js → vite/registry-generator.js} +126 -48
  58. package/dist/cmd/build/vite/registry-generator.js.map +1 -0
  59. package/dist/cmd/build/vite/route-discovery.d.ts +58 -0
  60. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -0
  61. package/dist/cmd/build/vite/route-discovery.js +125 -0
  62. package/dist/cmd/build/vite/route-discovery.js.map +1 -0
  63. package/dist/cmd/build/vite/server-bundler.d.ts +16 -0
  64. package/dist/cmd/build/vite/server-bundler.d.ts.map +1 -0
  65. package/dist/cmd/build/vite/server-bundler.js +194 -0
  66. package/dist/cmd/build/vite/server-bundler.js.map +1 -0
  67. package/dist/cmd/build/vite/vite-asset-server-config.d.ts +19 -0
  68. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -0
  69. package/dist/cmd/build/vite/vite-asset-server-config.js +105 -0
  70. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -0
  71. package/dist/cmd/build/vite/vite-asset-server.d.ts +23 -0
  72. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -0
  73. package/dist/cmd/build/vite/vite-asset-server.js +40 -0
  74. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -0
  75. package/dist/cmd/build/vite/vite-builder.d.ts +44 -0
  76. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -0
  77. package/dist/cmd/build/vite/vite-builder.js +232 -0
  78. package/dist/cmd/build/vite/vite-builder.js.map +1 -0
  79. package/dist/cmd/build/vite/workbench-generator.d.ts +10 -0
  80. package/dist/cmd/build/vite/workbench-generator.d.ts.map +1 -0
  81. package/dist/cmd/build/vite/workbench-generator.js +135 -0
  82. package/dist/cmd/build/vite/workbench-generator.js.map +1 -0
  83. package/dist/cmd/build/vite-bundler.d.ts +23 -0
  84. package/dist/cmd/build/vite-bundler.d.ts.map +1 -0
  85. package/dist/cmd/build/vite-bundler.js +79 -0
  86. package/dist/cmd/build/vite-bundler.js.map +1 -0
  87. package/dist/cmd/cloud/agent/get.d.ts.map +1 -1
  88. package/dist/cmd/cloud/agent/get.js +1 -0
  89. package/dist/cmd/cloud/agent/get.js.map +1 -1
  90. package/dist/cmd/cloud/agent/list.d.ts.map +1 -1
  91. package/dist/cmd/cloud/agent/list.js +1 -0
  92. package/dist/cmd/cloud/agent/list.js.map +1 -1
  93. package/dist/cmd/cloud/db/get.d.ts.map +1 -1
  94. package/dist/cmd/cloud/db/get.js +1 -0
  95. package/dist/cmd/cloud/db/get.js.map +1 -1
  96. package/dist/cmd/cloud/db/list.d.ts.map +1 -1
  97. package/dist/cmd/cloud/db/list.js +1 -0
  98. package/dist/cmd/cloud/db/list.js.map +1 -1
  99. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  100. package/dist/cmd/cloud/deploy.js +152 -128
  101. package/dist/cmd/cloud/deploy.js.map +1 -1
  102. package/dist/cmd/cloud/deployment/list.d.ts.map +1 -1
  103. package/dist/cmd/cloud/deployment/list.js +4 -0
  104. package/dist/cmd/cloud/deployment/list.js.map +1 -1
  105. package/dist/cmd/cloud/env/list.d.ts.map +1 -1
  106. package/dist/cmd/cloud/env/list.js +1 -0
  107. package/dist/cmd/cloud/env/list.js.map +1 -1
  108. package/dist/cmd/cloud/keyvalue/get.d.ts.map +1 -1
  109. package/dist/cmd/cloud/keyvalue/get.js +1 -0
  110. package/dist/cmd/cloud/keyvalue/get.js.map +1 -1
  111. package/dist/cmd/cloud/keyvalue/keys.d.ts.map +1 -1
  112. package/dist/cmd/cloud/keyvalue/keys.js +1 -0
  113. package/dist/cmd/cloud/keyvalue/keys.js.map +1 -1
  114. package/dist/cmd/cloud/keyvalue/list-namespaces.d.ts.map +1 -1
  115. package/dist/cmd/cloud/keyvalue/list-namespaces.js +1 -0
  116. package/dist/cmd/cloud/keyvalue/list-namespaces.js.map +1 -1
  117. package/dist/cmd/cloud/keyvalue/stats.d.ts.map +1 -1
  118. package/dist/cmd/cloud/keyvalue/stats.js +1 -0
  119. package/dist/cmd/cloud/keyvalue/stats.js.map +1 -1
  120. package/dist/cmd/cloud/secret/list.d.ts.map +1 -1
  121. package/dist/cmd/cloud/secret/list.js +1 -0
  122. package/dist/cmd/cloud/secret/list.js.map +1 -1
  123. package/dist/cmd/cloud/session/list.d.ts.map +1 -1
  124. package/dist/cmd/cloud/session/list.js +4 -0
  125. package/dist/cmd/cloud/session/list.js.map +1 -1
  126. package/dist/cmd/cloud/storage/get.d.ts.map +1 -1
  127. package/dist/cmd/cloud/storage/get.js +1 -0
  128. package/dist/cmd/cloud/storage/get.js.map +1 -1
  129. package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
  130. package/dist/cmd/cloud/storage/list.js +3 -0
  131. package/dist/cmd/cloud/storage/list.js.map +1 -1
  132. package/dist/cmd/cloud/stream/get.d.ts.map +1 -1
  133. package/dist/cmd/cloud/stream/get.js +1 -0
  134. package/dist/cmd/cloud/stream/get.js.map +1 -1
  135. package/dist/cmd/cloud/stream/list.d.ts.map +1 -1
  136. package/dist/cmd/cloud/stream/list.js +1 -0
  137. package/dist/cmd/cloud/stream/list.js.map +1 -1
  138. package/dist/cmd/cloud/vector/get.d.ts.map +1 -1
  139. package/dist/cmd/cloud/vector/get.js +1 -0
  140. package/dist/cmd/cloud/vector/get.js.map +1 -1
  141. package/dist/cmd/cloud/vector/search.d.ts.map +1 -1
  142. package/dist/cmd/cloud/vector/search.js +1 -0
  143. package/dist/cmd/cloud/vector/search.js.map +1 -1
  144. package/dist/cmd/dev/index.d.ts.map +1 -1
  145. package/dist/cmd/dev/index.js +291 -751
  146. package/dist/cmd/dev/index.js.map +1 -1
  147. package/dist/cmd/dev/sync.d.ts +1 -1
  148. package/dist/cmd/dev/sync.d.ts.map +1 -1
  149. package/dist/cmd/dev/sync.js +3 -0
  150. package/dist/cmd/dev/sync.js.map +1 -1
  151. package/dist/cmd/profile/show.d.ts.map +1 -1
  152. package/dist/cmd/profile/show.js +3 -4
  153. package/dist/cmd/profile/show.js.map +1 -1
  154. package/dist/cmd/project/create.d.ts.map +1 -1
  155. package/dist/cmd/project/create.js +3 -4
  156. package/dist/cmd/project/create.js.map +1 -1
  157. package/dist/cmd/project/list.d.ts.map +1 -1
  158. package/dist/cmd/project/list.js +1 -0
  159. package/dist/cmd/project/list.js.map +1 -1
  160. package/dist/cmd/project/show.d.ts.map +1 -1
  161. package/dist/cmd/project/show.js +1 -0
  162. package/dist/cmd/project/show.js.map +1 -1
  163. package/dist/cmd/upgrade/index.d.ts.map +1 -1
  164. package/dist/cmd/upgrade/index.js +5 -3
  165. package/dist/cmd/upgrade/index.js.map +1 -1
  166. package/dist/config.d.ts +1 -1
  167. package/dist/config.d.ts.map +1 -1
  168. package/dist/config.js +29 -11
  169. package/dist/config.js.map +1 -1
  170. package/dist/index.d.ts +1 -1
  171. package/dist/index.d.ts.map +1 -1
  172. package/dist/index.js.map +1 -1
  173. package/dist/runtime-bootstrap.d.ts +3 -2
  174. package/dist/runtime-bootstrap.d.ts.map +1 -1
  175. package/dist/runtime-bootstrap.js +7 -2
  176. package/dist/runtime-bootstrap.js.map +1 -1
  177. package/dist/schemas/deploy.d.ts +1 -1
  178. package/dist/types.d.ts +40 -1
  179. package/dist/types.d.ts.map +1 -1
  180. package/dist/types.js.map +1 -1
  181. package/dist/utils/bun-version-checker.d.ts +11 -0
  182. package/dist/utils/bun-version-checker.d.ts.map +1 -0
  183. package/dist/utils/bun-version-checker.js +56 -0
  184. package/dist/utils/bun-version-checker.js.map +1 -0
  185. package/dist/version-check.d.ts.map +1 -1
  186. package/dist/version-check.js +5 -2
  187. package/dist/version-check.js.map +1 -1
  188. package/package.json +10 -3
  189. package/src/auth.ts +9 -5
  190. package/src/banner.ts +1 -1
  191. package/src/cli.ts +228 -29
  192. package/src/cmd/auth/signup.ts +1 -1
  193. package/src/cmd/build/ast.ts +7 -0
  194. package/src/cmd/build/entry-generator.ts +404 -0
  195. package/src/cmd/build/index.ts +7 -28
  196. package/src/cmd/build/vite/agent-discovery.ts +467 -0
  197. package/src/cmd/build/vite/browser-env-plugin.ts +34 -0
  198. package/src/cmd/build/vite/bun-dev-server.ts +78 -0
  199. package/src/cmd/build/vite/config-loader.ts +70 -0
  200. package/src/cmd/build/vite/index.ts +166 -0
  201. package/src/cmd/build/vite/lifecycle-generator.ts +43 -0
  202. package/src/cmd/build/vite/metadata-generator.ts +602 -0
  203. package/src/cmd/build/vite/patch-plugin.ts +88 -0
  204. package/src/cmd/build/vite/registry-generator.ts +288 -0
  205. package/src/cmd/build/vite/route-discovery.ts +186 -0
  206. package/src/cmd/build/vite/server-bundler.ts +258 -0
  207. package/src/cmd/build/vite/vite-asset-server-config.ts +134 -0
  208. package/src/cmd/build/vite/vite-asset-server.ts +63 -0
  209. package/src/cmd/build/vite/vite-builder.ts +284 -0
  210. package/src/cmd/build/vite/workbench-generator.ts +152 -0
  211. package/src/cmd/build/vite-bundler.ts +110 -0
  212. package/src/cmd/cloud/agent/get.ts +2 -0
  213. package/src/cmd/cloud/agent/list.ts +1 -0
  214. package/src/cmd/cloud/db/get.ts +1 -0
  215. package/src/cmd/cloud/db/list.ts +1 -0
  216. package/src/cmd/cloud/deploy.ts +175 -144
  217. package/src/cmd/cloud/deployment/list.ts +4 -0
  218. package/src/cmd/cloud/env/list.ts +1 -0
  219. package/src/cmd/cloud/keyvalue/get.ts +1 -0
  220. package/src/cmd/cloud/keyvalue/keys.ts +1 -0
  221. package/src/cmd/cloud/keyvalue/list-namespaces.ts +1 -0
  222. package/src/cmd/cloud/keyvalue/stats.ts +2 -0
  223. package/src/cmd/cloud/secret/list.ts +1 -0
  224. package/src/cmd/cloud/session/list.ts +4 -0
  225. package/src/cmd/cloud/storage/get.ts +1 -0
  226. package/src/cmd/cloud/storage/list.ts +4 -0
  227. package/src/cmd/cloud/stream/get.ts +1 -0
  228. package/src/cmd/cloud/stream/list.ts +1 -0
  229. package/src/cmd/cloud/vector/get.ts +1 -0
  230. package/src/cmd/cloud/vector/search.ts +1 -0
  231. package/src/cmd/dev/index.ts +323 -914
  232. package/src/cmd/dev/sync.ts +5 -1
  233. package/src/cmd/profile/show.ts +3 -4
  234. package/src/cmd/project/create.ts +3 -4
  235. package/src/cmd/project/list.ts +1 -0
  236. package/src/cmd/project/show.ts +1 -0
  237. package/src/cmd/upgrade/index.ts +6 -3
  238. package/src/config.ts +31 -11
  239. package/src/index.ts +2 -0
  240. package/src/runtime-bootstrap.ts +8 -2
  241. package/src/types.ts +48 -1
  242. package/src/utils/bun-version-checker.ts +70 -0
  243. package/src/version-check.ts +6 -2
  244. package/dist/cmd/build/bundler.d.ts +0 -22
  245. package/dist/cmd/build/bundler.d.ts.map +0 -1
  246. package/dist/cmd/build/bundler.js +0 -766
  247. package/dist/cmd/build/bundler.js.map +0 -1
  248. package/dist/cmd/build/config-loader.d.ts +0 -16
  249. package/dist/cmd/build/config-loader.d.ts.map +0 -1
  250. package/dist/cmd/build/config-loader.js +0 -227
  251. package/dist/cmd/build/config-loader.js.map +0 -1
  252. package/dist/cmd/build/file.d.ts +0 -2
  253. package/dist/cmd/build/file.d.ts.map +0 -1
  254. package/dist/cmd/build/file.js +0 -10
  255. package/dist/cmd/build/file.js.map +0 -1
  256. package/dist/cmd/build/fix-duplicate-exports.d.ts +0 -2
  257. package/dist/cmd/build/fix-duplicate-exports.d.ts.map +0 -1
  258. package/dist/cmd/build/fix-duplicate-exports.js +0 -170
  259. package/dist/cmd/build/fix-duplicate-exports.js.map +0 -1
  260. package/dist/cmd/build/plugin.d.ts +0 -6
  261. package/dist/cmd/build/plugin.d.ts.map +0 -1
  262. package/dist/cmd/build/plugin.js +0 -645
  263. package/dist/cmd/build/plugin.js.map +0 -1
  264. package/dist/cmd/build/route-discovery.d.ts +0 -54
  265. package/dist/cmd/build/route-discovery.d.ts.map +0 -1
  266. package/dist/cmd/build/route-discovery.js +0 -148
  267. package/dist/cmd/build/route-discovery.js.map +0 -1
  268. package/dist/cmd/build/route-registry.d.ts +0 -38
  269. package/dist/cmd/build/route-registry.d.ts.map +0 -1
  270. package/dist/cmd/build/route-registry.js.map +0 -1
  271. package/src/cmd/build/bundler.ts +0 -927
  272. package/src/cmd/build/config-loader.ts +0 -268
  273. package/src/cmd/build/file.ts +0 -10
  274. package/src/cmd/build/fix-duplicate-exports.ts +0 -207
  275. package/src/cmd/build/plugin.ts +0 -782
  276. package/src/cmd/build/route-discovery.ts +0 -202
  277. package/src/cmd/build/route-registry.ts +0 -222
@@ -1,33 +1,52 @@
1
- /** biome-ignore-all lint/style/useTemplate: its easier */
2
1
  import { z } from 'zod';
3
2
  import { resolve, join } from 'node:path';
4
- import { bundle } from '../build/bundler';
5
- import { getServiceUrls } from '@agentuity/server';
6
- import { getBuildMetadata } from '../build/plugin';
7
- import { existsSync, type FSWatcher, watch, statSync, readdirSync } from 'node:fs';
8
- import {
9
- getDefaultConfigDir,
10
- loadProjectSDKKey,
11
- saveProjectDir,
12
- saveConfig,
13
- loadBuildMetadata,
14
- } from '../../config';
15
- import { type Config, createCommand } from '../../types';
3
+ import { existsSync } from 'node:fs';
4
+ import { internalExit } from '@agentuity/runtime';
5
+ import { createCommand } from '../../types';
6
+ import { startBunDevServer } from '../build/vite/bun-dev-server';
16
7
  import * as tui from '../../tui';
17
- import { createAgentTemplates, createAPITemplates } from './templates';
8
+ import { getCommand } from '../../command-prefix';
18
9
  import { generateEndpoint, type DevmodeResponse } from './api';
19
- import { APIClient, getAppBaseURL, getAPIBaseURL, getGravityDevModeURL } from '../../api';
10
+ import { APIClient, getAPIBaseURL, getAppBaseURL, getGravityDevModeURL } from '../../api';
20
11
  import { download } from './download';
21
12
  import { createDevmodeSyncService } from './sync';
22
13
  import { getDevmodeDeploymentId } from '../build/ast';
23
- import { getWorkbench } from '../build/workbench';
24
- import { BuildMetadata } from '@agentuity/server';
25
- import { getCommand } from '../../command-prefix';
26
- import { notifyWorkbenchClients } from '../../utils/workbench-notify';
27
- import { getEnvFilePaths, readEnvFile } from '../../env-util';
28
- import { writeAgentsDocs } from '../../agents-docs';
14
+ import { getDefaultConfigDir, saveConfig } from '../../config';
15
+ import type { Config } from '../../types';
16
+
17
+ const DEFAULT_PORT = 3500;
18
+ const MIN_PORT = 1024;
19
+ const MAX_PORT = 65535;
20
+
21
+ // Minimal interface for subprocess management
22
+ interface ProcessLike {
23
+ kill: (signal?: number | NodeJS.Signals) => void;
24
+ exitCode: number | null;
25
+ stdout?: AsyncIterable<Uint8Array>;
26
+ stderr?: AsyncIterable<Uint8Array>;
27
+ }
28
+
29
+ interface ServerLike {
30
+ close: () => void;
31
+ }
32
+
33
+ const getDefaultPort = (): number => {
34
+ const envPort = process.env.PORT;
35
+ if (!envPort) {
36
+ return DEFAULT_PORT;
37
+ }
38
+ const trimmed = envPort.trim();
39
+ if (!trimmed || !/^\d+$/.test(trimmed)) {
40
+ return DEFAULT_PORT;
41
+ }
42
+ const parsed = Number(trimmed);
43
+ if (!Number.isInteger(parsed) || parsed < MIN_PORT || parsed > MAX_PORT) {
44
+ return DEFAULT_PORT;
45
+ }
46
+ return parsed;
47
+ };
29
48
 
30
- const shouldDisableInteractive= (interactive?: boolean) => {
49
+ const shouldDisableInteractive = (interactive?: boolean) => {
31
50
  if (!interactive) {
32
51
  return true;
33
52
  }
@@ -56,32 +75,23 @@ export const command = createCommand({
56
75
  .describe('Turn on or off the public url'),
57
76
  port: z
58
77
  .number()
59
- .min(1024) // should we allow a lower root port? probably not?
60
- .max(65535)
61
- .default(3500)
62
- .describe('The TCP port to start the dev start'),
63
- watch: z
64
- .array(z.string())
65
- .optional()
66
- .describe(
67
- 'Additional paths to watch for changes (e.g., --watch ../packages/workbench/dist)'
68
- ),
78
+ .min(MIN_PORT)
79
+ .max(MAX_PORT)
80
+ .default(getDefaultPort())
81
+ .describe('The TCP port to start the dev server (also reads from PORT env)'),
69
82
  }),
70
83
  },
71
84
  optional: { auth: 'Continue without an account (local only)', project: true },
72
85
 
73
86
  async handler(ctx) {
74
- const { opts, logger, options, project, projectDir, auth } = ctx;
87
+ const { opts, logger, project, projectDir, auth } = ctx;
75
88
  let { config } = ctx;
76
89
 
77
- // Allow sync with mock service even without devmode endpoint
78
- const useMockService = process.env.DEVMODE_SYNC_SERVICE_MOCK === 'true';
79
- const apiClient = new APIClient(getAPIBaseURL(config), logger, config);
80
- const syncService = createDevmodeSyncService({ logger, apiClient, mock: useMockService });
81
-
82
- const rootDir = projectDir;
90
+ const rootDir = resolve(projectDir);
83
91
  const appTs = join(rootDir, 'app.ts');
84
92
  const srcDir = join(rootDir, 'src');
93
+
94
+ // Verify required files exist
85
95
  const mustHaves = [join(rootDir, 'package.json'), appTs, srcDir];
86
96
  const missing: string[] = [];
87
97
 
@@ -98,13 +108,17 @@ export const command = createCommand({
98
108
  for (const filename of missing) {
99
109
  tui.bullet(`Missing ${filename}`);
100
110
  }
101
- process.exit(1);
111
+ internalExit(1);
102
112
  }
103
113
 
104
- await saveProjectDir(rootDir);
105
-
106
- // Regenerate AGENTS.md files if they are missing (e.g., after node_modules reinstall)
107
- await writeAgentsDocs(rootDir, { onlyIfMissing: true });
114
+ // Setup devmode and gravity (if using public URL)
115
+ const useMockService = process.env.DEVMODE_SYNC_SERVICE_MOCK === 'true';
116
+ const apiClient = auth ? new APIClient(getAPIBaseURL(config), logger, config) : null;
117
+ createDevmodeSyncService({
118
+ logger,
119
+ apiClient,
120
+ mock: useMockService,
121
+ });
108
122
 
109
123
  let devmode: DevmodeResponse | undefined;
110
124
  let gravityBin: string | undefined;
@@ -112,7 +126,7 @@ export const command = createCommand({
112
126
  let appURL: string | undefined;
113
127
 
114
128
  if (auth && project && opts.public) {
115
- // Generate devmode endpoint only when using --public
129
+ // Generate devmode endpoint for public URL
116
130
  const endpoint = await tui.spinner({
117
131
  message: 'Connecting to Gravity',
118
132
  callback: () => {
@@ -120,6 +134,7 @@ export const command = createCommand({
120
134
  },
121
135
  clearOnSuccess: true,
122
136
  });
137
+
123
138
  const _config = { ...config } as Config;
124
139
  _config.devmode = {
125
140
  hostname: endpoint.hostname,
@@ -129,20 +144,12 @@ export const command = createCommand({
129
144
  devmode = endpoint;
130
145
  gravityURL = getGravityDevModeURL(project.region, config);
131
146
  appURL = `${getAppBaseURL(config)}/r/${project.projectId}`;
132
- logger.trace('gravity url: %s', gravityURL);
133
- }
134
-
135
- logger.debug(
136
- 'Getting devmode deployment id for projectId: %s, endpointId: %s',
137
- project?.projectId,
138
- devmode?.id
139
- );
140
- const deploymentId = getDevmodeDeploymentId(project?.projectId ?? '', devmode?.id ?? '');
141
147
 
142
- if (devmode && opts.public) {
148
+ // Download gravity client
143
149
  const configDir = getDefaultConfigDir();
144
150
  const gravityDir = join(configDir, 'gravity');
145
151
  let mustCheck = true;
152
+
146
153
  if (
147
154
  config?.gravity?.version &&
148
155
  existsSync(join(gravityDir, config.gravity.version, 'gravity')) &&
@@ -153,6 +160,7 @@ export const command = createCommand({
153
160
  gravityBin = join(gravityDir, config.gravity.version, 'gravity');
154
161
  }
155
162
  }
163
+
156
164
  if (mustCheck) {
157
165
  const res = await download(gravityDir);
158
166
  gravityBin = res.filename;
@@ -166,12 +174,27 @@ export const command = createCommand({
166
174
  }
167
175
  }
168
176
 
169
- const workbench = await getWorkbench(rootDir);
177
+ // Get workbench info from config (new Vite approach)
178
+ const { loadAgentuityConfig, getWorkbenchConfig } = await import(
179
+ '../build/vite/config-loader'
180
+ );
181
+ const agentuityConfig = await loadAgentuityConfig(rootDir, ctx.logger);
182
+ const workbenchConfigData = getWorkbenchConfig(agentuityConfig, true); // dev mode
183
+ const workbench = {
184
+ hasWorkbench: workbenchConfigData.enabled,
185
+ config: workbenchConfigData.enabled
186
+ ? { route: workbenchConfigData.route, headers: workbenchConfigData.headers }
187
+ : null,
188
+ };
170
189
 
171
- const canDoInput =
172
- interactive && !!(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
190
+ const deploymentId = getDevmodeDeploymentId(project?.projectId ?? '', devmode?.id ?? '');
173
191
 
192
+ // Calculate URLs for banner
174
193
  const padding = 12;
194
+ const workbenchUrl =
195
+ auth && project?.projectId
196
+ ? `${getAppBaseURL(config)}/w/${project.projectId}`
197
+ : `http://127.0.0.1:${opts.port}${workbench.config?.route ?? '/workbench'}`;
175
198
 
176
199
  const devmodebody =
177
200
  tui.muted(tui.padRight('Local:', padding)) +
@@ -181,929 +204,315 @@ export const command = createCommand({
181
204
  (devmode?.hostname ? tui.link(`https://${devmode.hostname}`) : tui.warn('Disabled')) +
182
205
  '\n' +
183
206
  tui.muted(tui.padRight('Workbench:', padding)) +
184
- (workbench.hasWorkbench
185
- ? tui.link(`http://127.0.0.1:${opts.port}${workbench.config?.route ?? '/workbench'}`)
186
- : tui.warn('Disabled')) +
207
+ (workbench.hasWorkbench ? tui.link(workbenchUrl) : tui.warn('Disabled')) +
187
208
  '\n' +
188
209
  tui.muted(tui.padRight('Dashboard:', padding)) +
189
210
  (appURL ? tui.link(appURL) : tui.warn('Disabled')) +
190
211
  '\n' +
191
- (canDoInput
212
+ (interactive
192
213
  ? '\n' + tui.muted('Press ') + tui.bold('h') + tui.muted(' for keyboard shortcuts')
193
214
  : '');
194
215
 
195
- function showBanner() {
196
- tui.banner('⨺ Agentuity DevMode', devmodebody, {
197
- padding: 2,
198
- topSpacer: false,
199
- bottomSpacer: false,
200
- centerTitle: false,
201
- });
202
- }
203
-
204
- showBanner();
205
-
206
- // Load .env file(s) based on config profile (Bun no longer auto-loads .env files)
207
- const isProduction = process.env.NODE_ENV === 'production' || config?.name !== 'local';
208
- const envFiles = getEnvFilePaths(rootDir, {
209
- configName: config?.name,
210
- isProduction,
216
+ tui.banner('⨺ Agentuity DevMode', devmodebody, {
217
+ padding: 2,
218
+ topSpacer: false,
219
+ bottomSpacer: false,
220
+ centerTitle: false,
211
221
  });
212
222
 
213
- // Load and merge all .env files (later files override earlier ones)
214
- let envVars: Record<string, string> = {};
215
- for (const envFilePath of envFiles) {
216
- if (await Bun.file(envFilePath).exists()) {
217
- const vars = await readEnvFile(envFilePath);
218
- envVars = { ...envVars, ...vars };
219
- logger.debug('Loaded environment variables from %s', envFilePath);
220
- }
221
- }
222
-
223
- // Start with process.env and merge in .env file vars
224
- const env: Record<string, string | undefined> = { ...process.env, ...envVars };
225
- env.AGENTUITY_SDK_DEV_MODE = 'true';
226
- env.AGENTUITY_ENV = 'development';
227
- env.NODE_ENV = 'development';
228
- env.AGENTUITY_REGION = project?.region;
229
- env.PORT = Number(opts.port).toFixed();
230
- env.AGENTUITY_PORT = env.PORT;
231
- const serviceUrls = getServiceUrls(project?.region);
232
- if (options.logLevel !== undefined) env.AGENTUITY_LOG_LEVEL = options.logLevel;
233
- // Pass through AGENTUITY_SDK_LOG_LEVEL for internal SDK logger
234
- if (process.env.AGENTUITY_SDK_LOG_LEVEL) {
235
- env.AGENTUITY_SDK_LOG_LEVEL = process.env.AGENTUITY_SDK_LOG_LEVEL;
236
- }
237
- env.AGENTUITY_FORCE_LOCAL_SERVICES = opts.local === true ? 'true' : 'false';
238
- if (project) {
239
- env.AGENTUITY_TRANSPORT_URL = serviceUrls.catalyst;
240
- env.AGENTUITY_CATALYST_URL = serviceUrls.catalyst;
241
- env.AGENTUITY_VECTOR_URL = serviceUrls.vector;
242
- env.AGENTUITY_KEYVALUE_URL = serviceUrls.keyvalue;
243
- env.AGENTUITY_STREAM_URL = serviceUrls.stream;
244
- env.AGENTUITY_CLOUD_ORG_ID = project.orgId;
245
- env.AGENTUITY_CLOUD_PROJECT_ID = project.projectId;
246
- }
247
- if (!process.stdout.isTTY) {
248
- env.NO_COLOR = '1';
249
- }
250
-
251
- const agentuityDir = resolve(rootDir, '.agentuity');
252
- const appPath = resolve(agentuityDir, 'app.js');
253
-
254
- // Load existing metadata file to use as previousMetadata for sync
255
- // This prevents reinserting agents/evals that haven't changed
256
- let previousMetadata: BuildMetadata | undefined;
257
- try {
258
- previousMetadata = await loadBuildMetadata(agentuityDir);
259
- logger.debug(
260
- 'Loaded previous metadata with %d agent(s)',
261
- previousMetadata.agents?.length ?? 0
262
- );
263
- } catch (_error) {
264
- // File doesn't exist yet (first run), that's okay
265
- logger.debug('No previous metadata file found, will treat all agents/evals as new');
266
- previousMetadata = undefined;
267
- }
268
-
269
- // Watch directories instead of files to survive atomic replacements (sed -i, cp)
270
- const watches = [rootDir];
271
-
272
- // Add additional watch paths from options
273
- if (opts.watch) {
274
- for (const watchPath of opts.watch) {
275
- const resolvedPath = resolve(rootDir, watchPath);
276
- if (existsSync(resolvedPath)) {
277
- watches.push(resolvedPath);
278
- logger.debug('Added additional watch path: %s', resolvedPath);
279
- } else {
280
- logger.warn('Watch path does not exist: %s', resolvedPath);
281
- }
282
- }
283
- }
284
- const watchers: FSWatcher[] = [];
285
- let failures = 0;
286
- let running = false;
287
- let pid = 0;
288
- let failed = false;
289
- let devServer: Bun.Subprocess | undefined;
290
- let exitPromise: Promise<number> | undefined;
291
- let restarting = false;
292
- let shuttingDownForRestart = false;
293
- let pendingRestart = false;
294
- let building = false;
295
- let buildCompletedAt = 0;
296
- const BUILD_COOLDOWN_MS = 500; // Ignore file changes for 500ms after build completes
297
- const templatedDirectories = new Map<string, number>(); // Track directories that just had templates created
298
- let metadata: Partial<BuildMetadata> | undefined;
299
- let showInitialReadyMessage = true;
300
- let serverStartTime = 0;
301
- let gravityClient: Bun.Subprocess | undefined;
302
- let initialStartupComplete = false;
303
-
304
- const sdkKey = await loadProjectSDKKey(logger, rootDir);
305
- if (!sdkKey) {
306
- tui.warning(`Couldn't find the AGENTUITY_SDK_KEY in ${rootDir} .env file`);
307
- }
308
- const gravityBinExists = gravityBin ? await Bun.file(gravityBin).exists() : true;
309
- if (!gravityBinExists) {
310
- logger.error(`Gravity binary not found at ${gravityBin}, skipping gravity client startup`);
311
- }
312
-
313
- async function restartGravityClient() {
314
- if (gravityClient) {
315
- gravityClient.kill('SIGINT');
316
- gravityClient.kill();
317
- }
318
- if (!devmode || !opts.public) {
319
- return;
320
- }
321
- try {
322
- gravityClient = Bun.spawn(
323
- [
324
- gravityBin!,
325
- '--endpoint-id',
326
- devmode.id,
327
- '--port',
328
- env.PORT!,
329
- '--url',
330
- gravityURL!,
331
- '--log-level',
332
- process.env.AGENTUITY_GRAVITY_LOG_LEVEL ?? 'error',
333
- ],
334
- {
335
- cwd: rootDir,
336
- stdout: 'inherit',
337
- stderr: 'inherit',
338
- stdin: 'ignore',
339
- env: { ...env, AGENTUITY_SDK_KEY: sdkKey },
340
- }
341
- );
342
- gravityClient.exited.then(() => {
343
- logger.debug('gravity client exited');
344
- });
345
- } catch (err) {
346
- logger.error(
347
- 'Failed to spawn gravity client: %s',
348
- err instanceof Error ? err.message : String(err)
349
- );
350
- }
351
- }
352
-
353
- // Track restart timestamps to detect restart loops
354
- const restartTimestamps: number[] = [];
355
- const MAX_RESTARTS = 10;
356
- const TIME_WINDOW_MS = 10000; // 10 seconds
357
-
358
- function checkRestartThrottle() {
359
- const now = Date.now();
360
- restartTimestamps.push(now);
361
-
362
- // Remove timestamps older than the time window
363
- while (restartTimestamps.length > 0 && now - restartTimestamps[0] > TIME_WINDOW_MS) {
364
- restartTimestamps.shift();
365
- }
366
-
367
- // Check if we've exceeded the threshold
368
- if (restartTimestamps.length >= MAX_RESTARTS) {
369
- tui.error(`Detected ${MAX_RESTARTS} restarts in ${TIME_WINDOW_MS / 1000} seconds`);
370
- tui.error(
371
- 'This usually indicates a file watcher loop (e.g., log files in the project root)'
372
- );
373
- tui.fatal('Too many rapid restarts, exiting to prevent infinite loop');
374
- }
375
- }
223
+ // Restart loop - allows server to restart on file changes
224
+ let shouldRestart = false;
225
+ let viteServer: ServerLike | null = null;
226
+ let gravityProcess: ProcessLike | null = null;
376
227
 
377
- let lastErrorLineCount = 0;
378
- let showedRestartMessage = false;
228
+ const restartServer = () => {
229
+ shouldRestart = true;
230
+ };
379
231
 
380
- function clearRestartMessage() {
381
- if (showedRestartMessage) {
382
- process.stdout.write('\x1b[1A\x1b[2K');
383
- showedRestartMessage = false;
384
- }
385
- }
232
+ const showWelcome = () => {
233
+ logger.info('DevMode ready 🚀');
234
+ };
386
235
 
387
- function failure(msg: string) {
388
- failed = true;
389
- failures++;
390
- // Exit immediately on initial startup failure
391
- if (!initialStartupComplete) {
392
- tui.fatal(msg);
393
- }
394
- // During hot reload, show error but don't exit unless too many failures
395
- if (failures >= 5) {
396
- tui.error(msg);
397
- tui.fatal('too many failures, exiting');
398
- } else {
399
- // Ensure we're on a new line before printing error
400
- tui.error(msg);
401
- // Track lines: 1 for "✗ Building..." + 1 for error message
402
- lastErrorLineCount = 2;
403
- }
404
- }
236
+ // Make restart function available globally for HMR plugin
237
+ (globalThis as Record<string, unknown>).__AGENTUITY_RESTART__ = restartServer;
405
238
 
406
- function clearLastError() {
407
- if (lastErrorLineCount > 0) {
408
- // Move cursor up and clear each line
409
- for (let i = 0; i < lastErrorLineCount; i++) {
410
- process.stdout.write('\x1b[1A\x1b[2K');
411
- }
412
- lastErrorLineCount = 0;
413
- }
414
- }
239
+ // Setup signal handlers once before the loop
240
+ const cleanup = async () => {
241
+ tui.info('Shutting down...');
415
242
 
416
- const kill = async () => {
417
- if (!running || !devServer) {
418
- logger.trace('kill() called but server not running');
419
- return;
243
+ // Close Vite asset server first
244
+ if (viteServer) {
245
+ await viteServer.close();
420
246
  }
421
247
 
422
- logger.trace('Killing dev server (pid: %d)', pid);
423
- shuttingDownForRestart = true;
424
- running = false;
425
- process.kill(pid, 'SIGINT');
426
- try {
427
- // Kill the process group (negative PID kills entire group)
428
- process.kill(-pid, 'SIGTERM');
429
- logger.trace('Sent SIGTERM to process group');
430
- } catch {
431
- // Fallback: kill the direct process
248
+ // Kill gravity client with SIGTERM first, then SIGKILL as fallback
249
+ if (gravityProcess) {
432
250
  try {
433
- if (devServer) {
434
- devServer.kill();
435
- logger.trace('Killed dev server process directly');
251
+ gravityProcess.kill('SIGTERM');
252
+ // Give it a moment to gracefully shutdown
253
+ await new Promise((resolve) => setTimeout(resolve, 100));
254
+ if (gravityProcess.exitCode === null) {
255
+ gravityProcess.kill('SIGKILL');
436
256
  }
437
- } catch {
438
- // Ignore if already dead
439
- logger.trace('Process already dead');
257
+ } catch (err) {
258
+ logger.debug('Error killing gravity process: %s', err);
440
259
  }
441
260
  }
442
261
 
443
- // Wait for the server to actually exit
444
- if (exitPromise) {
445
- logger.trace('Waiting for dev server to exit...');
446
- await exitPromise;
447
- logger.trace('Dev server exited');
448
- }
449
-
450
- devServer = undefined;
451
- exitPromise = undefined;
452
- shuttingDownForRestart = false;
453
- };
454
-
455
- // Handle signals to ensure entire process tree is killed
456
- const cleanup = (exitCode = 0) => {
457
- logger.trace('cleanup() called with exitCode=%d', exitCode);
458
- if (gravityClient) {
459
- logger.debug('calling kill on gravity client with pid: %d', gravityClient.pid);
460
- gravityClient.kill('SIGINT');
461
- gravityClient = undefined;
462
- }
463
- if (pid && running) {
464
- kill();
465
- }
466
- for (const watcher of watchers) {
467
- watcher.close();
468
- }
469
- watchers.length = 0;
470
- process.exit(exitCode);
262
+ internalExit(0);
471
263
  };
472
264
 
473
265
  process.on('SIGINT', cleanup);
474
266
  process.on('SIGTERM', cleanup);
475
- process.on('SIGQUIT', cleanup);
267
+
268
+ // Ensure gravity client is always killed on exit (even if cleanup is bypassed)
269
+ // Use SIGKILL for immediate termination since the process is already exiting
476
270
  process.on('exit', () => {
477
- // Synchronous cleanup on exit
478
- if (gravityClient) {
271
+ if (gravityProcess && gravityProcess.exitCode === null) {
479
272
  try {
480
- gravityClient.kill('SIGINT');
481
- } catch {
482
- // Ignore errors
273
+ gravityProcess.kill('SIGKILL');
274
+ } catch (_err) {
275
+ // Ignore errors during exit cleanup
483
276
  }
484
277
  }
485
278
  });
486
279
 
487
- async function restart() {
488
- // Queue restart if already restarting
489
- if (restarting) {
490
- logger.trace('Restart already in progress, queuing another restart');
491
- pendingRestart = true;
492
- return;
493
- }
280
+ while (true) {
281
+ shouldRestart = false;
494
282
 
495
- logger.trace('restart() called, restarting=%s, running=%s', restarting, running);
496
- restarting = true;
497
- pendingRestart = false;
498
- failed = false;
499
283
  try {
500
- if (running) {
501
- logger.trace('Server is running, killing before restart');
502
- checkRestartThrottle();
503
- tui.info('Restarting on file change');
504
- showedRestartMessage = true;
505
-
506
- // Notify workbench clients before killing the server
507
- await notifyWorkbenchClients({
508
- port: opts.port,
509
- message: 'restarting',
510
- });
511
-
512
- // Small delay to ensure the restart message is processed before killing server
513
- await new Promise((resolve) => setTimeout(resolve, 200));
514
-
515
- await kill();
516
- logger.trace('Server killed, continuing with restart');
517
- // Continue with restart after kill completes
518
- } else {
519
- logger.trace('Initial server start');
520
- }
521
- logger.trace('Starting typecheck and build...');
522
-
523
- // Clear any previous error before starting new build
524
- clearLastError();
525
- clearRestartMessage();
526
-
527
- try {
528
- await tui.spinner({
529
- message: 'Building...',
530
- clearOnSuccess: true,
531
- callback: async () => {
532
- logger.trace('Bundle starting...');
533
- building = true;
534
- await bundle({
535
- rootDir,
536
- dev: true,
537
- projectId: project?.projectId,
538
- deploymentId,
539
- port: opts.port,
540
- region: project?.region ?? 'local',
541
- logger,
542
- workbench,
543
- });
544
- building = false;
545
- buildCompletedAt = Date.now();
546
- logger.trace('Bundle completed successfully');
547
- logger.trace('tsc starting...');
548
- const tscExitCode = await tui.runCommand({
549
- command: 'tsc',
550
- cmd: ['bunx', 'tsc', '--noEmit'],
551
- cwd: rootDir,
552
- clearOnSuccess: true,
553
- truncate: false,
554
- maxLinesOutput: 2,
555
- maxLinesOnFailure: 15,
556
- });
557
- if (tscExitCode !== 0) {
558
- logger.trace('tsc failed with exit code %d', tscExitCode);
559
- failure('Type check failed');
560
- return;
561
- }
562
- logger.trace('tsc completed successfully');
563
- await restartGravityClient();
564
- },
565
- });
566
- } catch (error) {
567
- building = false;
568
- const e = error as Error;
569
- if (e.constructor.name === 'AggregateError') {
570
- const ex = e as AggregateError;
571
- for (const err of ex.errors) {
572
- if (err) {
573
- failure(err);
574
- return;
575
- }
284
+ // Generate entry file for Vite before starting dev server
285
+ await tui.spinner({
286
+ message: 'Generating entry file',
287
+ callback: async () => {
288
+ const { generateEntryFile } = await import('../build/entry-generator');
289
+ await generateEntryFile({
290
+ rootDir,
291
+ projectId: project?.projectId ?? '',
292
+ deploymentId,
293
+ logger,
294
+ mode: 'dev',
295
+ });
296
+ },
297
+ clearOnSuccess: true,
298
+ });
299
+ } catch (error) {
300
+ tui.error(`Failed to generate entry file: ${error}`);
301
+ tui.warn('Waiting for file changes to retry...');
302
+
303
+ // Wait for next restart trigger
304
+ await new Promise<void>((resolve) => {
305
+ const checkRestart = setInterval(() => {
306
+ if (shouldRestart) {
307
+ clearInterval(checkRestart);
308
+ resolve();
576
309
  }
577
- return;
578
- }
579
- const errorMsg = error instanceof Error ? error.message : String(error);
580
- failure(errorMsg);
581
- return;
582
- }
583
-
584
- logger.trace('Typecheck and build completed');
585
-
586
- if (failed) {
587
- logger.trace('Restart failed, returning early');
588
- return;
589
- }
590
-
591
- // Reset failure counter on successful build
592
- failures = 0;
310
+ }, 100);
311
+ });
312
+ continue;
313
+ }
593
314
 
594
- logger.trace('Checking if app file exists: %s', appPath);
595
- if (!existsSync(appPath)) {
596
- logger.trace('App file not found: %s', appPath);
597
- failure(`App file not found: ${appPath}`);
598
- return;
599
- }
600
- logger.trace('App file exists, getting build metadata...');
315
+ try {
316
+ // Start Bun dev server (with Vite asset server for HMR)
317
+ const result = await startBunDevServer({
318
+ rootDir,
319
+ port: opts.port,
320
+ projectId: project?.projectId,
321
+ orgId: project?.orgId,
322
+ deploymentId,
323
+ logger,
324
+ });
601
325
 
602
- metadata = getBuildMetadata();
603
- logger.trace('Build metadata retrieved');
326
+ viteServer = result.viteAssetServer.server;
327
+ // Note: Bun server runs in-process, no separate app process needed
604
328
 
605
- // Sync agents and evals to API if in devmode with auth
606
- if (auth && project && apiClient) {
607
- try {
608
- logger.debug('Loading build metadata for sync...');
609
- const currentMetadata = await loadBuildMetadata(agentuityDir);
610
- logger.debug(
611
- 'Found %d agent(s) and %d route(s) in metadata',
612
- currentMetadata.agents?.length ?? 0,
613
- currentMetadata.routes?.length ?? 0
614
- );
615
- if (currentMetadata.agents) {
616
- for (const agent of currentMetadata.agents) {
617
- logger.debug(
618
- 'Agent: id=%s, name=%s, version=%s, evals=%d',
619
- agent.id,
620
- agent.name,
621
- agent.version,
622
- agent.evals?.length ?? 0
623
- );
624
- if (agent.evals) {
625
- for (const evalItem of agent.evals) {
626
- logger.debug(
627
- ' Eval: id=%s, name=%s, version=%s',
628
- evalItem.id,
629
- evalItem.name,
630
- evalItem.version
631
- );
632
- }
633
- }
634
- }
329
+ // Wait for app.ts to finish loading (Vite is ready but app may still be initializing)
330
+ // Give it 2 seconds to ensure app initialization completes
331
+ await new Promise((resolve) => setTimeout(resolve, 2000));
332
+ } catch (error) {
333
+ tui.error(`Failed to start dev server: ${error}`);
334
+ tui.warn('Waiting for file changes to retry...');
335
+
336
+ // Wait for next restart trigger
337
+ await new Promise<void>((resolve) => {
338
+ const checkRestart = setInterval(() => {
339
+ if (shouldRestart) {
340
+ clearInterval(checkRestart);
341
+ resolve();
635
342
  }
636
- logger.debug('Syncing agents and evals...');
343
+ }, 100);
344
+ });
345
+ continue;
346
+ }
637
347
 
638
- await syncService.sync(
639
- currentMetadata,
640
- previousMetadata,
641
- project.projectId,
642
- deploymentId
643
- );
644
- previousMetadata = currentMetadata;
645
- logger.debug('Sync completed successfully');
646
- } catch (error) {
647
- logger.error('Failed to sync agents/evals: %s', error);
648
- if (error instanceof Error) {
649
- logger.error('Error stack: %s', error.stack);
348
+ try {
349
+ // Start gravity client if we have devmode
350
+ if (gravityBin && gravityURL && devmode) {
351
+ logger.trace('Starting gravity client: %s', gravityBin);
352
+ gravityProcess = Bun.spawn(
353
+ [
354
+ gravityBin,
355
+ '--endpoint-id',
356
+ devmode.id,
357
+ '--port',
358
+ opts.port.toString(),
359
+ '--url',
360
+ gravityURL,
361
+ '--log-level',
362
+ process.env.AGENTUITY_GRAVITY_LOG_LEVEL ?? 'error',
363
+ ],
364
+ {
365
+ cwd: rootDir,
366
+ stdout: 'pipe',
367
+ stderr: 'pipe',
368
+ detached: false, // Ensure gravity dies with parent process
650
369
  }
651
- // Don't fail the build, just log the error
652
- }
653
- } else {
654
- logger.trace(
655
- 'Skipping sync - auth=%s, project=%s, devmode=%s, apiClient=%s',
656
- !!auth,
657
- !!project,
658
- !!devmode,
659
- !!apiClient
660
370
  );
661
- }
662
371
 
663
- logger.trace('Starting dev server: %s', appPath);
664
- // Use shell to run in a process group for proper cleanup
665
- // The 'exec' ensures the shell is replaced by the actual process
666
- logger.trace('Spawning dev server process...');
667
- devServer = Bun.spawn(['sh', '-c', `exec bun run "${appPath}"`], {
668
- cwd: rootDir,
669
- stdout: 'inherit',
670
- stderr: 'inherit',
671
- stdin: process.stdin.isTTY ? 'ignore' : 'inherit', // Don't inherit stdin, we handle it ourselves
672
- env,
673
- });
674
-
675
- logger.trace('Dev server process spawned, setting up state...');
676
- running = true;
677
- failed = false;
678
- pid = devServer.pid;
679
- exitPromise = devServer.exited;
680
- serverStartTime = Date.now();
681
- logger.trace('Dev server started (pid: %d)', pid);
682
-
683
- if (showInitialReadyMessage) {
684
- showInitialReadyMessage = false;
685
- logger.info('DevMode ready 🚀');
686
- logger.trace('Initial ready message logged');
687
- // Mark initial startup complete immediately to prevent watcher restarts
688
- initialStartupComplete = true;
689
- logger.trace('Initial startup complete, file watcher restarts now enabled');
690
- }
691
-
692
- // Notify workbench clients that the server is alive and ready
693
- // Use setTimeout to ensure server is fully ready before notifying
694
- setTimeout(async () => {
695
- await notifyWorkbenchClients({
696
- port: opts.port,
697
- message: 'alive',
698
- });
699
- }, 500);
700
-
701
- logger.trace('Attaching exit handler to dev server process...');
702
- // Attach non-blocking exit handler
703
- exitPromise
704
- .then((exitCode) => {
705
- const runtime = Date.now() - serverStartTime;
706
- logger.trace(
707
- 'Dev server exited with code %d (shuttingDownForRestart=%s, runtime=%dms)',
708
- exitCode,
709
- shuttingDownForRestart,
710
- runtime
711
- );
712
- running = false;
713
- devServer = undefined;
714
- exitPromise = undefined;
715
- // If server exited immediately after starting (< 2 seconds), treat as failure and restart
716
- if (runtime < 2000 && !shuttingDownForRestart) {
717
- logger.trace('Server exited too quickly, treating as failure and restarting');
718
- failure('Server exited immediately after starting');
719
- // Trigger a restart after a short delay
720
- setTimeout(() => {
721
- if (!running && !restarting) {
722
- logger.trace('Triggering restart after quick exit');
723
- restart();
372
+ // Log gravity output
373
+ (async () => {
374
+ try {
375
+ if (gravityProcess?.stdout) {
376
+ for await (const chunk of gravityProcess.stdout) {
377
+ const text = new TextDecoder().decode(chunk);
378
+ logger.debug('[gravity] %s', text.trim());
724
379
  }
725
- }, 100);
726
- return;
727
- }
728
- // Only exit the CLI if this is a clean exit AND not a restart AND server ran for a while
729
- if (exitCode === 0 && !shuttingDownForRestart && runtime >= 2000) {
730
- logger.trace('Clean exit, stopping CLI');
731
- cleanup(exitCode);
732
- }
733
- // Non-zero exit codes are treated as restartable failures
734
- // But if it's exit code 1 (common error exit), also exit the CLI
735
- if (exitCode === 1 && !shuttingDownForRestart && runtime >= 2000) {
736
- logger.trace('Server exited with error code 1, stopping CLI');
737
- cleanup(exitCode);
380
+ }
381
+ } catch (err) {
382
+ logger.error('Error reading gravity stdout: %s', err);
738
383
  }
739
- })
740
- .catch((error) => {
741
- logger.trace(
742
- 'Dev server exit error (shuttingDownForRestart=%s): %s',
743
- shuttingDownForRestart,
744
- error
745
- );
746
- running = false;
747
- devServer = undefined;
748
- exitPromise = undefined;
749
- if (!shuttingDownForRestart) {
750
- if (error instanceof Error) {
751
- failure(`Dev server failed: ${error.message}`);
752
- } else {
753
- failure('Dev server failed');
384
+ })();
385
+
386
+ (async () => {
387
+ try {
388
+ if (gravityProcess?.stderr) {
389
+ for await (const chunk of gravityProcess.stderr) {
390
+ const text = new TextDecoder().decode(chunk);
391
+ logger.warn('[gravity] %s', text.trim());
392
+ }
754
393
  }
394
+ } catch (err) {
395
+ logger.error('Error reading gravity stderr: %s', err);
755
396
  }
756
- });
757
- } catch (error) {
758
- logger.trace('Restart caught error: %s', error);
759
- if (error instanceof Error) {
760
- logger.trace('Error message: %s, stack: %s', error.message, error.stack);
761
- failure(`Dev server failed: ${error.message}`);
762
- } else {
763
- logger.trace('Non-Error exception: %s', String(error));
764
- failure('Dev server failed');
765
- }
766
- running = false;
767
- devServer = undefined;
768
- } finally {
769
- logger.trace('Entering restart() finally block...');
770
- const hadPendingRestart = pendingRestart;
771
- restarting = false;
772
- pendingRestart = false;
773
- logger.trace(
774
- 'restart() completed, restarting=%s, hadPendingRestart=%s',
775
- restarting,
776
- hadPendingRestart
777
- );
397
+ })();
778
398
 
779
- // If another restart was queued while we were restarting, trigger it now
780
- if (hadPendingRestart) {
781
- logger.trace('Triggering queued restart');
782
- setImmediate(restart);
399
+ logger.debug('Gravity client started');
783
400
  }
784
- }
785
- }
786
-
787
- logger.trace('Starting initial build and server');
788
- await restart();
789
- logger.trace('Initial restart completed, setting up watchers');
790
- logger.trace('initialStartupComplete is now: %s', initialStartupComplete);
791
401
 
792
- // Setup keyboard shortcuts (only if we have a TTY)
793
- if (canDoInput) {
794
- logger.trace('Setting up keyboard shortcuts');
795
- process.stdin.setRawMode(true);
796
- process.stdin.resume();
797
- process.stdin.setEncoding('utf8');
402
+ // Sync service integration
403
+ // TODO: Integrate sync service with Vite's buildStart/buildEnd hooks
404
+ // The sync service will be called when metadata changes are detected
798
405
 
799
- const showHelp = () => {
800
- console.log('\n' + tui.bold('Keyboard Shortcuts:'));
801
- console.log(tui.muted(' h') + ' - show this help');
802
- console.log(tui.muted(' c') + ' - clear console');
803
- console.log(tui.muted(' r') + ' - restart server');
804
- console.log(tui.muted(' o') + ' - show routes');
805
- console.log(tui.muted(' a') + ' - show agents');
806
- console.log(tui.muted(' q') + ' - quit\n');
807
- };
808
-
809
- const showRoutes = () => {
810
- tui.info('API Route Detail');
811
- tui.table(metadata?.routes ?? [], ['method', 'path', 'filename']);
812
- };
813
-
814
- const showAgents = () => {
815
- tui.info('Agent Detail');
816
- tui.table(metadata?.agents ?? [], ['name', 'filename', 'description']);
817
- };
818
-
819
- process.stdin.on('data', (data) => {
820
- const key = data.toString();
406
+ // Handle keyboard shortcuts
407
+ if (interactive && process.stdin.isTTY && process.stdout.isTTY) {
408
+ process.stdin.setRawMode(true);
409
+ process.stdin.resume();
410
+ process.stdin.setEncoding('utf8');
411
+
412
+ const showHelp = () => {
413
+ console.log('\n' + tui.bold('Keyboard Shortcuts:'));
414
+ console.log(tui.muted(' h') + ' - show this help');
415
+ console.log(tui.muted(' c') + ' - clear console');
416
+ console.log(tui.muted(' q') + ' - quit\n');
417
+ };
418
+
419
+ process.stdin.on('data', (data) => {
420
+ const key = data.toString();
421
+
422
+ // Handle Ctrl+C
423
+ if (key === '\u0003') {
424
+ internalExit(0);
425
+ }
821
426
 
822
- // Handle Ctrl+C
823
- if (key === '\u0003') {
824
- cleanup();
825
- return;
427
+ switch (key) {
428
+ case 'h':
429
+ showHelp();
430
+ break;
431
+ case 'c':
432
+ console.clear();
433
+ tui.banner('⨺ Agentuity DevMode', devmodebody, {
434
+ padding: 2,
435
+ topSpacer: false,
436
+ bottomSpacer: false,
437
+ centerTitle: false,
438
+ });
439
+ break;
440
+ case 'q':
441
+ internalExit(0);
442
+ break;
443
+ default:
444
+ console.log(data);
445
+ break;
446
+ }
447
+ });
826
448
  }
827
449
 
828
- // Handle other shortcuts
829
- switch (key) {
830
- case 'h':
831
- showHelp();
832
- break;
833
- case 'c':
834
- console.clear();
835
- showBanner();
836
- break;
837
- case 'r':
838
- tui.info('Manually restarting server...');
839
- restart();
840
- break;
841
- case 'o':
842
- showRoutes();
843
- break;
844
- case 'a':
845
- showAgents();
846
- break;
847
- case 'q':
848
- tui.info('Shutting down...');
849
- cleanup();
850
- break;
851
- }
852
- });
450
+ showWelcome();
853
451
 
854
- logger.trace('✓ Keyboard shortcuts enabled');
855
- } else {
856
- if (process.stdin) {
857
- // still need to monitor stdin in case we are pipeing into another process or file etc
858
- if (typeof process.stdin.setRawMode === 'function') {
859
- process.stdin.setRawMode(true);
860
- }
861
- process.stdin.resume();
862
- process.stdin.on('data', (data) => {
863
- const key = data.toString();
864
- // Handle Ctrl+C
865
- if (key === '\u0003') {
866
- cleanup();
867
- return;
868
- }
452
+ // Wait for restart signal
453
+ await new Promise<void>((resolve) => {
454
+ const checkRestart = setInterval(() => {
455
+ if (shouldRestart) {
456
+ clearInterval(checkRestart);
457
+ resolve();
458
+ }
459
+ }, 100);
869
460
  });
870
- }
871
- logger.trace('❌ Keyboard shortcuts disabled');
872
- }
873
-
874
- // Patterns to ignore (generated files that change during build)
875
- const ignorePatterns = [
876
- /\.generated\.(js|ts|d\.ts)$/,
877
- /registry\.generated\.ts$/,
878
- /types\.generated\.d\.ts$/,
879
- /client\.generated\.js$/,
880
- /\.tmp$/,
881
- /\.tsbuildinfo$/,
882
- /\.agentuity\//,
883
- // Ignore temporary files created by sed (e.g., sedUprJj0)
884
- /\/sed[A-Za-z0-9]+$/,
885
- ];
886
-
887
- // Helper to check if a file is a temporary file created by sed
888
- const isSedTempFile = (filePath: string): boolean => {
889
- const basename = filePath.split('/').pop() || '';
890
- return /^sed[A-Za-z0-9]+$/.test(basename);
891
- };
892
-
893
- logger.trace('Setting up file watchers for: %s', watches.join(', '));
894
- for (const watchDir of watches) {
895
- try {
896
- logger.trace('Setting up watcher for %s', watchDir);
897
- const watcher = watch(watchDir, { recursive: true }, (eventType, changedFile) => {
898
- const absPath = changedFile ? resolve(watchDir, changedFile) : watchDir;
899
-
900
- // Ignore file changes during initial startup to prevent spurious restarts
901
- if (!initialStartupComplete) {
902
- logger.trace(
903
- 'File change ignored (initial startup): %s (event: %s, file: %s)',
904
- watchDir,
905
- eventType,
906
- changedFile
907
- );
908
- return;
909
- }
910
-
911
- // Ignore file changes during active build to prevent loops
912
- if (building) {
913
- logger.trace(
914
- 'File change ignored (build in progress): %s (event: %s, file: %s)',
915
- watchDir,
916
- eventType,
917
- changedFile
918
- );
919
- return;
920
- }
921
-
922
- // Ignore file changes immediately after build completes (cooldown period)
923
- // This prevents restarts from build output files that are written asynchronously
924
- if (buildCompletedAt > 0 && Date.now() - buildCompletedAt < BUILD_COOLDOWN_MS) {
925
- logger.trace(
926
- 'File change ignored (build cooldown): %s (event: %s, file: %s)',
927
- watchDir,
928
- eventType,
929
- changedFile
930
- );
931
- return;
932
- }
933
-
934
- // Ignore node_modules folder
935
- if (absPath.includes('node_modules')) {
936
- logger.trace(
937
- 'File change ignored (node_modules): %s (event: %s, file: %s)',
938
- watchDir,
939
- eventType,
940
- changedFile
941
- );
942
- return;
943
- }
944
-
945
- // Ignore .git folder
946
- if (changedFile && (changedFile === '.git' || changedFile.startsWith('.git/'))) {
947
- logger.trace(
948
- 'File change ignored (.git folder): %s (event: %s, file: %s)',
949
- watchDir,
950
- eventType,
951
- changedFile
952
- );
953
- return;
954
- }
955
-
956
- // Ignore changes in .agentuity directory (build output)
957
- // Check both relative path and normalized absolute path
958
- const isInAgentuityDir =
959
- (changedFile &&
960
- (changedFile === '.agentuity' || changedFile.startsWith('.agentuity/'))) ||
961
- resolve(absPath).startsWith(agentuityDir);
962
- if (isInAgentuityDir) {
963
- logger.trace(
964
- 'File change ignored (.agentuity dir): %s (event: %s, file: %s)',
965
- watchDir,
966
- eventType,
967
- changedFile
968
- );
969
- return;
970
- }
971
461
 
972
- // Ignore changes to src/web/public directory (static assets, not code)
973
- if (changedFile && changedFile === 'src/web/public') {
974
- logger.trace(
975
- 'File change ignored (static assets dir): %s (event: %s, file: %s)',
976
- watchDir,
977
- eventType,
978
- changedFile
979
- );
980
- return;
981
- }
982
-
983
- // Check for .tmp file renames that replace watched files (BEFORE ignoring)
984
- // This handles cases like sed -i.tmp where agent.ts.tmp is renamed to agent.ts
985
- if (eventType === 'rename' && changedFile && changedFile.endsWith('.tmp')) {
986
- const targetFile = changedFile.slice(0, -4); // Remove .tmp suffix
987
- const targetAbsPath = resolve(watchDir, targetFile);
988
-
989
- // Only trigger restart for source files (ts, tsx, js, jsx, etc.)
990
- const isSourceFile = /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(targetFile);
462
+ // Restart triggered - cleanup and loop
463
+ tui.info('Restarting server...');
991
464
 
992
- // Check if target file exists and is not in ignored directories
993
- const targetExists = existsSync(targetAbsPath);
994
- const inNodeModules = targetAbsPath.includes('node_modules');
995
- const inAgentuityDir =
996
- (targetFile &&
997
- (targetFile === '.agentuity' || targetFile.startsWith('.agentuity/'))) ||
998
- resolve(targetAbsPath).startsWith(agentuityDir);
999
- let isDirectory = false;
1000
- if (targetExists) {
1001
- try {
1002
- isDirectory = statSync(targetAbsPath).isDirectory();
1003
- } catch (err) {
1004
- logger.trace('Failed to stat target file: %s', err);
1005
- }
1006
- }
465
+ // Close Vite asset server
466
+ if (viteServer) {
467
+ await viteServer.close();
468
+ }
1007
469
 
1008
- if (
1009
- isSourceFile &&
1010
- targetExists &&
1011
- !inNodeModules &&
1012
- !inAgentuityDir &&
1013
- !isDirectory
1014
- ) {
1015
- logger.trace(
1016
- 'File change detected (temp file rename): %s -> %s',
1017
- absPath,
1018
- targetAbsPath
1019
- );
1020
- restart();
1021
- return;
470
+ if (gravityProcess) {
471
+ try {
472
+ gravityProcess.kill('SIGTERM');
473
+ await new Promise((resolve) => setTimeout(resolve, 100));
474
+ if (gravityProcess.exitCode === null) {
475
+ gravityProcess.kill('SIGKILL');
1022
476
  }
477
+ } catch (err) {
478
+ logger.debug('Error killing gravity process during restart: %s', err);
1023
479
  }
480
+ }
1024
481
 
1025
- // Ignore generated files to prevent restart loops
1026
- if (changedFile) {
1027
- // Check for sed temporary files
1028
- if (isSedTempFile(changedFile)) {
1029
- logger.trace(
1030
- 'File change ignored (sed temp file): %s (event: %s, file: %s)',
1031
- watchDir,
1032
- eventType,
1033
- changedFile
1034
- );
1035
- return;
1036
- }
1037
- // Check other ignore patterns
1038
- for (const pattern of ignorePatterns) {
1039
- if (pattern.test(changedFile)) {
1040
- logger.trace(
1041
- 'File change ignored (generated file): %s (event: %s, file: %s)',
1042
- watchDir,
1043
- eventType,
1044
- changedFile
1045
- );
1046
- return;
1047
- }
1048
- }
1049
- }
482
+ // Brief pause before restart
483
+ await new Promise((resolve) => setTimeout(resolve, 500));
484
+ } catch (error) {
485
+ tui.error(`Error during server operation: ${error}`);
486
+ tui.warn('Waiting for file changes to retry...');
1050
487
 
1051
- // Handle new empty directories in src/agent/ or src/api/ paths
1052
- if (
1053
- eventType === 'rename' &&
1054
- existsSync(absPath) &&
1055
- statSync(absPath).isDirectory() &&
1056
- readdirSync(absPath).length === 0
1057
- ) {
1058
- if (changedFile?.startsWith('src/agent/')) {
1059
- logger.debug('agent directory created: %s', changedFile);
1060
- createAgentTemplates(absPath);
1061
- // Mark this directory as recently templated to avoid immediate rebuild
1062
- templatedDirectories.set(absPath, Date.now());
1063
- // Schedule cleanup of this marker after enough time for file events
1064
- setTimeout(() => templatedDirectories.delete(absPath), 1000);
1065
- // Don't restart - wait for the template files to trigger the rebuild
1066
- return;
1067
- } else if (changedFile?.startsWith('src/api/')) {
1068
- logger.debug('api directory created: %s', changedFile);
1069
- createAPITemplates(absPath);
1070
- // Mark this directory as recently templated to avoid immediate rebuild
1071
- templatedDirectories.set(absPath, Date.now());
1072
- // Schedule cleanup of this marker after enough time for file events
1073
- setTimeout(() => templatedDirectories.delete(absPath), 1000);
1074
- // Don't restart - wait for the template files to trigger the rebuild
1075
- return;
1076
- }
1077
- }
488
+ // Cleanup on error - close Vite asset server
489
+ if (viteServer) {
490
+ await viteServer.close();
491
+ }
1078
492
 
1079
- // Check if this file/directory was just templated - skip restart to avoid race condition
1080
- for (const [templatedPath, timestamp] of templatedDirectories.entries()) {
1081
- if (absPath.startsWith(templatedPath) && Date.now() - timestamp < 500) {
1082
- logger.trace(
1083
- 'Ignoring event in recently templated directory: %s',
1084
- templatedPath
1085
- );
1086
- return;
493
+ if (gravityProcess) {
494
+ try {
495
+ gravityProcess.kill('SIGTERM');
496
+ await new Promise((resolve) => setTimeout(resolve, 100));
497
+ if (gravityProcess.exitCode === null) {
498
+ gravityProcess.kill('SIGKILL');
1087
499
  }
500
+ } catch (err) {
501
+ logger.debug('Error killing gravity process on error: %s', err);
1088
502
  }
1089
-
1090
- logger.trace(
1091
- 'File change detected: %s (event: %s, file: %s)',
1092
- absPath,
1093
- eventType,
1094
- changedFile
1095
- );
1096
- restart();
503
+ }
504
+ if (viteServer) await viteServer.close();
505
+
506
+ // Wait for next restart trigger
507
+ await new Promise<void>((resolve) => {
508
+ const checkRestart = setInterval(() => {
509
+ if (shouldRestart) {
510
+ clearInterval(checkRestart);
511
+ resolve();
512
+ }
513
+ }, 100);
1097
514
  });
1098
- watchers.push(watcher);
1099
- logger.trace('✓ Watcher added for %s', watchDir);
1100
- } catch (error) {
1101
- logger.error('Failed to setup watcher for %s: %s', watchDir, error);
1102
515
  }
1103
516
  }
1104
- logger.debug('Dev server watching for changes');
1105
-
1106
- // Keep the handler alive indefinitely
1107
- await new Promise(() => {}).catch(() => cleanup());
1108
517
  },
1109
518
  });