@agentuity/cli 0.0.97 → 0.0.99

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/banner.d.ts.map +1 -1
  2. package/dist/banner.js +22 -6
  3. package/dist/banner.js.map +1 -1
  4. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  5. package/dist/cmd/build/entry-generator.js +85 -64
  6. package/dist/cmd/build/entry-generator.js.map +1 -1
  7. package/dist/cmd/build/vite/bun-dev-server.d.ts +3 -7
  8. package/dist/cmd/build/vite/bun-dev-server.d.ts.map +1 -1
  9. package/dist/cmd/build/vite/bun-dev-server.js +38 -16
  10. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  11. package/dist/cmd/build/vite/server-bundler.d.ts.map +1 -1
  12. package/dist/cmd/build/vite/server-bundler.js +11 -1
  13. package/dist/cmd/build/vite/server-bundler.js.map +1 -1
  14. package/dist/cmd/build/vite/vite-asset-server-config.d.ts.map +1 -1
  15. package/dist/cmd/build/vite/vite-asset-server-config.js +17 -1
  16. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  17. package/dist/cmd/build/vite/vite-asset-server.js +1 -1
  18. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  19. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  20. package/dist/cmd/build/vite/vite-builder.js +21 -2
  21. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  22. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  23. package/dist/cmd/cloud/deploy.js +32 -21
  24. package/dist/cmd/cloud/deploy.js.map +1 -1
  25. package/dist/cmd/dev/file-watcher.d.ts +24 -0
  26. package/dist/cmd/dev/file-watcher.d.ts.map +1 -0
  27. package/dist/cmd/dev/file-watcher.js +183 -0
  28. package/dist/cmd/dev/file-watcher.js.map +1 -0
  29. package/dist/cmd/dev/index.d.ts.map +1 -1
  30. package/dist/cmd/dev/index.js +61 -23
  31. package/dist/cmd/dev/index.js.map +1 -1
  32. package/dist/cmd/setup/index.d.ts.map +1 -1
  33. package/dist/cmd/setup/index.js +3 -3
  34. package/dist/cmd/setup/index.js.map +1 -1
  35. package/dist/repl.js +2 -2
  36. package/dist/repl.js.map +1 -1
  37. package/dist/types.d.ts +6 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/types.js.map +1 -1
  40. package/package.json +8 -4
  41. package/src/banner.ts +25 -6
  42. package/src/cmd/build/entry-generator.ts +85 -65
  43. package/src/cmd/build/vite/bun-dev-server.ts +45 -18
  44. package/src/cmd/build/vite/server-bundler.ts +16 -1
  45. package/src/cmd/build/vite/vite-asset-server-config.ts +22 -1
  46. package/src/cmd/build/vite/vite-asset-server.ts +1 -1
  47. package/src/cmd/build/vite/vite-builder.ts +29 -2
  48. package/src/cmd/cloud/deploy.ts +41 -24
  49. package/src/cmd/dev/file-watcher.ts +234 -0
  50. package/src/cmd/dev/index.ts +69 -24
  51. package/src/cmd/setup/index.ts +7 -3
  52. package/src/repl.ts +2 -2
  53. package/src/types.ts +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentuity/cli",
3
- "version": "0.0.97",
3
+ "version": "0.0.99",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Agentuity employees and contributors",
6
6
  "type": "module",
@@ -17,6 +17,10 @@
17
17
  "./vite-plugin": {
18
18
  "import": "./dist/cmd/build/vite/index.js",
19
19
  "types": "./dist/cmd/build/vite/index.d.ts"
20
+ },
21
+ "./runtime-bootstrap": {
22
+ "import": "./dist/runtime-bootstrap.js",
23
+ "types": "./dist/runtime-bootstrap.d.ts"
20
24
  }
21
25
  },
22
26
  "files": [
@@ -40,8 +44,8 @@
40
44
  "prepublishOnly": "bun run clean && bun run build"
41
45
  },
42
46
  "dependencies": {
43
- "@agentuity/core": "0.0.97",
44
- "@agentuity/server": "0.0.97",
47
+ "@agentuity/core": "0.0.99",
48
+ "@agentuity/server": "0.0.99",
45
49
  "@datasert/cronjs-parser": "^1.4.0",
46
50
  "@terascope/fetch-github-release": "^2.2.1",
47
51
  "@vitejs/plugin-react": "^5.1.2",
@@ -61,7 +65,7 @@
61
65
  "zod": "^4.1.12"
62
66
  },
63
67
  "devDependencies": {
64
- "@agentuity/test-utils": "0.0.97",
68
+ "@agentuity/test-utils": "0.0.99",
65
69
  "@types/adm-zip": "^0.5.7",
66
70
  "@types/bun": "latest",
67
71
  "@types/tar-fs": "^2.0.4",
package/src/banner.ts CHANGED
@@ -29,10 +29,31 @@ export function generateBanner(version?: string, compact?: true): string {
29
29
  const versionLabel = ' Version: '; // Include leading space
30
30
  const versionLink = LINKS
31
31
  ? link(getReleaseUrl(_version), _version, WHITE ?? undefined)
32
- : _version;
32
+ : WHITE + _version + RESET;
33
33
  const versionLinkWidth = getDisplayWidth(stripAnsi(versionLink));
34
34
  const versionPadding = width - versionLabel.length - versionLinkWidth - 1;
35
35
 
36
+ const docsLabel = ' Docs: ';
37
+ const docsLink = LINKS
38
+ ? link('https://preview.agentuity.dev', 'preview.agentuity.dev', WHITE!)
39
+ : WHITE + 'https://preview.agentuity.dev' + RESET;
40
+ const docsWidth = getDisplayWidth(stripAnsi(docsLink));
41
+ const docsPadding = width - docsLabel.length - docsWidth - 1;
42
+
43
+ const communityLabel = ' Community: ';
44
+ const communityLink = LINKS
45
+ ? link('https://discord.gg/agentuity', 'discord.gg/agentuity', WHITE!)
46
+ : WHITE + 'https://discord.gg/agentuity' + RESET;
47
+ const communityWidth = getDisplayWidth(stripAnsi(communityLink));
48
+ const communityPadding = width - communityLabel.length - communityWidth - 1;
49
+
50
+ const dashboardLabel = ' Dashboard: ';
51
+ const dashboardLink = LINKS
52
+ ? link('https://app-v1.agentuity.com', 'app-v1.agentuity.com', WHITE!)
53
+ : WHITE + 'https://app-v1.agentuity.com' + RESET;
54
+ const dashboardWidth = getDisplayWidth(stripAnsi(dashboardLink));
55
+ const dashboardPadding = width - dashboardLabel.length - dashboardWidth - 1;
56
+
36
57
  const lines = [
37
58
  CYAN + '╭────────────────────────────────────────────────────╮' + RESET,
38
59
  CYAN + `│ ⨺ Agentuity ${WHITE}The full-stack platform for AI agents${CYAN} │` + RESET,
@@ -44,18 +65,16 @@ export function generateBanner(version?: string, compact?: true): string {
44
65
  RESET,
45
66
  compact
46
67
  ? undefined
47
- : CYAN +
48
- `│ Docs: ${link('https://preview.agentuity.dev', undefined, WHITE!)}${CYAN} │` +
49
- RESET,
68
+ : CYAN + `│${docsLabel}${docsLink + ''.padEnd(docsPadding) + CYAN} │` + RESET,
50
69
  compact
51
70
  ? undefined
52
71
  : CYAN +
53
- `│ Community: ${link('https://discord.gg/agentuity', undefined, WHITE!)}${CYAN} │` +
72
+ `│${communityLabel}${communityLink + ''.padEnd(communityPadding) + CYAN} │` +
54
73
  RESET,
55
74
  compact
56
75
  ? undefined
57
76
  : CYAN +
58
- `│ Dashboard: ${link('https://app-v1.agentuity.com', undefined, WHITE!)}${CYAN} │` +
77
+ `│${dashboardLabel}${dashboardLink + ''.padEnd(dashboardPadding) + CYAN} │` +
59
78
  RESET,
60
79
  CYAN + '╰────────────────────────────────────────────────────╯' + RESET,
61
80
  ].filter(Boolean) as string[];
@@ -76,23 +76,13 @@ export async function generateEntryFile(options: GenerateEntryOptions): Promise<
76
76
  `import { `,
77
77
  ...runtimeImports,
78
78
  `} from '@agentuity/runtime';`,
79
+ `import type { Context } from 'hono';`,
79
80
  `import { websocket } from 'hono/bun';`, // Always use Bun WebSocket (dev and prod)
80
81
  !isDev && hasWebFrontend ? `import { serveStatic } from 'hono/bun';` : '',
81
82
  ].filter(Boolean);
82
83
 
83
84
  imports.push(`import { type LogLevel } from '@agentuity/core';`);
84
-
85
- // HMR setup (dev only)
86
- const hmrSetup = isDev
87
- ? `
88
- // HMR restart handler
89
- if (typeof (globalThis as any).__AGENTUITY_RESTART__ === 'undefined') {
90
- (globalThis as any).__AGENTUITY_RESTART__ = () => {
91
- console.log('[HMR] Restart triggered but handler not ready yet');
92
- };
93
- }
94
- `
95
- : '';
85
+ imports.push(`import { bootstrapRuntimeEnv } from '@agentuity/cli/runtime-bootstrap';`);
96
86
 
97
87
  // Generate route mounting code for all discovered routes
98
88
  const routeImportsAndMounts: string[] = [];
@@ -146,7 +136,7 @@ app.route('/', workbenchRouter);
146
136
  // Asset proxy routes - Forward Vite-specific requests to asset server
147
137
  const VITE_ASSET_PORT = ${vitePort};
148
138
 
149
- const proxyToVite = async (c) => {
139
+ const proxyToVite = async (c: Context) => {
150
140
  const viteUrl = \`http://127.0.0.1:\${VITE_ASSET_PORT}\${c.req.path}\`;
151
141
  const controller = new AbortController();
152
142
  const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout
@@ -206,54 +196,82 @@ app.get('/*.css', proxyToVite);
206
196
  let webRoutes = '';
207
197
  if (hasWebFrontend) {
208
198
  if (isDev) {
209
- const htmlPath = join(srcDir, 'web', 'index.html');
210
199
  webRoutes = `
211
200
  // Web routes (dev mode with Vite HMR via proxy)
212
- const devHtmlHandler = async (c) => {
213
- const html = await Bun.file('${htmlPath}').text();
214
- const withHmr = html
215
- // Fix relative paths to use proxy routes (with or without ./)
216
- .replace(/src=["'](?:\\.\\/)?([^"'\\/]+\\.tsx?)["']/g, 'src="/src/web/$1"')
217
- // Inject Vite HMR scripts - point directly to Vite asset server for WebSocket
218
- .replace(
219
- '</head>',
220
- \`<script type="module">
221
- import RefreshRuntime from '/@react-refresh'
222
- RefreshRuntime.injectIntoGlobalHook(window)
223
- window.$RefreshReg$ = () => {}
224
- window.$RefreshSig$ = () => (type) => type
225
- </script>
226
- <script type="module">
227
- // Configure Vite client to connect to asset server for HMR WebSocket
228
- window.__VITE_HMR_BASE_URL__ = 'http://127.0.0.1:${vitePort}';
229
- </script>
230
- <script type="module" src="http://127.0.0.1:${vitePort}/@vite/client"></script>
231
- </head>\`
232
- );
233
- return c.html(withHmr);
201
+ // Proxy HTML from Vite to let @vitejs/plugin-react handle React Fast Refresh preamble
202
+ const devHtmlHandler = async (c: Context) => {
203
+ const viteUrl = \`http://127.0.0.1:${vitePort}/src/web/index.html\`;
204
+
205
+ try {
206
+ otel.logger.debug('[Proxy] GET /src/web/index.html -> Vite:%d', ${vitePort});
207
+ const res = await fetch(viteUrl, { signal: AbortSignal.timeout(10000) });
208
+
209
+ // Get HTML text and transform relative paths to absolute
210
+ const html = await res.text();
211
+ const transformedHtml = html
212
+ .replace(/src="\\.\\//g, 'src="/src/web/')
213
+ .replace(/href="\\.\\//g, 'href="/src/web/');
214
+
215
+ return new Response(transformedHtml, {
216
+ status: res.status,
217
+ headers: res.headers,
218
+ });
219
+ } catch (err) {
220
+ otel.logger.error('Failed to proxy HTML to Vite: %s', err instanceof Error ? err.message : String(err));
221
+ return c.text('Vite asset server error (HTML)', 500);
222
+ }
234
223
  };
235
224
  app.get('/', devHtmlHandler);
236
225
  // 404 for unmatched API/system routes
237
- app.all('/_agentuity/*', (c) => c.notFound());
238
- app.all('/api/*', (c) => c.notFound());
239
- ${hasWorkbench ? '' : `app.all('/workbench/*', (c) => c.notFound());`}
240
- // SPA fallback - serve index.html for all other GET requests
241
- // This is last so user routes, API routes, and workbench routes match first
242
- app.get('*', devHtmlHandler);
226
+ app.all('/_agentuity/*', (c: Context) => c.notFound());
227
+ app.all('/api/*', (c: Context) => c.notFound());
228
+ ${hasWorkbench ? '' : `app.all('/workbench/*', (c: Context) => c.notFound());`}
229
+ // SPA fallback - serve index.html for client-side routing
230
+ // Asset requests (/*.js, /*.tsx, /*.css, etc.) are handled by Vite proxy routes if present,
231
+ // otherwise we check for file extensions to avoid returning HTML for missing assets
232
+ app.get('*', (c: Context) => {
233
+ const path = c.req.path;
234
+ // If path has a file extension and Vite proxy isn't handling it, return 404
235
+ // This prevents returning HTML for missing assets like /foo.js
236
+ if (${!vitePort} && /\\.[a-zA-Z0-9]+$/.test(path)) {
237
+ return c.notFound();
238
+ }
239
+ return devHtmlHandler(c);
240
+ });
243
241
  `;
244
242
  } else {
245
243
  webRoutes = `
246
244
  // Web routes (production - static files)
247
245
  import { readFileSync } from 'node:fs';
248
246
  const indexHtml = readFileSync(import.meta.dir + '/client/index.html', 'utf-8');
247
+
248
+ app.get('/', (c: Context) => c.html(indexHtml));
249
+
250
+ // Serve static assets from /assets/* (Vite bundled output)
249
251
  app.use('/assets/*', serveStatic({ root: import.meta.dir + '/client' }));
250
- app.get('/', (c) => c.html(indexHtml));
251
- // 404 for unmatched API/system routes
252
- app.all('/_agentuity/*', (c) => c.notFound());
253
- app.all('/api/*', (c) => c.notFound());
254
- ${hasWorkbench ? '' : `app.all('/workbench/*', (c) => c.notFound());`}
255
- // SPA fallback - serve index.html for all other GET requests
256
- app.get('*', (c) => c.html(indexHtml));
252
+
253
+ // Serve static public assets (favicon.ico, robots.txt, etc. from Vite's public folder)
254
+ app.use('/*', serveStatic({ root: import.meta.dir + '/client', rewriteRequestPath: (path) => path }));
255
+
256
+ // 404 for unmatched API/system routes (IMPORTANT: comes before SPA fallback)
257
+ app.all('/_agentuity/*', (c: Context) => c.notFound());
258
+ app.all('/api/*', (c: Context) => c.notFound());
259
+ ${hasWorkbench ? '' : `app.all('/workbench/*', (c: Context) => c.notFound());`}
260
+
261
+ // SPA fallback with asset protection
262
+ // In production, we need to distinguish between:
263
+ // - SPA routes like /dashboard, /users/123 (should return HTML)
264
+ // - Missing assets like /foo.js, /bar.css (should return 404)
265
+ // We check for file extensions to detect asset requests
266
+ app.get('*', (c: Context) => {
267
+ const path = c.req.path;
268
+ // If path has a file extension, it's likely an asset request
269
+ // Return 404 instead of serving HTML
270
+ if (/\\.[a-zA-Z0-9]+$/.test(path)) {
271
+ return c.notFound();
272
+ }
273
+ return c.html(indexHtml);
274
+ });
257
275
  `;
258
276
  }
259
277
  }
@@ -265,7 +283,7 @@ app.get('*', (c) => c.html(indexHtml));
265
283
  ? isDev
266
284
  ? `
267
285
  // Workbench route (dev mode - let Vite serve source files with HMR)
268
- app.get('${workbenchRoute}', async (c) => {
286
+ app.get('${workbenchRoute}', async (c: Context) => {
269
287
  const html = await Bun.file('${workbenchSrcDir}/index.html').text();
270
288
  // Rewrite script/css paths to use Vite's @fs protocol
271
289
  const withVite = html
@@ -281,7 +299,7 @@ import { readFileSync, existsSync } from 'node:fs';
281
299
  const workbenchIndexPath = import.meta.dir + '/workbench/index.html';
282
300
  if (existsSync(workbenchIndexPath)) {
283
301
  const workbenchIndex = readFileSync(workbenchIndexPath, 'utf-8');
284
- app.get('${workbenchRoute}', (c) => c.html(workbenchIndex));
302
+ app.get('${workbenchRoute}', (c: Context) => c.html(workbenchIndex));
285
303
  app.get('${workbenchRoute}/*', serveStatic({ root: import.meta.dir + '/workbench' }));
286
304
  }
287
305
  `
@@ -290,9 +308,7 @@ if (existsSync(workbenchIndexPath)) {
290
308
  // Server startup (same for dev and prod - Bun.serve with native WebSocket)
291
309
  const serverStartup = `
292
310
  // Start Bun server${isDev ? ' (dev mode with Vite asset proxy)' : ''}
293
- if (typeof Bun !== 'undefined' && !import.meta.main) {
294
- // Not the main module, skip server startup (this is being imported/analyzed)
295
- } else if (typeof Bun !== 'undefined') {
311
+ if (typeof Bun !== 'undefined') {
296
312
  // Enable process exit protection now that we're starting the server
297
313
  enableProcessExitProtection();
298
314
 
@@ -313,16 +329,25 @@ if (typeof Bun !== 'undefined' && !import.meta.main) {
313
329
 
314
330
  const code = `// Auto-generated by Agentuity for ${mode} mode
315
331
  // DO NOT EDIT - This file is regenerated on every build
316
- // NOTE: Bun auto-loads .env files from CWD before executing JavaScript
317
-
318
332
  ${imports.join('\n')}
319
333
 
320
- ${hmrSetup}
334
+ // Step 0: Bootstrap runtime environment (load profile-specific .env files)
335
+ // Only in development - production env vars are injected by platform
336
+ // This must happen BEFORE any imports that depend on environment variables
337
+ if (process.env.NODE_ENV !== 'production') {
338
+ // Pass project directory (parent of .agentuity/) so .env files are loaded correctly
339
+ await bootstrapRuntimeEnv({ projectDir: import.meta.dir + '/..' });
340
+ }
321
341
 
322
342
  // Step 1: Initialize telemetry and services
323
343
  const serverUrl = \`http://127.0.0.1:\${process.env.PORT || '3500'}\`;
324
344
  const otel = register({ processors: [], logLevel: (process.env.AGENTUITY_LOG_LEVEL || 'info') as LogLevel });
325
- const servicesResult = createServices(otel.logger, undefined, serverUrl);
345
+
346
+ // Get app state and config for use below
347
+ const appState = getAppState();
348
+ const appConfig = getAppConfig();
349
+
350
+ createServices(otel.logger, appConfig, serverUrl);
326
351
 
327
352
  // Make logger and tracer globally available for user's app.ts
328
353
  setGlobalLogger(otel.logger);
@@ -352,10 +377,6 @@ app.use('/api/*', createAgentMiddleware(''));
352
377
  // Step 4: Import user's app.ts (runs createApp, gets state/config)
353
378
  await import('../app.ts');
354
379
 
355
- // Get app state and config for use below
356
- const appState = getAppState();
357
- const appConfig = getAppConfig();
358
-
359
380
  // Step 5: Initialize providers
360
381
  const threadProvider = getThreadProvider();
361
382
  const sessionProvider = getSessionProvider();
@@ -366,8 +387,8 @@ await sessionProvider.initialize(appState);
366
387
  // Step 6: Mount routes (AFTER middleware is applied)
367
388
 
368
389
  // System health/idle endpoints
369
- const healthHandler = (c: any) => c.text('OK');
370
- const idleHandler = (c: any) => {
390
+ const healthHandler = (c: Context) => c.text('OK');
391
+ const idleHandler = (c: Context) => {
371
392
  // Check if server is idle (no pending requests/connections)
372
393
  const server = (globalThis as any).__AGENTUITY_SERVER__;
373
394
  if (!server) return c.text('NO', { status: 200 });
@@ -381,7 +402,6 @@ const idleHandler = (c: any) => {
381
402
  return c.text('OK', { status: 200 });
382
403
  };
383
404
 
384
- // Mount on both /_agentuity/* and /* for backwards compatibility
385
405
  app.get('/_agentuity/health', healthHandler);
386
406
  app.get('/_health', healthHandler);
387
407
  app.get('/_agentuity/idle', idleHandler);
@@ -6,7 +6,6 @@
6
6
  */
7
7
 
8
8
  import type { Logger } from '../../../types';
9
- import { startViteAssetServer } from './vite-asset-server';
10
9
 
11
10
  export interface BunDevServerOptions {
12
11
  rootDir: string;
@@ -15,20 +14,21 @@ export interface BunDevServerOptions {
15
14
  orgId?: string;
16
15
  deploymentId?: string;
17
16
  logger: Logger;
17
+ vitePort: number; // Port of already-running Vite asset server
18
18
  }
19
19
 
20
20
  export interface BunDevServerResult {
21
- viteAssetServer: { server: { close: () => void | Promise<void> }; port: number };
22
21
  bunServerPort: number;
23
22
  }
24
23
 
25
24
  /**
26
- * Start Bun dev server with Vite asset server for HMR
25
+ * Start Bun dev server (Vite asset server must already be running)
26
+ * Generates entry file with proxy routes pointing to Vite
27
27
  */
28
28
  export async function startBunDevServer(options: BunDevServerOptions): Promise<BunDevServerResult> {
29
- const { rootDir, port = 3500, projectId = '', deploymentId = '', logger } = options;
29
+ const { rootDir, port = 3500, projectId = '', deploymentId = '', logger, vitePort } = options;
30
30
 
31
- logger.debug('Starting Bun dev server with Vite asset server for HMR...');
31
+ logger.debug('Starting Bun dev server (Vite already running on port %d)...', vitePort);
32
32
 
33
33
  // Generate workbench source files if enabled (dev mode)
34
34
  const { loadAgentuityConfig, getWorkbenchConfig } = await import('./config-loader');
@@ -41,17 +41,8 @@ export async function startBunDevServer(options: BunDevServerOptions): Promise<B
41
41
  await generateWorkbenchFiles(rootDir, projectId, workbenchConfig, logger);
42
42
  }
43
43
 
44
- // Step 1: Start Vite asset server FIRST and get its dynamic port
45
- logger.info('🎨 Starting Vite asset server for HMR...');
46
- const viteAssetServer = await startViteAssetServer({
47
- rootDir,
48
- logger,
49
- workbenchPath: workbenchConfig.enabled ? workbenchConfig.route : undefined,
50
- });
51
- const vitePort = viteAssetServer.port;
52
-
53
44
  // Step 2: Generate entry file with Vite port for asset proxying
54
- logger.info('📝 Generating entry file with asset proxy configuration...');
45
+ logger.debug('📝 Generating entry file with asset proxy configuration...');
55
46
  const { generateEntryFile } = await import('../entry-generator');
56
47
  await generateEntryFile({
57
48
  rootDir,
@@ -64,15 +55,51 @@ export async function startBunDevServer(options: BunDevServerOptions): Promise<B
64
55
  });
65
56
 
66
57
  // Step 3: Load the generated app - this will start Bun.serve() internally
67
- logger.info('📦 Loading generated app (Bun server will start)...');
58
+ logger.debug('📦 Loading generated app (Bun server will start)...');
68
59
  const appPath = `${rootDir}/.agentuity/app.generated.ts`;
60
+
61
+ // Set PORT env var so the generated app uses the correct port
62
+ process.env.PORT = String(port);
63
+
69
64
  await import(appPath);
70
65
 
71
- logger.info(`✅ Bun dev server started on http://127.0.0.1:${port}`);
66
+ // Wait for server to actually start listening
67
+ // The generated app sets (globalThis as any).__AGENTUITY_SERVER__ when server starts
68
+ const maxRetries = 50; // Increased retries for slower systems
69
+ const retryDelay = 100; // ms
70
+ let serverReady = false;
71
+
72
+ for (let i = 0; i < maxRetries; i++) {
73
+ // Check if global server object exists
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ if ((globalThis as any).__AGENTUITY_SERVER__) {
76
+ // Server object exists, now verify it's actually listening by making a request
77
+ try {
78
+ await fetch(`http://127.0.0.1:${port}/`, {
79
+ method: 'HEAD',
80
+ signal: AbortSignal.timeout(1000),
81
+ });
82
+ // Any response (even 404) means server is listening
83
+ serverReady = true;
84
+ break;
85
+ } catch {
86
+ // Connection refused or timeout - server not ready yet
87
+ }
88
+ }
89
+ // Wait before next check
90
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
91
+ }
92
+
93
+ if (!serverReady) {
94
+ throw new Error(
95
+ `Bun server failed to start on port ${port} after ${maxRetries * retryDelay}ms`
96
+ );
97
+ }
98
+
99
+ logger.debug(`Bun dev server started on http://127.0.0.1:${port}`);
72
100
  logger.debug(`Asset requests (/@vite/*, /src/web/*, etc.) proxied to Vite:${vitePort}`);
73
101
 
74
102
  return {
75
- viteAssetServer,
76
103
  bunServerPort: port,
77
104
  };
78
105
  }
@@ -35,18 +35,32 @@ export async function installExternalsAndBuild(options: ServerBundleOptions): Pr
35
35
  // These are devDependencies that may exist in node_modules but aren't needed at runtime
36
36
  const buildToolExternals = ['@babel/*', 'lightningcss', '@vitejs/*', 'vite', 'esbuild'];
37
37
 
38
- // Load custom externals from agentuity.config.ts if it exists
38
+ // Load custom externals and define from agentuity.config.ts if it exists
39
39
  const customExternals: string[] = [];
40
+ let userDefine: Record<string, string> = {};
40
41
  const configPath = join(rootDir, 'agentuity.config.ts');
41
42
  if (await Bun.file(configPath).exists()) {
42
43
  try {
43
44
  const config = await import(configPath);
44
45
  const userConfig = config.default;
46
+
47
+ // Load custom externals (legacy build.external support)
45
48
  if (userConfig?.build?.external && Array.isArray(userConfig.build.external)) {
46
49
  customExternals.push(
47
50
  ...userConfig.build.external.filter((e: unknown) => typeof e === 'string')
48
51
  );
49
52
  }
53
+
54
+ // Load custom define values
55
+ if (userConfig?.define && typeof userConfig.define === 'object') {
56
+ userDefine = userConfig.define;
57
+ if (Object.keys(userDefine).length > 0) {
58
+ logger.debug(
59
+ 'Loaded %d custom define(s) from agentuity.config.ts for server bundle',
60
+ Object.keys(userDefine).length
61
+ );
62
+ }
63
+ }
50
64
  } catch (error) {
51
65
  logger.info('Failed to load agentuity.config.ts for externals:', error);
52
66
  }
@@ -197,6 +211,7 @@ export async function installExternalsAndBuild(options: ServerBundleOptions): Pr
197
211
  minify: !dev,
198
212
  sourcemap: (dev ? 'inline' : 'external') as 'inline' | 'external',
199
213
  external,
214
+ define: userDefine, // Include custom define values from agentuity.config.ts
200
215
  naming: {
201
216
  entry: 'app.js', // Output as app.js (not app.generated.js)
202
217
  },
@@ -24,6 +24,22 @@ export async function generateAssetServerConfig(
24
24
  ): Promise<InlineConfig> {
25
25
  const { rootDir, logger, workbenchPath, port } = options;
26
26
 
27
+ // Load custom user config for define values and plugins
28
+ const { loadAgentuityConfig } = await import('./config-loader');
29
+ const userConfig = await loadAgentuityConfig(rootDir, logger);
30
+ const userDefine = userConfig?.define || {};
31
+ const userPlugins = userConfig?.plugins || [];
32
+
33
+ if (Object.keys(userDefine).length > 0) {
34
+ logger.debug(
35
+ 'Loaded %d custom define(s) from agentuity.config.ts',
36
+ Object.keys(userDefine).length
37
+ );
38
+ }
39
+ if (userPlugins.length > 0) {
40
+ logger.debug('Loaded %d custom plugin(s) from agentuity.config.ts', userPlugins.length);
41
+ }
42
+
27
43
  // Load path aliases from tsconfig.json if available
28
44
  const tsconfigPath = join(rootDir, 'tsconfig.json');
29
45
  let alias = {};
@@ -82,6 +98,9 @@ export async function generateAssetServerConfig(
82
98
 
83
99
  // Define environment variables for browser
84
100
  define: {
101
+ // Merge user-defined constants first
102
+ ...userDefine,
103
+ // Then add default defines (these will override any user-defined protected keys)
85
104
  ...(workbenchPath
86
105
  ? { 'import.meta.env.AGENTUITY_PUBLIC_WORKBENCH_PATH': JSON.stringify(workbenchPath) }
87
106
  : {}),
@@ -91,8 +110,10 @@ export async function generateAssetServerConfig(
91
110
  'process.env.NODE_ENV': JSON.stringify('development'),
92
111
  },
93
112
 
94
- // Minimal plugins - just React and HMR
113
+ // Plugins: User plugins first (e.g., Tailwind), then React and browser env
95
114
  plugins: [
115
+ // User-defined plugins from agentuity.config.ts (e.g., Tailwind CSS)
116
+ ...userPlugins,
96
117
  // React plugin for JSX/TSX transformation and Fast Refresh
97
118
  (await import('@vitejs/plugin-react')).default(),
98
119
  // Browser env plugin to map process.env to import.meta.env
@@ -55,7 +55,7 @@ export async function startViteAssetServer(
55
55
  // Get the actual port Vite is using (may differ from preferred if port was taken)
56
56
  const actualPort = server.config.server.port || preferredPort;
57
57
 
58
- logger.info(`✅ Vite asset server started on port ${actualPort}`);
58
+ logger.debug(`✅ Vite asset server started on port ${actualPort}`);
59
59
  if (actualPort !== preferredPort) {
60
60
  logger.debug(`Port ${preferredPort} was taken, using ${actualPort} instead`);
61
61
  }
@@ -54,7 +54,7 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
54
54
  logger,
55
55
  });
56
56
  } catch (error) {
57
- logger.error('server-bundler import or execution failed:', error);
57
+ logger.error('server-bundler import or execution failed: %s', error);
58
58
  throw error;
59
59
  }
60
60
  return;
@@ -85,11 +85,20 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
85
85
  logger.debug('Loaded %d custom plugin(s) from agentuity.config.ts', userPlugins.length);
86
86
  }
87
87
 
88
+ // Merge custom define values from user config
89
+ const userDefine = userConfig?.define || {};
90
+ if (Object.keys(userDefine).length > 0) {
91
+ logger.debug(
92
+ 'Loaded %d custom define(s) from agentuity.config.ts',
93
+ Object.keys(userDefine).length
94
+ );
95
+ }
96
+
88
97
  // Determine CDN base URL for production builds
89
98
  const isLocalRegion = options.region === 'local';
90
99
  const cdnBaseUrl =
91
100
  !dev && deploymentId && !isLocalRegion
92
- ? `https://static.agentuity.com/${deploymentId}/`
101
+ ? `https://static.agentuity.com/${deploymentId}/client/`
93
102
  : undefined;
94
103
 
95
104
  viteConfig = {
@@ -99,6 +108,9 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
99
108
  publicDir: join(rootDir, 'src', 'web', 'public'),
100
109
  base: cdnBaseUrl, // CDN URL for production assets
101
110
  define: {
111
+ // Merge user-defined constants first
112
+ ...userDefine,
113
+ // Then add default defines (these will override any user-defined protected keys)
102
114
  // Set workbench path if enabled (use import.meta.env for client code)
103
115
  'import.meta.env.AGENTUITY_PUBLIC_WORKBENCH_PATH': workbenchEnabled
104
116
  ? JSON.stringify(workbenchRoute)
@@ -121,11 +133,26 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
121
133
  // Ensure route ends with / for Vite base
122
134
  const base = workbenchRoute.endsWith('/') ? workbenchRoute : `${workbenchRoute}/`;
123
135
 
136
+ // Load custom user config for define values (same as client mode)
137
+ const { loadAgentuityConfig } = await import('./config-loader');
138
+ const userConfig = await loadAgentuityConfig(rootDir, logger);
139
+ const userDefine = userConfig?.define || {};
140
+ if (Object.keys(userDefine).length > 0) {
141
+ logger.debug(
142
+ 'Loaded %d custom define(s) from agentuity.config.ts for workbench',
143
+ Object.keys(userDefine).length
144
+ );
145
+ }
146
+
124
147
  viteConfig = {
125
148
  root: join(rootDir, '.agentuity/workbench-src'), // Use generated workbench source
126
149
  base, // All workbench assets are under the configured route
127
150
  plugins: [react(), patchPlugin({ logger, dev })],
128
151
  envPrefix: ['VITE_', 'AGENTUITY_PUBLIC_', 'PUBLIC_'],
152
+ define: {
153
+ // Merge user-defined constants
154
+ ...userDefine,
155
+ },
129
156
  build: {
130
157
  outDir: join(rootDir, '.agentuity/workbench'),
131
158
  rollupOptions: {