@agentuity/cli 0.0.107 → 0.0.108

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 (53) hide show
  1. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  2. package/dist/cmd/build/entry-generator.js +43 -50
  3. package/dist/cmd/build/entry-generator.js.map +1 -1
  4. package/dist/cmd/build/index.d.ts.map +1 -1
  5. package/dist/cmd/build/index.js +9 -9
  6. package/dist/cmd/build/index.js.map +1 -1
  7. package/dist/cmd/build/typecheck.d.ts +23 -0
  8. package/dist/cmd/build/typecheck.d.ts.map +1 -0
  9. package/dist/cmd/build/typecheck.js +38 -0
  10. package/dist/cmd/build/typecheck.js.map +1 -0
  11. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  12. package/dist/cmd/build/vite/vite-asset-server-config.js +15 -8
  13. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  14. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  15. package/dist/cmd/build/vite/vite-asset-server.js +6 -2
  16. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  17. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  18. package/dist/cmd/build/vite/vite-builder.js +14 -2
  19. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  20. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  21. package/dist/cmd/cloud/deploy.js +67 -6
  22. package/dist/cmd/cloud/deploy.js.map +1 -1
  23. package/dist/cmd/dev/index.d.ts.map +1 -1
  24. package/dist/cmd/dev/index.js +623 -578
  25. package/dist/cmd/dev/index.js.map +1 -1
  26. package/dist/schema-parser.d.ts.map +1 -1
  27. package/dist/schema-parser.js +17 -3
  28. package/dist/schema-parser.js.map +1 -1
  29. package/dist/tsc-output-parser.d.ts +54 -0
  30. package/dist/tsc-output-parser.d.ts.map +1 -0
  31. package/dist/tsc-output-parser.js +926 -0
  32. package/dist/tsc-output-parser.js.map +1 -0
  33. package/dist/tui.d.ts +77 -0
  34. package/dist/tui.d.ts.map +1 -1
  35. package/dist/tui.js +27 -2
  36. package/dist/tui.js.map +1 -1
  37. package/dist/typescript-errors.d.ts +26 -0
  38. package/dist/typescript-errors.d.ts.map +1 -0
  39. package/dist/typescript-errors.js +249 -0
  40. package/dist/typescript-errors.js.map +1 -0
  41. package/package.json +4 -4
  42. package/src/cmd/build/entry-generator.ts +43 -51
  43. package/src/cmd/build/index.ts +13 -10
  44. package/src/cmd/build/typecheck.ts +55 -0
  45. package/src/cmd/build/vite/vite-asset-server-config.ts +17 -8
  46. package/src/cmd/build/vite/vite-asset-server.ts +6 -2
  47. package/src/cmd/build/vite/vite-builder.ts +13 -2
  48. package/src/cmd/cloud/deploy.ts +80 -8
  49. package/src/cmd/dev/index.ts +713 -657
  50. package/src/schema-parser.ts +17 -3
  51. package/src/tsc-output-parser.ts +1115 -0
  52. package/src/tui.ts +40 -2
  53. package/src/typescript-errors.ts +382 -0
@@ -13,6 +13,7 @@ import { download } from './download';
13
13
  import { createDevmodeSyncService } from './sync';
14
14
  import { getDevmodeDeploymentId } from '../build/ast';
15
15
  import { getDefaultConfigDir, saveConfig, loadProjectSDKKey } from '../../config';
16
+ import { typecheck } from '../build/typecheck';
16
17
  import { createFileWatcher } from './file-watcher';
17
18
  import { regenerateSkillsAsync } from './skills';
18
19
  import { prepareDevLock, releaseLockSync } from './dev-lock';
@@ -42,6 +43,9 @@ async function killLingeringGravityProcesses(logger) {
42
43
  // Brief pause to let processes fully terminate
43
44
  await new Promise((resolve) => setTimeout(resolve, 100));
44
45
  }
46
+ else if (result.exitCode === 1) {
47
+ logger.debug('no lingering gravity processes found');
48
+ }
45
49
  }
46
50
  catch {
47
51
  // pkill not available or failed - not critical, continue
@@ -169,638 +173,679 @@ export const command = createCommand({
169
173
  // Kill any lingering gravity processes from previous dev sessions
170
174
  // This is a fallback for cases where the lockfile was corrupted
171
175
  await killLingeringGravityProcesses(logger);
172
- // Setup devmode and gravity (if using public URL)
173
- const useMockService = process.env.DEVMODE_SYNC_SERVICE_MOCK === 'true';
174
- const apiClient = auth ? new APIClient(getAPIBaseURL(config), logger, config) : null;
175
- const syncService = apiClient
176
- ? createDevmodeSyncService({
177
- logger,
178
- apiClient,
179
- mock: useMockService,
180
- })
181
- : null;
182
- // Track previous metadata for sync diffing
183
- let previousMetadata;
184
- let devmode;
185
- let gravityBin;
186
- let gravityURL;
187
- let appURL;
188
- if (auth && project && opts.public) {
189
- // Generate devmode endpoint for public URL
190
- const endpoint = await tui.spinner({
191
- message: 'Connecting to Gravity',
192
- callback: () => {
193
- return generateEndpoint(apiClient, project.projectId, config?.devmode?.hostname);
194
- },
195
- clearOnSuccess: true,
196
- });
197
- const _config = { ...config };
198
- _config.devmode = {
199
- hostname: endpoint.hostname,
200
- };
201
- await saveConfig(_config);
202
- config = _config;
203
- devmode = endpoint;
204
- gravityURL = getGravityDevModeURL(project.region, config);
205
- appURL = `${getAppBaseURL(config)}/r/${project.projectId}`;
206
- // Download gravity client
207
- const configDir = getDefaultConfigDir();
208
- const gravityDir = join(configDir, 'gravity');
209
- let mustCheck = true;
210
- if (config?.gravity?.version &&
211
- existsSync(join(gravityDir, config.gravity.version, 'gravity')) &&
212
- config?.gravity?.checked) {
213
- if (Date.now() - config.gravity.checked < 3.6e6) {
214
- mustCheck = false;
215
- gravityBin = join(gravityDir, config.gravity.version, 'gravity');
216
- }
217
- }
218
- if (mustCheck) {
219
- const res = await download(gravityDir);
220
- gravityBin = res.filename;
176
+ try {
177
+ // Setup devmode and gravity (if using public URL)
178
+ const useMockService = process.env.DEVMODE_SYNC_SERVICE_MOCK === 'true';
179
+ const apiClient = auth ? new APIClient(getAPIBaseURL(config), logger, config) : null;
180
+ const syncService = apiClient
181
+ ? createDevmodeSyncService({
182
+ logger,
183
+ apiClient,
184
+ mock: useMockService,
185
+ })
186
+ : null;
187
+ // Track previous metadata for sync diffing
188
+ let previousMetadata;
189
+ let devmode;
190
+ let gravityBin;
191
+ let gravityURL;
192
+ let appURL;
193
+ if (auth && project && opts.public) {
194
+ // Generate devmode endpoint for public URL
195
+ const endpoint = await tui.spinner({
196
+ message: 'Connecting to Gravity',
197
+ callback: () => {
198
+ return generateEndpoint(apiClient, project.projectId, config?.devmode?.hostname);
199
+ },
200
+ clearOnSuccess: true,
201
+ });
221
202
  const _config = { ...config };
222
- _config.gravity = {
223
- checked: Date.now(),
224
- version: res.version,
203
+ _config.devmode = {
204
+ hostname: endpoint.hostname,
225
205
  };
226
206
  await saveConfig(_config);
227
207
  config = _config;
208
+ devmode = endpoint;
209
+ gravityURL = getGravityDevModeURL(project.region, config);
210
+ appURL = `${getAppBaseURL(config)}/r/${project.projectId}`;
211
+ // Download gravity client
212
+ const configDir = getDefaultConfigDir();
213
+ const gravityDir = join(configDir, 'gravity');
214
+ let mustCheck = true;
215
+ if (config?.gravity?.version &&
216
+ existsSync(join(gravityDir, config.gravity.version, 'gravity')) &&
217
+ config?.gravity?.checked) {
218
+ if (Date.now() - config.gravity.checked < 3.6e6) {
219
+ mustCheck = false;
220
+ gravityBin = join(gravityDir, config.gravity.version, 'gravity');
221
+ }
222
+ }
223
+ if (mustCheck) {
224
+ const res = await download(gravityDir);
225
+ gravityBin = res.filename;
226
+ const _config = { ...config };
227
+ _config.gravity = {
228
+ checked: Date.now(),
229
+ version: res.version,
230
+ };
231
+ await saveConfig(_config);
232
+ config = _config;
233
+ }
228
234
  }
229
- }
230
- // Get workbench info from config (new Vite approach)
231
- const { loadAgentuityConfig, getWorkbenchConfig } = await import('../build/vite/config-loader');
232
- const agentuityConfig = await loadAgentuityConfig(rootDir, ctx.logger);
233
- const workbenchConfigData = getWorkbenchConfig(agentuityConfig, true); // dev mode
234
- const workbench = {
235
- hasWorkbench: workbenchConfigData.enabled,
236
- config: workbenchConfigData.enabled
237
- ? { route: workbenchConfigData.route, headers: workbenchConfigData.headers }
238
- : null,
239
- };
240
- const deploymentId = getDevmodeDeploymentId(project?.projectId ?? '', devmode?.id ?? '');
241
- // Calculate URLs for banner
242
- const padding = 12;
243
- const workbenchUrl = auth && project?.projectId
244
- ? `${getAppBaseURL(config)}/w/${project.projectId}`
245
- : `http://127.0.0.1:${opts.port}${workbench.config?.route ?? '/workbench'}`;
246
- const devmodebody = tui.muted(tui.padRight('Local:', padding)) +
247
- tui.link(`http://127.0.0.1:${opts.port}`) +
248
- '\n' +
249
- tui.muted(tui.padRight('Public:', padding)) +
250
- (devmode?.hostname ? tui.link(`https://${devmode.hostname}`) : tui.warn('Disabled')) +
251
- '\n' +
252
- tui.muted(tui.padRight('Workbench:', padding)) +
253
- (workbench.hasWorkbench ? tui.link(workbenchUrl) : tui.warn('Disabled')) +
254
- '\n' +
255
- tui.muted(tui.padRight('Dashboard:', padding)) +
256
- (appURL ? tui.link(appURL) : tui.warn('Disabled')) +
257
- '\n' +
258
- (interactive
259
- ? '\n' + tui.muted('Press ') + tui.bold('h') + tui.muted(' for keyboard shortcuts')
260
- : '');
261
- tui.banner('⨺ Agentuity DevMode', devmodebody, {
262
- padding: 2,
263
- topSpacer: false,
264
- bottomSpacer: false,
265
- centerTitle: false,
266
- });
267
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
268
- const cliVersion = global.__CLI_SCHEMA__?.version ?? '';
269
- if (cliVersion) {
270
- regenerateSkillsAsync(rootDir, cliVersion, logger).catch(() => { });
271
- }
272
- // Start Vite asset server ONCE before restart loop
273
- // Vite handles frontend HMR independently and stays running across backend restarts
274
- let viteServer = null;
275
- let vitePort;
276
- try {
277
- logger.debug('Starting Vite asset server...');
278
- const viteResult = await startViteAssetServer({
279
- rootDir,
280
- logger,
281
- workbenchPath: workbench.config?.route,
235
+ // Get workbench info from config (new Vite approach)
236
+ const { loadAgentuityConfig, getWorkbenchConfig } = await import('../build/vite/config-loader');
237
+ const agentuityConfig = await loadAgentuityConfig(rootDir, ctx.logger);
238
+ const workbenchConfigData = getWorkbenchConfig(agentuityConfig, true); // dev mode
239
+ const workbench = {
240
+ hasWorkbench: workbenchConfigData.enabled,
241
+ config: workbenchConfigData.enabled
242
+ ? { route: workbenchConfigData.route, headers: workbenchConfigData.headers }
243
+ : null,
244
+ };
245
+ const deploymentId = getDevmodeDeploymentId(project?.projectId ?? '', devmode?.id ?? '');
246
+ // Calculate URLs for banner
247
+ const padding = 12;
248
+ const workbenchUrl = auth && project?.projectId
249
+ ? `${getAppBaseURL(config)}/w/${project.projectId}`
250
+ : `http://127.0.0.1:${opts.port}${workbench.config?.route ?? '/workbench'}`;
251
+ const devmodebody = tui.muted(tui.padRight('Local:', padding)) +
252
+ tui.link(`http://127.0.0.1:${opts.port}`) +
253
+ '\n' +
254
+ tui.muted(tui.padRight('Public:', padding)) +
255
+ (devmode?.hostname ? tui.link(`https://${devmode.hostname}`) : tui.warn('Disabled')) +
256
+ '\n' +
257
+ tui.muted(tui.padRight('Workbench:', padding)) +
258
+ (workbench.hasWorkbench ? tui.link(workbenchUrl) : tui.warn('Disabled')) +
259
+ '\n' +
260
+ tui.muted(tui.padRight('Dashboard:', padding)) +
261
+ (appURL ? tui.link(appURL) : tui.warn('Disabled')) +
262
+ '\n' +
263
+ (interactive
264
+ ? '\n' + tui.muted('Press ') + tui.bold('h') + tui.muted(' for keyboard shortcuts')
265
+ : '');
266
+ tui.banner('⨺ Agentuity DevMode', devmodebody, {
267
+ padding: 2,
268
+ topSpacer: false,
269
+ bottomSpacer: false,
270
+ centerTitle: false,
282
271
  });
283
- viteServer = viteResult.server;
284
- vitePort = viteResult.port;
285
- // Update dev lock with actual Vite port
286
- await devLock.updatePorts({ vite: vitePort });
287
- logger.debug(`Vite asset server running on port ${vitePort} (stays running across backend restarts)`);
288
- }
289
- catch (error) {
290
- tui.error(`Failed to start Vite asset server: ${error}`);
291
- await devLock.release();
292
- originalExit(1);
293
- return;
294
- }
295
- // Restart loop - allows BACKEND server to restart on file changes
296
- // Vite stays running and handles frontend changes via HMR
297
- let shouldRestart = false;
298
- let gravityProcess = null;
299
- let stdinListenerRegistered = false; // Track if stdin listener is already registered
300
- const restartServer = () => {
301
- shouldRestart = true;
302
- };
303
- const showWelcome = () => {
304
- logger.info('DevMode ready 🚀');
305
- };
306
- // Create file watcher for backend hot reload
307
- const fileWatcher = createFileWatcher({
308
- rootDir,
309
- logger,
310
- onRestart: restartServer,
311
- });
312
- // Start file watcher (will be paused during builds)
313
- fileWatcher.start();
314
- // Track if cleanup is in progress to avoid duplicate cleanup
315
- let cleaningUp = false;
316
- // Track if shutdown was requested (SIGINT/SIGTERM) to break the main loop
317
- let shutdownRequested = false;
318
- /**
319
- * Centralized cleanup function for all resources.
320
- * Called on restart, shutdown, and fatal errors.
321
- * @param exitAfter - If true, exit the process after cleanup
322
- * @param exitCode - Exit code to use if exitAfter is true
323
- * @param silent - If true, don't show "Shutting down" message
324
- */
325
- const cleanup = async (exitAfter = false, exitCode = 0, silent = false) => {
326
- if (cleaningUp)
327
- return;
328
- cleaningUp = true;
329
- if (!silent) {
330
- tui.info('Shutting down...');
331
- }
332
- // Stop file watcher first to prevent restart triggers during cleanup
333
- try {
334
- fileWatcher.stop();
335
- }
336
- catch (err) {
337
- logger.debug('Error stopping file watcher: %s', err);
272
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
273
+ const cliVersion = global.__CLI_SCHEMA__?.version ?? '';
274
+ if (cliVersion) {
275
+ regenerateSkillsAsync(rootDir, cliVersion, logger).catch(() => { });
338
276
  }
339
- // Stop Bun server
277
+ // Start Vite asset server ONCE before restart loop
278
+ // Vite handles frontend HMR independently and stays running across backend restarts
279
+ let viteServer = null;
280
+ let vitePort;
340
281
  try {
341
- await stopBunServer(opts.port, logger);
282
+ logger.debug('Starting Vite asset server...');
283
+ const viteResult = await startViteAssetServer({
284
+ rootDir,
285
+ logger,
286
+ workbenchPath: workbench.config?.route,
287
+ });
288
+ viteServer = viteResult.server;
289
+ vitePort = viteResult.port;
290
+ // Update dev lock with actual Vite port
291
+ await devLock.updatePorts({ vite: vitePort });
292
+ logger.debug(`Vite asset server running on port ${vitePort} (stays running across backend restarts)`);
342
293
  }
343
- catch (err) {
344
- logger.debug('Error stopping Bun server during cleanup: %s', err);
294
+ catch (error) {
295
+ tui.error(`Failed to start Vite asset server: ${error}`);
296
+ await devLock.release();
297
+ originalExit(1);
298
+ return;
345
299
  }
346
- // Kill gravity client with SIGTERM first, then SIGKILL as fallback
347
- if (gravityProcess) {
348
- logger.debug('Killing gravity process...');
349
- try {
350
- gravityProcess.kill('SIGTERM');
351
- // Give it a moment to gracefully shutdown
352
- await new Promise((resolve) => setTimeout(resolve, 150));
353
- if (gravityProcess.exitCode === null) {
354
- gravityProcess.kill('SIGKILL');
355
- }
356
- logger.debug('Gravity process killed');
357
- }
358
- catch (err) {
359
- logger.debug('Error killing gravity process: %s', err);
360
- }
361
- finally {
362
- gravityProcess = null;
300
+ // Restart loop - allows BACKEND server to restart on file changes
301
+ // Vite stays running and handles frontend changes via HMR
302
+ let shouldRestart = false;
303
+ let gravityProcess = null;
304
+ let stdinListenerRegistered = false; // Track if stdin listener is already registered
305
+ const restartServer = () => {
306
+ shouldRestart = true;
307
+ };
308
+ const showWelcome = () => {
309
+ logger.info('DevMode ready 🚀');
310
+ };
311
+ // Create file watcher for backend hot reload
312
+ const fileWatcher = createFileWatcher({
313
+ rootDir,
314
+ logger,
315
+ onRestart: restartServer,
316
+ });
317
+ // Start file watcher (will be paused during builds)
318
+ fileWatcher.start();
319
+ // Track if cleanup is in progress to avoid duplicate cleanup
320
+ let cleaningUp = false;
321
+ // Track if shutdown was requested (SIGINT/SIGTERM) to break the main loop
322
+ let shutdownRequested = false;
323
+ /**
324
+ * Centralized cleanup function for all resources.
325
+ * Called on restart, shutdown, and fatal errors.
326
+ * @param exitAfter - If true, exit the process after cleanup
327
+ * @param exitCode - Exit code to use if exitAfter is true
328
+ * @param silent - If true, don't show "Shutting down" message
329
+ */
330
+ const cleanup = async (exitAfter = false, exitCode = 0, silent = false) => {
331
+ if (cleaningUp)
332
+ return;
333
+ cleaningUp = true;
334
+ if (!silent) {
335
+ tui.info('Shutting down...');
363
336
  }
364
- }
365
- // Close Vite asset server with timeout to prevent hanging
366
- if (viteServer) {
367
- logger.debug('Closing Vite server...');
337
+ // Stop file watcher first to prevent restart triggers during cleanup
368
338
  try {
369
- // Use Promise.race with timeout to prevent hanging
370
- const closePromise = viteServer.close();
371
- const timeoutPromise = new Promise((resolve) => {
372
- setTimeout(() => {
373
- logger.debug('Vite server close timed out, continuing...');
374
- resolve();
375
- }, 2000);
376
- });
377
- await Promise.race([closePromise, timeoutPromise]);
378
- logger.debug('Vite server closed');
339
+ fileWatcher.stop();
379
340
  }
380
341
  catch (err) {
381
- logger.debug('Error closing Vite server: %s', err);
382
- }
383
- finally {
384
- viteServer = null;
342
+ logger.debug('Error stopping file watcher: %s', err);
385
343
  }
386
- }
387
- // Release the dev lockfile
388
- logger.debug('Releasing dev lock...');
389
- try {
390
- await devLock.release();
391
- logger.debug('Dev lock released');
392
- }
393
- catch (err) {
394
- logger.debug('Error releasing dev lock: %s', err);
395
- }
396
- // Reset cleanup flag if not exiting (allows restart)
397
- if (!exitAfter) {
398
- cleaningUp = false;
399
- }
400
- else {
401
- logger.debug('Exiting with code %d', exitCode);
402
- originalExit(exitCode);
403
- }
404
- };
405
- /**
406
- * Cleanup for restart: stops Bun server and Gravity, keeps Vite running
407
- */
408
- const cleanupForRestart = async () => {
409
- logger.debug('Cleaning up for restart...');
410
- // Stop Bun server
411
- try {
412
- await stopBunServer(opts.port, logger);
413
- }
414
- catch (err) {
415
- logger.debug('Error stopping Bun server for restart: %s', err);
416
- }
417
- // Kill gravity client
418
- if (gravityProcess) {
344
+ // Stop Bun server
419
345
  try {
420
- gravityProcess.kill('SIGTERM');
421
- await new Promise((resolve) => setTimeout(resolve, 150));
422
- if (gravityProcess.exitCode === null) {
423
- gravityProcess.kill('SIGKILL');
424
- }
346
+ await stopBunServer(opts.port, logger);
425
347
  }
426
348
  catch (err) {
427
- logger.debug('Error killing gravity process for restart: %s', err);
349
+ logger.debug('Error stopping Bun server during cleanup: %s', err);
428
350
  }
429
- finally {
430
- gravityProcess = null;
351
+ // Kill gravity client with SIGTERM first, then SIGKILL as fallback
352
+ if (gravityProcess) {
353
+ logger.debug('Killing gravity process...');
354
+ try {
355
+ gravityProcess.kill('SIGTERM');
356
+ // Give it a moment to gracefully shutdown
357
+ await new Promise((resolve) => setTimeout(resolve, 150));
358
+ if (gravityProcess.exitCode === null) {
359
+ gravityProcess.kill('SIGKILL');
360
+ }
361
+ logger.debug('Gravity process killed');
362
+ }
363
+ catch (err) {
364
+ logger.debug('Error killing gravity process: %s', err);
365
+ }
366
+ finally {
367
+ gravityProcess = null;
368
+ }
431
369
  }
432
- }
433
- };
434
- // SIGINT/SIGTERM: coordinate shutdown between bundle and dev resources
435
- let signalHandlersRegistered = false;
436
- if (!signalHandlersRegistered) {
437
- signalHandlersRegistered = true;
438
- const safeExit = async (code, reason) => {
439
- if (reason) {
440
- logger.debug('DevMode terminating (%d) due to: %s', code, reason);
370
+ // Close Vite asset server with timeout to prevent hanging
371
+ if (viteServer) {
372
+ logger.debug('Closing Vite server...');
373
+ try {
374
+ // Use Promise.race with timeout to prevent hanging
375
+ const closePromise = viteServer.close();
376
+ const timeoutPromise = new Promise((resolve) => {
377
+ setTimeout(() => {
378
+ logger.debug('Vite server close timed out, continuing...');
379
+ resolve();
380
+ }, 2000);
381
+ });
382
+ await Promise.race([closePromise, timeoutPromise]);
383
+ logger.debug('Vite server closed');
384
+ }
385
+ catch (err) {
386
+ logger.debug('Error closing Vite server: %s', err);
387
+ }
388
+ finally {
389
+ viteServer = null;
390
+ }
441
391
  }
442
- shutdownRequested = true;
443
- await cleanup(true, code);
444
- };
445
- process.on('SIGINT', () => {
446
- void safeExit(0, 'SIGINT');
447
- });
448
- process.on('SIGTERM', () => {
449
- void safeExit(0, 'SIGTERM');
450
- });
451
- // Handle uncaught exceptions - clean up and exit rather than limping on
452
- process.on('uncaughtException', (err) => {
453
- tui.error(`Uncaught exception: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
454
- void safeExit(1, 'uncaughtException');
455
- });
456
- // Handle unhandled rejections - log but don't exit (usually recoverable)
457
- process.on('unhandledRejection', (reason) => {
458
- logger.warn('Unhandled promise rejection: %s', reason instanceof Error ? (reason.stack ?? reason.message) : String(reason));
459
- });
460
- }
461
- // Ensure resources are always cleaned up on exit (synchronous fallback)
462
- process.on('exit', () => {
463
- // Kill gravity client with SIGKILL for immediate termination
464
- if (gravityProcess && gravityProcess.exitCode === null) {
392
+ // Release the dev lockfile
393
+ logger.debug('Releasing dev lock...');
465
394
  try {
466
- gravityProcess.kill('SIGKILL');
395
+ await devLock.release();
396
+ logger.debug('Dev lock released');
467
397
  }
468
- catch {
469
- // Ignore errors during exit cleanup
398
+ catch (err) {
399
+ logger.debug('Error releasing dev lock: %s', err);
470
400
  }
471
- }
472
- // Close Vite server synchronously if possible
473
- if (viteServer) {
474
- try {
475
- viteServer.close();
401
+ await killLingeringGravityProcesses(logger);
402
+ // Reset cleanup flag if not exiting (allows restart)
403
+ if (!exitAfter) {
404
+ cleaningUp = false;
476
405
  }
477
- catch {
478
- // Ignore errors during exit cleanup
406
+ else {
407
+ logger.debug('Exiting with code %d', exitCode);
408
+ originalExit(exitCode);
479
409
  }
480
- }
481
- // Stop Bun server synchronously (best effort)
482
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
483
- const server = globalThis.__AGENTUITY_SERVER__;
484
- if (server?.stop) {
410
+ };
411
+ /**
412
+ * Cleanup for restart: stops Bun server and Gravity, keeps Vite running
413
+ */
414
+ const cleanupForRestart = async () => {
415
+ logger.debug('Cleaning up for restart...');
416
+ // Stop Bun server
485
417
  try {
486
- server.stop(true);
418
+ await stopBunServer(opts.port, logger);
487
419
  }
488
- catch {
489
- // Ignore errors during exit cleanup
420
+ catch (err) {
421
+ logger.debug('Error stopping Bun server for restart: %s', err);
490
422
  }
491
- }
492
- // Release the dev lockfile synchronously
493
- releaseLockSync(rootDir);
494
- });
495
- while (!shutdownRequested) {
496
- shouldRestart = false;
497
- // Pause file watcher during build to avoid loops
498
- fileWatcher.pause();
499
- try {
500
- // Generate entry file and bundle for dev server (with LLM patches)
501
- await tui.spinner({
502
- message: 'Building dev bundle',
503
- callback: async () => {
504
- // Step 1: Generate workbench files if enabled (must be done before entry generation)
505
- if (workbenchConfigData.enabled) {
506
- logger.debug('Workbench enabled, generating source files before bundle...');
507
- const { generateWorkbenchFiles } = await import('../build/vite/workbench-generator');
508
- await generateWorkbenchFiles(rootDir, project?.projectId ?? '', workbenchConfigData, logger);
509
- }
510
- // Step 2: Generate entry file with workbench config
511
- // Note: vitePort is NOT passed here - the app reads process.env.VITE_PORT at runtime
512
- const { generateEntryFile } = await import('../build/entry-generator');
513
- await generateEntryFile({
514
- rootDir,
515
- projectId: project?.projectId ?? '',
516
- deploymentId,
517
- logger,
518
- mode: 'dev',
519
- workbench: workbenchConfigData.enabled ? workbenchConfigData : undefined,
520
- });
521
- // Step 3: Bundle the app with LLM patches (dev mode = no minification)
522
- // This produces .agentuity/app.js with AI Gateway routing patches applied
523
- const { installExternalsAndBuild } = await import('../build/vite/server-bundler');
524
- await installExternalsAndBuild({
525
- rootDir,
526
- dev: true, // DevMode: no minification, inline sourcemaps
527
- logger,
528
- });
529
- // Generate metadata file (needed for eval ID lookup at runtime)
530
- const { discoverAgents } = await import('../build/vite/agent-discovery');
531
- const { discoverRoutes } = await import('../build/vite/route-discovery');
532
- const { generateMetadata, writeMetadataFile } = await import('../build/vite/metadata-generator');
533
- const srcDir = join(rootDir, 'src');
534
- const promises = [];
535
- // Generate/update prompt files (non-blocking)
536
- promises.push(import('../build/vite/prompt-generator')
537
- .then(({ generatePromptFiles }) => generatePromptFiles(srcDir, logger))
538
- .catch((err) => logger.warn('Failed to generate prompt files: %s', err.message)));
539
- const agents = await discoverAgents(srcDir, project?.projectId ?? '', deploymentId, logger);
540
- const { routes } = await discoverRoutes(srcDir, project?.projectId ?? '', deploymentId, logger);
541
- const metadata = await generateMetadata({
542
- rootDir,
543
- projectId: project?.projectId ?? '',
544
- orgId: project?.orgId ?? '',
545
- deploymentId,
546
- agents,
547
- routes,
548
- dev: true,
549
- logger,
550
- });
551
- writeMetadataFile(rootDir, metadata, true, logger);
552
- // Sync metadata with backend (creates agents and evals in the database)
553
- if (syncService && project?.projectId) {
554
- promises.push(syncService.sync(metadata, previousMetadata, project.projectId, deploymentId));
555
- previousMetadata = metadata;
423
+ // Kill gravity client
424
+ if (gravityProcess) {
425
+ try {
426
+ gravityProcess.kill('SIGTERM');
427
+ await new Promise((resolve) => setTimeout(resolve, 150));
428
+ if (gravityProcess.exitCode === null) {
429
+ gravityProcess.kill('SIGKILL');
556
430
  }
557
- await Promise.all(promises);
558
- },
559
- clearOnSuccess: true,
431
+ }
432
+ catch (err) {
433
+ logger.debug('Error killing gravity process for restart: %s', err);
434
+ }
435
+ finally {
436
+ gravityProcess = null;
437
+ }
438
+ }
439
+ };
440
+ // SIGINT/SIGTERM: coordinate shutdown between bundle and dev resources
441
+ let signalHandlersRegistered = false;
442
+ if (!signalHandlersRegistered) {
443
+ signalHandlersRegistered = true;
444
+ const safeExit = async (code, reason) => {
445
+ if (reason) {
446
+ logger.debug('DevMode terminating (%d) due to: %s', code, reason);
447
+ }
448
+ shutdownRequested = true;
449
+ await cleanup(true, code);
450
+ };
451
+ process.on('SIGINT', () => {
452
+ void safeExit(0, 'SIGINT');
560
453
  });
561
- }
562
- catch (error) {
563
- tui.error(`Failed to build dev bundle: ${error}`);
564
- tui.warn('Waiting for file changes to retry...');
565
- // Resume watcher to detect changes for retry
566
- fileWatcher.resume();
567
- // Wait for next restart trigger
568
- await new Promise((resolve) => {
569
- const checkRestart = setInterval(() => {
570
- if (shouldRestart) {
571
- clearInterval(checkRestart);
572
- resolve();
573
- }
574
- }, 100);
454
+ process.on('SIGTERM', () => {
455
+ void safeExit(0, 'SIGTERM');
456
+ });
457
+ // Handle uncaught exceptions - clean up and exit rather than limping on
458
+ process.on('uncaughtException', (err) => {
459
+ tui.error(`Uncaught exception: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
460
+ void safeExit(1, 'uncaughtException');
461
+ });
462
+ // Handle unhandled rejections - log but don't exit (usually recoverable)
463
+ process.on('unhandledRejection', (reason) => {
464
+ logger.warn('Unhandled promise rejection: %s', reason instanceof Error ? (reason.stack ?? reason.message) : String(reason));
575
465
  });
576
- continue;
577
466
  }
578
- try {
579
- // Set environment variables for LLM provider patches BEFORE starting server
580
- // These must be set so the bundled patches can route LLM calls through AI Gateway
581
- const serviceUrls = getServiceUrls(project?.region);
582
- // Load SDK key from project .env files for AI Gateway routing
583
- // This must be set so the bundled AI SDK patches can inject the API key
584
- if (!process.env.AGENTUITY_SDK_KEY) {
585
- const sdkKey = await loadProjectSDKKey(logger, rootDir);
586
- if (sdkKey) {
587
- process.env.AGENTUITY_SDK_KEY = sdkKey;
467
+ // Ensure resources are always cleaned up on exit (synchronous fallback)
468
+ process.on('exit', () => {
469
+ // Kill gravity client with SIGKILL for immediate termination
470
+ if (gravityProcess && gravityProcess.exitCode === null) {
471
+ try {
472
+ gravityProcess.kill('SIGKILL');
588
473
  }
589
- else if (project) {
590
- tui.warn('AGENTUITY_SDK_KEY not found in .env file. Numerous features will be unavailable.');
591
- tui.bullet(`Run "${getCommand('cloud env pull')}" to sync your SDK key, or add AGENTUITY_SDK_KEY to your .env file.`);
474
+ catch {
475
+ // Ignore errors during exit cleanup
592
476
  }
593
477
  }
594
- process.env.AGENTUITY_SDK_DEV_MODE = 'true';
595
- process.env.AGENTUITY_ENV = 'development';
596
- process.env.NODE_ENV = 'development';
597
- if (project?.region) {
598
- process.env.AGENTUITY_REGION = project.region;
599
- }
600
- process.env.PORT = String(opts.port);
601
- process.env.AGENTUITY_PORT = process.env.PORT;
602
- if (project) {
603
- process.env.AGENTUITY_TRANSPORT_URL = serviceUrls.catalyst;
604
- process.env.AGENTUITY_CATALYST_URL = serviceUrls.catalyst;
605
- process.env.AGENTUITY_VECTOR_URL = serviceUrls.vector;
606
- process.env.AGENTUITY_KEYVALUE_URL = serviceUrls.keyvalue;
607
- process.env.AGENTUITY_SANDBOX_URL = serviceUrls.sandbox;
608
- process.env.AGENTUITY_STREAM_URL = serviceUrls.stream;
609
- process.env.AGENTUITY_CLOUD_ORG_ID = project.orgId;
610
- process.env.AGENTUITY_CLOUD_PROJECT_ID = project.projectId;
611
- }
612
- // Set Vite port for asset proxying in bundled app
613
- process.env.VITE_PORT = String(vitePort);
614
- logger.debug('Set VITE_PORT=%s for asset proxying', process.env.VITE_PORT);
615
- // Start Bun dev server (Vite already running, just start backend)
616
- await startBunDevServer({
617
- rootDir,
618
- port: opts.port,
619
- projectId: project?.projectId,
620
- orgId: project?.orgId,
621
- deploymentId,
622
- logger,
623
- vitePort, // Pass port of already-running Vite server
624
- });
625
- // Wait for app.ts to finish loading (Vite is ready but app may still be initializing)
626
- // Give it 2 seconds to ensure app initialization completes
627
- await new Promise((resolve) => setTimeout(resolve, 2000));
628
- // Check if shutdown was requested during startup
629
- if (shutdownRequested) {
630
- break;
478
+ // Close Vite server synchronously if possible
479
+ if (viteServer) {
480
+ try {
481
+ viteServer.close();
482
+ }
483
+ catch {
484
+ // Ignore errors during exit cleanup
485
+ }
631
486
  }
632
- }
633
- catch (error) {
634
- tui.error(`Failed to start dev server: ${error}`);
635
- tui.warn('Waiting for file changes to retry...');
636
- // Wait for next restart trigger or shutdown
637
- await new Promise((resolve) => {
638
- const checkRestart = setInterval(() => {
639
- if (shouldRestart || shutdownRequested) {
640
- clearInterval(checkRestart);
641
- resolve();
642
- }
643
- }, 100);
644
- });
645
- if (shutdownRequested) {
646
- break;
487
+ // Stop Bun server synchronously (best effort)
488
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
489
+ const server = globalThis.__AGENTUITY_SERVER__;
490
+ if (server?.stop) {
491
+ try {
492
+ server.stop(true);
493
+ }
494
+ catch {
495
+ // Ignore errors during exit cleanup
496
+ }
647
497
  }
648
- continue;
649
- }
650
- // Exit early if shutdown was requested
651
- if (shutdownRequested) {
652
- break;
653
- }
654
- try {
655
- // Start gravity client if we have devmode
656
- if (gravityBin && gravityURL && devmode) {
657
- logger.trace('Starting gravity client: %s', gravityBin);
658
- gravityProcess = Bun.spawn([
659
- gravityBin,
660
- '--endpoint-id',
661
- devmode.id,
662
- '--port',
663
- opts.port.toString(),
664
- '--url',
665
- gravityURL,
666
- '--log-level',
667
- process.env.AGENTUITY_GRAVITY_LOG_LEVEL ?? 'error',
668
- ], {
669
- cwd: rootDir,
670
- stdout: 'pipe',
671
- stderr: 'pipe',
672
- detached: false, // Ensure gravity dies with parent process
498
+ // Release the dev lockfile synchronously
499
+ releaseLockSync(rootDir);
500
+ });
501
+ while (!shutdownRequested) {
502
+ shouldRestart = false;
503
+ // Pause file watcher during build to avoid loops
504
+ fileWatcher.pause();
505
+ try {
506
+ let typeCheckErrors;
507
+ // Generate entry file and bundle for dev server (with LLM patches)
508
+ await tui.spinner({
509
+ message: 'Building dev bundle',
510
+ callback: async () => {
511
+ // Step 0: typecheck
512
+ typeCheckErrors = undefined;
513
+ const typeResult = await typecheck(rootDir);
514
+ if (!typeResult.success) {
515
+ typeCheckErrors = typeResult.output;
516
+ return;
517
+ }
518
+ // Step 1: Generate workbench files if enabled (must be done before entry generation)
519
+ if (workbenchConfigData.enabled) {
520
+ logger.debug('Workbench enabled, generating source files before bundle...');
521
+ const { generateWorkbenchFiles } = await import('../build/vite/workbench-generator');
522
+ await generateWorkbenchFiles(rootDir, project?.projectId ?? '', workbenchConfigData, logger);
523
+ }
524
+ // Step 2: Generate entry file with workbench config
525
+ // Note: vitePort is NOT passed here - the app reads process.env.VITE_PORT at runtime
526
+ const { generateEntryFile } = await import('../build/entry-generator');
527
+ await generateEntryFile({
528
+ rootDir,
529
+ projectId: project?.projectId ?? '',
530
+ deploymentId,
531
+ logger,
532
+ mode: 'dev',
533
+ workbench: workbenchConfigData.enabled ? workbenchConfigData : undefined,
534
+ });
535
+ // Step 3: Bundle the app with LLM patches (dev mode = no minification)
536
+ // This produces .agentuity/app.js with AI Gateway routing patches applied
537
+ const { installExternalsAndBuild } = await import('../build/vite/server-bundler');
538
+ await installExternalsAndBuild({
539
+ rootDir,
540
+ dev: true, // DevMode: no minification, inline sourcemaps
541
+ logger,
542
+ });
543
+ // Generate metadata file (needed for eval ID lookup at runtime)
544
+ const { discoverAgents } = await import('../build/vite/agent-discovery');
545
+ const { discoverRoutes } = await import('../build/vite/route-discovery');
546
+ const { generateMetadata, writeMetadataFile } = await import('../build/vite/metadata-generator');
547
+ const srcDir = join(rootDir, 'src');
548
+ const promises = [];
549
+ // Generate/update prompt files (non-blocking)
550
+ promises.push(import('../build/vite/prompt-generator')
551
+ .then(({ generatePromptFiles }) => generatePromptFiles(srcDir, logger))
552
+ .catch((err) => logger.warn('Failed to generate prompt files: %s', err.message)));
553
+ const agents = await discoverAgents(srcDir, project?.projectId ?? '', deploymentId, logger);
554
+ const { routes } = await discoverRoutes(srcDir, project?.projectId ?? '', deploymentId, logger);
555
+ const metadata = await generateMetadata({
556
+ rootDir,
557
+ projectId: project?.projectId ?? '',
558
+ orgId: project?.orgId ?? '',
559
+ deploymentId,
560
+ agents,
561
+ routes,
562
+ dev: true,
563
+ logger,
564
+ });
565
+ writeMetadataFile(rootDir, metadata, true, logger);
566
+ // Sync metadata with backend (creates agents and evals in the database)
567
+ if (syncService && project?.projectId) {
568
+ promises.push(syncService.sync(metadata, previousMetadata, project.projectId, deploymentId));
569
+ previousMetadata = metadata;
570
+ }
571
+ await Promise.all(promises);
572
+ },
573
+ clearOnSuccess: true,
673
574
  });
674
- // Register gravity process in dev lock for cleanup tracking
675
- const gravityPid = gravityProcess.pid;
676
- if (gravityPid) {
677
- await devLock.registerChild({
678
- pid: gravityPid,
679
- type: 'gravity',
680
- description: 'Gravity public URL tunnel',
681
- });
682
- }
683
- // Log gravity output
684
- (async () => {
685
- try {
686
- if (gravityProcess?.stdout) {
687
- for await (const chunk of gravityProcess.stdout) {
688
- const text = new TextDecoder().decode(chunk);
689
- logger.debug('[gravity] %s', text.trim());
690
- }
575
+ if (typeCheckErrors) {
576
+ console.log('');
577
+ console.log(typeCheckErrors);
578
+ console.log('');
579
+ fileWatcher.resume();
580
+ // wait for a file change or shutdown to trigger a recompile
581
+ while (true) {
582
+ if (shutdownRequested) {
583
+ return;
691
584
  }
692
- }
693
- catch (err) {
694
- logger.error('Error reading gravity stdout: %s', err);
695
- }
696
- })();
697
- (async () => {
698
- try {
699
- if (gravityProcess?.stderr) {
700
- for await (const chunk of gravityProcess.stderr) {
701
- const text = new TextDecoder().decode(chunk);
702
- logger.warn('[gravity] %s', text.trim());
703
- }
585
+ if (shouldRestart) {
586
+ break;
704
587
  }
588
+ await tui.spinner({
589
+ message: 'Waiting for changes...',
590
+ clearOnSuccess: true,
591
+ callback: () => Bun.sleep(1000),
592
+ });
705
593
  }
706
- catch (err) {
707
- logger.error('Error reading gravity stderr: %s', err);
708
- }
709
- })();
710
- logger.debug('Gravity client started');
594
+ }
711
595
  }
712
- // Sync service integration
713
- // TODO: Integrate sync service with Vite's buildStart/buildEnd hooks
714
- // The sync service will be called when metadata changes are detected
715
- // Handle keyboard shortcuts - only register listener once
716
- if (interactive &&
717
- process.stdin.isTTY &&
718
- process.stdout.isTTY &&
719
- !stdinListenerRegistered) {
720
- stdinListenerRegistered = true;
721
- process.stdin.setRawMode(true);
722
- process.stdin.resume();
723
- process.stdin.setEncoding('utf8');
724
- const showHelp = () => {
725
- console.log('\n' + tui.bold('Keyboard Shortcuts:'));
726
- console.log(tui.muted(' h') + ' - show this help');
727
- console.log(tui.muted(' c') + ' - clear console');
728
- console.log(tui.muted(' q') + ' - quit\n');
729
- };
730
- process.stdin.on('data', (data) => {
731
- const key = data.toString();
732
- // Handle Ctrl+C - send SIGINT to trigger graceful shutdown
733
- if (key === '\u0003') {
734
- process.kill(process.pid, 'SIGINT');
735
- return;
596
+ catch (error) {
597
+ tui.error(`Failed to build dev bundle: ${error}`);
598
+ tui.warn('Waiting for file changes to retry...');
599
+ // Resume watcher to detect changes for retry
600
+ fileWatcher.resume();
601
+ // Wait for next restart trigger
602
+ await new Promise((resolve) => {
603
+ const checkRestart = setInterval(() => {
604
+ if (shouldRestart) {
605
+ clearInterval(checkRestart);
606
+ resolve();
607
+ }
608
+ }, 100);
609
+ });
610
+ continue;
611
+ }
612
+ try {
613
+ // Set environment variables for LLM provider patches BEFORE starting server
614
+ // These must be set so the bundled patches can route LLM calls through AI Gateway
615
+ const serviceUrls = getServiceUrls(project?.region);
616
+ // Load SDK key from project .env files for AI Gateway routing
617
+ // This must be set so the bundled AI SDK patches can inject the API key
618
+ if (!process.env.AGENTUITY_SDK_KEY) {
619
+ const sdkKey = await loadProjectSDKKey(logger, rootDir);
620
+ if (sdkKey) {
621
+ process.env.AGENTUITY_SDK_KEY = sdkKey;
736
622
  }
737
- switch (key) {
738
- case 'h':
739
- showHelp();
740
- break;
741
- case 'c':
742
- console.clear();
743
- tui.banner('⨺ Agentuity DevMode', devmodebody, {
744
- padding: 2,
745
- topSpacer: false,
746
- bottomSpacer: false,
747
- centerTitle: false,
748
- });
749
- break;
750
- case 'q':
751
- void cleanup(true, 0);
752
- break;
753
- default:
754
- process.stdout.write(data);
755
- break;
623
+ else if (project) {
624
+ tui.warn('AGENTUITY_SDK_KEY not found in .env file. Numerous features will be unavailable.');
625
+ tui.bullet(`Run "${getCommand('cloud env pull')}" to sync your SDK key, or add AGENTUITY_SDK_KEY to your .env file.`);
756
626
  }
627
+ }
628
+ process.env.AGENTUITY_SDK_DEV_MODE = 'true';
629
+ process.env.AGENTUITY_ENV = 'development';
630
+ process.env.NODE_ENV = 'development';
631
+ process.env.AGENTUITY_PROJECT_DIR = rootDir;
632
+ if (project?.region) {
633
+ process.env.AGENTUITY_REGION = project.region;
634
+ }
635
+ process.env.PORT = String(opts.port);
636
+ process.env.AGENTUITY_PORT = process.env.PORT;
637
+ if (project) {
638
+ process.env.AGENTUITY_TRANSPORT_URL = serviceUrls.catalyst;
639
+ process.env.AGENTUITY_CATALYST_URL = serviceUrls.catalyst;
640
+ process.env.AGENTUITY_VECTOR_URL = serviceUrls.vector;
641
+ process.env.AGENTUITY_KEYVALUE_URL = serviceUrls.keyvalue;
642
+ process.env.AGENTUITY_SANDBOX_URL = serviceUrls.sandbox;
643
+ process.env.AGENTUITY_STREAM_URL = serviceUrls.stream;
644
+ process.env.AGENTUITY_CLOUD_ORG_ID = project.orgId;
645
+ process.env.AGENTUITY_CLOUD_PROJECT_ID = project.projectId;
646
+ }
647
+ // Set Vite port for asset proxying in bundled app
648
+ process.env.VITE_PORT = String(vitePort);
649
+ logger.debug('Set VITE_PORT=%s for asset proxying', process.env.VITE_PORT);
650
+ // Start Bun dev server (Vite already running, just start backend)
651
+ await startBunDevServer({
652
+ rootDir,
653
+ port: opts.port,
654
+ projectId: project?.projectId,
655
+ orgId: project?.orgId,
656
+ deploymentId,
657
+ logger,
658
+ vitePort, // Pass port of already-running Vite server
757
659
  });
660
+ // Wait for app.ts to finish loading (Vite is ready but app may still be initializing)
661
+ // Give it 2 seconds to ensure app initialization completes
662
+ await Bun.sleep(2000);
663
+ // Check if shutdown was requested during startup
664
+ if (shutdownRequested) {
665
+ break;
666
+ }
758
667
  }
759
- showWelcome();
760
- // Start/resume file watcher now that server is ready
761
- fileWatcher.resume();
762
- // Wait for restart signal or shutdown
763
- await new Promise((resolve) => {
764
- const checkRestart = setInterval(() => {
765
- if (shouldRestart || shutdownRequested) {
766
- clearInterval(checkRestart);
767
- resolve();
768
- }
769
- }, 100);
770
- });
771
- // Exit loop if shutdown was requested
772
- if (shutdownRequested) {
773
- break;
668
+ catch (error) {
669
+ tui.error(`Failed to start dev server: ${error}`);
670
+ tui.warn('Waiting for file changes to retry...');
671
+ // Wait for next restart trigger or shutdown
672
+ await new Promise((resolve) => {
673
+ const checkRestart = setInterval(() => {
674
+ if (shouldRestart || shutdownRequested) {
675
+ clearInterval(checkRestart);
676
+ resolve();
677
+ }
678
+ }, 100);
679
+ });
680
+ if (shutdownRequested) {
681
+ break;
682
+ }
683
+ continue;
774
684
  }
775
- // Restart triggered - cleanup and loop (Vite stays running)
776
- logger.debug('Restarting backend server...');
777
- // Clean up Bun server and Gravity (Vite stays running)
778
- await cleanupForRestart();
779
- // Brief pause before restart
780
- await new Promise((resolve) => setTimeout(resolve, 500));
781
- }
782
- catch (error) {
783
- tui.error(`Error during server operation: ${error}`);
784
- tui.warn('Waiting for file changes to retry...');
785
- // Cleanup on error (Vite stays running)
786
- await cleanupForRestart();
787
- // Exit if shutdown was requested during error handling
685
+ // Exit early if shutdown was requested
788
686
  if (shutdownRequested) {
789
687
  break;
790
688
  }
791
- // Resume file watcher to detect changes for retry
792
- fileWatcher.resume();
793
- // Wait for next restart trigger or shutdown
794
- await new Promise((resolve) => {
795
- const checkRestart = setInterval(() => {
796
- if (shouldRestart || shutdownRequested) {
797
- clearInterval(checkRestart);
798
- resolve();
689
+ try {
690
+ // Start gravity client if we have devmode
691
+ if (gravityBin && gravityURL && devmode && project) {
692
+ logger.trace('Starting gravity client: %s (cwd: %s, id: %s)', gravityBin, rootDir, devmode.id);
693
+ gravityProcess = Bun.spawn([
694
+ gravityBin,
695
+ '--endpoint-id',
696
+ devmode.id,
697
+ '--port',
698
+ opts.port.toString(),
699
+ '--url',
700
+ gravityURL,
701
+ '--log-level',
702
+ process.env.AGENTUITY_GRAVITY_LOG_LEVEL ?? 'error',
703
+ '--org-id',
704
+ project.orgId,
705
+ '--project-id',
706
+ project.projectId,
707
+ '--token',
708
+ process.env.AGENTUITY_SDK_KEY,
709
+ ], {
710
+ cwd: rootDir,
711
+ stdout: 'pipe',
712
+ stderr: 'pipe',
713
+ detached: false, // Ensure gravity dies with parent process
714
+ });
715
+ // Register gravity process in dev lock for cleanup tracking
716
+ const gravityPid = gravityProcess.pid;
717
+ if (gravityPid) {
718
+ await devLock.registerChild({
719
+ pid: gravityPid,
720
+ type: 'gravity',
721
+ description: 'Gravity public URL tunnel',
722
+ });
799
723
  }
800
- }, 100);
801
- });
724
+ // Log gravity output
725
+ (async () => {
726
+ try {
727
+ if (gravityProcess?.stdout) {
728
+ for await (const chunk of gravityProcess.stdout) {
729
+ const text = new TextDecoder().decode(chunk);
730
+ logger.debug('[gravity] %s', text.trim());
731
+ }
732
+ }
733
+ }
734
+ catch (err) {
735
+ logger.error('Error reading gravity stdout: %s', err);
736
+ }
737
+ })();
738
+ (async () => {
739
+ try {
740
+ if (gravityProcess?.stderr) {
741
+ for await (const chunk of gravityProcess.stderr) {
742
+ const text = new TextDecoder().decode(chunk);
743
+ logger.warn('[gravity] %s', text.trim());
744
+ }
745
+ }
746
+ }
747
+ catch (err) {
748
+ logger.error('Error reading gravity stderr: %s', err);
749
+ }
750
+ })();
751
+ logger.debug('Gravity client started');
752
+ }
753
+ // Handle keyboard shortcuts - only register listener once
754
+ if (interactive &&
755
+ process.stdin.isTTY &&
756
+ process.stdout.isTTY &&
757
+ !stdinListenerRegistered) {
758
+ stdinListenerRegistered = true;
759
+ process.stdin.setRawMode(true);
760
+ process.stdin.resume();
761
+ process.stdin.setEncoding('utf8');
762
+ const showHelp = () => {
763
+ console.log('\n' + tui.bold('Keyboard Shortcuts:'));
764
+ console.log(tui.muted(' h') + ' - show this help');
765
+ console.log(tui.muted(' c') + ' - clear console');
766
+ console.log(tui.muted(' q') + ' - quit\n');
767
+ };
768
+ process.stdin.on('data', (data) => {
769
+ const key = data.toString();
770
+ // Handle Ctrl+C - send SIGINT to trigger graceful shutdown
771
+ if (key === '\u0003') {
772
+ process.kill(process.pid, 'SIGINT');
773
+ return;
774
+ }
775
+ switch (key) {
776
+ case 'h':
777
+ showHelp();
778
+ break;
779
+ case 'c':
780
+ console.clear();
781
+ tui.banner('⨺ Agentuity DevMode', devmodebody, {
782
+ padding: 2,
783
+ topSpacer: false,
784
+ bottomSpacer: false,
785
+ centerTitle: false,
786
+ });
787
+ break;
788
+ case 'q':
789
+ void cleanup(true, 0);
790
+ break;
791
+ default:
792
+ process.stdout.write(data);
793
+ break;
794
+ }
795
+ });
796
+ }
797
+ showWelcome();
798
+ // Start/resume file watcher now that server is ready
799
+ fileWatcher.resume();
800
+ // Wait for restart signal or shutdown
801
+ await new Promise((resolve) => {
802
+ const checkRestart = setInterval(() => {
803
+ if (shouldRestart || shutdownRequested) {
804
+ clearInterval(checkRestart);
805
+ resolve();
806
+ }
807
+ }, 100);
808
+ });
809
+ // Exit loop if shutdown was requested
810
+ if (shutdownRequested) {
811
+ break;
812
+ }
813
+ // Restart triggered - cleanup and loop (Vite stays running)
814
+ logger.debug('Restarting backend server...');
815
+ // Clean up Bun server and Gravity (Vite stays running)
816
+ await cleanupForRestart();
817
+ // Brief pause before restart
818
+ await Bun.sleep(500);
819
+ }
820
+ catch (error) {
821
+ tui.error(`Error during server operation: ${error}`);
822
+ tui.warn('Waiting for file changes to retry...');
823
+ // Cleanup on error (Vite stays running)
824
+ await cleanupForRestart();
825
+ // Exit if shutdown was requested during error handling
826
+ if (shutdownRequested) {
827
+ break;
828
+ }
829
+ // Resume file watcher to detect changes for retry
830
+ fileWatcher.resume();
831
+ // Wait for next restart trigger or shutdown
832
+ await new Promise((resolve) => {
833
+ const checkRestart = setInterval(() => {
834
+ if (shouldRestart || shutdownRequested) {
835
+ clearInterval(checkRestart);
836
+ resolve();
837
+ }
838
+ }, 100);
839
+ });
840
+ }
802
841
  }
803
842
  }
843
+ finally {
844
+ /* brute force clean up */
845
+ await devLock.release();
846
+ await killLingeringGravityProcesses(logger);
847
+ releaseLockSync(rootDir);
848
+ }
804
849
  },
805
850
  });
806
851
  //# sourceMappingURL=index.js.map