@agentuity/cli 0.0.42 → 0.0.44

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 (249) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/cli.ts +7 -5
  4. package/dist/api.d.ts +3 -3
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/auth.d.ts +10 -2
  7. package/dist/auth.d.ts.map +1 -1
  8. package/dist/banner.d.ts.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cmd/auth/api.d.ts +4 -4
  11. package/dist/cmd/auth/api.d.ts.map +1 -1
  12. package/dist/cmd/auth/index.d.ts.map +1 -1
  13. package/dist/cmd/auth/login.d.ts.map +1 -1
  14. package/dist/cmd/auth/signup.d.ts.map +1 -1
  15. package/dist/cmd/auth/ssh/add.d.ts +2 -0
  16. package/dist/cmd/auth/ssh/add.d.ts.map +1 -0
  17. package/dist/cmd/auth/ssh/api.d.ts +16 -0
  18. package/dist/cmd/auth/ssh/api.d.ts.map +1 -0
  19. package/dist/cmd/auth/ssh/delete.d.ts +2 -0
  20. package/dist/cmd/auth/ssh/delete.d.ts.map +1 -0
  21. package/dist/cmd/auth/ssh/index.d.ts +3 -0
  22. package/dist/cmd/auth/ssh/index.d.ts.map +1 -0
  23. package/dist/cmd/auth/ssh/list.d.ts +2 -0
  24. package/dist/cmd/auth/ssh/list.d.ts.map +1 -0
  25. package/dist/cmd/auth/whoami.d.ts +2 -0
  26. package/dist/cmd/auth/whoami.d.ts.map +1 -0
  27. package/dist/cmd/bundle/ast.d.ts +14 -3
  28. package/dist/cmd/bundle/ast.d.ts.map +1 -1
  29. package/dist/cmd/bundle/ast.test.d.ts +2 -0
  30. package/dist/cmd/bundle/ast.test.d.ts.map +1 -0
  31. package/dist/cmd/bundle/bundler.d.ts +6 -1
  32. package/dist/cmd/bundle/bundler.d.ts.map +1 -1
  33. package/dist/cmd/bundle/file.d.ts.map +1 -1
  34. package/dist/cmd/bundle/fix-duplicate-exports.d.ts +2 -0
  35. package/dist/cmd/bundle/fix-duplicate-exports.d.ts.map +1 -0
  36. package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts +2 -0
  37. package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts.map +1 -0
  38. package/dist/cmd/bundle/index.d.ts +1 -1
  39. package/dist/cmd/bundle/index.d.ts.map +1 -1
  40. package/dist/cmd/bundle/plugin.d.ts +2 -0
  41. package/dist/cmd/bundle/plugin.d.ts.map +1 -1
  42. package/dist/cmd/cloud/deploy.d.ts.map +1 -0
  43. package/dist/cmd/cloud/domain.d.ts +17 -0
  44. package/dist/cmd/cloud/domain.d.ts.map +1 -0
  45. package/dist/cmd/cloud/index.d.ts.map +1 -0
  46. package/dist/cmd/cloud/resource/add.d.ts +2 -0
  47. package/dist/cmd/cloud/resource/add.d.ts.map +1 -0
  48. package/dist/cmd/cloud/resource/delete.d.ts +2 -0
  49. package/dist/cmd/cloud/resource/delete.d.ts.map +1 -0
  50. package/dist/cmd/cloud/resource/index.d.ts +3 -0
  51. package/dist/cmd/cloud/resource/index.d.ts.map +1 -0
  52. package/dist/cmd/cloud/resource/list.d.ts +2 -0
  53. package/dist/cmd/cloud/resource/list.d.ts.map +1 -0
  54. package/dist/cmd/cloud/scp/download.d.ts +2 -0
  55. package/dist/cmd/cloud/scp/download.d.ts.map +1 -0
  56. package/dist/cmd/cloud/scp/index.d.ts +3 -0
  57. package/dist/cmd/cloud/scp/index.d.ts.map +1 -0
  58. package/dist/cmd/cloud/scp/upload.d.ts +2 -0
  59. package/dist/cmd/cloud/scp/upload.d.ts.map +1 -0
  60. package/dist/cmd/cloud/ssh.d.ts +2 -0
  61. package/dist/cmd/cloud/ssh.d.ts.map +1 -0
  62. package/dist/cmd/dev/api.d.ts +18 -0
  63. package/dist/cmd/dev/api.d.ts.map +1 -0
  64. package/dist/cmd/dev/download.d.ts +11 -0
  65. package/dist/cmd/dev/download.d.ts.map +1 -0
  66. package/dist/cmd/dev/index.d.ts.map +1 -1
  67. package/dist/cmd/dev/templates.d.ts +3 -0
  68. package/dist/cmd/dev/templates.d.ts.map +1 -0
  69. package/dist/cmd/env/delete.d.ts +2 -0
  70. package/dist/cmd/env/delete.d.ts.map +1 -0
  71. package/dist/cmd/env/get.d.ts +2 -0
  72. package/dist/cmd/env/get.d.ts.map +1 -0
  73. package/dist/cmd/env/import.d.ts +2 -0
  74. package/dist/cmd/env/import.d.ts.map +1 -0
  75. package/dist/cmd/env/index.d.ts +2 -0
  76. package/dist/cmd/env/index.d.ts.map +1 -0
  77. package/dist/cmd/env/list.d.ts.map +1 -0
  78. package/dist/cmd/env/pull.d.ts +2 -0
  79. package/dist/cmd/env/pull.d.ts.map +1 -0
  80. package/dist/cmd/env/push.d.ts +2 -0
  81. package/dist/cmd/env/push.d.ts.map +1 -0
  82. package/dist/cmd/env/set.d.ts +2 -0
  83. package/dist/cmd/env/set.d.ts.map +1 -0
  84. package/dist/cmd/profile/show.d.ts.map +1 -1
  85. package/dist/cmd/project/create.d.ts.map +1 -1
  86. package/dist/cmd/project/delete.d.ts.map +1 -1
  87. package/dist/cmd/project/download.d.ts +1 -1
  88. package/dist/cmd/project/download.d.ts.map +1 -1
  89. package/dist/cmd/project/list.d.ts.map +1 -1
  90. package/dist/cmd/project/show.d.ts.map +1 -1
  91. package/dist/cmd/project/template-flow.d.ts +5 -1
  92. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  93. package/dist/cmd/secret/delete.d.ts +2 -0
  94. package/dist/cmd/secret/delete.d.ts.map +1 -0
  95. package/dist/cmd/secret/get.d.ts +2 -0
  96. package/dist/cmd/secret/get.d.ts.map +1 -0
  97. package/dist/cmd/secret/import.d.ts +2 -0
  98. package/dist/cmd/secret/import.d.ts.map +1 -0
  99. package/dist/cmd/secret/index.d.ts +2 -0
  100. package/dist/cmd/secret/index.d.ts.map +1 -0
  101. package/dist/cmd/secret/list.d.ts +2 -0
  102. package/dist/cmd/secret/list.d.ts.map +1 -0
  103. package/dist/cmd/secret/pull.d.ts +2 -0
  104. package/dist/cmd/secret/pull.d.ts.map +1 -0
  105. package/dist/cmd/secret/push.d.ts +2 -0
  106. package/dist/cmd/secret/push.d.ts.map +1 -0
  107. package/dist/cmd/secret/set.d.ts +2 -0
  108. package/dist/cmd/secret/set.d.ts.map +1 -0
  109. package/dist/cmd/version/index.d.ts.map +1 -1
  110. package/dist/config.d.ts +11 -3
  111. package/dist/config.d.ts.map +1 -1
  112. package/dist/crypto/box.d.ts +65 -0
  113. package/dist/crypto/box.d.ts.map +1 -0
  114. package/dist/crypto/box.test.d.ts +2 -0
  115. package/dist/crypto/box.test.d.ts.map +1 -0
  116. package/dist/download.d.ts.map +1 -1
  117. package/dist/env-util.d.ts +67 -0
  118. package/dist/env-util.d.ts.map +1 -0
  119. package/dist/env-util.test.d.ts +2 -0
  120. package/dist/env-util.test.d.ts.map +1 -0
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/schema-parser.d.ts.map +1 -1
  124. package/dist/steps.d.ts +4 -1
  125. package/dist/steps.d.ts.map +1 -1
  126. package/dist/terminal.d.ts.map +1 -1
  127. package/dist/tui.d.ts +32 -2
  128. package/dist/tui.d.ts.map +1 -1
  129. package/dist/types.d.ts +250 -127
  130. package/dist/types.d.ts.map +1 -1
  131. package/dist/utils/detectSubagent.d.ts +15 -0
  132. package/dist/utils/detectSubagent.d.ts.map +1 -0
  133. package/dist/utils/zip.d.ts +7 -0
  134. package/dist/utils/zip.d.ts.map +1 -0
  135. package/package.json +11 -3
  136. package/src/api-errors.md +2 -2
  137. package/src/api.ts +12 -7
  138. package/src/auth.ts +116 -7
  139. package/src/banner.ts +13 -6
  140. package/src/cli.ts +709 -36
  141. package/src/cmd/auth/api.ts +10 -16
  142. package/src/cmd/auth/index.ts +3 -1
  143. package/src/cmd/auth/login.ts +24 -8
  144. package/src/cmd/auth/signup.ts +15 -11
  145. package/src/cmd/auth/ssh/add.ts +263 -0
  146. package/src/cmd/auth/ssh/api.ts +94 -0
  147. package/src/cmd/auth/ssh/delete.ts +102 -0
  148. package/src/cmd/auth/ssh/index.ts +10 -0
  149. package/src/cmd/auth/ssh/list.ts +74 -0
  150. package/src/cmd/auth/whoami.ts +69 -0
  151. package/src/cmd/bundle/ast.test.ts +565 -0
  152. package/src/cmd/bundle/ast.ts +457 -44
  153. package/src/cmd/bundle/bundler.ts +255 -57
  154. package/src/cmd/bundle/file.ts +6 -12
  155. package/src/cmd/bundle/fix-duplicate-exports.test.ts +387 -0
  156. package/src/cmd/bundle/fix-duplicate-exports.ts +204 -0
  157. package/src/cmd/bundle/index.ts +11 -11
  158. package/src/cmd/bundle/patch/aisdk.ts +1 -1
  159. package/src/cmd/bundle/plugin.ts +373 -53
  160. package/src/cmd/cloud/deploy.ts +336 -0
  161. package/src/cmd/cloud/domain.ts +92 -0
  162. package/src/cmd/cloud/index.ts +11 -0
  163. package/src/cmd/cloud/resource/add.ts +56 -0
  164. package/src/cmd/cloud/resource/delete.ts +120 -0
  165. package/src/cmd/cloud/resource/index.ts +11 -0
  166. package/src/cmd/cloud/resource/list.ts +69 -0
  167. package/src/cmd/cloud/scp/download.ts +59 -0
  168. package/src/cmd/cloud/scp/index.ts +9 -0
  169. package/src/cmd/cloud/scp/upload.ts +62 -0
  170. package/src/cmd/cloud/ssh.ts +68 -0
  171. package/src/cmd/dev/api.ts +46 -0
  172. package/src/cmd/dev/download.ts +111 -0
  173. package/src/cmd/dev/index.ts +362 -34
  174. package/src/cmd/dev/templates.ts +84 -0
  175. package/src/cmd/env/delete.ts +47 -0
  176. package/src/cmd/env/get.ts +53 -0
  177. package/src/cmd/env/import.ts +102 -0
  178. package/src/cmd/env/index.ts +22 -0
  179. package/src/cmd/env/list.ts +56 -0
  180. package/src/cmd/env/pull.ts +80 -0
  181. package/src/cmd/env/push.ts +37 -0
  182. package/src/cmd/env/set.ts +71 -0
  183. package/src/cmd/index.ts +2 -2
  184. package/src/cmd/profile/show.ts +15 -6
  185. package/src/cmd/project/create.ts +7 -2
  186. package/src/cmd/project/delete.ts +75 -18
  187. package/src/cmd/project/download.ts +3 -3
  188. package/src/cmd/project/list.ts +8 -8
  189. package/src/cmd/project/show.ts +3 -7
  190. package/src/cmd/project/template-flow.ts +186 -48
  191. package/src/cmd/secret/delete.ts +40 -0
  192. package/src/cmd/secret/get.ts +54 -0
  193. package/src/cmd/secret/import.ts +64 -0
  194. package/src/cmd/secret/index.ts +22 -0
  195. package/src/cmd/secret/list.ts +56 -0
  196. package/src/cmd/secret/pull.ts +78 -0
  197. package/src/cmd/secret/push.ts +37 -0
  198. package/src/cmd/secret/set.ts +45 -0
  199. package/src/cmd/version/index.ts +2 -1
  200. package/src/config.ts +257 -27
  201. package/src/crypto/box.test.ts +431 -0
  202. package/src/crypto/box.ts +477 -0
  203. package/src/download.ts +1 -0
  204. package/src/env-util.test.ts +194 -0
  205. package/src/env-util.ts +290 -0
  206. package/src/index.ts +5 -1
  207. package/src/schema-parser.ts +2 -3
  208. package/src/steps.ts +144 -10
  209. package/src/terminal.ts +24 -23
  210. package/src/tui.ts +208 -68
  211. package/src/types.ts +292 -202
  212. package/src/utils/detectSubagent.ts +31 -0
  213. package/src/utils/zip.ts +38 -0
  214. package/dist/cmd/example/create-user.d.ts +0 -2
  215. package/dist/cmd/example/create-user.d.ts.map +0 -1
  216. package/dist/cmd/example/create.d.ts +0 -2
  217. package/dist/cmd/example/create.d.ts.map +0 -1
  218. package/dist/cmd/example/deploy.d.ts.map +0 -1
  219. package/dist/cmd/example/index.d.ts.map +0 -1
  220. package/dist/cmd/example/list.d.ts.map +0 -1
  221. package/dist/cmd/example/optional-auth.d.ts +0 -3
  222. package/dist/cmd/example/optional-auth.d.ts.map +0 -1
  223. package/dist/cmd/example/run-command.d.ts +0 -2
  224. package/dist/cmd/example/run-command.d.ts.map +0 -1
  225. package/dist/cmd/example/sound.d.ts +0 -3
  226. package/dist/cmd/example/sound.d.ts.map +0 -1
  227. package/dist/cmd/example/spinner.d.ts +0 -2
  228. package/dist/cmd/example/spinner.d.ts.map +0 -1
  229. package/dist/cmd/example/steps.d.ts +0 -2
  230. package/dist/cmd/example/steps.d.ts.map +0 -1
  231. package/dist/cmd/example/version.d.ts +0 -2
  232. package/dist/cmd/example/version.d.ts.map +0 -1
  233. package/dist/logger.d.ts +0 -24
  234. package/dist/logger.d.ts.map +0 -1
  235. package/src/cmd/example/create-user.ts +0 -38
  236. package/src/cmd/example/create.ts +0 -31
  237. package/src/cmd/example/deploy.ts +0 -36
  238. package/src/cmd/example/index.ts +0 -29
  239. package/src/cmd/example/list.ts +0 -32
  240. package/src/cmd/example/optional-auth.ts +0 -38
  241. package/src/cmd/example/run-command.ts +0 -45
  242. package/src/cmd/example/sound.ts +0 -14
  243. package/src/cmd/example/spinner.ts +0 -44
  244. package/src/cmd/example/steps.ts +0 -66
  245. package/src/cmd/example/version.ts +0 -13
  246. package/src/logger.ts +0 -235
  247. /package/dist/cmd/{example → cloud}/deploy.d.ts +0 -0
  248. /package/dist/cmd/{example → cloud}/index.d.ts +0 -0
  249. /package/dist/cmd/{example → env}/list.d.ts +0 -0
@@ -1,26 +1,49 @@
1
- import { createCommand } from '../../types';
1
+ /** biome-ignore-all lint/style/useTemplate: its easier */
2
2
  import { z } from 'zod';
3
+ import type { BuildMetadata } from '@agentuity/server';
3
4
  import { resolve, join } from 'node:path';
4
5
  import { bundle } from '../bundle/bundler';
5
- import { existsSync, FSWatcher, watch } from 'node:fs';
6
- import { loadBuildMetadata } from '../../config';
7
- import type { BuildMetadata } from '../../types';
6
+ import { getBuildMetadata } from '../bundle/plugin';
7
+ import { existsSync, type FSWatcher, watch, statSync, readdirSync } from 'node:fs';
8
+ import {
9
+ getDefaultConfigDir,
10
+ loadDevelopmentProjectSDKKey,
11
+ saveProjectDir,
12
+ saveConfig,
13
+ } from '../../config';
14
+ import { type Config, createCommand } from '../../types';
8
15
  import * as tui from '../../tui';
16
+ import { createAgentTemplates, createAPITemplates } from './templates';
17
+ import { generateEndpoint, type DevmodeResponse } from './api';
18
+ import { APIClient, getAPIBaseURL } from '../../api';
19
+ import { download } from './download';
9
20
 
10
21
  export const command = createCommand({
11
22
  name: 'dev',
12
23
  description: 'Build and run the development server',
13
24
  schema: {
14
25
  options: z.object({
15
- dir: z.string().optional().describe('Root directory of the project'),
26
+ local: z.boolean().optional().describe('Turn on local services (instead of cloud)'),
27
+ public: z
28
+ .boolean()
29
+ .optional()
30
+ .default(!process.env.CI)
31
+ .describe('Turn on or off the public url'),
32
+ port: z
33
+ .number()
34
+ .min(1024) // should we allow a lower root port? probably not?
35
+ .max(65535)
36
+ .default(3500)
37
+ .describe('The TCP port to start the dev start'),
16
38
  }),
17
39
  },
18
- optionalAuth: 'Continue without an account (local only)',
40
+ optional: { auth: 'Continue without an account (local only)', project: true },
19
41
 
20
42
  async handler(ctx) {
21
- const { opts, logger } = ctx;
43
+ const { opts, logger, options, project, projectDir, auth } = ctx;
44
+ let { config } = ctx;
22
45
 
23
- const rootDir = resolve(opts.dir || process.cwd());
46
+ const rootDir = projectDir;
24
47
  const appTs = join(rootDir, 'app.ts');
25
48
  const srcDir = join(rootDir, 'src');
26
49
  const mustHaves = [join(rootDir, 'package.json'), appTs, srcDir];
@@ -40,13 +63,69 @@ export const command = createCommand({
40
63
  process.exit(1);
41
64
  }
42
65
 
66
+ await saveProjectDir(rootDir);
67
+
68
+ let devmode: DevmodeResponse | undefined;
69
+ let gravityBin: string | undefined;
70
+
71
+ if (auth && project && opts.public) {
72
+ // Only create apiClient if auth is available
73
+ const apiClient = new APIClient(getAPIBaseURL(config), logger, config);
74
+ const endpoint = await tui.spinner({
75
+ message: 'Connecting to Gravity',
76
+ callback: () => {
77
+ return generateEndpoint(apiClient, project.projectId, config?.devmode?.hostname);
78
+ },
79
+ clearOnSuccess: true,
80
+ });
81
+ const _config = { ...config } as Config;
82
+ _config.devmode = {
83
+ hostname: endpoint.hostname,
84
+ };
85
+ await saveConfig(_config);
86
+ config = _config;
87
+ devmode = endpoint;
88
+ }
89
+
90
+ if (devmode) {
91
+ const configDir = getDefaultConfigDir();
92
+ const gravityDir = join(configDir, 'gravity');
93
+ let mustCheck = true;
94
+ if (
95
+ config?.gravity?.version &&
96
+ existsSync(join(gravityDir, config.gravity.version, 'gravity')) &&
97
+ config?.gravity?.checked
98
+ ) {
99
+ if (Date.now() - config.gravity.checked < 3.6e6) {
100
+ mustCheck = false;
101
+ gravityBin = join(gravityDir, config.gravity.version, 'gravity');
102
+ }
103
+ }
104
+ if (mustCheck) {
105
+ const res = await download(gravityDir);
106
+ gravityBin = res.filename;
107
+ const _config = { ...config } as Config;
108
+ _config.gravity = {
109
+ checked: Date.now(),
110
+ version: res.version,
111
+ };
112
+ await saveConfig(_config);
113
+ config = _config;
114
+ }
115
+ }
116
+
117
+ const canDoInput = !!(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
118
+
43
119
  const devmodebody =
44
- tui.muted('Local: ') +
45
- tui.link('http://127.0.0.1:3000') +
46
- '\n\n' +
47
- tui.muted('Press ') +
48
- tui.bold('h') +
49
- tui.muted(' for keyboard shortcuts');
120
+ tui.muted(tui.padRight('Local:', 10)) +
121
+ tui.link(`http://127.0.0.1:${opts.port}`) +
122
+ '\n' +
123
+ tui.muted(tui.padRight('Public:', 10)) +
124
+ (devmode?.hostname ? tui.link(`https://${devmode.hostname}`) : tui.warn('Disabled')) +
125
+ '\n' +
126
+ (canDoInput
127
+ ? '\n' + tui.muted('Press ') + tui.bold('h') + tui.muted(' for keyboard shortcuts')
128
+ : '');
50
129
 
51
130
  function showBanner() {
52
131
  tui.banner('⨺ Agentuity DevMode', devmodebody, {
@@ -63,8 +142,39 @@ export const command = createCommand({
63
142
  env.AGENTUITY_SDK_DEV_MODE = 'true';
64
143
  env.AGENTUITY_ENV = 'development';
65
144
  env.NODE_ENV = 'development';
66
- env.PORT = '3000';
145
+ env.PORT = Number(opts.port).toFixed();
67
146
  env.AGENTUITY_PORT = env.PORT;
147
+ if (options.logLevel !== undefined) env.AGENTUITY_LOG_LEVEL = options.logLevel;
148
+ // Pass through AGENTUITY_SDK_LOG_LEVEL for internal SDK logger
149
+ if (process.env.AGENTUITY_SDK_LOG_LEVEL) {
150
+ env.AGENTUITY_SDK_LOG_LEVEL = process.env.AGENTUITY_SDK_LOG_LEVEL;
151
+ }
152
+ env.AGENTUITY_FORCE_LOCAL_SERVICES = opts.local === true ? 'true' : 'false';
153
+ if (config?.overrides?.transport_url) {
154
+ env.AGENTUITY_TRANSPORT_URL = config.overrides.transport_url;
155
+ }
156
+ if (config?.overrides?.catalyst_url) {
157
+ env.AGENTUITY_CATALYST_URL = config.overrides.catalyst_url;
158
+ }
159
+ if (config?.overrides?.vector_url) {
160
+ env.AGENTUITY_VECTOR_URL = config.overrides.vector_url;
161
+ }
162
+ if (config?.overrides?.object_url) {
163
+ env.AGENTUITY_OBJECTSTORE_URL = config.overrides.object_url;
164
+ }
165
+ if (config?.overrides?.kv_url) {
166
+ env.AGENTUITY_KEYVALUE_URL = config.overrides.kv_url;
167
+ }
168
+ if (config?.overrides?.stream_url) {
169
+ env.AGENTUITY_STREAM_URL = config.overrides.stream_url;
170
+ }
171
+ if (project) {
172
+ env.AGENTUITY_CLOUD_ORG_ID = project.orgId;
173
+ env.AGENTUITY_CLOUD_PROJECT_ID = project.projectId;
174
+ }
175
+ if (!process.stdout.isTTY) {
176
+ env.NO_COLOR = '1';
177
+ }
68
178
 
69
179
  const agentuityDir = resolve(rootDir, '.agentuity');
70
180
  const appPath = resolve(agentuityDir, 'app.js');
@@ -82,7 +192,57 @@ export const command = createCommand({
82
192
  let shuttingDownForRestart = false;
83
193
  let pendingRestart = false;
84
194
  let building = false;
85
- let metadata: BuildMetadata | undefined;
195
+ let buildCompletedAt = 0;
196
+ const BUILD_COOLDOWN_MS = 500; // Ignore file changes for 500ms after build completes
197
+ let metadata: Partial<BuildMetadata> | undefined;
198
+ let showInitialReadyMessage = true;
199
+ let serverStartTime = 0;
200
+ let gravityClient: Bun.Subprocess | undefined;
201
+
202
+ if (gravityBin && devmode && project) {
203
+ const sdkKey = await loadDevelopmentProjectSDKKey(rootDir);
204
+ if (!sdkKey) {
205
+ tui.warning(`Couldn't find the AGENTUITY_SDK_KEY in ${rootDir} .env file`);
206
+ } else {
207
+ const gravityBinExists = await Bun.file(gravityBin).exists();
208
+ if (!gravityBinExists) {
209
+ logger.error(
210
+ `Gravity binary not found at ${gravityBin}, skipping gravity client startup`
211
+ );
212
+ } else {
213
+ try {
214
+ gravityClient = Bun.spawn(
215
+ [
216
+ gravityBin,
217
+ '--endpoint-id',
218
+ devmode.id,
219
+ '--port',
220
+ env.PORT,
221
+ '--url',
222
+ config?.overrides?.gravity_url ?? 'grpc://devmode.agentuity.com',
223
+ '--log-level',
224
+ 'error',
225
+ ],
226
+ {
227
+ cwd: rootDir,
228
+ stdout: 'inherit',
229
+ stderr: 'inherit',
230
+ stdin: 'ignore',
231
+ env: { ...env, AGENTUITY_SDK_KEY: sdkKey },
232
+ }
233
+ );
234
+ gravityClient.exited.then(() => {
235
+ logger.debug('gravity client exited');
236
+ });
237
+ } catch (err) {
238
+ logger.error(
239
+ 'Failed to spawn gravity client: %s',
240
+ err instanceof Error ? err.message : String(err)
241
+ );
242
+ }
243
+ }
244
+ }
245
+ }
86
246
 
87
247
  // Track restart timestamps to detect restart loops
88
248
  const restartTimestamps: number[] = [];
@@ -94,7 +254,7 @@ export const command = createCommand({
94
254
  restartTimestamps.push(now);
95
255
 
96
256
  // Remove timestamps older than the time window
97
- while (restartTimestamps.length > 0 && now - restartTimestamps[0]! > TIME_WINDOW_MS) {
257
+ while (restartTimestamps.length > 0 && now - restartTimestamps[0] > TIME_WINDOW_MS) {
98
258
  restartTimestamps.shift();
99
259
  }
100
260
 
@@ -158,7 +318,13 @@ export const command = createCommand({
158
318
  };
159
319
 
160
320
  // Handle signals to ensure entire process tree is killed
161
- const cleanup = () => {
321
+ const cleanup = (exitCode = 0) => {
322
+ logger.trace('cleanup() called with exitCode=%d', exitCode);
323
+ if (gravityClient) {
324
+ logger.debug('calling kill on gravity client with pid: %d', gravityClient.pid);
325
+ gravityClient.kill('SIGINT');
326
+ gravityClient = undefined;
327
+ }
162
328
  if (pid && running) {
163
329
  kill();
164
330
  }
@@ -166,11 +332,22 @@ export const command = createCommand({
166
332
  watcher.close();
167
333
  }
168
334
  watchers.length = 0;
169
- process.exit(0);
335
+ process.exit(exitCode);
170
336
  };
171
337
 
172
338
  process.on('SIGINT', cleanup);
173
339
  process.on('SIGTERM', cleanup);
340
+ process.on('SIGQUIT', cleanup);
341
+ process.on('exit', () => {
342
+ // Synchronous cleanup on exit
343
+ if (gravityClient) {
344
+ try {
345
+ gravityClient.kill('SIGINT');
346
+ } catch {
347
+ // Ignore errors
348
+ }
349
+ }
350
+ });
174
351
 
175
352
  async function restart() {
176
353
  // Queue restart if already restarting
@@ -195,6 +372,7 @@ export const command = createCommand({
195
372
  } else {
196
373
  logger.trace('Initial server start');
197
374
  }
375
+ logger.trace('Starting typecheck and build...');
198
376
  await Promise.all([
199
377
  tui.runCommand({
200
378
  command: 'tsc',
@@ -207,35 +385,44 @@ export const command = createCommand({
207
385
  }),
208
386
  tui.spinner('Building project', async () => {
209
387
  try {
388
+ logger.trace('Bundle starting...');
210
389
  building = true;
211
390
  await bundle({
212
391
  rootDir,
213
392
  dev: true,
214
393
  });
215
394
  building = false;
216
- } catch {
395
+ buildCompletedAt = Date.now();
396
+ logger.trace('Bundle completed successfully');
397
+ } catch (error) {
217
398
  building = false;
399
+ logger.trace('Bundle failed: %s', error);
218
400
  failure('Build failed');
219
401
  }
220
402
  }),
221
403
  ]);
404
+ logger.trace('Typecheck and build completed');
222
405
 
223
406
  if (failed) {
407
+ logger.trace('Restart failed, returning early');
224
408
  return;
225
409
  }
226
410
 
411
+ logger.trace('Checking if app file exists: %s', appPath);
227
412
  if (!existsSync(appPath)) {
413
+ logger.trace('App file not found: %s', appPath);
228
414
  failure(`App file not found: ${appPath}`);
229
415
  return;
230
416
  }
417
+ logger.trace('App file exists, getting build metadata...');
231
418
 
232
- metadata = await loadBuildMetadata(agentuityDir);
233
-
234
- env.AGENTUITY_LOG_LEVEL = logger.level;
419
+ metadata = getBuildMetadata();
420
+ logger.trace('Build metadata retrieved');
235
421
 
236
422
  logger.trace('Starting dev server: %s', appPath);
237
423
  // Use shell to run in a process group for proper cleanup
238
424
  // The 'exec' ensures the shell is replaced by the actual process
425
+ logger.trace('Spawning dev server process...');
239
426
  devServer = Bun.spawn(['sh', '-c', `exec bun run "${appPath}"`], {
240
427
  cwd: rootDir,
241
428
  stdout: 'inherit',
@@ -244,33 +431,57 @@ export const command = createCommand({
244
431
  env,
245
432
  });
246
433
 
434
+ logger.trace('Dev server process spawned, setting up state...');
247
435
  running = true;
248
436
  failed = false;
249
437
  pid = devServer.pid;
250
438
  exitPromise = devServer.exited;
439
+ serverStartTime = Date.now();
251
440
  logger.trace('Dev server started (pid: %d)', pid);
252
441
 
442
+ if (showInitialReadyMessage) {
443
+ showInitialReadyMessage = false;
444
+ logger.info('DevMode ready 🚀');
445
+ logger.trace('Initial ready message logged');
446
+ }
447
+
448
+ logger.trace('Attaching exit handler to dev server process...');
253
449
  // Attach non-blocking exit handler
254
450
  exitPromise
255
451
  .then((exitCode) => {
452
+ const runtime = Date.now() - serverStartTime;
256
453
  logger.trace(
257
- 'Dev server exited with code %d (shuttingDownForRestart=%s)',
454
+ 'Dev server exited with code %d (shuttingDownForRestart=%s, runtime=%dms)',
258
455
  exitCode,
259
- shuttingDownForRestart
456
+ shuttingDownForRestart,
457
+ runtime
260
458
  );
261
459
  running = false;
262
460
  devServer = undefined;
263
461
  exitPromise = undefined;
264
- // Only exit the CLI if this is a clean exit AND not a restart
265
- if (exitCode === 0 && !shuttingDownForRestart) {
462
+ // If server exited immediately after starting (< 2 seconds), treat as failure and restart
463
+ if (runtime < 2000 && !shuttingDownForRestart) {
464
+ logger.trace('Server exited too quickly, treating as failure and restarting');
465
+ failure('Server exited immediately after starting');
466
+ // Trigger a restart after a short delay
467
+ setTimeout(() => {
468
+ if (!running && !restarting) {
469
+ logger.trace('Triggering restart after quick exit');
470
+ restart();
471
+ }
472
+ }, 100);
473
+ return;
474
+ }
475
+ // Only exit the CLI if this is a clean exit AND not a restart AND server ran for a while
476
+ if (exitCode === 0 && !shuttingDownForRestart && runtime >= 2000) {
266
477
  logger.trace('Clean exit, stopping CLI');
267
- process.exit(exitCode);
478
+ cleanup(exitCode);
268
479
  }
269
480
  // Non-zero exit codes are treated as restartable failures
270
481
  // But if it's exit code 1 (common error exit), also exit the CLI
271
- if (exitCode === 1 && !shuttingDownForRestart) {
482
+ if (exitCode === 1 && !shuttingDownForRestart && runtime >= 2000) {
272
483
  logger.trace('Server exited with error code 1, stopping CLI');
273
- process.exit(exitCode);
484
+ cleanup(exitCode);
274
485
  }
275
486
  })
276
487
  .catch((error) => {
@@ -291,14 +502,18 @@ export const command = createCommand({
291
502
  }
292
503
  });
293
504
  } catch (error) {
505
+ logger.trace('Restart caught error: %s', error);
294
506
  if (error instanceof Error) {
507
+ logger.trace('Error message: %s, stack: %s', error.message, error.stack);
295
508
  failure(`Dev server failed: ${error.message}`);
296
509
  } else {
510
+ logger.trace('Non-Error exception: %s', String(error));
297
511
  failure('Dev server failed');
298
512
  }
299
513
  running = false;
300
514
  devServer = undefined;
301
515
  } finally {
516
+ logger.trace('Entering restart() finally block...');
302
517
  const hadPendingRestart = pendingRestart;
303
518
  restarting = false;
304
519
  pendingRestart = false;
@@ -321,7 +536,7 @@ export const command = createCommand({
321
536
  logger.trace('Initial restart completed, setting up watchers');
322
537
 
323
538
  // Setup keyboard shortcuts (only if we have a TTY)
324
- if (process.stdin.isTTY) {
539
+ if (canDoInput) {
325
540
  logger.trace('Setting up keyboard shortcuts');
326
541
  process.stdin.setRawMode(true);
327
542
  process.stdin.resume();
@@ -383,6 +598,23 @@ export const command = createCommand({
383
598
  });
384
599
 
385
600
  logger.trace('✓ Keyboard shortcuts enabled');
601
+ } else {
602
+ if (process.stdin) {
603
+ // still need to monitor stdin in case we are pipeing into another process or file etc
604
+ if (typeof process.stdin.setRawMode === 'function') {
605
+ process.stdin.setRawMode(true);
606
+ }
607
+ process.stdin.resume();
608
+ process.stdin.on('data', (data) => {
609
+ const key = data.toString();
610
+ // Handle Ctrl+C
611
+ if (key === '\u0003') {
612
+ cleanup();
613
+ return;
614
+ }
615
+ });
616
+ }
617
+ logger.trace('❌ Keyboard shortcuts disabled');
386
618
  }
387
619
 
388
620
  // Patterns to ignore (generated files that change during build)
@@ -391,14 +623,25 @@ export const command = createCommand({
391
623
  /registry\.generated\.ts$/,
392
624
  /types\.generated\.d\.ts$/,
393
625
  /client\.generated\.js$/,
626
+ /\.tmp$/,
627
+ /\.tsbuildinfo$/,
628
+ /\.agentuity\//,
629
+ // Ignore temporary files created by sed (e.g., sedUprJj0)
630
+ /\/sed[A-Za-z0-9]+$/,
394
631
  ];
395
632
 
633
+ // Helper to check if a file is a temporary file created by sed
634
+ const isSedTempFile = (filePath: string): boolean => {
635
+ const basename = filePath.split('/').pop() || '';
636
+ return /^sed[A-Za-z0-9]+$/.test(basename);
637
+ };
638
+
396
639
  logger.trace('Setting up file watchers for: %s', watches.join(', '));
397
640
  for (const watchDir of watches) {
398
641
  try {
399
642
  logger.trace('Setting up watcher for %s', watchDir);
400
643
  const watcher = watch(watchDir, { recursive: true }, (eventType, changedFile) => {
401
- const absPath = changedFile ? join(watchDir, changedFile) : watchDir;
644
+ const absPath = changedFile ? resolve(watchDir, changedFile) : watchDir;
402
645
 
403
646
  // Ignore file changes during active build to prevent loops
404
647
  if (building) {
@@ -411,6 +654,18 @@ export const command = createCommand({
411
654
  return;
412
655
  }
413
656
 
657
+ // Ignore file changes immediately after build completes (cooldown period)
658
+ // This prevents restarts from build output files that are written asynchronously
659
+ if (buildCompletedAt > 0 && Date.now() - buildCompletedAt < BUILD_COOLDOWN_MS) {
660
+ logger.trace(
661
+ 'File change ignored (build cooldown): %s (event: %s, file: %s)',
662
+ watchDir,
663
+ eventType,
664
+ changedFile
665
+ );
666
+ return;
667
+ }
668
+
414
669
  // Ignore node_modules folder
415
670
  if (absPath.includes('node_modules')) {
416
671
  logger.trace(
@@ -423,7 +678,12 @@ export const command = createCommand({
423
678
  }
424
679
 
425
680
  // Ignore changes in .agentuity directory (build output)
426
- if (absPath.startsWith(agentuityDir)) {
681
+ // Check both relative path and normalized absolute path
682
+ const isInAgentuityDir =
683
+ (changedFile &&
684
+ (changedFile === '.agentuity' || changedFile.startsWith('.agentuity/'))) ||
685
+ resolve(absPath).startsWith(agentuityDir);
686
+ if (isInAgentuityDir) {
427
687
  logger.trace(
428
688
  'File change ignored (.agentuity dir): %s (event: %s, file: %s)',
429
689
  watchDir,
@@ -444,8 +704,61 @@ export const command = createCommand({
444
704
  return;
445
705
  }
446
706
 
707
+ // Check for .tmp file renames that replace watched files (BEFORE ignoring)
708
+ // This handles cases like sed -i.tmp where agent.ts.tmp is renamed to agent.ts
709
+ if (eventType === 'rename' && changedFile && changedFile.endsWith('.tmp')) {
710
+ const targetFile = changedFile.slice(0, -4); // Remove .tmp suffix
711
+ const targetAbsPath = resolve(watchDir, targetFile);
712
+
713
+ // Only trigger restart for source files (ts, tsx, js, jsx, etc.)
714
+ const isSourceFile = /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(targetFile);
715
+
716
+ // Check if target file exists and is not in ignored directories
717
+ const targetExists = existsSync(targetAbsPath);
718
+ const inNodeModules = targetAbsPath.includes('node_modules');
719
+ const inAgentuityDir =
720
+ (targetFile &&
721
+ (targetFile === '.agentuity' || targetFile.startsWith('.agentuity/'))) ||
722
+ resolve(targetAbsPath).startsWith(agentuityDir);
723
+ let isDirectory = false;
724
+ if (targetExists) {
725
+ try {
726
+ isDirectory = statSync(targetAbsPath).isDirectory();
727
+ } catch (err) {
728
+ logger.trace('Failed to stat target file: %s', err);
729
+ }
730
+ }
731
+
732
+ if (
733
+ isSourceFile &&
734
+ targetExists &&
735
+ !inNodeModules &&
736
+ !inAgentuityDir &&
737
+ !isDirectory
738
+ ) {
739
+ logger.trace(
740
+ 'File change detected (temp file rename): %s -> %s',
741
+ absPath,
742
+ targetAbsPath
743
+ );
744
+ restart();
745
+ return;
746
+ }
747
+ }
748
+
447
749
  // Ignore generated files to prevent restart loops
448
750
  if (changedFile) {
751
+ // Check for sed temporary files
752
+ if (isSedTempFile(changedFile)) {
753
+ logger.trace(
754
+ 'File change ignored (sed temp file): %s (event: %s, file: %s)',
755
+ watchDir,
756
+ eventType,
757
+ changedFile
758
+ );
759
+ return;
760
+ }
761
+ // Check other ignore patterns
449
762
  for (const pattern of ignorePatterns) {
450
763
  if (pattern.test(changedFile)) {
451
764
  logger.trace(
@@ -459,6 +772,21 @@ export const command = createCommand({
459
772
  }
460
773
  }
461
774
 
775
+ if (
776
+ eventType === 'rename' &&
777
+ existsSync(absPath) &&
778
+ statSync(absPath).isDirectory() &&
779
+ readdirSync(absPath).length === 0
780
+ ) {
781
+ if (changedFile?.startsWith('src/agents/')) {
782
+ logger.debug('agent directory created: %s', changedFile);
783
+ createAgentTemplates(absPath);
784
+ } else if (changedFile?.startsWith('src/apis/')) {
785
+ logger.debug('api directory created: %s', changedFile);
786
+ createAPITemplates(absPath);
787
+ }
788
+ }
789
+
462
790
  logger.trace(
463
791
  'File change detected: %s (event: %s, file: %s)',
464
792
  absPath,
@@ -476,6 +804,6 @@ export const command = createCommand({
476
804
  logger.debug('Dev server watching for changes');
477
805
 
478
806
  // Keep the handler alive indefinitely
479
- await new Promise(() => {});
807
+ await new Promise(() => {}).catch(() => cleanup());
480
808
  },
481
809
  });
@@ -0,0 +1,84 @@
1
+ /* eslint-disable no-control-regex */
2
+ import { writeFileSync } from 'node:fs';
3
+ import { basename, join } from 'node:path';
4
+
5
+ const newAgentTemplate = (name: string) => `import { createAgent } from '@agentuity/runtime';
6
+ import { z } from 'zod';
7
+
8
+ const agent = createAgent({
9
+ metadata: {
10
+ name: '${name}',
11
+ description: 'Add my agent description here',
12
+ },
13
+ schema: {
14
+ input: z.string(),
15
+ output: z.string(),
16
+ },
17
+ handler: async (_c, input) => {
18
+ // TODO: add your code here
19
+ return input;
20
+ },
21
+ });
22
+
23
+ export default agent;
24
+ `;
25
+
26
+ const newAgentRouteTemplate = (name: string) => `import { createRouter } from '@agentuity/runtime';
27
+
28
+ const router = createRouter();
29
+
30
+ router.get('/', async (c) => {
31
+ // TODO: add your code here
32
+ const output = await c.agent.${name}.run('hello world');
33
+ return c.text(output);
34
+ });
35
+
36
+ export default router;
37
+
38
+ `;
39
+
40
+ const newAPIRouteTemplate = (_name: string) => `import { createRouter } from '@agentuity/runtime';
41
+
42
+ const router = createRouter();
43
+
44
+ router.get('/', async (c) => {
45
+ // TODO: add your code here
46
+ return c.text('Hello');
47
+ });
48
+
49
+ export default router;
50
+
51
+ `;
52
+
53
+ const invalidDirRegex = /[<>:"/\\|?*]/;
54
+
55
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: checking for invalid control characters in directory names
56
+ const invalidControlChars = /[\u0000-\u001F]/;
57
+ const reservedWindowsNames = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
58
+ const invalidTrailing = /[. ]$/;
59
+
60
+ function isValidDirectoryName(name: string): boolean {
61
+ return (
62
+ !invalidDirRegex.test(name) &&
63
+ !invalidControlChars.test(name) &&
64
+ !reservedWindowsNames.test(name) &&
65
+ !invalidTrailing.test(name)
66
+ );
67
+ }
68
+
69
+ export function createAgentTemplates(dir: string) {
70
+ const name = basename(dir);
71
+ if (!isValidDirectoryName(name)) {
72
+ return;
73
+ }
74
+ writeFileSync(join(dir, 'agent.ts'), newAgentTemplate(name));
75
+ writeFileSync(join(dir, 'route.ts'), newAgentRouteTemplate(name));
76
+ }
77
+
78
+ export function createAPITemplates(dir: string) {
79
+ const name = basename(dir);
80
+ if (!isValidDirectoryName(name)) {
81
+ return;
82
+ }
83
+ writeFileSync(join(dir, 'route.ts'), newAPIRouteTemplate(name));
84
+ }