@agentuity/cli 0.0.108 → 0.0.110

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 (87) hide show
  1. package/dist/build-report.d.ts +201 -0
  2. package/dist/build-report.d.ts.map +1 -0
  3. package/dist/build-report.js +335 -0
  4. package/dist/build-report.js.map +1 -0
  5. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  6. package/dist/cmd/build/entry-generator.js +9 -3
  7. package/dist/cmd/build/entry-generator.js.map +1 -1
  8. package/dist/cmd/build/index.d.ts.map +1 -1
  9. package/dist/cmd/build/index.js +44 -1
  10. package/dist/cmd/build/index.js.map +1 -1
  11. package/dist/cmd/build/typecheck.d.ts +7 -1
  12. package/dist/cmd/build/typecheck.d.ts.map +1 -1
  13. package/dist/cmd/build/typecheck.js +11 -1
  14. package/dist/cmd/build/typecheck.js.map +1 -1
  15. package/dist/cmd/build/vite/index.d.ts +2 -1
  16. package/dist/cmd/build/vite/index.d.ts.map +1 -1
  17. package/dist/cmd/build/vite/index.js +2 -1
  18. package/dist/cmd/build/vite/index.js.map +1 -1
  19. package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -1
  20. package/dist/cmd/build/vite/metadata-generator.js +2 -4
  21. package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
  22. package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
  23. package/dist/cmd/build/vite/registry-generator.js +56 -18
  24. package/dist/cmd/build/vite/registry-generator.js.map +1 -1
  25. package/dist/cmd/build/vite/vite-builder.d.ts +3 -0
  26. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  27. package/dist/cmd/build/vite/vite-builder.js +14 -1
  28. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  29. package/dist/cmd/build/vite-bundler.d.ts +3 -0
  30. package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
  31. package/dist/cmd/build/vite-bundler.js +14 -5
  32. package/dist/cmd/build/vite-bundler.js.map +1 -1
  33. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  34. package/dist/cmd/cloud/deploy.js +86 -9
  35. package/dist/cmd/cloud/deploy.js.map +1 -1
  36. package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
  37. package/dist/cmd/cloud/deployment/show.js +0 -1
  38. package/dist/cmd/cloud/deployment/show.js.map +1 -1
  39. package/dist/cmd/dev/index.d.ts.map +1 -1
  40. package/dist/cmd/dev/index.js +109 -15
  41. package/dist/cmd/dev/index.js.map +1 -1
  42. package/dist/cmd/project/auth/generate.d.ts +5 -0
  43. package/dist/cmd/project/auth/generate.d.ts.map +1 -0
  44. package/dist/cmd/project/auth/generate.js +102 -0
  45. package/dist/cmd/project/auth/generate.js.map +1 -0
  46. package/dist/cmd/project/auth/index.d.ts +2 -0
  47. package/dist/cmd/project/auth/index.d.ts.map +1 -0
  48. package/dist/cmd/project/auth/index.js +21 -0
  49. package/dist/cmd/project/auth/index.js.map +1 -0
  50. package/dist/cmd/project/auth/init.d.ts +2 -0
  51. package/dist/cmd/project/auth/init.d.ts.map +1 -0
  52. package/dist/cmd/project/auth/init.js +220 -0
  53. package/dist/cmd/project/auth/init.js.map +1 -0
  54. package/dist/cmd/project/auth/shared.d.ts +88 -0
  55. package/dist/cmd/project/auth/shared.d.ts.map +1 -0
  56. package/dist/cmd/project/auth/shared.js +435 -0
  57. package/dist/cmd/project/auth/shared.js.map +1 -0
  58. package/dist/cmd/project/index.d.ts.map +1 -1
  59. package/dist/cmd/project/index.js +9 -1
  60. package/dist/cmd/project/index.js.map +1 -1
  61. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  62. package/dist/cmd/project/template-flow.js +106 -0
  63. package/dist/cmd/project/template-flow.js.map +1 -1
  64. package/dist/types.d.ts +1 -3
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js +1 -2
  67. package/dist/types.js.map +1 -1
  68. package/package.json +5 -4
  69. package/src/build-report.ts +457 -0
  70. package/src/cmd/build/entry-generator.ts +9 -3
  71. package/src/cmd/build/index.ts +51 -1
  72. package/src/cmd/build/typecheck.ts +19 -1
  73. package/src/cmd/build/vite/index.ts +4 -1
  74. package/src/cmd/build/vite/metadata-generator.ts +4 -6
  75. package/src/cmd/build/vite/registry-generator.ts +58 -19
  76. package/src/cmd/build/vite/vite-builder.ts +18 -1
  77. package/src/cmd/build/vite-bundler.ts +17 -4
  78. package/src/cmd/cloud/deploy.ts +105 -11
  79. package/src/cmd/cloud/deployment/show.ts +0 -1
  80. package/src/cmd/dev/index.ts +115 -14
  81. package/src/cmd/project/auth/generate.ts +116 -0
  82. package/src/cmd/project/auth/index.ts +21 -0
  83. package/src/cmd/project/auth/init.ts +263 -0
  84. package/src/cmd/project/auth/shared.ts +534 -0
  85. package/src/cmd/project/index.ts +9 -1
  86. package/src/cmd/project/template-flow.ts +125 -0
  87. package/src/types.ts +1 -2
@@ -478,12 +478,10 @@ export async function generateMetadata(options: MetadataGeneratorOptions): Promi
478
478
  if (options.deploymentOptions.pullRequestNumber) {
479
479
  git.pull_request = {
480
480
  number: options.deploymentOptions.pullRequestNumber,
481
- commentId: options.deploymentOptions.pullRequestCommentId,
482
- url: options.deploymentOptions.pullRequestURL,
481
+ url: options.deploymentOptions.pullRequestUrl,
483
482
  };
484
- delete git.pullRequestCommentId;
485
483
  delete git.pullRequestNumber;
486
- delete git.pullRequestURL;
484
+ delete git.pullRequestUrl;
487
485
  }
488
486
  metadata.deployment.git = git;
489
487
  }
@@ -706,10 +704,10 @@ function generateAgentsMd(metadata: BuildMetadata): string {
706
704
  lines.push('.agentuity/');
707
705
  lines.push('├── app.js # Bundled server application');
708
706
  lines.push('├── agentuity.metadata.json # Build metadata and schemas');
709
- if (metadata.assets?.some((a) => a.filename.startsWith('client/'))) {
707
+ if (metadata.assets?.some((a: { filename: string }) => a.filename.startsWith('client/'))) {
710
708
  lines.push('├── client/ # Frontend assets (fallback, CDN by default)');
711
709
  }
712
- if (metadata.assets?.some((a) => a.filename.startsWith('public/'))) {
710
+ if (metadata.assets?.some((a: { filename: string }) => a.filename.startsWith('public/'))) {
713
711
  lines.push('├── public/ # Static assets');
714
712
  }
715
713
  lines.push('└── AGENTS.md # This file');
@@ -36,11 +36,14 @@ export function generateAgentRegistry(srcDir: string, agents: AgentMetadata[]):
36
36
  const generatedDir = join(srcDir, 'generated');
37
37
  const registryPath = join(generatedDir, 'registry.ts');
38
38
 
39
+ // Sort agents by name for deterministic output
40
+ const sortedAgents = [...agents].sort((a, b) => a.name.localeCompare(b.name));
41
+
39
42
  // Detect naming collisions in generated identifiers
40
43
  const generatedNames = new Set<string>();
41
44
  const collisions: string[] = [];
42
45
 
43
- for (const agent of agents) {
46
+ for (const agent of sortedAgents) {
44
47
  const camelName = toCamelCase(agent.name);
45
48
 
46
49
  if (generatedNames.has(camelName)) {
@@ -59,7 +62,7 @@ export function generateAgentRegistry(srcDir: string, agents: AgentMetadata[]):
59
62
  }
60
63
 
61
64
  // Generate imports for all agents
62
- const imports = agents
65
+ const imports = sortedAgents
63
66
  .map(({ name, filename }) => {
64
67
  const camelName = toCamelCase(name);
65
68
  // Handle both './agent/...' and 'src/agent/...' formats
@@ -80,7 +83,7 @@ export function generateAgentRegistry(srcDir: string, agents: AgentMetadata[]):
80
83
  .join('\n');
81
84
 
82
85
  // Generate schema type exports for all agents
83
- const schemaTypeExports = agents
86
+ const schemaTypeExports = sortedAgents
84
87
  .map(({ name, description }) => {
85
88
  const camelName = toCamelCase(name);
86
89
  const pascalName = toPascalCase(name);
@@ -122,7 +125,7 @@ export function generateAgentRegistry(srcDir: string, agents: AgentMetadata[]):
122
125
  .join('\n');
123
126
 
124
127
  // Generate flat registry structure with JSDoc
125
- const registry = agents
128
+ const registry = sortedAgents
126
129
  .map(({ name, description }) => {
127
130
  const camelName = toCamelCase(name);
128
131
  const pascalName = toPascalCase(name);
@@ -138,7 +141,7 @@ export function generateAgentRegistry(srcDir: string, agents: AgentMetadata[]):
138
141
 
139
142
  // Generate flat agent type definitions for AgentRegistry interface augmentation
140
143
  // Uses the exported Agent types defined above
141
- const runtimeAgentTypes = agents
144
+ const runtimeAgentTypes = sortedAgents
142
145
  .map(({ name }) => {
143
146
  const camelName = toCamelCase(name);
144
147
  const pascalName = toPascalCase(name);
@@ -231,7 +234,7 @@ function generateRPCRegistryType(
231
234
  apiRoutes: RouteInfo[],
232
235
  websocketRoutes: RouteInfo[],
233
236
  sseRoutes: RouteInfo[],
234
- _agentImports: Map<string, string>,
237
+ agentImports: Map<string, string>,
235
238
  _schemaImportAliases: Map<string, Map<string, string>>,
236
239
  agentMetadataMap: Map<string, AgentMetadata>
237
240
  ): string {
@@ -263,6 +266,10 @@ function generateRPCRegistryType(
263
266
  // Add path segments - sanitize for valid TypeScript property names
264
267
  for (let i = 0; i < pathParts.length; i++) {
265
268
  const part = sanitizePathSegment(pathParts[i]);
269
+ // Skip empty segments (e.g., wildcards like '*' that sanitize to '')
270
+ if (!part) {
271
+ continue;
272
+ }
266
273
  if (!current[part]) {
267
274
  current[part] = {};
268
275
  }
@@ -292,8 +299,12 @@ function generateRPCRegistryType(
292
299
  // Only reference type names if route has actual schemas extracted, otherwise use 'never'
293
300
  // Note: hasValidator may be true (e.g., zValidator('query', ...)) but no schemas extracted
294
301
  // because only 'json' validators extract input schemas
302
+ // Also check if agentVariable exists but import wasn't added (missing agentImportPath)
303
+ const hasValidAgentImport = route.agentVariable
304
+ ? !!agentImports.get(route.agentVariable)
305
+ : false;
295
306
  const hasSchemas =
296
- route.inputSchemaVariable || route.outputSchemaVariable || route.agentVariable;
307
+ route.inputSchemaVariable || route.outputSchemaVariable || hasValidAgentImport;
297
308
 
298
309
  current[terminalMethod] = {
299
310
  input: hasSchemas ? `${pascalName}Input` : 'never',
@@ -315,7 +326,9 @@ function generateRPCRegistryType(
315
326
  function treeToTypeString(node: NestedNode, indent: string = '\t\t'): string {
316
327
  const lines: string[] = [];
317
328
 
318
- for (const [key, value] of Object.entries(node)) {
329
+ // Sort entries alphabetically for deterministic output
330
+ const sortedEntries = Object.entries(node).sort(([a], [b]) => a.localeCompare(b));
331
+ for (const [key, value] of sortedEntries) {
319
332
  if (
320
333
  value &&
321
334
  typeof value === 'object' &&
@@ -400,6 +413,10 @@ function generateRPCRuntimeMetadata(
400
413
  // Sanitize path segments for valid property names (must match type generation)
401
414
  for (const part of pathParts) {
402
415
  const sanitized = sanitizePathSegment(part);
416
+ // Skip empty segments (e.g., wildcards like '*' that sanitize to '')
417
+ if (!sanitized) {
418
+ continue;
419
+ }
403
420
  if (!current[sanitized]) current[sanitized] = {};
404
421
  current = current[sanitized];
405
422
  }
@@ -421,7 +438,21 @@ function generateRPCRuntimeMetadata(
421
438
  websocketRoutes.forEach((r) => addRoute(r, 'websocket'));
422
439
  sseRoutes.forEach((r) => addRoute(r, 'sse'));
423
440
 
424
- return JSON.stringify(tree, null, '\t\t');
441
+ // Sort object keys recursively for deterministic output
442
+ const sortObject = (obj: MetadataNode): MetadataNode => {
443
+ const sorted: MetadataNode = {};
444
+ for (const key of Object.keys(obj).sort()) {
445
+ const value = obj[key];
446
+ if (value && typeof value === 'object' && !('type' in value)) {
447
+ sorted[key] = sortObject(value as MetadataNode);
448
+ } else {
449
+ sorted[key] = value;
450
+ }
451
+ }
452
+ return sorted;
453
+ };
454
+
455
+ return JSON.stringify(sortObject(tree), null, '\t\t');
425
456
  }
426
457
 
427
458
  /**
@@ -450,10 +481,13 @@ export function generateRouteRegistry(
450
481
  // If we can't read package.json, assume no React dependency
451
482
  }
452
483
 
453
- // Filter routes by type
454
- const apiRoutes = routes.filter((r) => r.routeType === 'api' || r.routeType === 'stream');
455
- const websocketRoutes = routes.filter((r) => r.routeType === 'websocket');
456
- const sseRoutes = routes.filter((r) => r.routeType === 'sse');
484
+ // Filter routes by type and sort by path for deterministic output
485
+ const sortByPath = (a: RouteInfo, b: RouteInfo) => a.path.localeCompare(b.path);
486
+ const apiRoutes = routes
487
+ .filter((r) => r.routeType === 'api' || r.routeType === 'stream')
488
+ .sort(sortByPath);
489
+ const websocketRoutes = routes.filter((r) => r.routeType === 'websocket').sort(sortByPath);
490
+ const sseRoutes = routes.filter((r) => r.routeType === 'sse').sort(sortByPath);
457
491
 
458
492
  const allRoutes = [...apiRoutes, ...websocketRoutes, ...sseRoutes];
459
493
 
@@ -637,8 +671,10 @@ export function generateRouteRegistry(
637
671
  agentMeta = agentMetadataMap.get(route.agentVariable);
638
672
  }
639
673
 
640
- if (route.agentVariable) {
641
- const importName = agentImports.get(route.agentVariable)!;
674
+ // Only generate agent-based types if the import was successfully added
675
+ // (import is only added when hasValidator && agentVariable && agentImportPath are all present)
676
+ const importName = route.agentVariable ? agentImports.get(route.agentVariable) : undefined;
677
+ if (importName) {
642
678
  inputType = `InferInput<typeof ${importName}['inputSchema']>`;
643
679
  outputType = `InferOutput<typeof ${importName}['outputSchema']>`;
644
680
  inputSchemaType = `typeof ${importName} extends { inputSchema?: infer I } ? I : never`;
@@ -724,10 +760,16 @@ export function generateRouteRegistry(
724
760
  .replace(/_+/g, '_');
725
761
  const pascalName = toPascalCase(safeName);
726
762
 
763
+ // Use the exported schema types we generated above
764
+ // Note: agentImports.get() may return undefined if import wasn't added
765
+ const importName = route.agentVariable ? agentImports.get(route.agentVariable) : null;
766
+
727
767
  // Use 'never' types if no schemas were actually extracted
728
768
  // Note: hasValidator may be true (e.g., zValidator('query', ...)) but no schemas extracted
729
769
  // because only 'json' validators extract input schemas
730
- if (!route.inputSchemaVariable && !route.outputSchemaVariable && !route.agentVariable) {
770
+ // Also check if agentVariable exists but import wasn't added (missing agentImportPath)
771
+ const hasValidAgentImport = route.agentVariable ? !!importName : false;
772
+ if (!route.inputSchemaVariable && !route.outputSchemaVariable && !hasValidAgentImport) {
731
773
  const streamValue = route.stream === true ? 'true' : 'false';
732
774
  return `\t'${routeKey}': {
733
775
  \t\tinputSchema: never;
@@ -735,9 +777,6 @@ export function generateRouteRegistry(
735
777
  \t\tstream: ${streamValue};
736
778
  \t};`;
737
779
  }
738
-
739
- // Use the exported schema types we generated above
740
- const importName = route.agentVariable ? agentImports.get(route.agentVariable)! : null;
741
780
  const streamValue = importName
742
781
  ? `typeof ${importName} extends { stream?: infer S } ? S : false`
743
782
  : route.stream === true
@@ -10,6 +10,7 @@ import { createRequire } from 'node:module';
10
10
  import type { InlineConfig, Plugin } from 'vite';
11
11
  import type { Logger, DeployOptions } from '../../../types';
12
12
  import { browserEnvPlugin } from './browser-env-plugin';
13
+ import type { BuildReportCollector } from '../../../build-report';
13
14
 
14
15
  /**
15
16
  * Vite plugin to flatten the output structure for index.html
@@ -56,6 +57,8 @@ export interface ViteBuildOptions {
56
57
  workbenchEnabled?: boolean;
57
58
  logger: Logger;
58
59
  deploymentOptions?: DeployOptions;
60
+ /** Optional collector for structured error reporting */
61
+ collector?: BuildReportCollector;
59
62
  }
60
63
 
61
64
  /**
@@ -86,6 +89,11 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
86
89
  const { generateLifecycleTypes } = await import('./lifecycle-generator');
87
90
  await generateLifecycleTypes(rootDir, srcDir, logger);
88
91
 
92
+ // Load workbench config for entry file generation
93
+ const { loadAgentuityConfig, getWorkbenchConfig } = await import('./config-loader');
94
+ const config = await loadAgentuityConfig(rootDir, logger);
95
+ const workbenchConfig = getWorkbenchConfig(config, dev);
96
+
89
97
  // Then, generate the entry file
90
98
  const { generateEntryFile } = await import('../entry-generator');
91
99
  await generateEntryFile({
@@ -94,6 +102,7 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
94
102
  deploymentId: deploymentId || '',
95
103
  logger,
96
104
  mode: dev ? 'dev' : 'prod',
105
+ workbench: workbenchConfig.enabled ? workbenchConfig : undefined,
97
106
  });
98
107
 
99
108
  // Finally, build with Bun.build
@@ -250,7 +259,7 @@ interface BuildResult {
250
259
  * Run all builds in sequence: client -> workbench (if enabled) -> server
251
260
  */
252
261
  export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Promise<BuildResult> {
253
- const { rootDir, projectId = '', dev = false, logger } = options;
262
+ const { rootDir, projectId = '', dev = false, logger, collector } = options;
254
263
 
255
264
  if (!dev) {
256
265
  rmSync(join(rootDir, '.agentuity'), { force: true, recursive: true });
@@ -305,6 +314,7 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
305
314
  // 2. Build client (only if web frontend exists)
306
315
  if (hasWebFrontend) {
307
316
  logger.debug('Building client assets...');
317
+ const endClientDiagnostic = collector?.startDiagnostic('client-build');
308
318
  const started = Date.now();
309
319
  await runViteBuild({
310
320
  ...options,
@@ -314,6 +324,7 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
314
324
  });
315
325
  result.client.included = true;
316
326
  result.client.duration = Date.now() - started;
327
+ endClientDiagnostic?.();
317
328
  } else {
318
329
  logger.debug('Skipping client build - no src/web/index.html found');
319
330
  }
@@ -321,6 +332,7 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
321
332
  // 3. Build workbench (if enabled in config)
322
333
  if (workbenchConfig.enabled) {
323
334
  logger.debug('Building workbench assets...');
335
+ const endWorkbenchDiagnostic = collector?.startDiagnostic('workbench-build');
324
336
  const started = Date.now();
325
337
  await runViteBuild({
326
338
  ...options,
@@ -330,17 +342,21 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
330
342
  });
331
343
  result.workbench.included = true;
332
344
  result.workbench.duration = Date.now() - started;
345
+ endWorkbenchDiagnostic?.();
333
346
  }
334
347
 
335
348
  // 4. Build server
336
349
  logger.debug('Building server...');
350
+ const endServerDiagnostic = collector?.startDiagnostic('server-build');
337
351
  const serverStarted = Date.now();
338
352
  await runViteBuild({ ...options, mode: 'server' });
339
353
  result.server.included = true;
340
354
  result.server.duration = Date.now() - serverStarted;
355
+ endServerDiagnostic?.();
341
356
 
342
357
  // 5. Generate metadata (after all builds complete)
343
358
  logger.debug('Generating metadata...');
359
+ const endMetadataDiagnostic = collector?.startDiagnostic('metadata-generation');
344
360
  const { generateMetadata, writeMetadataFile } = await import('./metadata-generator');
345
361
 
346
362
  // Generate metadata
@@ -357,6 +373,7 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
357
373
  });
358
374
 
359
375
  writeMetadataFile(rootDir, metadata, dev, logger);
376
+ endMetadataDiagnostic?.();
360
377
  logger.debug('Registry and metadata generation complete');
361
378
 
362
379
  logger.debug('All builds complete');
@@ -12,6 +12,7 @@ import { runAllBuilds } from './vite/vite-builder';
12
12
  import { checkAndUpgradeDependencies } from '../../utils/dependency-checker';
13
13
  import { checkBunVersion } from '../../utils/bun-version-checker';
14
14
  import * as tui from '../../tui';
15
+ import type { BuildReportCollector } from '../../build-report';
15
16
 
16
17
  const AppFileNotFoundError = StructuredError('AppFileNotFoundError');
17
18
  const BuildFailedError = StructuredError('BuildFailedError');
@@ -26,6 +27,8 @@ export interface ViteBundleOptions {
26
27
  port?: number;
27
28
  logger: Logger;
28
29
  deploymentOptions?: DeployOptions;
30
+ /** Optional collector for structured error reporting */
31
+ collector?: BuildReportCollector;
29
32
  }
30
33
 
31
34
  /**
@@ -41,6 +44,7 @@ export async function viteBundle(options: ViteBundleOptions): Promise<{ output:
41
44
  port = 3500,
42
45
  logger,
43
46
  deploymentOptions,
47
+ collector,
44
48
  } = options;
45
49
 
46
50
  const output: string[] = [];
@@ -52,8 +56,10 @@ export async function viteBundle(options: ViteBundleOptions): Promise<{ output:
52
56
  // Verify app.ts exists
53
57
  const appFile = join(rootDir, 'app.ts');
54
58
  if (!(await Bun.file(appFile).exists())) {
59
+ const errorMessage = `App file not found at expected location: ${appFile}`;
60
+ collector?.addGeneralError('build', errorMessage, 'BUILD001');
55
61
  throw new AppFileNotFoundError({
56
- message: `App file not found at expected location: ${appFile}`,
62
+ message: errorMessage,
57
63
  });
58
64
  }
59
65
 
@@ -63,16 +69,20 @@ export async function viteBundle(options: ViteBundleOptions): Promise<{ output:
63
69
  .then((s) => s.isDirectory())
64
70
  .catch(() => false);
65
71
  if (!srcDirExists) {
72
+ const errorMessage = `Source directory not found: ${srcDir}`;
73
+ collector?.addGeneralError('build', errorMessage, 'BUILD002');
66
74
  throw new BuildFailedError({
67
- message: `Source directory not found: ${srcDir}`,
75
+ message: errorMessage,
68
76
  });
69
77
  }
70
78
 
71
79
  // Check and upgrade @agentuity/* dependencies if needed
72
80
  const upgradeResult = await checkAndUpgradeDependencies(rootDir, logger);
73
81
  if (upgradeResult.failed.length > 0 && process.stdin.isTTY) {
82
+ const errorMessage = `Failed to upgrade dependencies: ${upgradeResult.failed.join(', ')}`;
83
+ collector?.addGeneralError('build', errorMessage, 'BUILD003');
74
84
  throw new BuildFailedError({
75
- message: `Failed to upgrade dependencies: ${upgradeResult.failed.join(', ')}`,
85
+ message: errorMessage,
76
86
  });
77
87
  }
78
88
 
@@ -90,6 +100,7 @@ export async function viteBundle(options: ViteBundleOptions): Promise<{ output:
90
100
  deploymentId,
91
101
  logger,
92
102
  deploymentOptions,
103
+ collector,
93
104
  });
94
105
 
95
106
  if (result.client.included) {
@@ -106,8 +117,10 @@ export async function viteBundle(options: ViteBundleOptions): Promise<{ output:
106
117
 
107
118
  return { output };
108
119
  } catch (error) {
120
+ const errorMessage = `Build failed: ${error instanceof Error ? error.message : String(error)}`;
121
+ collector?.addGeneralError('build', errorMessage, 'BUILD004');
109
122
  throw new BuildFailedError({
110
- message: `Build failed: ${error instanceof Error ? error.message : String(error)}`,
123
+ message: errorMessage,
111
124
  });
112
125
  }
113
126
  }
@@ -45,6 +45,7 @@ import { getCommand } from '../../command-prefix';
45
45
  import * as domain from '../../domain';
46
46
  import { ErrorCode } from '../../errors';
47
47
  import { typecheck } from '../build/typecheck';
48
+ import { BuildReportCollector, setGlobalCollector, clearGlobalCollector } from '../../build-report';
48
49
 
49
50
  const DeploymentCancelledError = StructuredError(
50
51
  'DeploymentCancelled',
@@ -93,7 +94,12 @@ export const deploySubcommand = createSubcommand({
93
94
  options: z.intersection(
94
95
  DeployOptionsSchema,
95
96
  z.object({
96
- saveTypeErrors: z.string().optional().describe('file path to save typecheck errors'),
97
+ reportFile: z
98
+ .string()
99
+ .optional()
100
+ .describe(
101
+ 'file path to save build report JSON with errors, warnings, and diagnostics'
102
+ ),
97
103
  })
98
104
  ),
99
105
  response: DeployResponseSchema,
@@ -102,6 +108,14 @@ export const deploySubcommand = createSubcommand({
102
108
  async handler(ctx) {
103
109
  const { project, apiClient, projectDir, config, options, logger, opts } = ctx;
104
110
 
111
+ // Initialize build report collector if reportFile is specified
112
+ const collector = new BuildReportCollector();
113
+ if (opts.reportFile) {
114
+ collector.setOutputPath(opts.reportFile);
115
+ collector.enableAutoWrite();
116
+ setGlobalCollector(collector);
117
+ }
118
+
105
119
  let deployment: Deployment | undefined;
106
120
  let build: BuildMetadata | undefined;
107
121
  let instructions: DeploymentInstructions | undefined;
@@ -239,8 +253,13 @@ export const deploySubcommand = createSubcommand({
239
253
  }
240
254
  let capturedOutput: string[] = [];
241
255
  const rootDir = resolve(projectDir);
256
+
257
+ // Run typecheck with collector for error reporting
258
+ const endTypecheckDiagnostic = collector.startDiagnostic('typecheck');
242
259
  const started = Date.now();
243
- const typeResult = await typecheck(rootDir);
260
+ const typeResult = await typecheck(rootDir, { collector });
261
+ endTypecheckDiagnostic();
262
+
244
263
  if (typeResult.success) {
245
264
  capturedOutput.push(
246
265
  tui.muted(
@@ -248,9 +267,10 @@ export const deploySubcommand = createSubcommand({
248
267
  )
249
268
  );
250
269
  } else {
251
- if ('errors' in typeResult && opts.saveTypeErrors) {
252
- const f = Bun.file(opts.saveTypeErrors);
253
- await f.write(JSON.stringify(typeResult.errors));
270
+ // Errors already added to collector by typecheck()
271
+ // Write report before returning error
272
+ if (opts.reportFile) {
273
+ await collector.forceWrite();
254
274
  }
255
275
  return stepError('Typecheck failed\n\n' + typeResult.output);
256
276
  }
@@ -264,6 +284,7 @@ export const deploySubcommand = createSubcommand({
264
284
  region: project.region,
265
285
  logger: ctx.logger,
266
286
  deploymentOptions: opts,
287
+ collector,
267
288
  });
268
289
  capturedOutput = [...capturedOutput, ...bundleResult.output];
269
290
  build = await loadBuildMetadata(join(projectDir, '.agentuity'));
@@ -275,6 +296,10 @@ export const deploySubcommand = createSubcommand({
275
296
  return stepSuccess(capturedOutput.length > 0 ? capturedOutput : undefined);
276
297
  } catch (ex) {
277
298
  const _ex = ex as Error;
299
+ // Write report before returning error
300
+ if (opts.reportFile) {
301
+ await collector.forceWrite();
302
+ }
278
303
  return stepError(
279
304
  _ex.message ?? 'Error building your project',
280
305
  _ex,
@@ -294,6 +319,8 @@ export const deploySubcommand = createSubcommand({
294
319
  return stepError('deployment instructions were null');
295
320
  }
296
321
 
322
+ // Start diagnostic for zip/encrypt phase
323
+ const endZipDiagnostic = collector.startDiagnostic('zip-package');
297
324
  progress(5);
298
325
  ctx.logger.trace('Starting deployment zip creation');
299
326
  // zip up the assets folder
@@ -319,8 +346,11 @@ export const deploySubcommand = createSubcommand({
319
346
  });
320
347
  ctx.logger.trace(`Deployment zip created: ${deploymentZip}`);
321
348
 
349
+ endZipDiagnostic();
350
+
322
351
  progress(20);
323
352
  // Encrypt the deployment zip using the public key from deployment
353
+ const endEncryptDiagnostic = collector.startDiagnostic('encrypt');
324
354
  const encryptedZip = join(tmpdir(), `${deployment.id}.enc.zip`);
325
355
  try {
326
356
  ctx.logger.trace('Creating public key');
@@ -348,8 +378,11 @@ export const deploySubcommand = createSubcommand({
348
378
  dst.end();
349
379
  });
350
380
  ctx.logger.trace('Stream finished');
381
+ endEncryptDiagnostic();
351
382
 
352
383
  progress(50);
384
+ // Start code upload diagnostic
385
+ const endCodeUploadDiagnostic = collector.startDiagnostic('code-upload');
353
386
  ctx.logger.trace(`Uploading deployment to ${instructions.deployment}`);
354
387
  const zipfile = Bun.file(encryptedZip);
355
388
  const fileSize = await zipfile.size;
@@ -364,8 +397,15 @@ export const deploySubcommand = createSubcommand({
364
397
  });
365
398
  ctx.logger.trace(`Upload response: ${resp.status}`);
366
399
  if (!resp.ok) {
367
- return stepError(`Error uploading deployment: ${await resp.text()}`);
400
+ endCodeUploadDiagnostic();
401
+ const errorMsg = `Error uploading deployment: ${await resp.text()}`;
402
+ collector.addGeneralError('deploy', errorMsg, 'DEPLOY002');
403
+ if (opts.reportFile) {
404
+ await collector.forceWrite();
405
+ }
406
+ return stepError(errorMsg);
368
407
  }
408
+ endCodeUploadDiagnostic();
369
409
 
370
410
  progress(70);
371
411
  ctx.logger.trace('Consuming response body');
@@ -385,11 +425,17 @@ export const deploySubcommand = createSubcommand({
385
425
  progress(80);
386
426
  let bytes = 0;
387
427
  if (build?.assets) {
428
+ // Start CDN upload diagnostic
429
+ const endCdnUploadDiagnostic = collector.startDiagnostic('cdn-upload');
388
430
  ctx.logger.trace(`Uploading ${build.assets.length} assets`);
389
431
  if (!instructions.assets) {
390
- return stepError(
391
- 'server did not provide asset upload URLs; upload aborted'
392
- );
432
+ const errorMsg =
433
+ 'server did not provide asset upload URLs; upload aborted';
434
+ collector.addGeneralError('deploy', errorMsg, 'DEPLOY006');
435
+ if (opts.reportFile) {
436
+ await collector.forceWrite();
437
+ }
438
+ return stepError(errorMsg);
393
439
  }
394
440
 
395
441
  // Workaround for Bun crash in compiled executables (https://github.com/agentuity/sdk/issues/191)
@@ -452,11 +498,17 @@ export const deploySubcommand = createSubcommand({
452
498
  const resps = await Promise.all(promises);
453
499
  for (const r of resps) {
454
500
  if (!r.ok) {
455
- return stepError(`error uploading asset: ${await r.text()}`);
501
+ const errorMsg = `error uploading asset: ${await r.text()}`;
502
+ collector.addGeneralError('deploy', errorMsg, 'DEPLOY006');
503
+ if (opts.reportFile) {
504
+ await collector.forceWrite();
505
+ }
506
+ return stepError(errorMsg);
456
507
  }
457
508
  }
458
509
  }
459
510
  ctx.logger.trace('Asset uploads complete');
511
+ endCdnUploadDiagnostic();
460
512
  progress(95);
461
513
  }
462
514
 
@@ -503,6 +555,7 @@ export const deploySubcommand = createSubcommand({
503
555
  const dashboard = `${appUrl}/r/${deployment.id}`;
504
556
 
505
557
  // Poll for deployment status with optional log streaming
558
+ const endDeploymentWaitDiagnostic = collector.startDiagnostic('deployment-wait');
506
559
  const pollInterval = 500;
507
560
  const maxAttempts = 600;
508
561
  let attempts = 0;
@@ -615,11 +668,16 @@ export const deploySubcommand = createSubcommand({
615
668
  },
616
669
  })
617
670
  .then(() => {
671
+ endDeploymentWaitDiagnostic();
618
672
  tui.success('Your project was deployed!');
619
673
  })
620
- .catch((ex) => {
674
+ .catch(async (ex) => {
675
+ endDeploymentWaitDiagnostic();
621
676
  // Handle cancellation
622
677
  if (ex instanceof DeploymentCancelledError) {
678
+ if (opts.reportFile) {
679
+ await collector.forceWrite();
680
+ }
623
681
  tui.warning('Deployment cancelled');
624
682
  process.exit(130); // Standard exit code for SIGINT
625
683
  }
@@ -628,6 +686,18 @@ export const deploySubcommand = createSubcommand({
628
686
  exwithmessage.message === 'Deployment failed'
629
687
  ? ''
630
688
  : exwithmessage.toString();
689
+
690
+ // Add error to collector
691
+ const isTimeout = exwithmessage.message === 'Deployment timed out';
692
+ collector.addGeneralError(
693
+ 'deploy',
694
+ msg || 'Deployment failed',
695
+ isTimeout ? 'DEPLOY003' : 'DEPLOY004'
696
+ );
697
+ if (opts.reportFile) {
698
+ await collector.forceWrite();
699
+ }
700
+
631
701
  tui.error(`Your deployment failed to start${msg ? `: ${msg}` : ''}`);
632
702
  if (logs.length) {
633
703
  const logsDir = join(getDefaultConfigDir(), 'logs');
@@ -687,9 +757,22 @@ export const deploySubcommand = createSubcommand({
687
757
  },
688
758
  });
689
759
 
760
+ endDeploymentWaitDiagnostic();
690
761
  tui.success('Your project was deployed!');
691
762
  }
692
763
  } catch (ex) {
764
+ endDeploymentWaitDiagnostic();
765
+ const exwithmessage = ex as { message: string };
766
+ const isTimeout = exwithmessage?.message === 'Deployment timed out';
767
+ collector.addGeneralError(
768
+ 'deploy',
769
+ exwithmessage?.message || String(ex),
770
+ isTimeout ? 'DEPLOY003' : 'DEPLOY004'
771
+ );
772
+ if (opts.reportFile) {
773
+ await collector.forceWrite();
774
+ }
775
+
693
776
  const lines = [`${ex}`, ''];
694
777
  lines.push(
695
778
  `${tui.ICONS.arrow} ${
@@ -741,6 +824,12 @@ export const deploySubcommand = createSubcommand({
741
824
  });
742
825
  }
743
826
 
827
+ // Write final report on success
828
+ if (opts.reportFile) {
829
+ await collector.forceWrite();
830
+ }
831
+ clearGlobalCollector();
832
+
744
833
  return {
745
834
  success: true,
746
835
  deploymentId: deployment.id,
@@ -756,6 +845,11 @@ export const deploySubcommand = createSubcommand({
756
845
  : undefined,
757
846
  };
758
847
  } catch (ex) {
848
+ collector.addGeneralError('deploy', String(ex), 'DEPLOY004');
849
+ if (opts.reportFile) {
850
+ await collector.forceWrite();
851
+ }
852
+ clearGlobalCollector();
759
853
  tui.fatal(`unexpected error trying to deploy project. ${ex}`);
760
854
  }
761
855
  },
@@ -36,7 +36,6 @@ const DeploymentShowResponseSchema = z.object({
36
36
  .object({
37
37
  number: z.number(),
38
38
  url: z.string().optional(),
39
- commentId: z.string().optional(),
40
39
  })
41
40
  .optional(),
42
41
  })