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