@agentuity/cli 1.0.28 → 1.0.30

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 (40) hide show
  1. package/dist/agent-detection.d.ts.map +1 -1
  2. package/dist/agent-detection.js +23 -3
  3. package/dist/agent-detection.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +2 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cmd/cloud/keyvalue/repl.js +1 -1
  8. package/dist/cmd/cloud/keyvalue/repl.js.map +1 -1
  9. package/dist/cmd/cloud/keyvalue/search.js +2 -2
  10. package/dist/cmd/cloud/keyvalue/search.js.map +1 -1
  11. package/dist/cmd/cloud/storage/config.d.ts +2 -0
  12. package/dist/cmd/cloud/storage/config.d.ts.map +1 -0
  13. package/dist/cmd/cloud/storage/config.js +202 -0
  14. package/dist/cmd/cloud/storage/config.js.map +1 -0
  15. package/dist/cmd/cloud/storage/index.d.ts.map +1 -1
  16. package/dist/cmd/cloud/storage/index.js +2 -0
  17. package/dist/cmd/cloud/storage/index.js.map +1 -1
  18. package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
  19. package/dist/cmd/cloud/storage/list.js +19 -0
  20. package/dist/cmd/cloud/storage/list.js.map +1 -1
  21. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  22. package/dist/cmd/project/template-flow.js +30 -2
  23. package/dist/cmd/project/template-flow.js.map +1 -1
  24. package/dist/domain.d.ts.map +1 -1
  25. package/dist/domain.js +48 -0
  26. package/dist/domain.js.map +1 -1
  27. package/dist/tui.d.ts.map +1 -1
  28. package/dist/tui.js +3 -1
  29. package/dist/tui.js.map +1 -1
  30. package/package.json +6 -6
  31. package/src/agent-detection.ts +23 -3
  32. package/src/cli.ts +1 -0
  33. package/src/cmd/cloud/keyvalue/repl.ts +1 -1
  34. package/src/cmd/cloud/keyvalue/search.ts +2 -2
  35. package/src/cmd/cloud/storage/config.ts +238 -0
  36. package/src/cmd/cloud/storage/index.ts +2 -0
  37. package/src/cmd/cloud/storage/list.ts +18 -0
  38. package/src/cmd/project/template-flow.ts +31 -1
  39. package/src/domain.ts +52 -4
  40. package/src/tui.ts +2 -1
@@ -0,0 +1,238 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ getBucketConfig,
4
+ updateBucketConfig,
5
+ deleteBucketConfig,
6
+ BucketConfigResponseError,
7
+ listOrgResources,
8
+ type BucketConfigUpdate,
9
+ type BucketConfig,
10
+ StorageTierSchema,
11
+ } from '@agentuity/server';
12
+ import { createSubcommand } from '../../../types';
13
+ import * as tui from '../../../tui';
14
+ import { getCatalystAPIClient, getGlobalCatalystAPIClient } from '../../../config';
15
+ import { getCommand } from '../../../command-prefix';
16
+ import { getResourceInfo, setResourceInfo } from '../../../cache';
17
+
18
+ function displayConfig(config: BucketConfig) {
19
+ tui.newline();
20
+ console.log(tui.bold('Bucket: ') + config.bucket_name);
21
+ console.log(
22
+ tui.bold('Storage Tier: ') + (config.storage_tier ?? tui.muted('default'))
23
+ );
24
+ console.log(
25
+ tui.bold('TTL: ') +
26
+ (config.ttl != null ? `${config.ttl}s` : tui.muted('default'))
27
+ );
28
+ console.log(
29
+ tui.bold('Public: ') +
30
+ (config.public != null ? String(config.public) : tui.muted('default'))
31
+ );
32
+ console.log(
33
+ tui.bold('Cache Control: ') + (config.cache_control ?? tui.muted('default'))
34
+ );
35
+
36
+ if (config.cors) {
37
+ console.log(tui.bold('CORS:'));
38
+ if (config.cors.allowed_origins?.length) {
39
+ console.log(' Origins: ' + config.cors.allowed_origins.join(', '));
40
+ }
41
+ if (config.cors.allowed_methods?.length) {
42
+ console.log(' Methods: ' + config.cors.allowed_methods.join(', '));
43
+ }
44
+ if (config.cors.allowed_headers?.length) {
45
+ console.log(' Headers: ' + config.cors.allowed_headers.join(', '));
46
+ }
47
+ if (config.cors.expose_headers?.length) {
48
+ console.log(' Expose: ' + config.cors.expose_headers.join(', '));
49
+ }
50
+ if (config.cors.max_age_seconds != null) {
51
+ console.log(' Max Age: ' + config.cors.max_age_seconds + 's');
52
+ }
53
+ } else {
54
+ console.log(tui.bold('CORS: ') + tui.muted('default'));
55
+ }
56
+
57
+ if (config.additional_headers && Object.keys(config.additional_headers).length > 0) {
58
+ console.log(tui.bold('Headers:'));
59
+ for (const [key, value] of Object.entries(config.additional_headers)) {
60
+ console.log(` ${key}: ${value}`);
61
+ }
62
+ } else {
63
+ console.log(tui.bold('Headers: ') + tui.muted('default'));
64
+ }
65
+ tui.newline();
66
+ }
67
+
68
+ export const configSubcommand = createSubcommand({
69
+ name: 'config',
70
+ description: 'View or update bucket configuration',
71
+ tags: ['slow', 'requires-auth'],
72
+ requires: { auth: true },
73
+ optional: { org: true },
74
+ idempotent: true,
75
+ examples: [
76
+ {
77
+ command: `${getCommand('cloud storage config')} my-bucket`,
78
+ description: 'View bucket configuration',
79
+ },
80
+ {
81
+ command: `${getCommand('cloud storage config')} my-bucket --ttl 3600 --public`,
82
+ description: 'Update bucket TTL and make it public',
83
+ },
84
+ {
85
+ command: `${getCommand('cloud storage config')} my-bucket --storage-tier ARCHIVE`,
86
+ description: 'Change the storage tier',
87
+ },
88
+ {
89
+ command: `${getCommand('cloud storage config')} my-bucket --reset`,
90
+ description: 'Reset all configuration to system defaults',
91
+ },
92
+ ],
93
+ schema: {
94
+ args: z.object({
95
+ name: z.string().describe('The name of the storage bucket'),
96
+ }),
97
+ options: z.object({
98
+ reset: z.boolean().optional().describe('Reset all configuration to system defaults'),
99
+ storageTier: StorageTierSchema.optional().describe('Storage tier'),
100
+ ttl: z.coerce.number().optional().describe('Object TTL in seconds (0 to clear)'),
101
+ public: z.boolean().optional().describe('Make bucket publicly accessible'),
102
+ cacheControl: z.string().optional().describe('Cache-Control header value'),
103
+ cors: z.string().optional().describe('CORS configuration as JSON string'),
104
+ additionalHeaders: z
105
+ .string()
106
+ .optional()
107
+ .describe('Additional headers as JSON key-value pairs'),
108
+ }),
109
+ response: z.object({
110
+ bucket_name: z.string(),
111
+ storage_tier: z.string().nullable().optional(),
112
+ ttl: z.number().nullable().optional(),
113
+ public: z.boolean().nullable().optional(),
114
+ cache_control: z.string().nullable().optional(),
115
+ cors: z.any().nullable().optional(),
116
+ additional_headers: z.record(z.string(), z.string()).nullable().optional(),
117
+ }),
118
+ },
119
+
120
+ async handler(ctx) {
121
+ const { logger, args, opts, options, auth, config } = ctx;
122
+ const { name: bucketName } = args;
123
+
124
+ const profileName = config?.name ?? 'production';
125
+ const catalystClient = await getGlobalCatalystAPIClient(logger, auth, profileName);
126
+
127
+ // Look up bucket to get cloud_region
128
+ const cachedInfo = await getResourceInfo('bucket', profileName, bucketName);
129
+ const orgId = ctx.orgId ?? cachedInfo?.orgId;
130
+
131
+ const resources = await tui.spinner({
132
+ message: 'Looking up bucket...',
133
+ clearOnSuccess: true,
134
+ callback: () => listOrgResources(catalystClient, { type: 's3', orgId }),
135
+ });
136
+
137
+ const bucket = resources.s3.find((s3) => s3.bucket_name === bucketName);
138
+ if (!bucket) {
139
+ throw new BucketConfigResponseError({ message: `Bucket "${bucketName}" not found` });
140
+ }
141
+
142
+ // Cache the bucket info for future lookups
143
+ if (bucket.cloud_region && bucket.org_id) {
144
+ await setResourceInfo(
145
+ 'bucket',
146
+ profileName,
147
+ bucket.bucket_name,
148
+ bucket.cloud_region,
149
+ bucket.org_id
150
+ );
151
+ }
152
+
153
+ if (!bucket.cloud_region) {
154
+ throw new BucketConfigResponseError({
155
+ message: `Bucket "${bucketName}" is missing region information`,
156
+ });
157
+ }
158
+
159
+ // Create regional client for bucket config operations (orgId required for CLI auth)
160
+ const regionalClient = getCatalystAPIClient(logger, auth, bucket.cloud_region, bucket.org_id);
161
+
162
+ // Handle --reset flag (DELETE)
163
+ if (opts.reset) {
164
+ await tui.spinner({
165
+ message: 'Resetting bucket configuration...',
166
+ clearOnSuccess: true,
167
+ callback: () => deleteBucketConfig(regionalClient, bucketName),
168
+ });
169
+ if (!options.json) {
170
+ tui.success(`Configuration reset to defaults for bucket "${bucketName}"`);
171
+ }
172
+ return { bucket_name: bucketName };
173
+ }
174
+
175
+ // Check if any update flags are present
176
+ const hasUpdateFlags =
177
+ opts.storageTier !== undefined ||
178
+ opts.ttl !== undefined ||
179
+ opts.public !== undefined ||
180
+ opts.cacheControl !== undefined ||
181
+ opts.cors !== undefined ||
182
+ opts.additionalHeaders !== undefined;
183
+
184
+ if (hasUpdateFlags) {
185
+ // Build update payload
186
+ const update: BucketConfigUpdate = {};
187
+
188
+ if (opts.storageTier !== undefined) update.storage_tier = opts.storageTier;
189
+ if (opts.ttl !== undefined) update.ttl = opts.ttl === 0 ? null : opts.ttl;
190
+ if (opts.public !== undefined) update.public = opts.public;
191
+ if (opts.cacheControl !== undefined) update.cache_control = opts.cacheControl;
192
+
193
+ // Parse JSON flags
194
+ if (opts.cors !== undefined) {
195
+ try {
196
+ update.cors = JSON.parse(opts.cors);
197
+ } catch {
198
+ throw new BucketConfigResponseError({
199
+ message: 'Invalid JSON for --cors flag',
200
+ });
201
+ }
202
+ }
203
+ if (opts.additionalHeaders !== undefined) {
204
+ try {
205
+ update.additional_headers = JSON.parse(opts.additionalHeaders);
206
+ } catch {
207
+ throw new BucketConfigResponseError({
208
+ message: 'Invalid JSON for --additionalHeaders flag',
209
+ });
210
+ }
211
+ }
212
+
213
+ const result = await tui.spinner({
214
+ message: 'Updating bucket configuration...',
215
+ clearOnSuccess: true,
216
+ callback: () => updateBucketConfig(regionalClient, bucketName, update),
217
+ });
218
+
219
+ if (!options.json) {
220
+ displayConfig(result);
221
+ tui.success(`Configuration updated for bucket "${bucketName}"`);
222
+ }
223
+ return result;
224
+ }
225
+
226
+ // No update flags — GET and display
227
+ const getResult = await tui.spinner({
228
+ message: 'Fetching bucket configuration...',
229
+ clearOnSuccess: true,
230
+ callback: () => getBucketConfig(regionalClient, bucketName),
231
+ });
232
+
233
+ if (!options.json) {
234
+ displayConfig(getResult);
235
+ }
236
+ return getResult;
237
+ },
238
+ });
@@ -1,4 +1,5 @@
1
1
  import { createCommand } from '../../../types';
2
+ import { configSubcommand } from './config';
2
3
  import { createSubcommand } from './create';
3
4
  import { listSubcommand } from './list';
4
5
  import { deleteSubcommand } from './delete';
@@ -20,6 +21,7 @@ export const storageCommand = createCommand({
20
21
  },
21
22
  ],
22
23
  subcommands: [
24
+ configSubcommand,
23
25
  createSubcommand,
24
26
  listSubcommand,
25
27
  getSubcommand,
@@ -23,6 +23,9 @@ const StorageListResponseSchema = z.object({
23
23
  bucket_type: z.string().optional().describe('Bucket type (user or snapshots)'),
24
24
  internal: z.boolean().optional().describe('Whether this is a system-managed bucket'),
25
25
  description: z.string().optional().describe('Optional description of the bucket'),
26
+ object_count: z.number().int().optional().describe('Number of objects in this bucket'),
27
+ total_size: z.number().int().optional().describe('Total size of objects in bytes'),
28
+ last_event_at: z.string().optional().describe('Last activity timestamp'),
26
29
  })
27
30
  )
28
31
  .optional()
@@ -256,6 +259,18 @@ export const listSubcommand = createSubcommand({
256
259
  }
257
260
  if (s3.region) console.log(` Region: ${tui.muted(s3.region)}`);
258
261
  if (s3.endpoint) console.log(` Endpoint: ${tui.muted(s3.endpoint)}`);
262
+ if (s3.object_count != null) {
263
+ const sizeStr = s3.total_size != null ? tui.formatBytes(s3.total_size) : 'unknown';
264
+ console.log(` Objects: ${tui.muted(`${s3.object_count.toLocaleString()} (${sizeStr})`)}`);
265
+ }
266
+ if (s3.last_event_at) {
267
+ const date = new Date(s3.last_event_at);
268
+ if (Number.isNaN(date.getTime())) {
269
+ console.log(` Activity: ${tui.muted('unknown')}`);
270
+ } else {
271
+ console.log(` Activity: ${tui.muted(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }))}`);
272
+ }
273
+ }
259
274
  tui.newline();
260
275
  }
261
276
  }
@@ -274,6 +289,9 @@ export const listSubcommand = createSubcommand({
274
289
  bucket_type: s3.bucket_type,
275
290
  internal: s3.internal,
276
291
  description: s3.description ?? undefined,
292
+ object_count: s3.object_count ?? undefined,
293
+ total_size: s3.total_size ?? undefined,
294
+ last_event_at: s3.last_event_at ?? undefined,
277
295
  })),
278
296
  };
279
297
  },
@@ -11,6 +11,7 @@ import {
11
11
  APIClient as ServerAPIClient,
12
12
  createResources,
13
13
  validateDatabaseName,
14
+ validateBucketName,
14
15
  } from '@agentuity/server';
15
16
  import type { Logger } from '@agentuity/core';
16
17
  import * as tui from '../../tui';
@@ -440,11 +441,40 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<CreateF
440
441
  // Process storage action
441
442
  switch (s3_action) {
442
443
  case 'Create New': {
444
+ let bucketName: string | undefined;
445
+ let bucketDescription: string | undefined;
446
+
447
+ // Only prompt for name/description in interactive mode
448
+ if (isInteractive) {
449
+ const bucketNameInput = await prompt.text({
450
+ message: 'Bucket name',
451
+ hint: 'Optional - lowercase letters, digits, hyphens only',
452
+ validate: (value: string) => {
453
+ const trimmed = value.trim();
454
+ if (trimmed === '') return true;
455
+ const result = validateBucketName(trimmed);
456
+ return result.valid ? true : result.error!;
457
+ },
458
+ });
459
+ bucketName = bucketNameInput.trim() || undefined;
460
+ bucketDescription =
461
+ (await prompt.text({
462
+ message: 'Bucket description',
463
+ hint: 'Optional - press Enter to skip',
464
+ })) || undefined;
465
+ }
466
+
443
467
  const created = await tui.spinner({
444
468
  message: 'Provisioning New Bucket',
445
469
  clearOnSuccess: true,
446
470
  callback: async () => {
447
- return createResources(catalystClient!, orgId!, region!, [{ type: 's3' }]);
471
+ return createResources(catalystClient!, orgId!, region!, [
472
+ {
473
+ type: 's3',
474
+ name: bucketName,
475
+ description: bucketDescription,
476
+ },
477
+ ]);
448
478
  },
449
479
  });
450
480
  // Collect env vars from newly created resource
package/src/domain.ts CHANGED
@@ -95,6 +95,30 @@ async function fetchDNSRecord(name: string, type: string): Promise<string | null
95
95
  return records[0] ?? null;
96
96
  }
97
97
 
98
+ /**
99
+ * Check if a domain has a valid TLS certificate by making a HEAD request.
100
+ * This also triggers Let's Encrypt certificate provisioning on first access.
101
+ * Returns true if the TLS certificate is valid (any HTTP status code received).
102
+ * Returns false if the certificate is not yet provisioned (timeout or TLS error).
103
+ */
104
+ async function checkTLSCertificate(domain: string): Promise<boolean> {
105
+ try {
106
+ await fetch(`https://${domain}`, {
107
+ method: 'HEAD',
108
+ signal: AbortSignal.timeout(timeoutMs),
109
+ redirect: 'manual',
110
+ // @ts-expect-error - cache is supported by Bun's fetch at runtime but missing from type definitions
111
+ cache: 'no-store',
112
+ });
113
+ // Any HTTP response means TLS handshake succeeded and certificate is valid
114
+ return true;
115
+ } catch {
116
+ // Timeout, TLS certificate error, connection refused, etc.
117
+ // All indicate the certificate is not yet provisioned
118
+ return false;
119
+ }
120
+ }
121
+
98
122
  const LOCAL_DNS = 'agentuity.io';
99
123
  const PRODUCTION_DNS = 'agentuity.run';
100
124
 
@@ -171,15 +195,27 @@ export async function checkCustomDomainForDNS(
171
195
  if (timeoutId) clearTimeout(timeoutId);
172
196
  });
173
197
 
174
- if (result) {
175
- if (result === proxy) {
198
+ if (result) {
199
+ if (result === proxy) {
200
+ // DNS is correct — verify TLS certificate (also triggers Let's Encrypt provisioning)
201
+ const tlsValid = await checkTLSCertificate(domain);
202
+ if (tlsValid) {
203
+ return {
204
+ domain,
205
+ target: proxy,
206
+ aRecordTarget,
207
+ recordType: 'CNAME',
208
+ success: true,
209
+ } as DNSSuccess;
210
+ }
176
211
  return {
177
212
  domain,
178
213
  target: proxy,
179
214
  aRecordTarget,
180
215
  recordType: 'CNAME',
181
216
  success: true,
182
- } as DNSSuccess;
217
+ pending: true,
218
+ } as DNSPending;
183
219
  }
184
220
  return {
185
221
  domain,
@@ -242,13 +278,25 @@ export async function checkCustomDomainForDNS(
242
278
  if (domainARecords.length > 0) {
243
279
  const matching = domainARecords.some((a) => ionIPs.includes(a));
244
280
  if (matching) {
281
+ // DNS is correct — verify TLS certificate (also triggers Let's Encrypt provisioning)
282
+ const tlsValid = await checkTLSCertificate(domain);
283
+ if (tlsValid) {
284
+ return {
285
+ domain,
286
+ target: proxy,
287
+ aRecordTarget,
288
+ recordType: 'A',
289
+ success: true,
290
+ } as DNSSuccess;
291
+ }
245
292
  return {
246
293
  domain,
247
294
  target: proxy,
248
295
  aRecordTarget,
249
296
  recordType: 'A',
250
297
  success: true,
251
- } as DNSSuccess;
298
+ pending: true,
299
+ } as DNSPending;
252
300
  }
253
301
  return {
254
302
  domain,
package/src/tui.ts CHANGED
@@ -2235,7 +2235,8 @@ export function formatBytes(bytes: number): string {
2235
2235
  if (bytes < 1024) return `${bytes} B`;
2236
2236
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
2237
2237
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
2238
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
2238
+ if (bytes < 1024 * 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
2239
+ return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
2239
2240
  }
2240
2241
 
2241
2242
  export function clearLastLines(n: number, s?: (v: string) => void) {