@agentuity/cli 1.0.15 → 1.0.16

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/cmd/build/entry-generator.d.ts.map +1 -1
  2. package/dist/cmd/build/entry-generator.js +5 -2
  3. package/dist/cmd/build/entry-generator.js.map +1 -1
  4. package/dist/cmd/build/vite/metadata-generator.d.ts.map +1 -1
  5. package/dist/cmd/build/vite/metadata-generator.js +1 -37
  6. package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
  7. package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
  8. package/dist/cmd/build/vite/registry-generator.js +15 -2
  9. package/dist/cmd/build/vite/registry-generator.js.map +1 -1
  10. package/dist/cmd/build/vite/vite-asset-server-config.js +1 -1
  11. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  12. package/dist/cmd/build/vite/vite-asset-server.d.ts.map +1 -1
  13. package/dist/cmd/build/vite/vite-asset-server.js +42 -39
  14. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  15. package/dist/cmd/cloud/sandbox/get.d.ts.map +1 -1
  16. package/dist/cmd/cloud/sandbox/get.js +5 -0
  17. package/dist/cmd/cloud/sandbox/get.js.map +1 -1
  18. package/dist/cmd/cloud/sandbox/snapshot/build.d.ts.map +1 -1
  19. package/dist/cmd/cloud/sandbox/snapshot/build.js +32 -1
  20. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  21. package/dist/cmd/cloud/sandbox/snapshot/generate.d.ts.map +1 -1
  22. package/dist/cmd/cloud/sandbox/snapshot/generate.js +7 -0
  23. package/dist/cmd/cloud/sandbox/snapshot/generate.js.map +1 -1
  24. package/dist/cmd/cloud/storage/create.d.ts.map +1 -1
  25. package/dist/cmd/cloud/storage/create.js +58 -26
  26. package/dist/cmd/cloud/storage/create.js.map +1 -1
  27. package/dist/cmd/cloud/storage/delete.d.ts.map +1 -1
  28. package/dist/cmd/cloud/storage/delete.js +108 -16
  29. package/dist/cmd/cloud/storage/delete.js.map +1 -1
  30. package/package.json +6 -6
  31. package/src/cmd/build/entry-generator.ts +6 -2
  32. package/src/cmd/build/vite/metadata-generator.ts +1 -39
  33. package/src/cmd/build/vite/registry-generator.ts +17 -2
  34. package/src/cmd/build/vite/vite-asset-server-config.ts +1 -1
  35. package/src/cmd/build/vite/vite-asset-server.ts +53 -40
  36. package/src/cmd/cloud/sandbox/get.ts +5 -0
  37. package/src/cmd/cloud/sandbox/snapshot/build.ts +41 -0
  38. package/src/cmd/cloud/sandbox/snapshot/generate.ts +7 -0
  39. package/src/cmd/cloud/storage/create.ts +62 -27
  40. package/src/cmd/cloud/storage/delete.ts +136 -17
@@ -35,6 +35,12 @@ description: My sandbox snapshot
35
35
  # - ffmpeg
36
36
  # - imagemagick=8:6.9*
37
37
 
38
+ # Optional: npm/bun packages to install globally
39
+ # Installed via 'bun install -g' at sandbox startup
40
+ # packages:
41
+ # - opencode-ai
42
+ # - typescript
43
+
38
44
  # Optional: Files to include from the build context directory
39
45
  # Supports glob patterns and negative patterns (prefix with !)
40
46
  # Files are placed in /home/agentuity/ in the sandbox
@@ -68,6 +74,7 @@ const TEMPLATE_JSON = {
68
74
  name: 'my-snapshot',
69
75
  description: 'My sandbox snapshot',
70
76
  dependencies: ['curl'],
77
+ packages: ['opencode-ai'],
71
78
  files: ['src/**', '*.js', '!**/*.test.js'],
72
79
  env: {
73
80
  NODE_ENV: 'production',
@@ -1,10 +1,11 @@
1
1
  import { z } from 'zod';
2
- import { createResources } from '@agentuity/server';
2
+ import { createResources, APIError, validateBucketName } from '@agentuity/server';
3
3
  import { createSubcommand as defineSubcommand } from '../../../types';
4
4
  import * as tui from '../../../tui';
5
5
  import { getCatalystAPIClient } from '../../../config';
6
6
  import { getCommand } from '../../../command-prefix';
7
7
  import { isDryRunMode, outputDryRun } from '../../../explain';
8
+ import { ErrorCode } from '../../../errors';
8
9
  import { addResourceEnvVars } from '../../../env-util';
9
10
 
10
11
  export const createSubcommand = defineSubcommand({
@@ -23,6 +24,10 @@ export const createSubcommand = defineSubcommand({
23
24
  command: getCommand('cloud storage new'),
24
25
  description: 'Alias for "cloud storage create" (shorthand "new")',
25
26
  },
27
+ {
28
+ command: getCommand('cloud storage create --name my-bucket'),
29
+ description: 'Create a new cloud storage bucket with a custom name',
30
+ },
26
31
  {
27
32
  command: getCommand('--dry-run cloud storage create'),
28
33
  description: 'Dry-run: display what would be created without making changes',
@@ -30,6 +35,7 @@ export const createSubcommand = defineSubcommand({
30
35
  ],
31
36
  schema: {
32
37
  options: z.object({
38
+ name: z.string().optional().describe('Custom bucket name'),
33
39
  description: z.string().optional().describe('Optional description for the bucket'),
34
40
  }),
35
41
  response: z.object({
@@ -41,9 +47,22 @@ export const createSubcommand = defineSubcommand({
41
47
  async handler(ctx) {
42
48
  const { logger, orgId, region, auth, options, opts } = ctx;
43
49
 
50
+ // Validate bucket name if provided
51
+ if (opts.name) {
52
+ const validation = validateBucketName(opts.name);
53
+ if (!validation.valid) {
54
+ tui.fatal(validation.error!, ErrorCode.INVALID_ARGUMENT);
55
+ }
56
+ }
57
+
44
58
  // Handle dry-run mode
45
59
  if (isDryRunMode(options)) {
46
- const message = `Would create storage bucket in region: ${region}`;
60
+ let message = opts.name
61
+ ? `Would create storage bucket with name: ${opts.name} in region: ${region}`
62
+ : `Would create storage bucket in region: ${region}`;
63
+ if (opts.description) {
64
+ message += ` with description: "${opts.description}"`;
65
+ }
47
66
  outputDryRun(message, options);
48
67
  if (!options.json) {
49
68
  tui.newline();
@@ -51,41 +70,57 @@ export const createSubcommand = defineSubcommand({
51
70
  }
52
71
  return {
53
72
  success: false,
54
- name: 'dry-run-bucket',
73
+ name: opts.name || 'dry-run-bucket',
55
74
  };
56
75
  }
57
76
 
58
77
  const catalystClient = getCatalystAPIClient(logger, auth, region);
59
78
 
60
- const created = await tui.spinner({
61
- message: `Creating storage in ${region}`,
62
- clearOnSuccess: true,
63
- callback: async () => {
64
- return createResources(catalystClient, orgId, region!, [
65
- { type: 's3', description: opts.description },
66
- ]);
67
- },
68
- });
79
+ try {
80
+ const created = await tui.spinner({
81
+ message: `Creating storage in ${region}`,
82
+ clearOnSuccess: true,
83
+ callback: async () => {
84
+ return createResources(catalystClient, orgId, region!, [
85
+ { type: 's3', name: opts.name, description: opts.description },
86
+ ]);
87
+ },
88
+ });
89
+
90
+ const resource = created[0];
91
+ if (resource) {
92
+ // Write environment variables to .env if running inside a project
93
+ if (ctx.projectDir && resource.env && Object.keys(resource.env).length > 0) {
94
+ await addResourceEnvVars(ctx.projectDir, resource.env);
95
+ if (!options.json) {
96
+ tui.info('Environment variables written to .env');
97
+ }
98
+ }
69
99
 
70
- const resource = created[0];
71
- if (resource) {
72
- // Write environment variables to .env if running inside a project
73
- if (ctx.projectDir && resource.env && Object.keys(resource.env).length > 0) {
74
- await addResourceEnvVars(ctx.projectDir, resource.env);
75
100
  if (!options.json) {
76
- tui.info('Environment variables written to .env');
101
+ tui.success(`Created storage: ${tui.bold(resource.name)}`);
77
102
  }
103
+ return {
104
+ success: true,
105
+ name: resource.name,
106
+ };
107
+ } else {
108
+ tui.fatal('Failed to create storage');
78
109
  }
79
-
80
- if (!options.json) {
81
- tui.success(`Created storage: ${tui.bold(resource.name)}`);
110
+ } catch (ex) {
111
+ if (ex instanceof APIError) {
112
+ if (ex.status === 409) {
113
+ const bucketName = opts.name || 'auto-generated';
114
+ tui.fatal(
115
+ `bucket with the name "${bucketName}" already exists. Use another name or don't specify --name for a unique name to be generated automatically.`,
116
+ ErrorCode.INVALID_ARGUMENT
117
+ );
118
+ }
119
+ if (ex.status === 400) {
120
+ tui.fatal(ex.message || 'invalid bucket name', ErrorCode.INVALID_ARGUMENT);
121
+ }
82
122
  }
83
- return {
84
- success: true,
85
- name: resource.name,
86
- };
87
- } else {
88
- tui.fatal('Failed to create storage');
123
+ throw ex;
89
124
  }
90
125
  },
91
126
  });
@@ -14,7 +14,7 @@ import { getResourceInfo, setResourceInfo, deleteResourceRegion } from '../../..
14
14
  export const deleteSubcommand = createSubcommand({
15
15
  name: 'delete',
16
16
  aliases: ['rm', 'del', 'remove'],
17
- description: 'Delete a storage resource or file',
17
+ description: 'Delete a storage resource, file, or folder',
18
18
  tags: ['destructive', 'deletes-resource', 'slow', 'requires-auth', 'requires-deployment'],
19
19
  idempotent: false,
20
20
  requires: { auth: true },
@@ -32,6 +32,10 @@ export const deleteSubcommand = createSubcommand({
32
32
  command: getCommand('cloud storage delete'),
33
33
  description: 'Interactive selection to delete a bucket',
34
34
  },
35
+ {
36
+ command: getCommand('cloud storage delete my-bucket path/to/folder'),
37
+ description: 'Delete a folder and all its contents from a bucket',
38
+ },
35
39
  {
36
40
  command: getCommand('--dry-run cloud storage delete my-bucket'),
37
41
  description: 'Dry-run: show what would be deleted without making changes',
@@ -48,6 +52,7 @@ export const deleteSubcommand = createSubcommand({
48
52
  response: z.object({
49
53
  success: z.boolean().describe('Whether deletion succeeded'),
50
54
  name: z.string().describe('Deleted bucket or file name'),
55
+ count: z.number().optional().describe('Number of files deleted (for folder deletion)'),
51
56
  }),
52
57
  },
53
58
 
@@ -116,7 +121,7 @@ export const deleteSubcommand = createSubcommand({
116
121
  bucketName = response.bucket;
117
122
  }
118
123
 
119
- // If filename is provided, delete the file from the bucket
124
+ // If filename is provided, delete the file or folder from the bucket
120
125
  if (args.filename) {
121
126
  const bucket = resources.s3.find((s3) => s3.bucket_name === bucketName);
122
127
 
@@ -131,22 +136,143 @@ export const deleteSubcommand = createSubcommand({
131
136
  );
132
137
  }
133
138
 
139
+ const s3Client = createS3Client({
140
+ endpoint: bucket.endpoint,
141
+ access_key: bucket.access_key,
142
+ secret_key: bucket.secret_key,
143
+ region: bucket.region,
144
+ });
145
+
146
+ const filePath = args.filename;
147
+
148
+ // Check if path represents a folder by listing objects under it
149
+ const folderPrefix = filePath.endsWith('/') ? filePath : filePath + '/';
150
+ const folderContents: Array<{ key: string }> = [];
151
+ let continuationToken: string | undefined;
152
+ let isTruncated = false;
153
+
154
+ do {
155
+ const folderResult = await s3Client.list({
156
+ prefix: folderPrefix,
157
+ ...(continuationToken ? { continuationToken } : {}),
158
+ });
159
+ if (folderResult.contents) {
160
+ folderContents.push(...folderResult.contents);
161
+ }
162
+ continuationToken = folderResult.nextContinuationToken;
163
+ isTruncated = folderResult.isTruncated ?? false;
164
+ } while (isTruncated);
165
+
166
+ if (folderContents.length > 0) {
167
+ // Path is a folder — recursive delete
168
+ const keysToDelete = folderContents.map((obj: { key: string }) => obj.key);
169
+
170
+ // Handle dry-run mode
171
+ if (isDryRunMode(options)) {
172
+ outputDryRun(
173
+ `Would delete ${keysToDelete.length} file${keysToDelete.length === 1 ? '' : 's'} under ${folderPrefix} from bucket ${bucketName}`,
174
+ options
175
+ );
176
+ if (!options.json) {
177
+ tui.newline();
178
+ tui.info('[DRY RUN] Folder deletion skipped');
179
+ }
180
+ return { success: false, name: filePath, count: keysToDelete.length };
181
+ }
182
+
183
+ // Confirm
184
+ if (!opts.confirm) {
185
+ tui.warning(
186
+ `You are about to delete ${tui.bold(String(keysToDelete.length))} file${keysToDelete.length === 1 ? '' : 's'} under folder: ${tui.bold(folderPrefix)} from bucket: ${tui.bold(bucketName)}`
187
+ );
188
+ const confirm = await enquirer.prompt<{ confirm: boolean }>({
189
+ type: 'confirm',
190
+ name: 'confirm',
191
+ message: 'Are you sure you want to delete this folder and all its contents?',
192
+ initial: false,
193
+ });
194
+ if (!confirm.confirm) {
195
+ tui.info('Deletion cancelled');
196
+ return { success: false, name: filePath };
197
+ }
198
+ }
199
+
200
+ // Delete all files
201
+ await tui.spinner({
202
+ message: `Deleting ${keysToDelete.length} file${keysToDelete.length === 1 ? '' : 's'} under ${folderPrefix} from ${bucketName}`,
203
+ clearOnSuccess: true,
204
+ callback: async () => {
205
+ const errors: Array<{ key: string; error: string }> = [];
206
+ for (const key of keysToDelete) {
207
+ try {
208
+ await s3Client.delete(key);
209
+ } catch (err) {
210
+ errors.push({
211
+ key,
212
+ error: err instanceof Error ? err.message : String(err),
213
+ });
214
+ }
215
+ }
216
+ if (errors.length > 0) {
217
+ const failedKeys = errors.map((e) => e.key).join(', ');
218
+ throw new Error(
219
+ `Failed to delete ${errors.length} file${errors.length === 1 ? '' : 's'}: ${failedKeys}`
220
+ );
221
+ }
222
+ },
223
+ });
224
+
225
+ // Also delete the exact file if it exists (handles file+folder name conflicts)
226
+ // Skip if filePath was already deleted as part of folder contents (e.g., trailing-slash folder markers)
227
+ if (!keysToDelete.includes(filePath)) {
228
+ const exactFileCheck = await s3Client.list({ prefix: filePath });
229
+ const exactFile = (exactFileCheck.contents || []).find(
230
+ (obj: { key: string }) => obj.key === filePath
231
+ );
232
+ if (exactFile) {
233
+ await s3Client.delete(filePath);
234
+ keysToDelete.push(filePath);
235
+ }
236
+ }
237
+
238
+ if (!options.json) {
239
+ tui.success(
240
+ `Deleted ${tui.bold(String(keysToDelete.length))} file${keysToDelete.length === 1 ? '' : 's'} under ${tui.bold(folderPrefix)} from ${tui.bold(bucketName)}`
241
+ );
242
+ }
243
+
244
+ return { success: true, name: filePath, count: keysToDelete.length };
245
+ }
246
+
247
+ // Not a folder — check if exact file exists
248
+ const fileResult = await s3Client.list({ prefix: filePath });
249
+ const exactMatch = (fileResult.contents || []).find(
250
+ (obj: { key: string }) => obj.key === filePath
251
+ );
252
+
253
+ if (!exactMatch) {
254
+ tui.fatal(
255
+ `No file or folder found at '${filePath}' in bucket '${bucketName}'`,
256
+ ErrorCode.RESOURCE_NOT_FOUND
257
+ );
258
+ }
259
+
134
260
  // Handle dry-run mode
135
261
  if (isDryRunMode(options)) {
136
- outputDryRun(`Would delete file ${args.filename} from bucket ${bucketName}`, options);
262
+ outputDryRun(`Would delete file ${filePath} from bucket ${bucketName}`, options);
137
263
  if (!options.json) {
138
264
  tui.newline();
139
265
  tui.info('[DRY RUN] File deletion skipped');
140
266
  }
141
267
  return {
142
268
  success: false,
143
- name: args.filename,
269
+ name: filePath,
144
270
  };
145
271
  }
146
272
 
147
273
  if (!opts.confirm) {
148
274
  tui.warning(
149
- `You are about to delete file: ${tui.bold(args.filename)} from bucket: ${tui.bold(bucketName)}`
275
+ `You are about to delete file: ${tui.bold(filePath)} from bucket: ${tui.bold(bucketName)}`
150
276
  );
151
277
 
152
278
  const confirm = await enquirer.prompt<{ confirm: boolean }>({
@@ -158,32 +284,25 @@ export const deleteSubcommand = createSubcommand({
158
284
 
159
285
  if (!confirm.confirm) {
160
286
  tui.info('Deletion cancelled');
161
- return { success: false, name: args.filename };
287
+ return { success: false, name: filePath };
162
288
  }
163
289
  }
164
290
 
165
- const s3Client = createS3Client({
166
- endpoint: bucket.endpoint,
167
- access_key: bucket.access_key,
168
- secret_key: bucket.secret_key,
169
- region: bucket.region,
170
- });
171
-
172
291
  await tui.spinner({
173
- message: `Deleting ${args.filename} from ${bucketName}`,
292
+ message: `Deleting ${filePath} from ${bucketName}`,
174
293
  clearOnSuccess: true,
175
294
  callback: async () => {
176
- await s3Client.delete(args.filename!);
295
+ await s3Client.delete(filePath);
177
296
  },
178
297
  });
179
298
 
180
299
  if (!options.json) {
181
- tui.success(`Deleted file: ${tui.bold(args.filename)} from ${tui.bold(bucketName)}`);
300
+ tui.success(`Deleted file: ${tui.bold(filePath)} from ${tui.bold(bucketName)}`);
182
301
  }
183
302
 
184
303
  return {
185
304
  success: true,
186
- name: args.filename,
305
+ name: filePath,
187
306
  };
188
307
  }
189
308