@agentuity/cli 0.1.13 → 0.1.15

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 (173) hide show
  1. package/dist/auth.d.ts +1 -1
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +12 -7
  4. package/dist/auth.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +94 -97
  7. package/dist/cli.js.map +1 -1
  8. package/dist/cmd/ai/index.d.ts.map +1 -1
  9. package/dist/cmd/ai/index.js +6 -1
  10. package/dist/cmd/ai/index.js.map +1 -1
  11. package/dist/cmd/ai/opencode/index.d.ts +3 -0
  12. package/dist/cmd/ai/opencode/index.d.ts.map +1 -0
  13. package/dist/cmd/ai/opencode/index.js +27 -0
  14. package/dist/cmd/ai/opencode/index.js.map +1 -0
  15. package/dist/cmd/ai/opencode/install.d.ts +3 -0
  16. package/dist/cmd/ai/opencode/install.d.ts.map +1 -0
  17. package/dist/cmd/ai/opencode/install.js +102 -0
  18. package/dist/cmd/ai/opencode/install.js.map +1 -0
  19. package/dist/cmd/ai/opencode/run.d.ts +3 -0
  20. package/dist/cmd/ai/opencode/run.d.ts.map +1 -0
  21. package/dist/cmd/ai/opencode/run.js +88 -0
  22. package/dist/cmd/ai/opencode/run.js.map +1 -0
  23. package/dist/cmd/ai/opencode/uninstall.d.ts +3 -0
  24. package/dist/cmd/ai/opencode/uninstall.d.ts.map +1 -0
  25. package/dist/cmd/ai/opencode/uninstall.js +82 -0
  26. package/dist/cmd/ai/opencode/uninstall.js.map +1 -0
  27. package/dist/cmd/auth/index.d.ts.map +1 -1
  28. package/dist/cmd/auth/index.js +3 -0
  29. package/dist/cmd/auth/index.js.map +1 -1
  30. package/dist/cmd/auth/org/index.d.ts +2 -0
  31. package/dist/cmd/auth/org/index.d.ts.map +1 -0
  32. package/dist/cmd/auth/org/index.js +121 -0
  33. package/dist/cmd/auth/org/index.js.map +1 -0
  34. package/dist/cmd/build/vite/beacon-plugin.d.ts +19 -0
  35. package/dist/cmd/build/vite/beacon-plugin.d.ts.map +1 -0
  36. package/dist/cmd/build/vite/beacon-plugin.js +137 -0
  37. package/dist/cmd/build/vite/beacon-plugin.js.map +1 -0
  38. package/dist/cmd/build/vite/vite-builder.d.ts +2 -0
  39. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  40. package/dist/cmd/build/vite/vite-builder.js +12 -2
  41. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  42. package/dist/cmd/build/webanalytics-generator.js +25 -9
  43. package/dist/cmd/build/webanalytics-generator.js.map +1 -1
  44. package/dist/cmd/cloud/db/get.d.ts.map +1 -1
  45. package/dist/cmd/cloud/db/get.js +7 -0
  46. package/dist/cmd/cloud/db/get.js.map +1 -1
  47. package/dist/cmd/cloud/db/list.d.ts.map +1 -1
  48. package/dist/cmd/cloud/db/list.js +19 -6
  49. package/dist/cmd/cloud/db/list.js.map +1 -1
  50. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  51. package/dist/cmd/cloud/deploy.js +24 -1
  52. package/dist/cmd/cloud/deploy.js.map +1 -1
  53. package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
  54. package/dist/cmd/cloud/deployment/show.js +5 -0
  55. package/dist/cmd/cloud/deployment/show.js.map +1 -1
  56. package/dist/cmd/cloud/index.d.ts.map +1 -1
  57. package/dist/cmd/cloud/index.js +3 -0
  58. package/dist/cmd/cloud/index.js.map +1 -1
  59. package/dist/cmd/cloud/region/index.d.ts +2 -0
  60. package/dist/cmd/cloud/region/index.d.ts.map +1 -0
  61. package/dist/cmd/cloud/region/index.js +136 -0
  62. package/dist/cmd/cloud/region/index.js.map +1 -0
  63. package/dist/cmd/cloud/sandbox/snapshot/build.d.ts.map +1 -1
  64. package/dist/cmd/cloud/sandbox/snapshot/build.js +35 -5
  65. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  66. package/dist/cmd/cloud/scp/download.d.ts.map +1 -1
  67. package/dist/cmd/cloud/scp/download.js +4 -2
  68. package/dist/cmd/cloud/scp/download.js.map +1 -1
  69. package/dist/cmd/cloud/scp/upload.d.ts.map +1 -1
  70. package/dist/cmd/cloud/scp/upload.js +4 -2
  71. package/dist/cmd/cloud/scp/upload.js.map +1 -1
  72. package/dist/cmd/cloud/ssh.d.ts.map +1 -1
  73. package/dist/cmd/cloud/ssh.js +3 -1
  74. package/dist/cmd/cloud/ssh.js.map +1 -1
  75. package/dist/cmd/cloud/storage/get.d.ts.map +1 -1
  76. package/dist/cmd/cloud/storage/get.js +12 -5
  77. package/dist/cmd/cloud/storage/get.js.map +1 -1
  78. package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
  79. package/dist/cmd/cloud/storage/list.js +10 -0
  80. package/dist/cmd/cloud/storage/list.js.map +1 -1
  81. package/dist/cmd/dev/index.d.ts.map +1 -1
  82. package/dist/cmd/dev/index.js +62 -5
  83. package/dist/cmd/dev/index.js.map +1 -1
  84. package/dist/cmd/help/index.d.ts.map +1 -1
  85. package/dist/cmd/help/index.js +8 -18
  86. package/dist/cmd/help/index.js.map +1 -1
  87. package/dist/cmd/project/create.d.ts.map +1 -1
  88. package/dist/cmd/project/create.js +18 -9
  89. package/dist/cmd/project/create.js.map +1 -1
  90. package/dist/cmd/project/download.d.ts +3 -1
  91. package/dist/cmd/project/download.d.ts.map +1 -1
  92. package/dist/cmd/project/download.js +5 -0
  93. package/dist/cmd/project/download.js.map +1 -1
  94. package/dist/cmd/project/import.d.ts +2 -0
  95. package/dist/cmd/project/import.d.ts.map +1 -0
  96. package/dist/cmd/project/import.js +88 -0
  97. package/dist/cmd/project/import.js.map +1 -0
  98. package/dist/cmd/project/index.d.ts.map +1 -1
  99. package/dist/cmd/project/index.js +3 -0
  100. package/dist/cmd/project/index.js.map +1 -1
  101. package/dist/cmd/project/reconcile.d.ts +67 -0
  102. package/dist/cmd/project/reconcile.d.ts.map +1 -0
  103. package/dist/cmd/project/reconcile.js +458 -0
  104. package/dist/cmd/project/reconcile.js.map +1 -0
  105. package/dist/cmd/project/template-flow.d.ts +13 -1
  106. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  107. package/dist/cmd/project/template-flow.js +56 -18
  108. package/dist/cmd/project/template-flow.js.map +1 -1
  109. package/dist/config.d.ts +8 -3
  110. package/dist/config.d.ts.map +1 -1
  111. package/dist/config.js +50 -21
  112. package/dist/config.js.map +1 -1
  113. package/dist/legacy-check.d.ts.map +1 -1
  114. package/dist/legacy-check.js +8 -0
  115. package/dist/legacy-check.js.map +1 -1
  116. package/dist/program-ref.d.ts +4 -0
  117. package/dist/program-ref.d.ts.map +1 -0
  118. package/dist/program-ref.js +8 -0
  119. package/dist/program-ref.js.map +1 -0
  120. package/dist/regions.d.ts +8 -0
  121. package/dist/regions.d.ts.map +1 -0
  122. package/dist/regions.js +77 -0
  123. package/dist/regions.js.map +1 -0
  124. package/dist/tui/prompt.d.ts.map +1 -1
  125. package/dist/tui/prompt.js +7 -1
  126. package/dist/tui/prompt.js.map +1 -1
  127. package/dist/tui.d.ts.map +1 -1
  128. package/dist/tui.js +79 -25
  129. package/dist/tui.js.map +1 -1
  130. package/dist/types.d.ts +1 -0
  131. package/dist/types.d.ts.map +1 -1
  132. package/dist/types.js +1 -0
  133. package/dist/types.js.map +1 -1
  134. package/package.json +7 -7
  135. package/src/auth.ts +14 -13
  136. package/src/cli.ts +118 -114
  137. package/src/cmd/ai/index.ts +6 -1
  138. package/src/cmd/ai/opencode/index.ts +28 -0
  139. package/src/cmd/ai/opencode/install.ts +120 -0
  140. package/src/cmd/ai/opencode/run.ts +103 -0
  141. package/src/cmd/ai/opencode/uninstall.ts +90 -0
  142. package/src/cmd/auth/index.ts +3 -0
  143. package/src/cmd/auth/org/index.ts +142 -0
  144. package/src/cmd/build/vite/beacon-plugin.ts +164 -0
  145. package/src/cmd/build/vite/vite-builder.ts +19 -2
  146. package/src/cmd/build/webanalytics-generator.ts +25 -9
  147. package/src/cmd/cloud/db/get.ts +7 -0
  148. package/src/cmd/cloud/db/list.ts +20 -6
  149. package/src/cmd/cloud/deploy.ts +32 -1
  150. package/src/cmd/cloud/deployment/show.ts +5 -0
  151. package/src/cmd/cloud/index.ts +3 -0
  152. package/src/cmd/cloud/region/index.ts +157 -0
  153. package/src/cmd/cloud/sandbox/snapshot/build.ts +42 -5
  154. package/src/cmd/cloud/scp/download.ts +6 -2
  155. package/src/cmd/cloud/scp/upload.ts +6 -2
  156. package/src/cmd/cloud/ssh.ts +5 -1
  157. package/src/cmd/cloud/storage/get.ts +12 -5
  158. package/src/cmd/cloud/storage/list.ts +11 -0
  159. package/src/cmd/dev/index.ts +62 -5
  160. package/src/cmd/help/index.ts +8 -22
  161. package/src/cmd/project/create.ts +19 -9
  162. package/src/cmd/project/download.ts +7 -1
  163. package/src/cmd/project/import.ts +98 -0
  164. package/src/cmd/project/index.ts +3 -0
  165. package/src/cmd/project/reconcile.ts +606 -0
  166. package/src/cmd/project/template-flow.ts +69 -18
  167. package/src/config.ts +58 -22
  168. package/src/legacy-check.ts +10 -0
  169. package/src/program-ref.ts +11 -0
  170. package/src/regions.ts +95 -0
  171. package/src/tui/prompt.ts +10 -3
  172. package/src/tui.ts +82 -26
  173. package/src/types.ts +2 -0
@@ -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
  });
@@ -13,6 +13,8 @@ const DBListResponseSchema = z.object({
13
13
  description: z.string().optional().describe('Database description'),
14
14
  url: z.string().optional().describe('Database connection URL'),
15
15
  cloud_region: z.string().optional().describe('Cloud region where database is hosted'),
16
+ org_id: z.string().optional().describe('Organization ID that owns this database'),
17
+ org_name: z.string().optional().describe('Organization name that owns this database'),
16
18
  })
17
19
  )
18
20
  .describe('List of database resources'),
@@ -65,6 +67,10 @@ export const listSubcommand = createSubcommand({
65
67
  const shouldShowCredentials = opts.showCredentials === true;
66
68
  const shouldMask = !options.json && !shouldShowCredentials;
67
69
 
70
+ // Check if resources span multiple orgs
71
+ const uniqueOrgIds = new Set(resources.db.map((db) => db.org_id));
72
+ const showOrgColumn = uniqueOrgIds.size > 1;
73
+
68
74
  if (!options.json) {
69
75
  if (resources.db.length === 0) {
70
76
  tui.info('No databases found');
@@ -73,12 +79,18 @@ export const listSubcommand = createSubcommand({
73
79
  console.log(db.name);
74
80
  }
75
81
  } else {
76
- const tableData = resources.db.map((db) => ({
77
- Name: db.name,
78
- Description: db.description ?? '',
79
- Region: db.cloud_region,
80
- URL: db.url ? (shouldMask ? tui.maskSecret(db.url) : db.url) : '',
81
- }));
82
+ const tableData = resources.db.map((db) => {
83
+ const row: Record<string, string> = {
84
+ Name: db.name,
85
+ };
86
+ if (showOrgColumn) {
87
+ row.Organization = db.org_name || db.org_id;
88
+ }
89
+ row.Description = db.description ?? '';
90
+ row.Region = db.cloud_region;
91
+ row.URL = db.url ? (shouldMask ? tui.maskSecret(db.url) : db.url) : '';
92
+ return row;
93
+ });
82
94
  tui.table(tableData);
83
95
  }
84
96
  }
@@ -89,6 +101,8 @@ export const listSubcommand = createSubcommand({
89
101
  description: db.description ?? undefined,
90
102
  url: db.url ?? undefined,
91
103
  cloud_region: db.cloud_region,
104
+ org_id: db.org_id,
105
+ org_name: db.org_name,
92
106
  })),
93
107
  };
94
108
  },
@@ -121,7 +121,38 @@ export const deploySubcommand = createSubcommand({
121
121
  },
122
122
 
123
123
  async handler(ctx) {
124
- const { project, apiClient, projectDir, config, options, logger, opts } = ctx;
124
+ let { project } = ctx;
125
+ const { apiClient, projectDir, config, options, logger, opts, auth } = ctx;
126
+
127
+ // Verify project access and offer import if needed
128
+ const { reconcileProject } = await import('../project/reconcile');
129
+ const { isTTY } = await import('../../auth');
130
+
131
+ const reconcileResult = await reconcileProject({
132
+ dir: projectDir,
133
+ auth,
134
+ apiClient,
135
+ config: config!,
136
+ logger,
137
+ interactive: isTTY(),
138
+ });
139
+
140
+ if (reconcileResult.status === 'error') {
141
+ tui.fatal(reconcileResult.message!, ErrorCode.PROJECT_NOT_FOUND);
142
+ }
143
+
144
+ if (reconcileResult.status === 'skipped') {
145
+ tui.fatal(
146
+ 'Project must be registered with Agentuity Cloud to deploy.',
147
+ ErrorCode.PROJECT_NOT_FOUND
148
+ );
149
+ }
150
+
151
+ if (reconcileResult.status === 'imported' && reconcileResult.project) {
152
+ // Project was imported - use the new project config
153
+ project = reconcileResult.project;
154
+ tui.newline();
155
+ }
125
156
 
126
157
  // Initialize build report collector if reportFile is specified
127
158
  const collector = new BuildReportCollector();
@@ -19,6 +19,7 @@ const DeploymentShowResponseSchema = z.object({
19
19
  resourceStorage: z.string().nullable().optional().describe('the storage name'),
20
20
  deploymentLogsURL: z.string().nullable().optional().describe('the url to the deployment logs'),
21
21
  buildLogsURL: z.string().nullable().optional().describe('the url to the build logs'),
22
+ dnsRecords: z.array(z.string()).optional().describe('DNS records for custom domains'),
22
23
  metadata: z
23
24
  .object({
24
25
  git: z
@@ -124,6 +125,9 @@ export const showSubcommand = createSubcommand({
124
125
  if (deployment.buildLogsURL) {
125
126
  tableData['Build Logs'] = tui.link(deployment.buildLogsURL);
126
127
  }
128
+ if (deployment.dnsRecords && deployment.dnsRecords.length > 0) {
129
+ tableData['DNS Records'] = deployment.dnsRecords.join(', ');
130
+ }
127
131
 
128
132
  tui.table([tableData], Object.keys(tableData), { layout: 'vertical', padStart: ' ' });
129
133
 
@@ -190,6 +194,7 @@ export const showSubcommand = createSubcommand({
190
194
  resourceStorage: deployment.resourceStorage ?? undefined,
191
195
  deploymentLogsURL: deployment.deploymentLogsURL ?? undefined,
192
196
  buildLogsURL: deployment.buildLogsURL ?? undefined,
197
+ dnsRecords: deployment.dnsRecords ?? undefined,
193
198
  };
194
199
  } catch (ex) {
195
200
  tui.fatal(`Failed to show deployment: ${ex}`);
@@ -15,6 +15,7 @@ import apikeyCommand from './apikey';
15
15
  import streamCommand from './stream';
16
16
  import vectorCommand from './vector';
17
17
  import sandboxCommand from './sandbox';
18
+ import { regionSubcommand } from './region';
18
19
  import { getCommand } from '../../command-prefix';
19
20
 
20
21
  export const command = createCommand({
@@ -24,6 +25,7 @@ export const command = createCommand({
24
25
  examples: [
25
26
  { command: getCommand('cloud deploy'), description: 'Deploy your agent to the cloud' },
26
27
  { command: getCommand('cloud deployment list'), description: 'List all deployments' },
28
+ { command: getCommand('cloud region select'), description: 'Set default region' },
27
29
  ],
28
30
  subcommands: [
29
31
  apikeyCommand,
@@ -42,5 +44,6 @@ export const command = createCommand({
42
44
  sshSubcommand,
43
45
  scpSubcommand,
44
46
  deploymentCommand,
47
+ regionSubcommand,
45
48
  ],
46
49
  });
@@ -0,0 +1,157 @@
1
+ import { z } from 'zod';
2
+ import { createSubcommand, createCommand } from '../../../types';
3
+ import { getCommand } from '../../../command-prefix';
4
+ import { saveRegion, clearRegion } from '../../../config';
5
+ import * as tui from '../../../tui';
6
+
7
+ const selectCommand = createSubcommand({
8
+ name: 'select',
9
+ description: 'Set the default cloud region for all commands',
10
+ tags: ['fast', 'requires-auth'],
11
+ requires: { auth: true, regions: true },
12
+ examples: [
13
+ { command: getCommand('cloud region select'), description: 'Select default region' },
14
+ {
15
+ command: getCommand('cloud region select usc'),
16
+ description: 'Set specific region as default',
17
+ },
18
+ ],
19
+ schema: {
20
+ args: z.object({
21
+ region: z.string().optional().describe('Region code to set as default'),
22
+ }),
23
+ response: z.object({
24
+ region: z.string().describe('The selected region code'),
25
+ description: z.string().describe('The region description'),
26
+ }),
27
+ },
28
+
29
+ async handler(ctx) {
30
+ const { regions, args, options } = ctx;
31
+
32
+ let selectedRegion = args.region;
33
+
34
+ if (selectedRegion) {
35
+ const region = regions.find((r) => r.region === selectedRegion);
36
+ if (!region) {
37
+ const available = regions.map((r) => r.region).join(', ');
38
+ tui.fatal(`Region '${selectedRegion}' not found. Available regions: ${available}`);
39
+ }
40
+ } else {
41
+ if (!process.stdin.isTTY) {
42
+ tui.fatal(
43
+ 'Region code required in non-interactive mode. Usage: ' +
44
+ getCommand('cloud region select <region>')
45
+ );
46
+ }
47
+
48
+ if (regions.length === 1) {
49
+ selectedRegion = regions[0].region;
50
+ } else {
51
+ const response = await tui.createPrompt().select<string>({
52
+ message: 'Select a default region',
53
+ options: regions.map((r) => ({
54
+ value: r.region,
55
+ label: `${r.description} ${tui.muted(`(${r.region})`)}`,
56
+ })),
57
+ });
58
+ selectedRegion = response;
59
+ }
60
+ }
61
+
62
+ await saveRegion(selectedRegion);
63
+
64
+ const selectedRegionInfo = regions.find((r) => r.region === selectedRegion)!;
65
+
66
+ if (!options.json) {
67
+ tui.success(
68
+ `Default region set to ${tui.bold(selectedRegionInfo.description)} (${selectedRegion})`
69
+ );
70
+ }
71
+
72
+ return { region: selectedRegion, description: selectedRegionInfo.description };
73
+ },
74
+ });
75
+
76
+ const unselectCommand = createSubcommand({
77
+ name: 'unselect',
78
+ description: 'Clear the default region preference',
79
+ tags: ['fast'],
80
+ examples: [
81
+ { command: getCommand('cloud region unselect'), description: 'Clear default region' },
82
+ ],
83
+ schema: {
84
+ response: z.object({
85
+ cleared: z.boolean().describe('Whether the preference was cleared'),
86
+ }),
87
+ },
88
+
89
+ async handler(ctx) {
90
+ const { options, config } = ctx;
91
+
92
+ const hadRegion = !!config?.preferences?.region;
93
+
94
+ if (hadRegion && process.stdin.isTTY) {
95
+ const confirmed = await tui.confirm('Clear default region preference?', true);
96
+ if (!confirmed) {
97
+ if (!options.json) {
98
+ tui.info('Cancelled');
99
+ }
100
+ return { cleared: false };
101
+ }
102
+ }
103
+
104
+ await clearRegion();
105
+
106
+ if (!options.json) {
107
+ if (hadRegion) {
108
+ tui.success('Default region cleared');
109
+ } else {
110
+ tui.info('No default region was set');
111
+ }
112
+ }
113
+
114
+ return { cleared: hadRegion };
115
+ },
116
+ });
117
+
118
+ const currentCommand = createSubcommand({
119
+ name: 'current',
120
+ description: 'Show the current default region',
121
+ tags: ['read-only', 'fast'],
122
+ idempotent: true,
123
+ examples: [
124
+ { command: getCommand('cloud region current'), description: 'Show default region' },
125
+ {
126
+ command: getCommand('cloud region current --json'),
127
+ description: 'Show output in JSON format',
128
+ },
129
+ ],
130
+ schema: {
131
+ response: z.string().nullable().describe('The current region code or null if not set'),
132
+ },
133
+
134
+ async handler(ctx) {
135
+ const { options, config } = ctx;
136
+ const region = config?.preferences?.region || null;
137
+
138
+ if (!options.json) {
139
+ if (region) {
140
+ console.log(region);
141
+ }
142
+ }
143
+
144
+ return region;
145
+ },
146
+ });
147
+
148
+ export const regionSubcommand = createCommand({
149
+ name: 'region',
150
+ description: 'Manage default cloud region preference',
151
+ tags: ['fast'],
152
+ examples: [
153
+ { command: getCommand('cloud region select'), description: 'Set default region' },
154
+ { command: getCommand('cloud region current'), description: 'Show current default' },
155
+ ],
156
+ subcommands: [selectCommand, unselectCommand, currentCommand],
157
+ });
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { resolve, join, extname } from 'node:path';
3
- import { existsSync, statSync } from 'node:fs';
3
+ import { existsSync, statSync, createReadStream, createWriteStream } from 'node:fs';
4
4
  import { YAML } from 'bun';
5
5
  import * as tar from 'tar';
6
6
  import { createCommand } from '../../../../types';
@@ -14,8 +14,9 @@ import {
14
14
  import type { SnapshotFileInfo } from '@agentuity/server';
15
15
  import { getCatalystAPIClient } from '../../../../config';
16
16
  import { validateAptDependencies } from '../../../../utils/apt-validator';
17
+ import { encryptFIPSKEMDEMStream } from '../../../../crypto/box';
17
18
  import { tmpdir } from 'node:os';
18
- import { randomUUID, createHash } from 'node:crypto';
19
+ import { randomUUID, createHash, createPublicKey } from 'node:crypto';
19
20
  import { rm } from 'node:fs/promises';
20
21
 
21
22
  export const SNAPSHOT_TAG_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
@@ -575,6 +576,7 @@ export const buildSubcommand = createCommand({
575
576
  description: finalDescription,
576
577
  contentHash,
577
578
  force: opts.force,
579
+ encrypt: true,
578
580
  orgId,
579
581
  });
580
582
  },
@@ -609,19 +611,54 @@ export const buildSubcommand = createCommand({
609
611
  };
610
612
  }
611
613
 
614
+ // Encrypt the archive if public key is provided
615
+ let uploadPath = archivePath;
616
+ let uploadSize = archiveSize;
617
+
618
+ if (initResult.publicKey) {
619
+ const encryptedPath = join(tempDir, 'snapshot.tar.gz.enc');
620
+
621
+ await tui.spinner({
622
+ message: 'Encrypting snapshot...',
623
+ type: 'simple',
624
+ clearOnSuccess: true,
625
+ callback: async () => {
626
+ const publicKey = createPublicKey({
627
+ key: initResult.publicKey!,
628
+ format: 'pem',
629
+ type: 'spki',
630
+ });
631
+
632
+ const src = createReadStream(archivePath);
633
+ const dst = createWriteStream(encryptedPath);
634
+
635
+ await encryptFIPSKEMDEMStream(publicKey, src, dst);
636
+
637
+ await new Promise<void>((resolve, reject) => {
638
+ dst.once('finish', resolve);
639
+ dst.once('error', reject);
640
+ dst.end();
641
+ });
642
+ },
643
+ });
644
+
645
+ uploadPath = encryptedPath;
646
+ uploadSize = Bun.file(encryptedPath).size;
647
+ }
648
+
612
649
  await tui.spinner({
613
650
  message: 'Uploading snapshot...',
614
651
  type: 'progress',
615
652
  clearOnSuccess: true,
616
653
  callback: async (updateProgress) => {
617
- const archiveBuffer = await archiveFile.arrayBuffer();
654
+ const uploadFile = Bun.file(uploadPath);
618
655
  const response = await fetch(initResult.uploadUrl!, {
619
656
  method: 'PUT',
620
657
  headers: {
621
658
  'Content-Type': 'application/gzip',
622
- 'Content-Length': String(archiveSize),
659
+ 'Content-Length': String(uploadSize),
623
660
  },
624
- body: archiveBuffer,
661
+ body: uploadFile,
625
662
  });
626
663
 
627
664
  if (!response.ok) {
@@ -52,7 +52,7 @@ export const downloadCommand = createSubcommand({
52
52
  },
53
53
 
54
54
  async handler(ctx) {
55
- const { apiClient, args, opts, project, projectDir, config, logger, auth, orgId } = ctx;
55
+ const { apiClient, args, opts, project, projectDir, config, logger, auth } = ctx;
56
56
 
57
57
  let identifier = opts?.identifier ?? project?.projectId;
58
58
 
@@ -60,8 +60,12 @@ export const downloadCommand = createSubcommand({
60
60
  identifier = await tui.showProjectList(apiClient, true);
61
61
  }
62
62
 
63
- // Look up region from identifier (project/deployment)
63
+ // Look up region from identifier (project/deployment/sandbox)
64
64
  const profileName = config?.name;
65
+
66
+ // For sandbox identifiers, use saved org preference (no prompting)
67
+ const orgId = identifier.startsWith('sbx_') ? config?.preferences?.orgId : undefined;
68
+
65
69
  const region = await getIdentifierRegion(
66
70
  logger,
67
71
  auth,
@@ -55,7 +55,7 @@ export const uploadCommand = createSubcommand({
55
55
  prerequisites: ['cloud deploy'],
56
56
 
57
57
  async handler(ctx) {
58
- const { apiClient, args, opts, project, projectDir, config, logger, auth, orgId } = ctx;
58
+ const { apiClient, args, opts, project, projectDir, config, logger, auth } = ctx;
59
59
 
60
60
  let identifier = opts?.identifier ?? project?.projectId;
61
61
 
@@ -63,8 +63,12 @@ export const uploadCommand = createSubcommand({
63
63
  identifier = await tui.showProjectList(apiClient, true);
64
64
  }
65
65
 
66
- // Look up region from identifier (project/deployment)
66
+ // Look up region from identifier (project/deployment/sandbox)
67
67
  const profileName = config?.name;
68
+
69
+ // For sandbox identifiers, use saved org preference (no prompting)
70
+ const orgId = identifier.startsWith('sbx_') ? config?.preferences?.orgId : undefined;
71
+
68
72
  const region = await getIdentifierRegion(
69
73
  logger,
70
74
  auth,
@@ -46,7 +46,7 @@ export const sshSubcommand = createSubcommand({
46
46
  schema: { args, options },
47
47
 
48
48
  async handler(ctx) {
49
- const { apiClient, project, projectDir, args, config, opts, logger, auth, orgId } = ctx;
49
+ const { apiClient, project, projectDir, args, config, opts, logger, auth } = ctx;
50
50
 
51
51
  let projectId = project?.projectId;
52
52
  let identifier = args?.identifier;
@@ -70,6 +70,10 @@ export const sshSubcommand = createSubcommand({
70
70
  // Look up region from identifier (project/deployment/sandbox)
71
71
  const profileName = config?.name;
72
72
  const targetIdentifier = identifier ?? projectId!;
73
+
74
+ // For sandbox identifiers, use saved org preference (no prompting)
75
+ const orgId = targetIdentifier.startsWith('sbx_') ? config?.preferences?.orgId : undefined;
76
+
73
77
  const region = await getIdentifierRegion(
74
78
  logger,
75
79
  auth,
@@ -13,6 +13,8 @@ const StorageGetResponseSchema = z.object({
13
13
  secret_key: z.string().optional().describe('S3 secret key'),
14
14
  region: z.string().optional().describe('S3 region'),
15
15
  endpoint: z.string().optional().describe('S3 endpoint URL'),
16
+ org_id: z.string().optional().describe('Organization ID that owns this bucket'),
17
+ org_name: z.string().optional().describe('Organization name that owns this bucket'),
16
18
  });
17
19
 
18
20
  export const getSubcommand = createSubcommand({
@@ -100,24 +102,27 @@ export const getSubcommand = createSubcommand({
100
102
  const shouldMask = !options.json && !shouldShowCredentials;
101
103
 
102
104
  if (!options.json) {
103
- console.log(tui.bold('Bucket Name: ') + bucket.bucket_name);
105
+ console.log(tui.bold('Bucket Name: ') + bucket.bucket_name);
106
+ if (bucket.org_name || bucket.org_id) {
107
+ console.log(tui.bold('Organization: ') + (bucket.org_name || bucket.org_id));
108
+ }
104
109
  if (bucket.access_key) {
105
110
  const displayAccessKey = shouldMask
106
111
  ? tui.maskSecret(bucket.access_key)
107
112
  : bucket.access_key;
108
- console.log(tui.bold('Access Key: ') + displayAccessKey);
113
+ console.log(tui.bold('Access Key: ') + displayAccessKey);
109
114
  }
110
115
  if (bucket.secret_key) {
111
116
  const displaySecretKey = shouldMask
112
117
  ? tui.maskSecret(bucket.secret_key)
113
118
  : bucket.secret_key;
114
- console.log(tui.bold('Secret Key: ') + displaySecretKey);
119
+ console.log(tui.bold('Secret Key: ') + displaySecretKey);
115
120
  }
116
121
  if (bucket.region) {
117
- console.log(tui.bold('Region: ') + bucket.region);
122
+ console.log(tui.bold('Region: ') + bucket.region);
118
123
  }
119
124
  if (bucket.endpoint) {
120
- console.log(tui.bold('Endpoint: ') + bucket.endpoint);
125
+ console.log(tui.bold('Endpoint: ') + bucket.endpoint);
121
126
  }
122
127
  }
123
128
 
@@ -127,6 +132,8 @@ export const getSubcommand = createSubcommand({
127
132
  secret_key: bucket.secret_key ?? undefined,
128
133
  region: bucket.region ?? undefined,
129
134
  endpoint: bucket.endpoint ?? undefined,
135
+ org_id: bucket.org_id,
136
+ org_name: bucket.org_name,
130
137
  };
131
138
  },
132
139
  });
@@ -18,6 +18,8 @@ const StorageListResponseSchema = z.object({
18
18
  region: z.string().optional().describe('S3 region'),
19
19
  endpoint: z.string().optional().describe('S3 endpoint URL'),
20
20
  cloud_region: z.string().optional().describe('Cloud region where bucket is hosted'),
21
+ org_id: z.string().optional().describe('Organization ID that owns this bucket'),
22
+ org_name: z.string().optional().describe('Organization name that owns this bucket'),
21
23
  })
22
24
  )
23
25
  .optional()
@@ -189,6 +191,10 @@ export const listSubcommand = createSubcommand({
189
191
  const shouldShowCredentials = opts.showCredentials === true;
190
192
  const shouldMask = !options.json && !shouldShowCredentials;
191
193
 
194
+ // Check if resources span multiple orgs
195
+ const uniqueOrgIds = new Set(resources.s3.map((s3) => s3.org_id));
196
+ const showOrgColumn = uniqueOrgIds.size > 1;
197
+
192
198
  if (!options.json) {
193
199
  if (resources.s3.length === 0) {
194
200
  tui.info('No storage buckets found');
@@ -203,6 +209,9 @@ export const listSubcommand = createSubcommand({
203
209
  continue;
204
210
  }
205
211
  console.log(tui.bold(s3.bucket_name));
212
+ if (showOrgColumn) {
213
+ console.log(` Organization: ${tui.muted(s3.org_name || s3.org_id)}`);
214
+ }
206
215
  if (s3.access_key) {
207
216
  const displayAccessKey = shouldMask
208
217
  ? tui.maskSecret(s3.access_key)
@@ -230,6 +239,8 @@ export const listSubcommand = createSubcommand({
230
239
  region: s3.region ?? undefined,
231
240
  endpoint: s3.endpoint ?? undefined,
232
241
  cloud_region: s3.cloud_region,
242
+ org_id: s3.org_id,
243
+ org_name: s3.org_name,
233
244
  })),
234
245
  };
235
246
  },