@agentuity/cli 0.1.12 → 0.1.14

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 (140) hide show
  1. package/dist/auth.d.ts +1 -1
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +6 -2
  4. package/dist/auth.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +44 -91
  7. package/dist/cli.js.map +1 -1
  8. package/dist/cmd/auth/index.d.ts.map +1 -1
  9. package/dist/cmd/auth/index.js +3 -0
  10. package/dist/cmd/auth/index.js.map +1 -1
  11. package/dist/cmd/auth/org/index.d.ts +2 -0
  12. package/dist/cmd/auth/org/index.d.ts.map +1 -0
  13. package/dist/cmd/auth/org/index.js +121 -0
  14. package/dist/cmd/auth/org/index.js.map +1 -0
  15. package/dist/cmd/build/vite/beacon-plugin.d.ts +19 -0
  16. package/dist/cmd/build/vite/beacon-plugin.d.ts.map +1 -0
  17. package/dist/cmd/build/vite/beacon-plugin.js +137 -0
  18. package/dist/cmd/build/vite/beacon-plugin.js.map +1 -0
  19. package/dist/cmd/build/vite/vite-builder.d.ts +2 -0
  20. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  21. package/dist/cmd/build/vite/vite-builder.js +12 -2
  22. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  23. package/dist/cmd/build/webanalytics-generator.js +25 -9
  24. package/dist/cmd/build/webanalytics-generator.js.map +1 -1
  25. package/dist/cmd/cloud/db/get.d.ts.map +1 -1
  26. package/dist/cmd/cloud/db/get.js +7 -0
  27. package/dist/cmd/cloud/db/get.js.map +1 -1
  28. package/dist/cmd/cloud/db/list.d.ts.map +1 -1
  29. package/dist/cmd/cloud/db/list.js +19 -6
  30. package/dist/cmd/cloud/db/list.js.map +1 -1
  31. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  32. package/dist/cmd/cloud/deploy.js +24 -1
  33. package/dist/cmd/cloud/deploy.js.map +1 -1
  34. package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
  35. package/dist/cmd/cloud/deployment/show.js +5 -0
  36. package/dist/cmd/cloud/deployment/show.js.map +1 -1
  37. package/dist/cmd/cloud/index.d.ts.map +1 -1
  38. package/dist/cmd/cloud/index.js +3 -0
  39. package/dist/cmd/cloud/index.js.map +1 -1
  40. package/dist/cmd/cloud/region/index.d.ts +2 -0
  41. package/dist/cmd/cloud/region/index.d.ts.map +1 -0
  42. package/dist/cmd/cloud/region/index.js +136 -0
  43. package/dist/cmd/cloud/region/index.js.map +1 -0
  44. package/dist/cmd/cloud/sandbox/snapshot/build.d.ts.map +1 -1
  45. package/dist/cmd/cloud/sandbox/snapshot/build.js +35 -5
  46. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  47. package/dist/cmd/cloud/scp/download.d.ts.map +1 -1
  48. package/dist/cmd/cloud/scp/download.js +4 -2
  49. package/dist/cmd/cloud/scp/download.js.map +1 -1
  50. package/dist/cmd/cloud/scp/upload.d.ts.map +1 -1
  51. package/dist/cmd/cloud/scp/upload.js +4 -2
  52. package/dist/cmd/cloud/scp/upload.js.map +1 -1
  53. package/dist/cmd/cloud/ssh.d.ts.map +1 -1
  54. package/dist/cmd/cloud/ssh.js +3 -1
  55. package/dist/cmd/cloud/ssh.js.map +1 -1
  56. package/dist/cmd/cloud/storage/get.d.ts.map +1 -1
  57. package/dist/cmd/cloud/storage/get.js +12 -5
  58. package/dist/cmd/cloud/storage/get.js.map +1 -1
  59. package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
  60. package/dist/cmd/cloud/storage/list.js +10 -0
  61. package/dist/cmd/cloud/storage/list.js.map +1 -1
  62. package/dist/cmd/dev/index.d.ts.map +1 -1
  63. package/dist/cmd/dev/index.js +62 -5
  64. package/dist/cmd/dev/index.js.map +1 -1
  65. package/dist/cmd/help/index.d.ts.map +1 -1
  66. package/dist/cmd/help/index.js +8 -18
  67. package/dist/cmd/help/index.js.map +1 -1
  68. package/dist/cmd/project/create.d.ts.map +1 -1
  69. package/dist/cmd/project/create.js +10 -7
  70. package/dist/cmd/project/create.js.map +1 -1
  71. package/dist/cmd/project/import.d.ts +2 -0
  72. package/dist/cmd/project/import.d.ts.map +1 -0
  73. package/dist/cmd/project/import.js +88 -0
  74. package/dist/cmd/project/import.js.map +1 -0
  75. package/dist/cmd/project/index.d.ts.map +1 -1
  76. package/dist/cmd/project/index.js +3 -0
  77. package/dist/cmd/project/index.js.map +1 -1
  78. package/dist/cmd/project/reconcile.d.ts +67 -0
  79. package/dist/cmd/project/reconcile.d.ts.map +1 -0
  80. package/dist/cmd/project/reconcile.js +458 -0
  81. package/dist/cmd/project/reconcile.js.map +1 -0
  82. package/dist/cmd/project/template-flow.d.ts +11 -1
  83. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  84. package/dist/cmd/project/template-flow.js +25 -7
  85. package/dist/cmd/project/template-flow.js.map +1 -1
  86. package/dist/config.d.ts +8 -3
  87. package/dist/config.d.ts.map +1 -1
  88. package/dist/config.js +50 -21
  89. package/dist/config.js.map +1 -1
  90. package/dist/legacy-check.d.ts.map +1 -1
  91. package/dist/legacy-check.js +8 -0
  92. package/dist/legacy-check.js.map +1 -1
  93. package/dist/program-ref.d.ts +4 -0
  94. package/dist/program-ref.d.ts.map +1 -0
  95. package/dist/program-ref.js +8 -0
  96. package/dist/program-ref.js.map +1 -0
  97. package/dist/regions.d.ts +8 -0
  98. package/dist/regions.d.ts.map +1 -0
  99. package/dist/regions.js +77 -0
  100. package/dist/regions.js.map +1 -0
  101. package/dist/tui.d.ts.map +1 -1
  102. package/dist/tui.js +5 -4
  103. package/dist/tui.js.map +1 -1
  104. package/dist/types.d.ts +1 -0
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/types.js +1 -0
  107. package/dist/types.js.map +1 -1
  108. package/package.json +6 -6
  109. package/src/auth.ts +8 -8
  110. package/src/cli.ts +52 -108
  111. package/src/cmd/auth/index.ts +3 -0
  112. package/src/cmd/auth/org/index.ts +142 -0
  113. package/src/cmd/build/vite/beacon-plugin.ts +162 -0
  114. package/src/cmd/build/vite/vite-builder.ts +15 -2
  115. package/src/cmd/build/webanalytics-generator.ts +25 -9
  116. package/src/cmd/cloud/db/get.ts +7 -0
  117. package/src/cmd/cloud/db/list.ts +20 -6
  118. package/src/cmd/cloud/deploy.ts +32 -1
  119. package/src/cmd/cloud/deployment/show.ts +5 -0
  120. package/src/cmd/cloud/index.ts +3 -0
  121. package/src/cmd/cloud/region/index.ts +157 -0
  122. package/src/cmd/cloud/sandbox/snapshot/build.ts +42 -5
  123. package/src/cmd/cloud/scp/download.ts +6 -2
  124. package/src/cmd/cloud/scp/upload.ts +6 -2
  125. package/src/cmd/cloud/ssh.ts +5 -1
  126. package/src/cmd/cloud/storage/get.ts +12 -5
  127. package/src/cmd/cloud/storage/list.ts +11 -0
  128. package/src/cmd/dev/index.ts +62 -5
  129. package/src/cmd/help/index.ts +8 -22
  130. package/src/cmd/project/create.ts +10 -7
  131. package/src/cmd/project/import.ts +98 -0
  132. package/src/cmd/project/index.ts +3 -0
  133. package/src/cmd/project/reconcile.ts +606 -0
  134. package/src/cmd/project/template-flow.ts +37 -7
  135. package/src/config.ts +58 -22
  136. package/src/legacy-check.ts +10 -0
  137. package/src/program-ref.ts +11 -0
  138. package/src/regions.ts +95 -0
  139. package/src/tui.ts +6 -4
  140. package/src/types.ts +1 -0
package/src/cli.ts CHANGED
@@ -1,8 +1,6 @@
1
- import { mkdir, unlink } from 'node:fs/promises';
2
1
  import { homedir } from 'node:os';
3
- import { resolve, join } from 'node:path';
2
+ import { resolve } from 'node:path';
4
3
  import { Command } from 'commander';
5
- import { getDefaultConfigDir } from './config';
6
4
  import type {
7
5
  CommandDefinition,
8
6
  SubcommandDefinition,
@@ -17,7 +15,8 @@ import type {
17
15
  } from './types';
18
16
  import { showBanner, generateBanner } from './banner';
19
17
  import { requireAuth, optionalAuth, requireOrg, optionalOrg as selectOptionalOrg } from './auth';
20
- import { listRegions, type RegionList, ValidationOutputError } from '@agentuity/server';
18
+ import { type RegionList, ValidationOutputError } from '@agentuity/server';
19
+ import { fetchRegionsWithCache } from './regions';
21
20
  import enquirer from 'enquirer';
22
21
  import * as tui from './tui';
23
22
  import { parseArgsSchema, parseOptionsSchema, buildValidationInput } from './schema-parser';
@@ -27,6 +26,7 @@ import { ErrorCode, ExitCode, createError, exitWithError } from './errors';
27
26
  import { getCommand } from './command-prefix';
28
27
  import { isValidateMode, outputValidation, type ValidationResult } from './output';
29
28
  import { StructuredError } from '@agentuity/core';
29
+ import { setProgram } from './program-ref';
30
30
 
31
31
  /**
32
32
  * Check if an error is a CLI input validation error (Zod error from schema parsing),
@@ -462,6 +462,7 @@ async function promptProjectSelection(baseCtx: CommandContext): Promise<ProjectC
462
462
 
463
463
  export async function createCLI(version: string): Promise<Command> {
464
464
  const program = new Command();
465
+ setProgram(program);
465
466
 
466
467
  program
467
468
  .name('agentuity')
@@ -690,89 +691,6 @@ interface ResolveRegionOptions {
690
691
  region?: string;
691
692
  }
692
693
 
693
- const REGIONS_CACHE_MAX_AGE_MS = 5 * 24 * 60 * 60 * 1000; // 5 days
694
- const LEGACY_REGIONS_CACHE_FILE = 'regions.json';
695
-
696
- function getRegionsCacheFile(profileName: string): string {
697
- return `regions-${profileName}.json`;
698
- }
699
-
700
- async function removeLegacyRegionsCache(logger: Logger): Promise<void> {
701
- try {
702
- const legacyPath = join(getDefaultConfigDir(), LEGACY_REGIONS_CACHE_FILE);
703
- const file = Bun.file(legacyPath);
704
- if (await file.exists()) {
705
- await unlink(legacyPath);
706
- logger.trace('removed legacy regions cache file');
707
- }
708
- } catch {
709
- // Ignore errors when removing legacy file
710
- }
711
- }
712
-
713
- interface RegionsCacheData {
714
- timestamp: number;
715
- regions: RegionList;
716
- }
717
-
718
- async function getCachedRegions(profileName: string, logger: Logger): Promise<RegionList | null> {
719
- try {
720
- // Clean up legacy single-file cache from older versions
721
- await removeLegacyRegionsCache(logger);
722
-
723
- const cachePath = join(getDefaultConfigDir(), getRegionsCacheFile(profileName));
724
- const file = Bun.file(cachePath);
725
- if (!(await file.exists())) {
726
- return null;
727
- }
728
- const data: RegionsCacheData = await file.json();
729
- const age = Date.now() - data.timestamp;
730
- if (age > REGIONS_CACHE_MAX_AGE_MS) {
731
- logger.trace('regions cache expired for profile %s (age: %dms)', profileName, age);
732
- return null;
733
- }
734
- logger.trace('using cached regions for profile %s (age: %dms)', profileName, age);
735
- return data.regions;
736
- } catch (error) {
737
- logger.trace('failed to read regions cache for profile %s: %s', profileName, error);
738
- return null;
739
- }
740
- }
741
-
742
- async function saveRegionsCache(
743
- profileName: string,
744
- regions: RegionList,
745
- logger: Logger
746
- ): Promise<void> {
747
- try {
748
- const cacheDir = getDefaultConfigDir();
749
- await mkdir(cacheDir, { recursive: true });
750
- const cachePath = join(cacheDir, getRegionsCacheFile(profileName));
751
- const data: RegionsCacheData = {
752
- timestamp: Date.now(),
753
- regions,
754
- };
755
- await Bun.write(cachePath, JSON.stringify(data));
756
- logger.trace('saved regions cache for profile %s', profileName);
757
- } catch (error) {
758
- logger.trace('failed to save regions cache for profile %s: %s', profileName, error);
759
- }
760
- }
761
-
762
- async function fetchRegionsWithCache(
763
- profileName: string,
764
- apiClient: APIClientType,
765
- logger: Logger
766
- ): Promise<RegionList> {
767
- const cached = await getCachedRegions(profileName, logger);
768
- if (cached) {
769
- return cached;
770
- }
771
- const regions = await listRegions(apiClient);
772
- await saveRegionsCache(profileName, regions, logger);
773
- return regions;
774
- }
775
-
776
694
  async function resolveRegion(opts: ResolveRegionOptions): Promise<string | undefined> {
777
695
  const { options, regions, logger, required } = opts;
778
696
 
@@ -1242,7 +1160,12 @@ async function registerSubcommand(
1242
1160
  ctx as CommandContext & { apiClient: APIClientType }
1243
1161
  );
1244
1162
  }
1245
- if ((normalized.requiresRegion || normalized.optionalRegion) && ctx.apiClient) {
1163
+ if (
1164
+ (normalized.requiresRegion ||
1165
+ normalized.optionalRegion ||
1166
+ normalized.requiresRegions) &&
1167
+ ctx.apiClient
1168
+ ) {
1246
1169
  const apiClient: APIClientType = ctx.apiClient as APIClientType;
1247
1170
  const regions = await tui.spinner({
1248
1171
  message: 'Fetching cloud regions',
@@ -1255,15 +1178,20 @@ async function registerSubcommand(
1255
1178
  );
1256
1179
  },
1257
1180
  });
1258
- const region = await resolveRegion({
1259
- options: options as Record<string, unknown>,
1260
- regions,
1261
- logger: baseCtx.logger,
1262
- required: !!normalized.requiresRegion,
1263
- region: project?.region,
1264
- });
1265
- if (region) {
1266
- ctx.region = region;
1181
+ if (normalized.requiresRegions) {
1182
+ ctx.regions = regions;
1183
+ }
1184
+ if (normalized.requiresRegion || normalized.optionalRegion) {
1185
+ const region = await resolveRegion({
1186
+ options: options as Record<string, unknown>,
1187
+ regions,
1188
+ logger: baseCtx.logger,
1189
+ required: !!normalized.requiresRegion,
1190
+ region: project?.region,
1191
+ });
1192
+ if (region) {
1193
+ ctx.region = region;
1194
+ }
1267
1195
  }
1268
1196
  }
1269
1197
  await executeOrValidate(
@@ -1318,7 +1246,12 @@ async function registerSubcommand(
1318
1246
  ctx as CommandContext & { apiClient: APIClientType }
1319
1247
  );
1320
1248
  }
1321
- if ((normalized.requiresRegion || normalized.optionalRegion) && ctx.apiClient) {
1249
+ if (
1250
+ (normalized.requiresRegion ||
1251
+ normalized.optionalRegion ||
1252
+ normalized.requiresRegions) &&
1253
+ ctx.apiClient
1254
+ ) {
1322
1255
  const apiClient: APIClientType = ctx.apiClient as APIClientType;
1323
1256
  const regions = await tui.spinner({
1324
1257
  message: 'Fetching cloud regions',
@@ -1331,15 +1264,20 @@ async function registerSubcommand(
1331
1264
  );
1332
1265
  },
1333
1266
  });
1334
- const region = await resolveRegion({
1335
- options: options as Record<string, unknown>,
1336
- regions,
1337
- logger: baseCtx.logger,
1338
- required: !!normalized.requiresRegion,
1339
- region: project?.region,
1340
- });
1341
- if (region) {
1342
- ctx.region = region;
1267
+ if (normalized.requiresRegions) {
1268
+ ctx.regions = regions;
1269
+ }
1270
+ if (normalized.requiresRegion || normalized.optionalRegion) {
1271
+ const region = await resolveRegion({
1272
+ options: options as Record<string, unknown>,
1273
+ regions,
1274
+ logger: baseCtx.logger,
1275
+ required: !!normalized.requiresRegion,
1276
+ region: project?.region,
1277
+ });
1278
+ if (region) {
1279
+ ctx.region = region;
1280
+ }
1343
1281
  }
1344
1282
  }
1345
1283
  if (subcommand.handler) {
@@ -1384,7 +1322,13 @@ async function registerSubcommand(
1384
1322
  );
1385
1323
  }
1386
1324
 
1387
- const auth = await optionalAuth(baseCtx as CommandContext<undefined>, continueText);
1325
+ // Check if --confirm flag is set to skip interactive prompts
1326
+ const skipPrompts = options.confirm === true;
1327
+ const auth = await optionalAuth(
1328
+ baseCtx as CommandContext<undefined>,
1329
+ continueText,
1330
+ skipPrompts
1331
+ );
1388
1332
 
1389
1333
  if (subcommand.schema) {
1390
1334
  try {
@@ -5,6 +5,7 @@ import { logoutCommand } from './logout';
5
5
  import { signupCommand } from './signup';
6
6
  import { whoamiCommand } from './whoami';
7
7
  import { sshSubcommand } from './ssh';
8
+ import { orgSubcommand } from './org';
8
9
  import { getCommand } from '../../command-prefix';
9
10
 
10
11
  export const command = createCommand({
@@ -14,6 +15,7 @@ export const command = createCommand({
14
15
  examples: [
15
16
  { command: getCommand('auth login'), description: 'Login to your account' },
16
17
  { command: getCommand('auth whoami'), description: 'Show current user info' },
18
+ { command: getCommand('auth org select'), description: 'Set default organization' },
17
19
  ],
18
20
  subcommands: [
19
21
  apikeyCommand,
@@ -22,5 +24,6 @@ export const command = createCommand({
22
24
  signupCommand,
23
25
  whoamiCommand,
24
26
  sshSubcommand,
27
+ orgSubcommand,
25
28
  ],
26
29
  });
@@ -0,0 +1,142 @@
1
+ import { z } from 'zod';
2
+ import { createSubcommand, createCommand } from '../../../types';
3
+ import { getCommand } from '../../../command-prefix';
4
+ import { saveOrgId, clearOrgId } from '../../../config';
5
+ import * as tui from '../../../tui';
6
+ import { listOrganizations } from '@agentuity/server';
7
+
8
+ const selectCommand = createSubcommand({
9
+ name: 'select',
10
+ description: 'Set the default organization for all commands',
11
+ tags: ['fast', 'requires-auth'],
12
+ requires: { auth: true, apiClient: true },
13
+ examples: [
14
+ { command: getCommand('auth org select'), description: 'Select default organization' },
15
+ {
16
+ command: getCommand('auth org select org_abc123'),
17
+ description: 'Set specific organization as default',
18
+ },
19
+ ],
20
+ schema: {
21
+ args: z.object({
22
+ org_id: z.string().optional().describe('Organization ID to set as default'),
23
+ }),
24
+ response: z.object({
25
+ orgId: z.string().describe('The selected organization ID'),
26
+ name: z.string().describe('The organization name'),
27
+ }),
28
+ },
29
+
30
+ async handler(ctx) {
31
+ const { apiClient, args, options } = ctx;
32
+ const argOrgId = args.org_id;
33
+
34
+ const orgs = await tui.spinner({
35
+ message: 'Fetching organizations',
36
+ clearOnSuccess: true,
37
+ callback: () => listOrganizations(apiClient),
38
+ });
39
+
40
+ if (orgs.length === 0) {
41
+ tui.fatal('No organizations found for your account');
42
+ }
43
+
44
+ let selectedOrgId = argOrgId;
45
+
46
+ if (selectedOrgId) {
47
+ const org = orgs.find((o) => o.id === selectedOrgId);
48
+ if (!org) {
49
+ tui.fatal(
50
+ `Organization '${selectedOrgId}' not found. Use '${getCommand('auth org select')}' to see available organizations.`
51
+ );
52
+ }
53
+ } else {
54
+ if (!process.stdin.isTTY) {
55
+ tui.fatal(
56
+ 'Organization ID required in non-interactive mode. Usage: ' +
57
+ getCommand('auth org select <org_id>')
58
+ );
59
+ }
60
+ selectedOrgId = await tui.selectOrganization(orgs);
61
+ }
62
+
63
+ await saveOrgId(selectedOrgId);
64
+
65
+ const selectedOrg = orgs.find((o) => o.id === selectedOrgId)!;
66
+
67
+ if (!options.json) {
68
+ tui.success(`Default organization set to ${tui.bold(selectedOrg.name)}`);
69
+ }
70
+
71
+ return { orgId: selectedOrgId, name: selectedOrg.name };
72
+ },
73
+ });
74
+
75
+ const unselectCommand = createSubcommand({
76
+ name: 'unselect',
77
+ description: 'Clear the default organization preference',
78
+ tags: ['fast'],
79
+ examples: [
80
+ { command: getCommand('auth org unselect'), description: 'Clear default organization' },
81
+ ],
82
+ schema: {
83
+ response: z.object({
84
+ cleared: z.boolean().describe('Whether the preference was cleared'),
85
+ }),
86
+ },
87
+
88
+ async handler(ctx) {
89
+ const { options, config } = ctx;
90
+
91
+ const hadOrg = !!config?.preferences?.orgId;
92
+ await clearOrgId();
93
+
94
+ if (!options.json) {
95
+ if (hadOrg) {
96
+ tui.success('Default organization cleared');
97
+ } else {
98
+ tui.info('No default organization was set');
99
+ }
100
+ }
101
+
102
+ return { cleared: hadOrg };
103
+ },
104
+ });
105
+
106
+ const currentCommand = createSubcommand({
107
+ name: 'current',
108
+ description: 'Show the current default organization',
109
+ tags: ['read-only', 'fast'],
110
+ idempotent: true,
111
+ examples: [
112
+ { command: getCommand('auth org current'), description: 'Show default organization' },
113
+ { command: getCommand('auth org current --json'), description: 'Show output in JSON format' },
114
+ ],
115
+ schema: {
116
+ response: z.string().nullable().describe('The current organization ID or null if not set'),
117
+ },
118
+
119
+ async handler(ctx) {
120
+ const { options, config } = ctx;
121
+ const orgId = config?.preferences?.orgId || null;
122
+
123
+ if (!options.json) {
124
+ if (orgId) {
125
+ console.log(orgId);
126
+ }
127
+ }
128
+
129
+ return orgId;
130
+ },
131
+ });
132
+
133
+ export const orgSubcommand = createCommand({
134
+ name: 'org',
135
+ description: 'Manage default organization preference',
136
+ tags: ['fast'],
137
+ examples: [
138
+ { command: getCommand('auth org select'), description: 'Set default organization' },
139
+ { command: getCommand('auth org current'), description: 'Show current default' },
140
+ ],
141
+ subcommands: [selectCommand, unselectCommand, currentCommand],
142
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Vite plugin to emit the analytics beacon as a hashed CDN asset
3
+ *
4
+ * This plugin:
5
+ * 1. Reads the pre-built beacon from @agentuity/frontend
6
+ * 2. Emits it as a Rollup asset with content-based hashing
7
+ * 3. Injects a <script data-agentuity-beacon> tag into the HTML
8
+ */
9
+
10
+ import type { Plugin, ResolvedConfig } from 'vite';
11
+ import { join } from 'node:path';
12
+ import { createRequire } from 'node:module';
13
+
14
+ export interface BeaconPluginOptions {
15
+ /** Whether analytics is enabled */
16
+ enabled: boolean;
17
+ }
18
+
19
+ /**
20
+ * Read the pre-built beacon script from @agentuity/frontend package
21
+ * @param projectRoot - The root directory of the project (for resolving node_modules)
22
+ */
23
+ async function readBeaconScript(projectRoot: string): Promise<string> {
24
+ let frontendPath: string | null = null;
25
+
26
+ // Strategy 1: Use Bun.resolve (works with workspaces and symlinks)
27
+ try {
28
+ frontendPath = await Bun.resolve('@agentuity/frontend', projectRoot);
29
+ } catch {
30
+ // Not found from project root
31
+ }
32
+
33
+ // Strategy 2: Try from this file's directory (for installed CLI case)
34
+ if (!frontendPath) {
35
+ try {
36
+ const thisDir = new URL('.', import.meta.url).pathname;
37
+ frontendPath = await Bun.resolve('@agentuity/frontend', thisDir);
38
+ } catch {
39
+ // Not found from CLI directory
40
+ }
41
+ }
42
+
43
+ // Strategy 3: Fallback to createRequire for Node.js compatibility
44
+ if (!frontendPath) {
45
+ try {
46
+ const projectRequire = createRequire(join(projectRoot, 'package.json'));
47
+ frontendPath = projectRequire.resolve('@agentuity/frontend');
48
+ } catch {
49
+ // Not found via createRequire
50
+ }
51
+ }
52
+
53
+ if (!frontendPath) {
54
+ throw new Error(
55
+ 'Could not resolve @agentuity/frontend. Ensure the package is installed and built.'
56
+ );
57
+ }
58
+
59
+ // The beacon.js file is in the dist folder of @agentuity/frontend
60
+ // frontendPath points to dist/index.js, so we go up one level
61
+ const packageDir = join(frontendPath, '..');
62
+ const beaconPath = join(packageDir, 'beacon.js');
63
+
64
+ const beaconFile = Bun.file(beaconPath);
65
+ if (!(await beaconFile.exists())) {
66
+ throw new Error(
67
+ `Beacon script not found at ${beaconPath}. Run "bun run build" in @agentuity/frontend first.`
68
+ );
69
+ }
70
+
71
+ return beaconFile.text();
72
+ }
73
+
74
+ /**
75
+ * Vite plugin that emits the analytics beacon as a hashed asset
76
+ * and injects a script tag into the HTML
77
+ */
78
+ export function beaconPlugin(options: BeaconPluginOptions): Plugin {
79
+ const { enabled } = options;
80
+
81
+ let resolvedConfig: ResolvedConfig | null = null;
82
+ let beaconReferenceId: string | null = null;
83
+
84
+ return {
85
+ name: 'agentuity:beacon',
86
+ apply: 'build',
87
+
88
+ configResolved(config) {
89
+ resolvedConfig = config;
90
+ },
91
+
92
+ async buildStart() {
93
+ if (!enabled) return;
94
+
95
+ try {
96
+ // Get project root from Vite config
97
+ const projectRoot = resolvedConfig?.root || process.cwd();
98
+ const beaconCode = await readBeaconScript(projectRoot);
99
+
100
+ // Emit the beacon as an asset - Rollup will hash it
101
+ beaconReferenceId = this.emitFile({
102
+ type: 'asset',
103
+ name: 'agentuity-beacon.js',
104
+ source: beaconCode,
105
+ });
106
+ } catch (error) {
107
+ this.error(`Failed to read beacon script: ${error instanceof Error ? error.message : String(error)}`);
108
+ }
109
+ },
110
+
111
+ // Use transformIndexHtml hook to modify the HTML
112
+ // This is called during the HTML transformation phase and works with Vite's HTML pipeline
113
+ transformIndexHtml: {
114
+ order: 'post',
115
+ handler(html, ctx) {
116
+ if (!enabled || !beaconReferenceId) return html;
117
+
118
+ // Get the final hashed filename using the bundle from the context
119
+ // We need to use ctx.bundle to access the emitted file
120
+ const bundle = ctx.bundle;
121
+ if (!bundle) return html;
122
+
123
+ // Find our beacon asset in the bundle
124
+ let beaconFileName: string | null = null;
125
+ for (const fileName of Object.keys(bundle)) {
126
+ if (fileName.includes('agentuity-beacon') && fileName.endsWith('.js')) {
127
+ beaconFileName = fileName;
128
+ break;
129
+ }
130
+ }
131
+
132
+ if (!beaconFileName) return html;
133
+
134
+ // Build the beacon URL using Vite's configured base (respects CDN URL)
135
+ // resolvedConfig.base is e.g., "https://cdn.agentuity.com/{deploymentId}/client/" or "/"
136
+ const base = resolvedConfig?.base || '/';
137
+ // Normalize: ensure base ends with / and beaconFileName doesn't start with /
138
+ const normalizedBase = base.endsWith('/') ? base : `${base}/`;
139
+ const normalizedFileName = beaconFileName.startsWith('/')
140
+ ? beaconFileName.slice(1)
141
+ : beaconFileName;
142
+ const beaconUrl = `${normalizedBase}${normalizedFileName}`;
143
+
144
+ // Inject a marker script tag
145
+ // The script must be sync (no async/defer) to patch history API before router loads
146
+ // Use data-agentuity-beacon attribute as a marker for config/session injection
147
+ const beaconScript = `<script data-agentuity-beacon src="${beaconUrl}"></script>`;
148
+
149
+ // Inject before </head>
150
+ if (html.includes('</head>')) {
151
+ return html.replace('</head>', `${beaconScript}</head>`);
152
+ }
153
+ // Fallback: inject at start of body
154
+ if (html.includes('<body')) {
155
+ return html.replace(/<body([^>]*)>/, `<body$1>${beaconScript}`);
156
+ }
157
+
158
+ return beaconScript + html;
159
+ },
160
+ },
161
+ };
162
+ }
@@ -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 { beaconPlugin } from './beacon-plugin';
13
14
  import type { BuildReportCollector } from '../../../build-report';
14
15
 
15
16
  /**
@@ -55,6 +56,8 @@ export interface ViteBuildOptions {
55
56
  deploymentId?: string;
56
57
  workbenchRoute?: string;
57
58
  workbenchEnabled?: boolean;
59
+ /** Whether analytics is enabled (for beacon injection in client build) */
60
+ analyticsEnabled?: boolean;
58
61
  logger: Logger;
59
62
  deploymentOptions?: DeployOptions;
60
63
  /** Optional collector for structured error reporting */
@@ -139,11 +142,17 @@ export async function runViteBuild(options: ViteBuildOptions): Promise<void> {
139
142
  const htmlPath = join(rootDir, 'src', 'web', 'index.html');
140
143
 
141
144
  // Use workbench config passed from runAllBuilds
142
- const { workbenchEnabled = false, workbenchRoute = '/workbench' } = options;
145
+ const { workbenchEnabled = false, workbenchRoute = '/workbench', analyticsEnabled = false } = options;
143
146
 
144
147
  // Load custom user plugins from agentuity.config.ts if it exists
145
148
  const clientOutDir = join(rootDir, '.agentuity/client');
146
- const plugins = [react(), browserEnvPlugin(), flattenHtmlOutputPlugin(clientOutDir)];
149
+ const plugins = [
150
+ react(),
151
+ browserEnvPlugin(),
152
+ flattenHtmlOutputPlugin(clientOutDir),
153
+ // Emit analytics beacon as hashed CDN asset (prod builds only)
154
+ beaconPlugin({ enabled: analyticsEnabled && !dev }),
155
+ ];
147
156
  const { loadAgentuityConfig } = await import('./config-loader');
148
157
  const userConfig = await loadAgentuityConfig(rootDir, logger);
149
158
  const userPlugins = userConfig?.plugins || [];
@@ -312,6 +321,9 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
312
321
  // Check if web frontend exists
313
322
  const hasWebFrontend = await Bun.file(join(rootDir, 'src', 'web', 'index.html')).exists();
314
323
 
324
+ // Check if analytics is enabled
325
+ const analyticsEnabled = config?.analytics !== false;
326
+
315
327
  // 2. Build client (only if web frontend exists)
316
328
  if (hasWebFrontend) {
317
329
  logger.debug('Building client assets...');
@@ -322,6 +334,7 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
322
334
  mode: 'client',
323
335
  workbenchEnabled: workbenchConfig.enabled,
324
336
  workbenchRoute: workbenchConfig.route,
337
+ analyticsEnabled,
325
338
  });
326
339
  result.client.included = true;
327
340
  result.client.duration = Date.now() - started;
@@ -114,6 +114,9 @@ import type { AnalyticsConfig } from './analytics-config';
114
114
  // Inject analytics config and script into HTML
115
115
  // Note: Only static config is injected (org, project, devmode, tracking options)
116
116
  // Session and thread IDs are read from cookies by the beacon script
117
+ //
118
+ // In production: beacon is served from CDN as a hashed asset (injected by Vite build)
119
+ // In development: beacon is served from /_agentuity/webanalytics/analytics.js route
117
120
  export function injectAnalytics(html: string, analyticsConfig: AnalyticsConfig): string {
118
121
  if (!analyticsConfig.enabled) return html;
119
122
 
@@ -132,7 +135,17 @@ export function injectAnalytics(html: string, analyticsConfig: AnalyticsConfig):
132
135
  const configScript = \`<script>window.__AGENTUITY_ANALYTICS__=\${JSON.stringify(pageConfig)};</script>\`;
133
136
  // Session script sets cookies and window.__AGENTUITY_SESSION__ (dynamic, not cached)
134
137
  const sessionScript = '<script src="/_agentuity/webanalytics/session.js" async></script>';
135
- // Beacon script - must be sync (not async/defer) to patch history API before the router loads
138
+
139
+ // In production, the beacon is already in HTML as a CDN asset (data-agentuity-beacon marker)
140
+ // Inject config/session BEFORE the beacon marker so config exists when beacon runs
141
+ const beaconMarker = '<script data-agentuity-beacon';
142
+ if (html.includes(beaconMarker)) {
143
+ // Production: inject config/session right before the beacon script
144
+ const injection = configScript + sessionScript;
145
+ return html.replace(beaconMarker, injection + beaconMarker);
146
+ }
147
+
148
+ // Development: beacon served from local route, inject all three scripts
136
149
  const beaconScript = '<script src="/_agentuity/webanalytics/analytics.js"></script>';
137
150
  const injection = configScript + sessionScript + beaconScript;
138
151
 
@@ -167,15 +180,18 @@ export function registerAnalyticsRoutes(app: ReturnType<typeof createRouter>): v
167
180
  });
168
181
  });
169
182
 
170
- // Static beacon script - can be cached
171
- app.get('/_agentuity/webanalytics/analytics.js', async (c: Context) => {
172
- return new Response(BEACON_SCRIPT, {
173
- headers: {
174
- 'Content-Type': 'application/javascript; charset=utf-8',
175
- 'Cache-Control': 'public, max-age=3600',
176
- },
183
+ // Dev mode only: serve beacon script from local route
184
+ // In production, the beacon is served from CDN as a hashed asset
185
+ if (runtimeIsDevMode()) {
186
+ app.get('/_agentuity/webanalytics/analytics.js', async (c: Context) => {
187
+ return new Response(BEACON_SCRIPT, {
188
+ headers: {
189
+ 'Content-Type': 'application/javascript; charset=utf-8',
190
+ 'Cache-Control': 'no-store, no-cache, must-revalidate',
191
+ },
192
+ });
177
193
  });
178
- });
194
+ }
179
195
  }
180
196
  `;
181
197
  }
@@ -12,6 +12,8 @@ const DBGetResponseSchema = z
12
12
  name: z.string().describe('Database name'),
13
13
  description: z.string().optional().describe('Database description'),
14
14
  url: z.string().optional().describe('Database connection URL'),
15
+ org_id: z.string().optional().describe('Organization ID that owns this database'),
16
+ org_name: z.string().optional().describe('Organization name that owns this database'),
15
17
  })
16
18
  .or(
17
19
  z.object({
@@ -175,6 +177,9 @@ export const getSubcommand = createSubcommand({
175
177
  const tableData: Record<string, string> = {
176
178
  Name: tui.bold(db.name),
177
179
  };
180
+ if (db.org_name || db.org_id) {
181
+ tableData['Organization'] = db.org_name || db.org_id;
182
+ }
178
183
  if (db.description) {
179
184
  tableData['Description'] = db.description;
180
185
  }
@@ -192,6 +197,8 @@ export const getSubcommand = createSubcommand({
192
197
  name: db.name,
193
198
  description: db.description ?? undefined,
194
199
  url: db.url ?? undefined,
200
+ org_id: db.org_id,
201
+ org_name: db.org_name,
195
202
  };
196
203
  },
197
204
  });