@agentuity/cli 0.0.86 → 0.0.88

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 (119) hide show
  1. package/bin/cli.ts +7 -0
  2. package/dist/bun-path.d.ts.map +1 -1
  3. package/dist/bun-path.js +1 -3
  4. package/dist/bun-path.js.map +1 -1
  5. package/dist/cli.js +3 -3
  6. package/dist/cmd/ai/index.d.ts.map +1 -1
  7. package/dist/cmd/ai/index.js +1 -0
  8. package/dist/cmd/ai/index.js.map +1 -1
  9. package/dist/cmd/build/ast.d.ts.map +1 -1
  10. package/dist/cmd/build/ast.js +5 -0
  11. package/dist/cmd/build/ast.js.map +1 -1
  12. package/dist/cmd/build/bundler.d.ts.map +1 -1
  13. package/dist/cmd/build/bundler.js +152 -7
  14. package/dist/cmd/build/bundler.js.map +1 -1
  15. package/dist/cmd/build/config-loader.d.ts +16 -0
  16. package/dist/cmd/build/config-loader.d.ts.map +1 -0
  17. package/dist/cmd/build/config-loader.js +165 -0
  18. package/dist/cmd/build/config-loader.js.map +1 -0
  19. package/dist/cmd/build/patch/_util.js +6 -6
  20. package/dist/cmd/build/patch/_util.js.map +1 -1
  21. package/dist/cmd/build/patch/llm.js +1 -1
  22. package/dist/cmd/build/patch/llm.js.map +1 -1
  23. package/dist/cmd/build/plugin.d.ts.map +1 -1
  24. package/dist/cmd/build/plugin.js +36 -15
  25. package/dist/cmd/build/plugin.js.map +1 -1
  26. package/dist/cmd/build/route-discovery.d.ts +8 -4
  27. package/dist/cmd/build/route-discovery.d.ts.map +1 -1
  28. package/dist/cmd/build/route-discovery.js +10 -5
  29. package/dist/cmd/build/route-discovery.js.map +1 -1
  30. package/dist/cmd/build/workbench.d.ts +1 -0
  31. package/dist/cmd/build/workbench.d.ts.map +1 -1
  32. package/dist/cmd/build/workbench.js +8 -1
  33. package/dist/cmd/build/workbench.js.map +1 -1
  34. package/dist/cmd/cloud/index.d.ts.map +1 -1
  35. package/dist/cmd/cloud/index.js +2 -0
  36. package/dist/cmd/cloud/index.js.map +1 -1
  37. package/dist/cmd/cloud/redis/get.d.ts +2 -0
  38. package/dist/cmd/cloud/redis/get.d.ts.map +1 -0
  39. package/dist/cmd/cloud/redis/get.js +62 -0
  40. package/dist/cmd/cloud/redis/get.js.map +1 -0
  41. package/dist/cmd/cloud/redis/index.d.ts +2 -0
  42. package/dist/cmd/cloud/redis/index.d.ts.map +1 -0
  43. package/dist/cmd/cloud/redis/index.js +13 -0
  44. package/dist/cmd/cloud/redis/index.js.map +1 -0
  45. package/dist/cmd/cloud/scp/download.js +3 -3
  46. package/dist/cmd/cloud/scp/download.js.map +1 -1
  47. package/dist/cmd/cloud/scp/upload.js +3 -3
  48. package/dist/cmd/cloud/scp/upload.js.map +1 -1
  49. package/dist/cmd/cloud/ssh.js +3 -3
  50. package/dist/cmd/cloud/ssh.js.map +1 -1
  51. package/dist/cmd/dev/index.d.ts.map +1 -1
  52. package/dist/cmd/dev/index.js +5 -0
  53. package/dist/cmd/dev/index.js.map +1 -1
  54. package/dist/cmd/index.d.ts.map +1 -1
  55. package/dist/cmd/index.js +7 -0
  56. package/dist/cmd/index.js.map +1 -1
  57. package/dist/cmd/profile/create.d.ts.map +1 -1
  58. package/dist/cmd/profile/create.js +1 -0
  59. package/dist/cmd/profile/create.js.map +1 -1
  60. package/dist/cmd/upgrade/index.d.ts +20 -0
  61. package/dist/cmd/upgrade/index.d.ts.map +1 -0
  62. package/dist/cmd/upgrade/index.js +307 -0
  63. package/dist/cmd/upgrade/index.js.map +1 -0
  64. package/dist/cmd/version/index.d.ts.map +1 -1
  65. package/dist/cmd/version/index.js +1 -0
  66. package/dist/cmd/version/index.js.map +1 -1
  67. package/dist/config.d.ts +1 -1
  68. package/dist/config.d.ts.map +1 -1
  69. package/dist/config.js +12 -94
  70. package/dist/config.js.map +1 -1
  71. package/dist/index.d.ts +1 -1
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/tui.d.ts +5 -0
  75. package/dist/tui.d.ts.map +1 -1
  76. package/dist/tui.js +23 -0
  77. package/dist/tui.js.map +1 -1
  78. package/dist/types.d.ts +73 -0
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/types.js.map +1 -1
  81. package/dist/utils/dependency-checker.d.ts +20 -0
  82. package/dist/utils/dependency-checker.d.ts.map +1 -0
  83. package/dist/utils/dependency-checker.js +161 -0
  84. package/dist/utils/dependency-checker.js.map +1 -0
  85. package/dist/version-check.d.ts +13 -0
  86. package/dist/version-check.d.ts.map +1 -0
  87. package/dist/version-check.js +177 -0
  88. package/dist/version-check.js.map +1 -0
  89. package/package.json +6 -4
  90. package/src/bun-path.ts +1 -3
  91. package/src/cli.ts +3 -3
  92. package/src/cmd/ai/index.ts +1 -0
  93. package/src/cmd/build/ast.ts +7 -0
  94. package/src/cmd/build/bundler.ts +181 -8
  95. package/src/cmd/build/config-loader.ts +200 -0
  96. package/src/cmd/build/patch/_util.ts +6 -6
  97. package/src/cmd/build/patch/llm.ts +1 -1
  98. package/src/cmd/build/plugin.ts +40 -17
  99. package/src/cmd/build/route-discovery.ts +10 -5
  100. package/src/cmd/build/workbench.ts +9 -1
  101. package/src/cmd/cloud/index.ts +2 -0
  102. package/src/cmd/cloud/redis/get.ts +72 -0
  103. package/src/cmd/cloud/redis/index.ts +13 -0
  104. package/src/cmd/cloud/scp/download.ts +3 -3
  105. package/src/cmd/cloud/scp/upload.ts +3 -3
  106. package/src/cmd/cloud/ssh.ts +3 -3
  107. package/src/cmd/dev/index.ts +11 -0
  108. package/src/cmd/index.ts +8 -0
  109. package/src/cmd/profile/create.ts +1 -0
  110. package/src/cmd/project/download.ts +1 -1
  111. package/src/cmd/upgrade/index.ts +365 -0
  112. package/src/cmd/version/index.ts +1 -0
  113. package/src/config.ts +12 -121
  114. package/src/git-helper.ts +4 -4
  115. package/src/index.ts +4 -0
  116. package/src/tui.ts +27 -0
  117. package/src/types.ts +80 -0
  118. package/src/utils/dependency-checker.ts +207 -0
  119. package/src/version-check.ts +234 -0
@@ -1,6 +1,6 @@
1
1
  import { $, semver } from 'bun';
2
2
  import { join, relative, resolve, dirname, basename } from 'node:path';
3
- import { cpSync, existsSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
3
+ import { cpSync, existsSync, mkdirSync, rmSync, readdirSync, readFileSync } from 'node:fs';
4
4
  import gitParseUrl from 'git-url-parse';
5
5
  import { StructuredError } from '@agentuity/core';
6
6
  import * as tui from '../../tui';
@@ -10,10 +10,12 @@ import { getFilesRecursively } from './file';
10
10
  import { getVersion } from '../../version';
11
11
  import type { Project } from '../../types';
12
12
  import { fixDuplicateExportsInDirectory } from './fix-duplicate-exports';
13
- import type { Logger } from '../../types';
13
+ import type { Logger, BuildContext } from '../../types';
14
14
  import { generateWorkbenchMainTsx, generateWorkbenchIndexHtml } from './workbench';
15
15
  import { analyzeWorkbench, type WorkbenchAnalysis } from './ast';
16
16
  import { type DeployOptions } from '../../schemas/deploy';
17
+ import { checkAndUpgradeDependencies } from '../../utils/dependency-checker';
18
+ import { loadBuildConfig, executeBuildConfig, mergeBuildConfig } from './config-loader';
17
19
 
18
20
  const minBunVersion = '>=1.3.3';
19
21
 
@@ -78,6 +80,77 @@ const InvalidBunVersion = StructuredError('InvalidBunVersion')<{
78
80
  required: string;
79
81
  }>();
80
82
 
83
+ /**
84
+ * Finds the workspace root by walking up the directory tree looking for a package.json with workspaces
85
+ */
86
+ function findWorkspaceRoot(startDir: string): string | null {
87
+ let currentDir = startDir;
88
+ while (true) {
89
+ const pkgPath = join(currentDir, 'package.json');
90
+ if (existsSync(pkgPath)) {
91
+ try {
92
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
93
+ if (pkg.workspaces) {
94
+ return currentDir;
95
+ }
96
+ } catch {
97
+ // Ignore parse errors, continue searching
98
+ }
99
+ }
100
+ const parent = resolve(currentDir, '..');
101
+ if (parent === currentDir) break; // reached filesystem root
102
+ currentDir = parent;
103
+ }
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Finds a package by searching multiple locations:
109
+ * 1. App-level node_modules
110
+ * 2. Workspace root node_modules
111
+ * 3. Workspace packages directory (for workspace packages)
112
+ *
113
+ * @param rootDir - Root directory of the project
114
+ * @param packageName - Package name (e.g., '@agentuity/workbench')
115
+ * @param logger - Optional logger for debug messages
116
+ * @returns Path to the package directory, or null if not found
117
+ */
118
+ function findPackagePath(rootDir: string, packageName: string, logger?: Logger): string | null {
119
+ const [scope, name] = packageName.startsWith('@')
120
+ ? packageName.slice(1).split('/')
121
+ : [null, packageName];
122
+
123
+ // 1. Try app-level node_modules
124
+ const appLevelPath = scope
125
+ ? join(rootDir, 'node_modules', `@${scope}`, name)
126
+ : join(rootDir, 'node_modules', name);
127
+ if (existsSync(appLevelPath)) {
128
+ logger?.debug(`Found ${packageName} at app level: ${appLevelPath}`);
129
+ return appLevelPath;
130
+ }
131
+
132
+ // 2. Try workspace root node_modules
133
+ const workspaceRoot = findWorkspaceRoot(rootDir);
134
+ if (workspaceRoot) {
135
+ const rootLevelPath = scope
136
+ ? join(workspaceRoot, 'node_modules', `@${scope}`, name)
137
+ : join(workspaceRoot, 'node_modules', name);
138
+ if (existsSync(rootLevelPath)) {
139
+ logger?.debug(`Found ${packageName} at workspace root: ${rootLevelPath}`);
140
+ return rootLevelPath;
141
+ }
142
+
143
+ // 3. Try workspace packages directory
144
+ const workspacePackagePath = join(workspaceRoot, 'packages', name);
145
+ if (existsSync(workspacePackagePath)) {
146
+ logger?.debug(`Found ${packageName} in workspace packages: ${workspacePackagePath}`);
147
+ return workspacePackagePath;
148
+ }
149
+ }
150
+
151
+ return null;
152
+ }
153
+
81
154
  const handleBuildFailure = (buildResult: BuildResult) => {
82
155
  // Collect all build errors with full details
83
156
  const errorMessages = buildResult.logs
@@ -130,6 +203,14 @@ export async function bundle({
130
203
  const versionOutput = await checkBunVersion();
131
204
  output.push(...versionOutput);
132
205
 
206
+ // Check and upgrade @agentuity/* dependencies if needed
207
+ const upgradeResult = await checkAndUpgradeDependencies(rootDir, logger);
208
+ if (upgradeResult.failed.length > 0 && process.stdin.isTTY) {
209
+ throw new BuildFailedError({
210
+ message: `Failed to upgrade dependencies: ${upgradeResult.failed.join(', ')}`,
211
+ });
212
+ }
213
+
133
214
  const outDir = customOutDir ?? join(rootDir, '.agentuity');
134
215
  const srcDir = join(rootDir, 'src');
135
216
 
@@ -293,8 +374,23 @@ export async function bundle({
293
374
  const tsconfigPath = join(rootDir, 'tsconfig.json');
294
375
  const hasTsconfig = existsSync(tsconfigPath);
295
376
 
377
+ // Load user build config (if it exists)
378
+ const buildConfigFunction = await loadBuildConfig(rootDir);
379
+
380
+ // Helper to create build context for config function
381
+ const createBuildContext = (): BuildContext => ({
382
+ rootDir,
383
+ dev,
384
+ outDir,
385
+ srcDir,
386
+ orgId,
387
+ projectId,
388
+ region,
389
+ logger,
390
+ });
391
+
296
392
  await (async () => {
297
- const config: Bun.BuildConfig = {
393
+ const baseConfig: Bun.BuildConfig = {
298
394
  entrypoints: appEntrypoints,
299
395
  root: rootDir,
300
396
  outdir: outDir,
@@ -321,7 +417,19 @@ export async function bundle({
321
417
  },
322
418
  tsconfig: hasTsconfig ? tsconfigPath : undefined,
323
419
  };
324
- const buildResult = await Bun.build(config);
420
+
421
+ // Apply user config for 'api' phase
422
+ let finalConfig = baseConfig;
423
+ if (buildConfigFunction) {
424
+ const userConfig = await executeBuildConfig(
425
+ buildConfigFunction,
426
+ 'api',
427
+ createBuildContext()
428
+ );
429
+ finalConfig = mergeBuildConfig(baseConfig, userConfig);
430
+ }
431
+
432
+ const buildResult = await Bun.build(finalConfig);
325
433
  if (!buildResult.success) {
326
434
  handleBuildFailure(buildResult);
327
435
  }
@@ -376,6 +484,9 @@ export async function bundle({
376
484
  }
377
485
  }
378
486
 
487
+ // must always set for the template always
488
+ define['process.env.AGENTUITY_PUBLIC_WORKBENCH_PATH'] = JSON.stringify('');
489
+
379
490
  // Analyze workbench config early to set environment variables for web build
380
491
  if (existsSync(appFile)) {
381
492
  if (!workbench) {
@@ -426,7 +537,7 @@ export async function bundle({
426
537
  mkdirSync(join(webOutDir, 'asset'), { recursive: true });
427
538
  const isLocalRegion = region === 'local' || region === 'l';
428
539
 
429
- const config: Bun.BuildConfig = {
540
+ const baseConfig: Bun.BuildConfig = {
430
541
  entrypoints: webEntrypoints,
431
542
  root: webDir,
432
543
  outdir: webOutDir,
@@ -455,7 +566,19 @@ export async function bundle({
455
566
  },
456
567
  tsconfig: hasTsconfig ? tsconfigPath : undefined,
457
568
  };
458
- const result = await Bun.build(config);
569
+
570
+ // Apply user config for 'web' phase
571
+ let finalConfig = baseConfig;
572
+ if (buildConfigFunction) {
573
+ const userConfig = await executeBuildConfig(
574
+ buildConfigFunction,
575
+ 'web',
576
+ createBuildContext()
577
+ );
578
+ finalConfig = mergeBuildConfig(baseConfig, userConfig);
579
+ }
580
+
581
+ const result = await Bun.build(finalConfig);
459
582
  if (result.success) {
460
583
  // Fix duplicate exports caused by Bun splitting bug
461
584
  // See: https://github.com/oven-sh/bun/issues/5344
@@ -495,12 +618,51 @@ export async function bundle({
495
618
 
496
619
  // Generate files using templates
497
620
  await Bun.write(join(tempWorkbenchDir, 'main.tsx'), generateWorkbenchMainTsx(config));
621
+
622
+ // Copy the pre-built standalone.css from workbench package instead of generating it
623
+ // This ensures we use the built version with Tailwind already processed
624
+ const workbenchPackagePath = findPackagePath(rootDir, '@agentuity/workbench', logger);
625
+ const workbenchStylesOut = join(tempWorkbenchDir, 'styles.css');
626
+
627
+ if (workbenchPackagePath) {
628
+ const workbenchStylesPath = join(workbenchPackagePath, 'dist', 'standalone.css');
629
+ const workbenchStylesSourcePath = join(workbenchPackagePath, 'src', 'standalone.css');
630
+
631
+ if (existsSync(workbenchStylesPath)) {
632
+ cpSync(workbenchStylesPath, workbenchStylesOut);
633
+ logger.debug('Copied workbench dist/standalone.css to temp workbench dir');
634
+ } else if (existsSync(workbenchStylesSourcePath)) {
635
+ // Fallback: copy source CSS file (contains Tailwind directives that need processing)
636
+ cpSync(workbenchStylesSourcePath, workbenchStylesOut);
637
+ logger.warn(
638
+ 'Workbench dist/standalone.css not found, using source CSS. Ensure @agentuity/workbench is built.'
639
+ );
640
+ } else {
641
+ throw new BuildFailedError({
642
+ message: `Workbench styles not found in ${workbenchPackagePath}. Expected either:
643
+ - ${workbenchStylesPath}
644
+ - ${workbenchStylesSourcePath}
645
+
646
+ Make sure @agentuity/workbench is built.`,
647
+ });
648
+ }
649
+ } else {
650
+ throw new BuildFailedError({
651
+ message: `Workbench package not found. Searched in:
652
+ - App-level node_modules
653
+ - Workspace root node_modules
654
+ - Workspace packages directory
655
+
656
+ Make sure @agentuity/workbench is installed or available in the workspace.`,
657
+ });
658
+ }
659
+
498
660
  const workbenchIndexFile = join(tempWorkbenchDir, 'index.html');
499
661
  await Bun.write(workbenchIndexFile, generateWorkbenchIndexHtml());
500
662
 
501
663
  // Bundle workbench using generated files
502
664
  // Disable splitting to avoid CommonJS/ESM module resolution conflicts
503
- const workbenchBuildConfig: Bun.BuildConfig = {
665
+ const workbenchBaseConfig: Bun.BuildConfig = {
504
666
  entrypoints: [workbenchIndexFile],
505
667
  outdir: join(outDir, 'workbench'),
506
668
  sourcemap: dev ? 'inline' : 'linked',
@@ -519,7 +681,18 @@ export async function bundle({
519
681
  },
520
682
  };
521
683
 
522
- const workbenchResult = await Bun.build(workbenchBuildConfig);
684
+ // Apply user config for 'workbench' phase
685
+ let finalWorkbenchConfig = workbenchBaseConfig;
686
+ if (buildConfigFunction) {
687
+ const userConfig = await executeBuildConfig(
688
+ buildConfigFunction,
689
+ 'workbench',
690
+ createBuildContext()
691
+ );
692
+ finalWorkbenchConfig = mergeBuildConfig(workbenchBaseConfig, userConfig);
693
+ }
694
+
695
+ const workbenchResult = await Bun.build(finalWorkbenchConfig);
523
696
  if (workbenchResult.success) {
524
697
  logger.debug('Workbench bundled successfully');
525
698
  // Clean up temp directory
@@ -0,0 +1,200 @@
1
+ import { join } from 'node:path';
2
+ import { StructuredError } from '@agentuity/core';
3
+ import type { BuildConfigFunction, BuildPhase, BuildContext, BuildConfig } from '../../types';
4
+
5
+ const BuildConfigLoadError = StructuredError('BuildConfigLoadError');
6
+ const BuildConfigValidationError = StructuredError('BuildConfigValidationError');
7
+
8
+ /**
9
+ * Reserved define keys that cannot be overridden by user config
10
+ */
11
+ const RESERVED_DEFINE_PREFIXES = ['process.env.AGENTUITY_', 'process.env.NODE_ENV'];
12
+
13
+ /**
14
+ * Load and validate agentuity.config.ts from the project root
15
+ */
16
+ export async function loadBuildConfig(rootDir: string): Promise<BuildConfigFunction | null> {
17
+ const configPath = join(rootDir, 'agentuity.config.ts');
18
+
19
+ // Check if config file exists
20
+ const configFile = Bun.file(configPath);
21
+ if (!(await configFile.exists())) {
22
+ return null; // No config file is OK - it's optional
23
+ }
24
+
25
+ try {
26
+ // Import the config file (Bun handles TypeScript natively)
27
+ // Use file:// URL to ensure absolute path resolution
28
+ const configModule = await import(`file://${configPath}`);
29
+
30
+ // Get the default export
31
+ const configFunction = configModule.default;
32
+
33
+ // Validate it's a function
34
+ if (typeof configFunction !== 'function') {
35
+ throw new BuildConfigValidationError({
36
+ message: `agentuity.config.ts must export a default function, got ${typeof configFunction}`,
37
+ });
38
+ }
39
+
40
+ return configFunction as BuildConfigFunction;
41
+ } catch (error) {
42
+ // If it's already our error, re-throw
43
+ if (error instanceof BuildConfigValidationError || error instanceof BuildConfigLoadError) {
44
+ throw error;
45
+ }
46
+
47
+ // Wrap other errors
48
+ throw new BuildConfigLoadError({
49
+ message: `Failed to load agentuity.config.ts: ${error instanceof Error ? error.message : String(error)}`,
50
+ cause: error instanceof Error ? error : undefined,
51
+ });
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Execute the build config function for a specific phase and validate the result
57
+ */
58
+ export async function executeBuildConfig(
59
+ configFunction: BuildConfigFunction,
60
+ phase: BuildPhase,
61
+ context: BuildContext
62
+ ): Promise<BuildConfig> {
63
+ try {
64
+ // Execute the config function (may be async)
65
+ const config = await configFunction(phase, context);
66
+
67
+ // Validate the result is an object
68
+ if (!config || typeof config !== 'object') {
69
+ throw new BuildConfigValidationError({
70
+ message: `Build config for phase "${phase}" must return an object, got ${typeof config}`,
71
+ });
72
+ }
73
+
74
+ // Validate plugins array if provided
75
+ if (config.plugins !== undefined) {
76
+ if (!Array.isArray(config.plugins)) {
77
+ throw new BuildConfigValidationError({
78
+ message: `Build config plugins for phase "${phase}" must be an array, got ${typeof config.plugins}`,
79
+ });
80
+ }
81
+ // Validate each plugin has a name property (basic BunPlugin check)
82
+ for (const plugin of config.plugins) {
83
+ if (!plugin || typeof plugin !== 'object' || !('name' in plugin)) {
84
+ throw new BuildConfigValidationError({
85
+ message: `Invalid plugin in phase "${phase}": plugins must be BunPlugin objects with a name property`,
86
+ });
87
+ }
88
+ }
89
+ }
90
+
91
+ // Validate external array if provided
92
+ if (config.external !== undefined) {
93
+ if (!Array.isArray(config.external)) {
94
+ throw new BuildConfigValidationError({
95
+ message: `Build config external for phase "${phase}" must be an array, got ${typeof config.external}`,
96
+ });
97
+ }
98
+ // Validate each external is a string
99
+ for (const ext of config.external) {
100
+ if (typeof ext !== 'string') {
101
+ throw new BuildConfigValidationError({
102
+ message: `Invalid external in phase "${phase}": all externals must be strings, got ${typeof ext}`,
103
+ });
104
+ }
105
+ }
106
+ }
107
+
108
+ // Validate and filter define object if provided
109
+ if (config.define !== undefined) {
110
+ if (typeof config.define !== 'object' || config.define === null) {
111
+ throw new BuildConfigValidationError({
112
+ message: `Build config define for phase "${phase}" must be an object, got ${typeof config.define}`,
113
+ });
114
+ }
115
+
116
+ // Check for reserved keys and filter them out
117
+ const filteredDefine: Record<string, string> = {};
118
+ const blockedKeys: string[] = [];
119
+
120
+ for (const [key, value] of Object.entries(config.define)) {
121
+ // Check if this key starts with any reserved prefix
122
+ const isReserved = RESERVED_DEFINE_PREFIXES.some((prefix) => key.startsWith(prefix));
123
+
124
+ if (isReserved) {
125
+ blockedKeys.push(key);
126
+ continue; // Skip reserved keys
127
+ }
128
+
129
+ // Validate value is a string
130
+ if (typeof value !== 'string') {
131
+ throw new BuildConfigValidationError({
132
+ message: `Build config define values for phase "${phase}" must be strings, got ${typeof value} for key "${key}"`,
133
+ });
134
+ }
135
+
136
+ filteredDefine[key] = value;
137
+ }
138
+
139
+ // Warn if we blocked any keys
140
+ if (blockedKeys.length > 0) {
141
+ context.logger.warn(
142
+ `Build config for phase "${phase}" attempted to override reserved define keys (ignored): ${blockedKeys.join(', ')}`
143
+ );
144
+ }
145
+
146
+ // Replace with filtered version
147
+ config.define = filteredDefine;
148
+ }
149
+
150
+ return config;
151
+ } catch (error) {
152
+ // If it's already our error, re-throw
153
+ if (error instanceof BuildConfigValidationError) {
154
+ throw error;
155
+ }
156
+
157
+ // Wrap other errors
158
+ throw new BuildConfigLoadError({
159
+ message: `Failed to execute build config for phase "${phase}": ${error instanceof Error ? error.message : String(error)}`,
160
+ cause: error instanceof Error ? error : undefined,
161
+ });
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Merge user build config with base Bun.BuildConfig
167
+ * User config is applied AFTER base config with safeguards
168
+ * Returns a complete Bun.BuildConfig with user overrides applied
169
+ */
170
+ export function mergeBuildConfig(
171
+ baseConfig: import('bun').BuildConfig,
172
+ userConfig: BuildConfig
173
+ ): import('bun').BuildConfig {
174
+ const merged = { ...baseConfig };
175
+
176
+ // Merge plugins (user plugins come AFTER Agentuity plugin)
177
+ if (userConfig.plugins && userConfig.plugins.length > 0) {
178
+ merged.plugins = [...(baseConfig.plugins ?? []), ...userConfig.plugins];
179
+ }
180
+
181
+ // Merge external (combine arrays, dedupe)
182
+ if (userConfig.external && userConfig.external.length > 0) {
183
+ const existingExternal = Array.isArray(baseConfig.external)
184
+ ? baseConfig.external
185
+ : baseConfig.external
186
+ ? [baseConfig.external]
187
+ : [];
188
+ merged.external = [...new Set([...existingExternal, ...userConfig.external])];
189
+ }
190
+
191
+ // Merge define (user defines come last, but reserved keys already filtered)
192
+ if (userConfig.define && Object.keys(userConfig.define).length > 0) {
193
+ merged.define = {
194
+ ...baseConfig.define,
195
+ ...userConfig.define,
196
+ };
197
+ }
198
+
199
+ return merged;
200
+ }
@@ -44,13 +44,13 @@ export function generateGatewayEnvGuard(
44
44
  provider: string
45
45
  ): string {
46
46
  return `{
47
- const apikey = process.env.AGENTUITY_SDK_KEY;
48
- const url = process.env.AGENTUITY_AIGATEWAY_URL || process.env.AGENTUITY_TRANSPORT_URL || (apikey ? 'https://agentuity.ai' : '');
49
- if (url && apikey) {
50
- process.env.${apikey} = ${apikeyval};
51
- process.env.${apibase} = url + '/gateway/${provider}';
47
+ const _agentuity_sdk_key = process.env.AGENTUITY_SDK_KEY;
48
+ const _agentuity_url = process.env.AGENTUITY_AIGATEWAY_URL || process.env.AGENTUITY_TRANSPORT_URL || (_agentuity_sdk_key ? 'https://agentuity.ai' : '');
49
+ if (_agentuity_url && _agentuity_sdk_key) {
50
+ process.env.${apikey} = _agentuity_sdk_key;
51
+ process.env.${apibase} = _agentuity_url + '/gateway/${provider}';
52
52
  console.debug('Enabled Agentuity AI Gateway for ${provider}');
53
- } else {
53
+ } else if (!process.env.${apikey}) {
54
54
  ${generateEnvWarning(apikey)}
55
55
  }
56
56
  }
@@ -24,7 +24,7 @@ export function generatePatches(): Map<string, PatchModule> {
24
24
  const patches = new Map<string, PatchModule>();
25
25
  registerLLMPatch(
26
26
  patches,
27
- '@anthropic-ai',
27
+ '@anthropic-ai/sdk',
28
28
  'index',
29
29
  'ANTHROPIC_API_KEY',
30
30
  'ANTHROPIC_BASE_URL',
@@ -336,8 +336,11 @@ const AgentuityBundler: BunPlugin = {
336
336
  // Setup workbench configuration - evaluate fresh each time during builds
337
337
  const workbenchConfig = await setupWorkbench(srcDir);
338
338
 
339
+ // Store web routes for later (must be registered AFTER API routes)
340
+ let webRoutesInsert: string | null = null;
341
+
339
342
  if (existsSync(indexFile)) {
340
- inserts.push(`import { serveStatic } from 'hono/bun';
343
+ webRoutesInsert = `import { serveStatic } from 'hono/bun';
341
344
  import { getRouter, registerDevModeRoutes } from '@agentuity/runtime';
342
345
  import { readFileSync, existsSync } from 'node:fs';
343
346
 
@@ -375,6 +378,8 @@ import { readFileSync, existsSync } from 'node:fs';
375
378
  index += html;
376
379
  }
377
380
  }
381
+ // make paths absolute
382
+ index = index.replaceAll('./web/', '/web/');
378
383
  const webstatic = serveStatic({ root: import.meta.dir + '/web' });
379
384
  // In dev mode, serve from source; in prod, serve from build output
380
385
  const publicRoot = ${isDevMode} ? ${JSON.stringify(join(srcDir, 'web', 'public'))} : import.meta.dir + '/web/public';
@@ -385,22 +390,22 @@ import { readFileSync, existsSync } from 'node:fs';
385
390
  // Serve public assets at root (e.g., /favicon.ico) - must be last
386
391
  router.get('/*', async (c, next) => {
387
392
  const path = c.req.path;
393
+ // Skip API routes - let them 404 naturally
394
+ if (path === '/api' || path.startsWith('/api/')) {
395
+ return c.notFound();
396
+ }
388
397
  // Prevent directory traversal attacks
389
398
  if (path.includes('..') || path.includes('%2e%2e')) {
390
399
  return c.notFound();
391
400
  }
392
- // Only serve from public folder at root (skip /web/* routes and /)
393
- if (path !== '/' && !path.startsWith('/web/')) {
394
- try {
395
- // serveStatic calls next() internally if file not found
396
- return await publicstatic(c, next);
397
- } catch (err) {
398
- return next();
399
- }
401
+ // Don't catch workbench routes (already handled above)
402
+ if (path.startsWith('/workbench')) {
403
+ return c.notFound();
400
404
  }
401
- return next();
405
+ // serve default for any path not explicitly matched
406
+ return c.html(index);
402
407
  });
403
- })();`);
408
+ })();`;
404
409
  }
405
410
 
406
411
  // Build agentInfo from all discovered agents and track directories
@@ -495,6 +500,14 @@ import { readFileSync, existsSync } from 'node:fs';
495
500
  }
496
501
 
497
502
  for (const apiFile of apiFiles) {
503
+ // Quick check: skip files that don't contain createRouter or Hono
504
+ // This avoids expensive AST parsing for utility files
505
+ const fileContent = await Bun.file(apiFile).text();
506
+ if (!fileContent.includes('createRouter') && !fileContent.includes('new Hono')) {
507
+ logger.trace(`Skipping ${apiFile}: no createRouter or Hono found`);
508
+ continue;
509
+ }
510
+
498
511
  try {
499
512
  const routes = await parseRoute(rootDir, apiFile, projectId, deploymentId);
500
513
 
@@ -574,12 +587,17 @@ import { readFileSync, existsSync } from 'node:fs';
574
587
  });
575
588
  }
576
589
  } catch (error) {
577
- // Skip files that don't have createRouter (they might be utilities)
578
- if (
579
- error instanceof Error &&
580
- error.message.includes('could not find an proper createRouter')
581
- ) {
582
- logger.trace(`Skipping ${apiFile}: no createRouter found`);
590
+ // Skip files that don't have proper router setup despite containing createRouter/Hono
591
+ // (e.g., files that import but don't use them, or have syntax errors)
592
+ if (error instanceof Error) {
593
+ if (
594
+ error.message.includes('could not find default export') ||
595
+ error.message.includes('could not find an proper createRouter')
596
+ ) {
597
+ logger.trace(`Skipping ${apiFile}: ${error.message}`);
598
+ } else {
599
+ throw error;
600
+ }
583
601
  } else {
584
602
  throw error;
585
603
  }
@@ -619,6 +637,11 @@ import { readFileSync, existsSync } from 'node:fs';
619
637
  })();`);
620
638
  }
621
639
 
640
+ // Add web routes AFTER API routes (catch-all must be last)
641
+ if (webRoutesInsert) {
642
+ inserts.push(webRoutesInsert);
643
+ }
644
+
622
645
  const file = Bun.file(args.path);
623
646
  let contents = await file.text();
624
647
  // Use AST-based parsing to reliably find createApp statement end
@@ -19,17 +19,21 @@ export interface DiscoveredRouteFile {
19
19
  }
20
20
 
21
21
  /**
22
- * Recursively discover all TypeScript route files in an API directory
22
+ * Recursively discover all TypeScript files in an API directory.
23
+ * Any .ts file CAN be a route if it exports a router, but not all files MUST be routes.
24
+ * Files without router exports (utility files, types, helpers) are discovered but gracefully skipped during processing.
25
+ *
23
26
  * Supports nested structures like:
24
- * - src/api/index.ts (root API router)
27
+ * - src/api/index.ts (root API router - handled separately)
25
28
  * - src/api/auth/route.ts -> mounted at /api/auth
29
+ * - src/api/auth/login.ts -> mounted at /api/auth
26
30
  * - src/api/v1/users/route.ts -> mounted at /api/v1/users
27
- * - src/api/admin/users/login.ts -> mounted at /api/admin/users (any .ts file works)
31
+ * - src/api/auth/helpers.ts -> discovered but skipped if no router export
28
32
  *
29
33
  * @param apiDir - Absolute path to the src/api directory
30
34
  * @param currentDir - Current directory being scanned (used for recursion)
31
35
  * @param results - Accumulated results (used for recursion)
32
- * @returns Array of discovered route files with mount information
36
+ * @returns Array of discovered TypeScript files with mount information
33
37
  */
34
38
  export function discoverRouteFiles(
35
39
  apiDir: string,
@@ -63,8 +67,9 @@ export function discoverRouteFiles(
63
67
 
64
68
  // For subdirectory files, determine mount path
65
69
  // src/api/auth/route.ts -> /api/auth
70
+ // src/api/auth/login.ts -> /api/auth
66
71
  // src/api/v1/users/route.ts -> /api/v1/users
67
- // src/api/admin/login.ts -> /api/admin
72
+ // src/api/admin/helpers.ts -> /api/admin (discovered but may be skipped during processing)
68
73
  const pathParts = relativePath.split('/');
69
74
  pathParts.pop(); // Remove filename
70
75
 
@@ -8,7 +8,6 @@ export function generateWorkbenchMainTsx(config: WorkbenchConfig): string {
8
8
  import React from 'react';
9
9
  import { createRoot } from 'react-dom/client';
10
10
  import { App } from '@agentuity/workbench';
11
- import '@agentuity/workbench/styles';
12
11
 
13
12
  // Root element
14
13
  const rootElement = document.getElementById('root');
@@ -23,6 +22,14 @@ root.render(<App configBase64="${encodedConfig}" />);
23
22
  `;
24
23
  }
25
24
 
25
+ export function generateWorkbenchStylesCss(): string {
26
+ // This file will be replaced with the actual dist/styles.css content during build
27
+ // We use @import here as a placeholder, but the bundler should resolve it to the built file
28
+ return `/* Generated workbench styles - will be replaced with dist/styles.css */
29
+ @import '@agentuity/workbench/styles-standalone';
30
+ `;
31
+ }
32
+
26
33
  export function generateWorkbenchIndexHtml(): string {
27
34
  return `<!DOCTYPE html>
28
35
  <html lang="en">
@@ -30,6 +37,7 @@ export function generateWorkbenchIndexHtml(): string {
30
37
  <meta charset="UTF-8">
31
38
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
32
39
  <title>Agentuity Workbench</title>
40
+ <link rel="stylesheet" href="./styles.css">
33
41
  </head>
34
42
  <body>
35
43
  <div id="root"></div>