@agentuity/cli 0.0.43 → 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 (209) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/api.d.ts +3 -3
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/auth.d.ts +10 -2
  6. package/dist/auth.d.ts.map +1 -1
  7. package/dist/banner.d.ts.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cmd/auth/api.d.ts +4 -4
  10. package/dist/cmd/auth/api.d.ts.map +1 -1
  11. package/dist/cmd/auth/index.d.ts.map +1 -1
  12. package/dist/cmd/auth/login.d.ts.map +1 -1
  13. package/dist/cmd/auth/signup.d.ts.map +1 -1
  14. package/dist/cmd/auth/ssh/add.d.ts +2 -0
  15. package/dist/cmd/auth/ssh/add.d.ts.map +1 -0
  16. package/dist/cmd/auth/ssh/api.d.ts +16 -0
  17. package/dist/cmd/auth/ssh/api.d.ts.map +1 -0
  18. package/dist/cmd/auth/ssh/delete.d.ts +2 -0
  19. package/dist/cmd/auth/ssh/delete.d.ts.map +1 -0
  20. package/dist/cmd/auth/ssh/index.d.ts +3 -0
  21. package/dist/cmd/auth/ssh/index.d.ts.map +1 -0
  22. package/dist/cmd/auth/ssh/list.d.ts +2 -0
  23. package/dist/cmd/auth/ssh/list.d.ts.map +1 -0
  24. package/dist/cmd/auth/whoami.d.ts.map +1 -1
  25. package/dist/cmd/bundle/ast.d.ts +14 -3
  26. package/dist/cmd/bundle/ast.d.ts.map +1 -1
  27. package/dist/cmd/bundle/ast.test.d.ts +2 -0
  28. package/dist/cmd/bundle/ast.test.d.ts.map +1 -0
  29. package/dist/cmd/bundle/bundler.d.ts +6 -1
  30. package/dist/cmd/bundle/bundler.d.ts.map +1 -1
  31. package/dist/cmd/bundle/file.d.ts.map +1 -1
  32. package/dist/cmd/bundle/fix-duplicate-exports.d.ts +2 -0
  33. package/dist/cmd/bundle/fix-duplicate-exports.d.ts.map +1 -0
  34. package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts +2 -0
  35. package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts.map +1 -0
  36. package/dist/cmd/bundle/plugin.d.ts +2 -0
  37. package/dist/cmd/bundle/plugin.d.ts.map +1 -1
  38. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  39. package/dist/cmd/cloud/domain.d.ts +17 -0
  40. package/dist/cmd/cloud/domain.d.ts.map +1 -0
  41. package/dist/cmd/cloud/index.d.ts.map +1 -1
  42. package/dist/cmd/cloud/resource/add.d.ts +2 -0
  43. package/dist/cmd/cloud/resource/add.d.ts.map +1 -0
  44. package/dist/cmd/cloud/resource/delete.d.ts +2 -0
  45. package/dist/cmd/cloud/resource/delete.d.ts.map +1 -0
  46. package/dist/cmd/cloud/resource/index.d.ts +3 -0
  47. package/dist/cmd/cloud/resource/index.d.ts.map +1 -0
  48. package/dist/cmd/cloud/resource/list.d.ts +2 -0
  49. package/dist/cmd/cloud/resource/list.d.ts.map +1 -0
  50. package/dist/cmd/cloud/scp/download.d.ts +2 -0
  51. package/dist/cmd/cloud/scp/download.d.ts.map +1 -0
  52. package/dist/cmd/cloud/scp/index.d.ts +3 -0
  53. package/dist/cmd/cloud/scp/index.d.ts.map +1 -0
  54. package/dist/cmd/cloud/scp/upload.d.ts +2 -0
  55. package/dist/cmd/cloud/scp/upload.d.ts.map +1 -0
  56. package/dist/cmd/cloud/ssh.d.ts +2 -0
  57. package/dist/cmd/cloud/ssh.d.ts.map +1 -0
  58. package/dist/cmd/dev/api.d.ts +18 -0
  59. package/dist/cmd/dev/api.d.ts.map +1 -0
  60. package/dist/cmd/dev/download.d.ts +11 -0
  61. package/dist/cmd/dev/download.d.ts.map +1 -0
  62. package/dist/cmd/dev/index.d.ts.map +1 -1
  63. package/dist/cmd/dev/templates.d.ts +3 -0
  64. package/dist/cmd/dev/templates.d.ts.map +1 -0
  65. package/dist/cmd/env/delete.d.ts.map +1 -1
  66. package/dist/cmd/env/get.d.ts.map +1 -1
  67. package/dist/cmd/env/import.d.ts.map +1 -1
  68. package/dist/cmd/env/list.d.ts.map +1 -1
  69. package/dist/cmd/env/pull.d.ts.map +1 -1
  70. package/dist/cmd/env/push.d.ts.map +1 -1
  71. package/dist/cmd/env/set.d.ts.map +1 -1
  72. package/dist/cmd/profile/show.d.ts.map +1 -1
  73. package/dist/cmd/project/create.d.ts.map +1 -1
  74. package/dist/cmd/project/delete.d.ts.map +1 -1
  75. package/dist/cmd/project/list.d.ts.map +1 -1
  76. package/dist/cmd/project/show.d.ts.map +1 -1
  77. package/dist/cmd/project/template-flow.d.ts +4 -0
  78. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  79. package/dist/cmd/secret/delete.d.ts.map +1 -1
  80. package/dist/cmd/secret/get.d.ts.map +1 -1
  81. package/dist/cmd/secret/import.d.ts.map +1 -1
  82. package/dist/cmd/secret/list.d.ts.map +1 -1
  83. package/dist/cmd/secret/pull.d.ts.map +1 -1
  84. package/dist/cmd/secret/push.d.ts.map +1 -1
  85. package/dist/cmd/secret/set.d.ts.map +1 -1
  86. package/dist/config.d.ts +9 -3
  87. package/dist/config.d.ts.map +1 -1
  88. package/dist/crypto/box.d.ts +65 -0
  89. package/dist/crypto/box.d.ts.map +1 -0
  90. package/dist/crypto/box.test.d.ts +2 -0
  91. package/dist/crypto/box.test.d.ts.map +1 -0
  92. package/dist/download.d.ts.map +1 -1
  93. package/dist/steps.d.ts +4 -1
  94. package/dist/steps.d.ts.map +1 -1
  95. package/dist/terminal.d.ts.map +1 -1
  96. package/dist/tui.d.ts +31 -1
  97. package/dist/tui.d.ts.map +1 -1
  98. package/dist/types.d.ts +249 -126
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/utils/detectSubagent.d.ts +15 -0
  101. package/dist/utils/detectSubagent.d.ts.map +1 -0
  102. package/dist/utils/zip.d.ts +7 -0
  103. package/dist/utils/zip.d.ts.map +1 -0
  104. package/package.json +11 -3
  105. package/src/api-errors.md +2 -2
  106. package/src/api.ts +12 -7
  107. package/src/auth.ts +116 -7
  108. package/src/banner.ts +13 -6
  109. package/src/cli.ts +695 -63
  110. package/src/cmd/auth/api.ts +10 -16
  111. package/src/cmd/auth/index.ts +2 -1
  112. package/src/cmd/auth/login.ts +24 -8
  113. package/src/cmd/auth/signup.ts +15 -11
  114. package/src/cmd/auth/ssh/add.ts +263 -0
  115. package/src/cmd/auth/ssh/api.ts +94 -0
  116. package/src/cmd/auth/ssh/delete.ts +102 -0
  117. package/src/cmd/auth/ssh/index.ts +10 -0
  118. package/src/cmd/auth/ssh/list.ts +74 -0
  119. package/src/cmd/auth/whoami.ts +13 -13
  120. package/src/cmd/bundle/ast.test.ts +565 -0
  121. package/src/cmd/bundle/ast.ts +457 -44
  122. package/src/cmd/bundle/bundler.ts +255 -57
  123. package/src/cmd/bundle/file.ts +6 -12
  124. package/src/cmd/bundle/fix-duplicate-exports.test.ts +387 -0
  125. package/src/cmd/bundle/fix-duplicate-exports.ts +204 -0
  126. package/src/cmd/bundle/index.ts +9 -9
  127. package/src/cmd/bundle/patch/aisdk.ts +1 -1
  128. package/src/cmd/bundle/plugin.ts +373 -53
  129. package/src/cmd/cloud/deploy.ts +300 -93
  130. package/src/cmd/cloud/domain.ts +92 -0
  131. package/src/cmd/cloud/index.ts +4 -1
  132. package/src/cmd/cloud/resource/add.ts +56 -0
  133. package/src/cmd/cloud/resource/delete.ts +120 -0
  134. package/src/cmd/cloud/resource/index.ts +11 -0
  135. package/src/cmd/cloud/resource/list.ts +69 -0
  136. package/src/cmd/cloud/scp/download.ts +59 -0
  137. package/src/cmd/cloud/scp/index.ts +9 -0
  138. package/src/cmd/cloud/scp/upload.ts +62 -0
  139. package/src/cmd/cloud/ssh.ts +68 -0
  140. package/src/cmd/dev/api.ts +46 -0
  141. package/src/cmd/dev/download.ts +111 -0
  142. package/src/cmd/dev/index.ts +360 -34
  143. package/src/cmd/dev/templates.ts +84 -0
  144. package/src/cmd/env/delete.ts +5 -20
  145. package/src/cmd/env/get.ts +5 -18
  146. package/src/cmd/env/import.ts +5 -20
  147. package/src/cmd/env/list.ts +5 -18
  148. package/src/cmd/env/pull.ts +10 -23
  149. package/src/cmd/env/push.ts +5 -23
  150. package/src/cmd/env/set.ts +5 -20
  151. package/src/cmd/index.ts +2 -2
  152. package/src/cmd/profile/show.ts +15 -6
  153. package/src/cmd/project/create.ts +7 -2
  154. package/src/cmd/project/delete.ts +75 -18
  155. package/src/cmd/project/download.ts +2 -2
  156. package/src/cmd/project/list.ts +8 -8
  157. package/src/cmd/project/show.ts +3 -7
  158. package/src/cmd/project/template-flow.ts +170 -72
  159. package/src/cmd/secret/delete.ts +5 -20
  160. package/src/cmd/secret/get.ts +5 -18
  161. package/src/cmd/secret/import.ts +5 -20
  162. package/src/cmd/secret/list.ts +5 -18
  163. package/src/cmd/secret/pull.ts +10 -23
  164. package/src/cmd/secret/push.ts +5 -23
  165. package/src/cmd/secret/set.ts +5 -20
  166. package/src/config.ts +224 -24
  167. package/src/crypto/box.test.ts +431 -0
  168. package/src/crypto/box.ts +477 -0
  169. package/src/download.ts +1 -0
  170. package/src/env-util.test.ts +1 -1
  171. package/src/steps.ts +65 -6
  172. package/src/terminal.ts +24 -23
  173. package/src/tui.ts +192 -61
  174. package/src/types.ts +291 -201
  175. package/src/utils/detectSubagent.ts +31 -0
  176. package/src/utils/zip.ts +38 -0
  177. package/dist/cmd/example/create-user.d.ts +0 -2
  178. package/dist/cmd/example/create-user.d.ts.map +0 -1
  179. package/dist/cmd/example/create.d.ts +0 -2
  180. package/dist/cmd/example/create.d.ts.map +0 -1
  181. package/dist/cmd/example/deploy.d.ts +0 -2
  182. package/dist/cmd/example/deploy.d.ts.map +0 -1
  183. package/dist/cmd/example/index.d.ts +0 -2
  184. package/dist/cmd/example/index.d.ts.map +0 -1
  185. package/dist/cmd/example/list.d.ts +0 -2
  186. package/dist/cmd/example/list.d.ts.map +0 -1
  187. package/dist/cmd/example/optional-auth.d.ts +0 -3
  188. package/dist/cmd/example/optional-auth.d.ts.map +0 -1
  189. package/dist/cmd/example/run-command.d.ts +0 -2
  190. package/dist/cmd/example/run-command.d.ts.map +0 -1
  191. package/dist/cmd/example/sound.d.ts +0 -3
  192. package/dist/cmd/example/sound.d.ts.map +0 -1
  193. package/dist/cmd/example/spinner.d.ts +0 -2
  194. package/dist/cmd/example/spinner.d.ts.map +0 -1
  195. package/dist/cmd/example/steps.d.ts +0 -2
  196. package/dist/cmd/example/steps.d.ts.map +0 -1
  197. package/dist/cmd/example/version.d.ts +0 -2
  198. package/dist/cmd/example/version.d.ts.map +0 -1
  199. package/src/cmd/example/create-user.ts +0 -38
  200. package/src/cmd/example/create.ts +0 -31
  201. package/src/cmd/example/deploy.ts +0 -36
  202. package/src/cmd/example/index.ts +0 -29
  203. package/src/cmd/example/list.ts +0 -32
  204. package/src/cmd/example/optional-auth.ts +0 -38
  205. package/src/cmd/example/run-command.ts +0 -45
  206. package/src/cmd/example/sound.ts +0 -14
  207. package/src/cmd/example/spinner.ts +0 -44
  208. package/src/cmd/example/steps.ts +0 -66
  209. package/src/cmd/example/version.ts +0 -13
@@ -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, saveProjectDir } 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, options } = 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];
@@ -42,13 +65,67 @@ export const command = createCommand({
42
65
 
43
66
  await saveProjectDir(rootDir);
44
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
+
45
119
  const devmodebody =
46
- tui.muted('Local: ') +
47
- tui.link('http://127.0.0.1:3000') +
48
- '\n\n' +
49
- tui.muted('Press ') +
50
- tui.bold('h') +
51
- 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
+ : '');
52
129
 
53
130
  function showBanner() {
54
131
  tui.banner('⨺ Agentuity DevMode', devmodebody, {
@@ -65,8 +142,39 @@ export const command = createCommand({
65
142
  env.AGENTUITY_SDK_DEV_MODE = 'true';
66
143
  env.AGENTUITY_ENV = 'development';
67
144
  env.NODE_ENV = 'development';
68
- env.PORT = '3000';
145
+ env.PORT = Number(opts.port).toFixed();
69
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
+ }
70
178
 
71
179
  const agentuityDir = resolve(rootDir, '.agentuity');
72
180
  const appPath = resolve(agentuityDir, 'app.js');
@@ -84,7 +192,57 @@ export const command = createCommand({
84
192
  let shuttingDownForRestart = false;
85
193
  let pendingRestart = false;
86
194
  let building = false;
87
- 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
+ }
88
246
 
89
247
  // Track restart timestamps to detect restart loops
90
248
  const restartTimestamps: number[] = [];
@@ -96,7 +254,7 @@ export const command = createCommand({
96
254
  restartTimestamps.push(now);
97
255
 
98
256
  // Remove timestamps older than the time window
99
- while (restartTimestamps.length > 0 && now - restartTimestamps[0]! > TIME_WINDOW_MS) {
257
+ while (restartTimestamps.length > 0 && now - restartTimestamps[0] > TIME_WINDOW_MS) {
100
258
  restartTimestamps.shift();
101
259
  }
102
260
 
@@ -160,7 +318,13 @@ export const command = createCommand({
160
318
  };
161
319
 
162
320
  // Handle signals to ensure entire process tree is killed
163
- 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
+ }
164
328
  if (pid && running) {
165
329
  kill();
166
330
  }
@@ -168,11 +332,22 @@ export const command = createCommand({
168
332
  watcher.close();
169
333
  }
170
334
  watchers.length = 0;
171
- process.exit(0);
335
+ process.exit(exitCode);
172
336
  };
173
337
 
174
338
  process.on('SIGINT', cleanup);
175
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
+ });
176
351
 
177
352
  async function restart() {
178
353
  // Queue restart if already restarting
@@ -197,6 +372,7 @@ export const command = createCommand({
197
372
  } else {
198
373
  logger.trace('Initial server start');
199
374
  }
375
+ logger.trace('Starting typecheck and build...');
200
376
  await Promise.all([
201
377
  tui.runCommand({
202
378
  command: 'tsc',
@@ -209,35 +385,44 @@ export const command = createCommand({
209
385
  }),
210
386
  tui.spinner('Building project', async () => {
211
387
  try {
388
+ logger.trace('Bundle starting...');
212
389
  building = true;
213
390
  await bundle({
214
391
  rootDir,
215
392
  dev: true,
216
393
  });
217
394
  building = false;
218
- } catch {
395
+ buildCompletedAt = Date.now();
396
+ logger.trace('Bundle completed successfully');
397
+ } catch (error) {
219
398
  building = false;
399
+ logger.trace('Bundle failed: %s', error);
220
400
  failure('Build failed');
221
401
  }
222
402
  }),
223
403
  ]);
404
+ logger.trace('Typecheck and build completed');
224
405
 
225
406
  if (failed) {
407
+ logger.trace('Restart failed, returning early');
226
408
  return;
227
409
  }
228
410
 
411
+ logger.trace('Checking if app file exists: %s', appPath);
229
412
  if (!existsSync(appPath)) {
413
+ logger.trace('App file not found: %s', appPath);
230
414
  failure(`App file not found: ${appPath}`);
231
415
  return;
232
416
  }
417
+ logger.trace('App file exists, getting build metadata...');
233
418
 
234
- metadata = await loadBuildMetadata(agentuityDir);
235
-
236
- env.AGENTUITY_LOG_LEVEL = options.logLevel;
419
+ metadata = getBuildMetadata();
420
+ logger.trace('Build metadata retrieved');
237
421
 
238
422
  logger.trace('Starting dev server: %s', appPath);
239
423
  // Use shell to run in a process group for proper cleanup
240
424
  // The 'exec' ensures the shell is replaced by the actual process
425
+ logger.trace('Spawning dev server process...');
241
426
  devServer = Bun.spawn(['sh', '-c', `exec bun run "${appPath}"`], {
242
427
  cwd: rootDir,
243
428
  stdout: 'inherit',
@@ -246,33 +431,57 @@ export const command = createCommand({
246
431
  env,
247
432
  });
248
433
 
434
+ logger.trace('Dev server process spawned, setting up state...');
249
435
  running = true;
250
436
  failed = false;
251
437
  pid = devServer.pid;
252
438
  exitPromise = devServer.exited;
439
+ serverStartTime = Date.now();
253
440
  logger.trace('Dev server started (pid: %d)', pid);
254
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...');
255
449
  // Attach non-blocking exit handler
256
450
  exitPromise
257
451
  .then((exitCode) => {
452
+ const runtime = Date.now() - serverStartTime;
258
453
  logger.trace(
259
- 'Dev server exited with code %d (shuttingDownForRestart=%s)',
454
+ 'Dev server exited with code %d (shuttingDownForRestart=%s, runtime=%dms)',
260
455
  exitCode,
261
- shuttingDownForRestart
456
+ shuttingDownForRestart,
457
+ runtime
262
458
  );
263
459
  running = false;
264
460
  devServer = undefined;
265
461
  exitPromise = undefined;
266
- // Only exit the CLI if this is a clean exit AND not a restart
267
- 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) {
268
477
  logger.trace('Clean exit, stopping CLI');
269
- process.exit(exitCode);
478
+ cleanup(exitCode);
270
479
  }
271
480
  // Non-zero exit codes are treated as restartable failures
272
481
  // But if it's exit code 1 (common error exit), also exit the CLI
273
- if (exitCode === 1 && !shuttingDownForRestart) {
482
+ if (exitCode === 1 && !shuttingDownForRestart && runtime >= 2000) {
274
483
  logger.trace('Server exited with error code 1, stopping CLI');
275
- process.exit(exitCode);
484
+ cleanup(exitCode);
276
485
  }
277
486
  })
278
487
  .catch((error) => {
@@ -293,14 +502,18 @@ export const command = createCommand({
293
502
  }
294
503
  });
295
504
  } catch (error) {
505
+ logger.trace('Restart caught error: %s', error);
296
506
  if (error instanceof Error) {
507
+ logger.trace('Error message: %s, stack: %s', error.message, error.stack);
297
508
  failure(`Dev server failed: ${error.message}`);
298
509
  } else {
510
+ logger.trace('Non-Error exception: %s', String(error));
299
511
  failure('Dev server failed');
300
512
  }
301
513
  running = false;
302
514
  devServer = undefined;
303
515
  } finally {
516
+ logger.trace('Entering restart() finally block...');
304
517
  const hadPendingRestart = pendingRestart;
305
518
  restarting = false;
306
519
  pendingRestart = false;
@@ -323,7 +536,7 @@ export const command = createCommand({
323
536
  logger.trace('Initial restart completed, setting up watchers');
324
537
 
325
538
  // Setup keyboard shortcuts (only if we have a TTY)
326
- if (process.stdin.isTTY) {
539
+ if (canDoInput) {
327
540
  logger.trace('Setting up keyboard shortcuts');
328
541
  process.stdin.setRawMode(true);
329
542
  process.stdin.resume();
@@ -385,6 +598,23 @@ export const command = createCommand({
385
598
  });
386
599
 
387
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');
388
618
  }
389
619
 
390
620
  // Patterns to ignore (generated files that change during build)
@@ -393,14 +623,25 @@ export const command = createCommand({
393
623
  /registry\.generated\.ts$/,
394
624
  /types\.generated\.d\.ts$/,
395
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]+$/,
396
631
  ];
397
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
+
398
639
  logger.trace('Setting up file watchers for: %s', watches.join(', '));
399
640
  for (const watchDir of watches) {
400
641
  try {
401
642
  logger.trace('Setting up watcher for %s', watchDir);
402
643
  const watcher = watch(watchDir, { recursive: true }, (eventType, changedFile) => {
403
- const absPath = changedFile ? join(watchDir, changedFile) : watchDir;
644
+ const absPath = changedFile ? resolve(watchDir, changedFile) : watchDir;
404
645
 
405
646
  // Ignore file changes during active build to prevent loops
406
647
  if (building) {
@@ -413,6 +654,18 @@ export const command = createCommand({
413
654
  return;
414
655
  }
415
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
+
416
669
  // Ignore node_modules folder
417
670
  if (absPath.includes('node_modules')) {
418
671
  logger.trace(
@@ -425,7 +678,12 @@ export const command = createCommand({
425
678
  }
426
679
 
427
680
  // Ignore changes in .agentuity directory (build output)
428
- 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) {
429
687
  logger.trace(
430
688
  'File change ignored (.agentuity dir): %s (event: %s, file: %s)',
431
689
  watchDir,
@@ -446,8 +704,61 @@ export const command = createCommand({
446
704
  return;
447
705
  }
448
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
+
449
749
  // Ignore generated files to prevent restart loops
450
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
451
762
  for (const pattern of ignorePatterns) {
452
763
  if (pattern.test(changedFile)) {
453
764
  logger.trace(
@@ -461,6 +772,21 @@ export const command = createCommand({
461
772
  }
462
773
  }
463
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
+
464
790
  logger.trace(
465
791
  'File change detected: %s (event: %s, file: %s)',
466
792
  absPath,
@@ -478,6 +804,6 @@ export const command = createCommand({
478
804
  logger.debug('Dev server watching for changes');
479
805
 
480
806
  // Keep the handler alive indefinitely
481
- await new Promise(() => {});
807
+ await new Promise(() => {}).catch(() => cleanup());
482
808
  },
483
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
+ }