@agentuity/cli 0.1.10 → 0.1.12

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 (126) hide show
  1. package/dist/auth.d.ts.map +1 -1
  2. package/dist/auth.js +6 -10
  3. package/dist/auth.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +89 -41
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cmd/auth/login.d.ts.map +1 -1
  8. package/dist/cmd/auth/login.js +49 -5
  9. package/dist/cmd/auth/login.js.map +1 -1
  10. package/dist/cmd/build/patch/_util.d.ts.map +1 -1
  11. package/dist/cmd/build/patch/_util.js +6 -2
  12. package/dist/cmd/build/patch/_util.js.map +1 -1
  13. package/dist/cmd/cloud/agent/get.d.ts.map +1 -1
  14. package/dist/cmd/cloud/agent/get.js +10 -6
  15. package/dist/cmd/cloud/agent/get.js.map +1 -1
  16. package/dist/cmd/cloud/db/create.d.ts.map +1 -1
  17. package/dist/cmd/cloud/db/create.js +14 -3
  18. package/dist/cmd/cloud/db/create.js.map +1 -1
  19. package/dist/cmd/cloud/db/get.d.ts.map +1 -1
  20. package/dist/cmd/cloud/db/get.js +13 -3
  21. package/dist/cmd/cloud/db/get.js.map +1 -1
  22. package/dist/cmd/cloud/db/list.d.ts.map +1 -1
  23. package/dist/cmd/cloud/db/list.js +17 -25
  24. package/dist/cmd/cloud/db/list.js.map +1 -1
  25. package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
  26. package/dist/cmd/cloud/deployment/show.js +50 -37
  27. package/dist/cmd/cloud/deployment/show.js.map +1 -1
  28. package/dist/cmd/cloud/sandbox/create.d.ts.map +1 -1
  29. package/dist/cmd/cloud/sandbox/create.js +19 -2
  30. package/dist/cmd/cloud/sandbox/create.js.map +1 -1
  31. package/dist/cmd/cloud/sandbox/execution/get.d.ts.map +1 -1
  32. package/dist/cmd/cloud/sandbox/execution/get.js +14 -11
  33. package/dist/cmd/cloud/sandbox/execution/get.js.map +1 -1
  34. package/dist/cmd/cloud/sandbox/get.d.ts.map +1 -1
  35. package/dist/cmd/cloud/sandbox/get.js +48 -39
  36. package/dist/cmd/cloud/sandbox/get.js.map +1 -1
  37. package/dist/cmd/cloud/sandbox/list.d.ts.map +1 -1
  38. package/dist/cmd/cloud/sandbox/list.js +6 -1
  39. package/dist/cmd/cloud/sandbox/list.js.map +1 -1
  40. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  41. package/dist/cmd/cloud/sandbox/run.js +5 -1
  42. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  43. package/dist/cmd/cloud/sandbox/snapshot/build.d.ts +5 -0
  44. package/dist/cmd/cloud/sandbox/snapshot/build.d.ts.map +1 -0
  45. package/dist/cmd/cloud/sandbox/snapshot/build.js +590 -0
  46. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -0
  47. package/dist/cmd/cloud/sandbox/snapshot/generate.d.ts +3 -0
  48. package/dist/cmd/cloud/sandbox/snapshot/generate.d.ts.map +1 -0
  49. package/dist/cmd/cloud/sandbox/snapshot/generate.js +129 -0
  50. package/dist/cmd/cloud/sandbox/snapshot/generate.js.map +1 -0
  51. package/dist/cmd/cloud/sandbox/snapshot/get.d.ts.map +1 -1
  52. package/dist/cmd/cloud/sandbox/snapshot/get.js +25 -6
  53. package/dist/cmd/cloud/sandbox/snapshot/get.js.map +1 -1
  54. package/dist/cmd/cloud/sandbox/snapshot/index.d.ts.map +1 -1
  55. package/dist/cmd/cloud/sandbox/snapshot/index.js +19 -1
  56. package/dist/cmd/cloud/sandbox/snapshot/index.js.map +1 -1
  57. package/dist/cmd/cloud/session/get.d.ts.map +1 -1
  58. package/dist/cmd/cloud/session/get.js +24 -23
  59. package/dist/cmd/cloud/session/get.js.map +1 -1
  60. package/dist/cmd/cloud/session/list.d.ts.map +1 -1
  61. package/dist/cmd/cloud/session/list.js +7 -2
  62. package/dist/cmd/cloud/session/list.js.map +1 -1
  63. package/dist/cmd/cloud/thread/get.d.ts.map +1 -1
  64. package/dist/cmd/cloud/thread/get.js +11 -8
  65. package/dist/cmd/cloud/thread/get.js.map +1 -1
  66. package/dist/cmd/cloud/thread/list.d.ts.map +1 -1
  67. package/dist/cmd/cloud/thread/list.js +6 -1
  68. package/dist/cmd/cloud/thread/list.js.map +1 -1
  69. package/dist/cmd/dev/file-watcher.d.ts.map +1 -1
  70. package/dist/cmd/dev/file-watcher.js +2 -0
  71. package/dist/cmd/dev/file-watcher.js.map +1 -1
  72. package/dist/cmd/dev/index.d.ts.map +1 -1
  73. package/dist/cmd/dev/index.js +62 -4
  74. package/dist/cmd/dev/index.js.map +1 -1
  75. package/dist/cmd/project/auth/shared.js +1 -1
  76. package/dist/cmd/project/auth/shared.js.map +1 -1
  77. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  78. package/dist/cmd/project/template-flow.js +15 -1
  79. package/dist/cmd/project/template-flow.js.map +1 -1
  80. package/dist/cmd/setup/index.d.ts.map +1 -1
  81. package/dist/cmd/setup/index.js +40 -3
  82. package/dist/cmd/setup/index.js.map +1 -1
  83. package/dist/config.d.ts.map +1 -1
  84. package/dist/config.js +10 -5
  85. package/dist/config.js.map +1 -1
  86. package/dist/steps.d.ts.map +1 -1
  87. package/dist/steps.js +4 -2
  88. package/dist/steps.js.map +1 -1
  89. package/dist/tui.d.ts +11 -1
  90. package/dist/tui.d.ts.map +1 -1
  91. package/dist/tui.js +47 -12
  92. package/dist/tui.js.map +1 -1
  93. package/dist/utils/apt-validator.js +3 -3
  94. package/dist/utils/apt-validator.js.map +1 -1
  95. package/package.json +6 -6
  96. package/src/auth.ts +12 -11
  97. package/src/cli.ts +121 -43
  98. package/src/cmd/auth/login.ts +57 -5
  99. package/src/cmd/build/patch/_util.ts +6 -2
  100. package/src/cmd/cloud/agent/get.ts +14 -6
  101. package/src/cmd/cloud/db/create.ts +15 -3
  102. package/src/cmd/cloud/db/get.ts +14 -3
  103. package/src/cmd/cloud/db/list.ts +16 -26
  104. package/src/cmd/cloud/deployment/show.ts +53 -47
  105. package/src/cmd/cloud/sandbox/create.ts +20 -2
  106. package/src/cmd/cloud/sandbox/execution/get.ts +16 -13
  107. package/src/cmd/cloud/sandbox/get.ts +48 -38
  108. package/src/cmd/cloud/sandbox/list.ts +6 -1
  109. package/src/cmd/cloud/sandbox/run.ts +5 -1
  110. package/src/cmd/cloud/sandbox/snapshot/build.ts +723 -0
  111. package/src/cmd/cloud/sandbox/snapshot/generate.ts +136 -0
  112. package/src/cmd/cloud/sandbox/snapshot/get.ts +29 -6
  113. package/src/cmd/cloud/sandbox/snapshot/index.ts +19 -1
  114. package/src/cmd/cloud/session/get.ts +25 -29
  115. package/src/cmd/cloud/session/list.ts +7 -2
  116. package/src/cmd/cloud/thread/get.ts +12 -8
  117. package/src/cmd/cloud/thread/list.ts +6 -1
  118. package/src/cmd/dev/file-watcher.ts +2 -0
  119. package/src/cmd/dev/index.ts +76 -4
  120. package/src/cmd/project/auth/shared.ts +1 -1
  121. package/src/cmd/project/template-flow.ts +15 -1
  122. package/src/cmd/setup/index.ts +41 -3
  123. package/src/config.ts +11 -5
  124. package/src/steps.ts +4 -2
  125. package/src/tui.ts +61 -12
  126. package/src/utils/apt-validator.ts +3 -3
@@ -0,0 +1,723 @@
1
+ import { z } from 'zod';
2
+ import { resolve, join, extname } from 'node:path';
3
+ import { existsSync, statSync } from 'node:fs';
4
+ import { YAML } from 'bun';
5
+ import * as tar from 'tar';
6
+ import { createCommand } from '../../../../types';
7
+ import * as tui from '../../../../tui';
8
+ import { getCommand } from '../../../../command-prefix';
9
+ import {
10
+ snapshotBuildInit,
11
+ snapshotBuildFinalize,
12
+ SnapshotBuildFileSchema,
13
+ } from '@agentuity/server';
14
+ import type { SnapshotFileInfo } from '@agentuity/server';
15
+ import { getCatalystAPIClient } from '../../../../config';
16
+ import { validateAptDependencies } from '../../../../utils/apt-validator';
17
+ import { tmpdir } from 'node:os';
18
+ import { randomUUID, createHash } from 'node:crypto';
19
+ import { rm } from 'node:fs/promises';
20
+
21
+ export const SNAPSHOT_TAG_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
22
+ export const MAX_SNAPSHOT_TAG_LENGTH = 128;
23
+
24
+ const SnapshotBuildResponseSchema = z.object({
25
+ snapshotId: z.string().describe('Snapshot ID'),
26
+ name: z.string().describe('Snapshot name'),
27
+ tag: z.string().nullable().optional().describe('Snapshot tag'),
28
+ runtime: z.string().describe('Runtime identifier'),
29
+ sizeBytes: z.number().describe('Snapshot size in bytes'),
30
+ fileCount: z.number().describe('Number of files in snapshot'),
31
+ createdAt: z.string().describe('Snapshot creation timestamp'),
32
+ unchanged: z.boolean().optional().describe('True if snapshot was unchanged'),
33
+ userMetadata: z
34
+ .record(z.string(), z.string())
35
+ .optional()
36
+ .describe('User-defined metadata key-value pairs'),
37
+ });
38
+
39
+ interface FileEntry {
40
+ path: string;
41
+ absolutePath: string;
42
+ size: number;
43
+ }
44
+
45
+ interface TreeNode {
46
+ name: string;
47
+ size?: number;
48
+ isFile: boolean;
49
+ children: Map<string, TreeNode>;
50
+ }
51
+
52
+ function buildFileTree(files: SnapshotFileInfo[]): TreeNode {
53
+ const root: TreeNode = { name: '', isFile: false, children: new Map() };
54
+
55
+ for (const file of files) {
56
+ const parts = file.path.split('/');
57
+ let current = root;
58
+
59
+ for (let i = 0; i < parts.length; i++) {
60
+ const part = parts[i];
61
+ if (!current.children.has(part)) {
62
+ current.children.set(part, {
63
+ name: part,
64
+ isFile: i === parts.length - 1,
65
+ children: new Map(),
66
+ });
67
+ }
68
+ current = current.children.get(part)!;
69
+
70
+ if (i === parts.length - 1) {
71
+ current.size = file.size;
72
+ current.isFile = true;
73
+ }
74
+ }
75
+ }
76
+
77
+ return root;
78
+ }
79
+
80
+ function printFileTree(files: SnapshotFileInfo[]): void {
81
+ const tree = buildFileTree(files);
82
+ printTreeNode(tree, ' ');
83
+ }
84
+
85
+ function printTreeNode(node: TreeNode, prefix: string): void {
86
+ const entries = Array.from(node.children.entries()).sort((a, b) => {
87
+ const aIsDir = !a[1].isFile;
88
+ const bIsDir = !b[1].isFile;
89
+ if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
90
+ return a[0].localeCompare(b[0]);
91
+ });
92
+
93
+ for (let i = 0; i < entries.length; i++) {
94
+ const [, child] = entries[i];
95
+ const isLast = i === entries.length - 1;
96
+ const connector = tui.muted(isLast ? '└── ' : '├── ');
97
+ const sizeStr =
98
+ child.isFile && child.size !== undefined ? ` (${tui.formatBytes(child.size)})` : '';
99
+
100
+ console.log(`${prefix}${connector}${child.name}${sizeStr}`);
101
+
102
+ if (child.children.size > 0) {
103
+ const newPrefix = prefix + (isLast ? ' ' : tui.muted('│ '));
104
+ printTreeNode(child, newPrefix);
105
+ }
106
+ }
107
+ }
108
+
109
+ function parseKeyValueArgs(args: string[] | undefined): Record<string, string> {
110
+ if (!args || args.length === 0) {
111
+ return {};
112
+ }
113
+
114
+ const result: Record<string, string> = {};
115
+ for (const arg of args) {
116
+ const eqIndex = arg.indexOf('=');
117
+ if (eqIndex === -1) {
118
+ throw new Error(`Invalid KEY=VALUE format: "${arg}"`);
119
+ }
120
+ const key = arg.slice(0, eqIndex);
121
+ const value = arg.slice(eqIndex + 1);
122
+ if (!key) {
123
+ throw new Error(`Invalid KEY=VALUE format: "${arg}" (empty key)`);
124
+ }
125
+ result[key] = value;
126
+ }
127
+ return result;
128
+ }
129
+
130
+ function substituteVariables(
131
+ values: Record<string, string>,
132
+ variables: Record<string, string>
133
+ ): Record<string, string> {
134
+ const result: Record<string, string> = {};
135
+ const varPattern = /\$\{([^}]+)\}/g;
136
+
137
+ for (const [key, value] of Object.entries(values)) {
138
+ let substituted = value;
139
+ let match: RegExpExecArray | null;
140
+
141
+ varPattern.lastIndex = 0;
142
+ while ((match = varPattern.exec(value)) !== null) {
143
+ const varName = match[1];
144
+ if (!(varName in variables)) {
145
+ throw new Error(
146
+ `Variable "\${${varName}}" in "${key}" is not defined. Use --env ${varName}=value to provide it.`
147
+ );
148
+ }
149
+ substituted = substituted.replace(match[0], variables[varName]);
150
+ }
151
+ result[key] = substituted;
152
+ }
153
+
154
+ return result;
155
+ }
156
+
157
+ async function resolveFileGlobs(
158
+ directory: string,
159
+ patterns: string[]
160
+ ): Promise<Map<string, FileEntry>> {
161
+ const files = new Map<string, FileEntry>();
162
+ const exclusions: string[] = [];
163
+ const inclusions: string[] = [];
164
+
165
+ for (const pattern of patterns) {
166
+ if (pattern.startsWith('!')) {
167
+ exclusions.push(pattern.slice(1));
168
+ } else {
169
+ inclusions.push(pattern);
170
+ }
171
+ }
172
+
173
+ for (const pattern of inclusions) {
174
+ const glob = new Bun.Glob(pattern);
175
+ for await (const file of glob.scan({ cwd: directory, dot: true })) {
176
+ const absolutePath = join(directory, file);
177
+ try {
178
+ const stat = statSync(absolutePath);
179
+ if (stat.isFile()) {
180
+ files.set(file, {
181
+ path: file,
182
+ absolutePath,
183
+ size: stat.size,
184
+ });
185
+ }
186
+ } catch {
187
+ // Skip files that can't be stat'd (broken symlinks, permission issues, etc.)
188
+ continue;
189
+ }
190
+ }
191
+ }
192
+
193
+ for (let pattern of exclusions) {
194
+ // If the pattern refers to a directory, auto-append /** to exclude all contents
195
+ const patternPath = join(directory, pattern);
196
+ try {
197
+ const stat = statSync(patternPath);
198
+ if (stat.isDirectory()) {
199
+ pattern = pattern.endsWith('/') ? `${pattern}**` : `${pattern}/**`;
200
+ }
201
+ } catch {
202
+ // Path doesn't exist or can't be stat'd, use pattern as-is
203
+ }
204
+
205
+ const glob = new Bun.Glob(pattern);
206
+ for await (const file of glob.scan({ cwd: directory, dot: true })) {
207
+ files.delete(file);
208
+ }
209
+ }
210
+
211
+ return files;
212
+ }
213
+
214
+ async function createTarGzArchive(
215
+ directory: string,
216
+ files: Map<string, FileEntry>,
217
+ outputPath: string
218
+ ): Promise<void> {
219
+ const filePaths = Array.from(files.keys());
220
+
221
+ await tar.create(
222
+ {
223
+ gzip: true,
224
+ file: outputPath,
225
+ cwd: directory,
226
+ },
227
+ filePaths
228
+ );
229
+ }
230
+
231
+ async function generateContentHash(params: {
232
+ runtime: string;
233
+ description?: string;
234
+ dependencies?: string[];
235
+ files: SnapshotFileInfo[];
236
+ fileHashes: Map<string, string>;
237
+ env?: Record<string, string>;
238
+ }): Promise<string> {
239
+ const hash = createHash('sha256');
240
+
241
+ hash.update(`runtime:${params.runtime}\n`);
242
+
243
+ if (params.description) {
244
+ hash.update(`description:${params.description}\n`);
245
+ }
246
+
247
+ if (params.dependencies && params.dependencies.length > 0) {
248
+ const sortedDeps = [...params.dependencies].sort();
249
+ hash.update(`dependencies:${sortedDeps.join(',')}\n`);
250
+ }
251
+
252
+ if (params.files.length > 0) {
253
+ const sortedFiles = [...params.files].sort((a, b) => a.path.localeCompare(b.path));
254
+ for (const file of sortedFiles) {
255
+ const contentHash = params.fileHashes.get(file.path) ?? '';
256
+ hash.update(`file:${file.path}:${file.size}:${contentHash}\n`);
257
+ }
258
+ }
259
+
260
+ if (params.env && Object.keys(params.env).length > 0) {
261
+ const sortedKeys = Object.keys(params.env).sort();
262
+ for (const key of sortedKeys) {
263
+ hash.update(`env:${key}=${params.env[key]}\n`);
264
+ }
265
+ }
266
+
267
+ return hash.digest('hex');
268
+ }
269
+
270
+ export const buildSubcommand = createCommand({
271
+ name: 'build',
272
+ description: 'Build a snapshot from a declarative file',
273
+ tags: ['slow', 'requires-auth'],
274
+ requires: { auth: true, org: true, region: true },
275
+ examples: [
276
+ {
277
+ command: getCommand('cloud sandbox snapshot build .'),
278
+ description: 'Build a snapshot from the current directory using agentuity-snapshot.yaml',
279
+ },
280
+ {
281
+ command: getCommand('cloud sandbox snapshot build ./project --file custom-build.yaml'),
282
+ description: 'Build using a custom build file',
283
+ },
284
+ {
285
+ command: getCommand(
286
+ 'cloud sandbox snapshot build . --env API_KEY=secret --tag production'
287
+ ),
288
+ description: 'Build with environment variable substitution and custom tag',
289
+ },
290
+ {
291
+ command: getCommand('cloud sandbox snapshot build . --dry-run'),
292
+ description: 'Validate the build file without uploading',
293
+ },
294
+ {
295
+ command: getCommand('cloud sandbox snapshot build . --force'),
296
+ description: 'Force rebuild even if content is unchanged',
297
+ },
298
+ ],
299
+ schema: {
300
+ args: z.object({
301
+ directory: z.string().describe('Directory containing files to include in snapshot'),
302
+ }),
303
+ options: z.object({
304
+ file: z
305
+ .string()
306
+ .optional()
307
+ .describe('Path to build file (defaults to agentuity-snapshot.[json|yaml|yml])'),
308
+ env: z
309
+ .array(z.string())
310
+ .optional()
311
+ .describe('Environment variable substitution (KEY=VALUE)'),
312
+ name: z.string().optional().describe('Snapshot name (overrides build file)'),
313
+ tag: z.string().optional().describe('Snapshot tag (defaults to "latest")'),
314
+ description: z.string().optional().describe('Snapshot description (overrides build file)'),
315
+ metadata: z.array(z.string()).optional().describe('Metadata key-value pairs (KEY=VALUE)'),
316
+ force: z.boolean().optional().describe('Force rebuild even if content is unchanged'),
317
+ }),
318
+ response: SnapshotBuildResponseSchema,
319
+ },
320
+
321
+ async handler(ctx) {
322
+ const { args, opts, options, auth, region, config, logger, orgId } = ctx;
323
+
324
+ const dryRun = options.dryRun === true;
325
+
326
+ const directory = resolve(args.directory);
327
+ if (!existsSync(directory)) {
328
+ logger.fatal(`Directory not found: ${directory}`);
329
+ }
330
+
331
+ let buildFilePath: string | undefined;
332
+ if (opts.file) {
333
+ buildFilePath = resolve(opts.file);
334
+ if (!existsSync(buildFilePath)) {
335
+ logger.fatal(`Build file not found: ${buildFilePath}`);
336
+ }
337
+ } else {
338
+ const candidates = [
339
+ 'agentuity-snapshot.yaml',
340
+ 'agentuity-snapshot.yml',
341
+ 'agentuity-snapshot.json',
342
+ ];
343
+ for (const candidate of candidates) {
344
+ const candidatePath = join(directory, candidate);
345
+ if (existsSync(candidatePath)) {
346
+ buildFilePath = candidatePath;
347
+ break;
348
+ }
349
+ }
350
+ if (!buildFilePath) {
351
+ logger.fatal(
352
+ `No build file found. Expected one of: ${candidates.join(', ')} in ${directory}`
353
+ );
354
+ }
355
+ }
356
+
357
+ const buildFileContent = await Bun.file(buildFilePath!).text();
358
+ const ext = extname(buildFilePath!).toLowerCase();
359
+ let parsedBuildFile: unknown;
360
+
361
+ try {
362
+ if (ext === '.yaml' || ext === '.yml') {
363
+ parsedBuildFile = YAML.parse(buildFileContent);
364
+ } else if (ext === '.json') {
365
+ parsedBuildFile = JSON.parse(buildFileContent);
366
+ } else {
367
+ logger.fatal(`Unsupported build file extension: ${ext}. Use .yaml, .yml, or .json`);
368
+ }
369
+ } catch (err) {
370
+ logger.fatal(`Failed to parse build file: ${err instanceof Error ? err.message : err}`);
371
+ }
372
+
373
+ const validationResult = SnapshotBuildFileSchema.safeParse(parsedBuildFile);
374
+ if (!validationResult.success) {
375
+ tui.error(`Invalid build file at ${buildFilePath}:`);
376
+ for (const issue of validationResult.error.issues) {
377
+ const path = issue.path.length > 0 ? issue.path.join('.') : 'root';
378
+ tui.bullet(`${path}: ${issue.message}`);
379
+ }
380
+ process.exit(1);
381
+ }
382
+
383
+ const buildConfig = validationResult.data;
384
+
385
+ if (opts.tag) {
386
+ if (opts.tag.length > MAX_SNAPSHOT_TAG_LENGTH) {
387
+ logger.fatal(
388
+ `Invalid snapshot tag: must be at most ${MAX_SNAPSHOT_TAG_LENGTH} characters`
389
+ );
390
+ }
391
+ if (!SNAPSHOT_TAG_REGEX.test(opts.tag)) {
392
+ logger.fatal(
393
+ 'Invalid snapshot tag: must only contain letters, numbers, dashes, underscores, and dots, and cannot start with a period or dash'
394
+ );
395
+ }
396
+ }
397
+
398
+ let envSubstitutions: Record<string, string> = {};
399
+ let metadataSubstitutions: Record<string, string> = {};
400
+ try {
401
+ envSubstitutions = parseKeyValueArgs(opts.env);
402
+ metadataSubstitutions = parseKeyValueArgs(opts.metadata);
403
+ } catch (err) {
404
+ logger.fatal(err instanceof Error ? err.message : String(err));
405
+ return undefined as never;
406
+ }
407
+
408
+ let finalEnv: Record<string, string> | undefined;
409
+ let finalMetadata: Record<string, string> | undefined;
410
+
411
+ // Name and Description: CLI options override build file
412
+ const finalName = opts.name ?? buildConfig.name;
413
+ const finalDescription = opts.description ?? buildConfig.description;
414
+
415
+ try {
416
+ if (buildConfig.env) {
417
+ finalEnv = substituteVariables(buildConfig.env, envSubstitutions);
418
+ }
419
+ if (buildConfig.metadata) {
420
+ finalMetadata = substituteVariables(buildConfig.metadata, metadataSubstitutions);
421
+ }
422
+ } catch (err) {
423
+ logger.fatal(err instanceof Error ? err.message : String(err));
424
+ return undefined as never;
425
+ }
426
+
427
+ if (buildConfig.dependencies && buildConfig.dependencies.length > 0) {
428
+ const aptValidation = await tui.spinner({
429
+ message: 'Validating apt dependencies...',
430
+ type: 'simple',
431
+ callback: async () => {
432
+ return await validateAptDependencies(
433
+ buildConfig.dependencies!,
434
+ region,
435
+ config,
436
+ logger
437
+ );
438
+ },
439
+ });
440
+
441
+ if (aptValidation.invalid.length > 0) {
442
+ tui.error('Invalid apt dependencies:');
443
+ for (const pkg of aptValidation.invalid) {
444
+ tui.bullet(`${pkg.package}: ${pkg.error}`);
445
+ if (pkg.availableVersions && pkg.availableVersions.length > 0) {
446
+ console.log(` Available versions: ${pkg.availableVersions.join(', ')}`);
447
+ }
448
+ console.log(` Search: ${pkg.searchUrl}`);
449
+ }
450
+ process.exit(1);
451
+ }
452
+ }
453
+
454
+ let files = new Map<string, FileEntry>();
455
+ if (buildConfig.files && buildConfig.files.length > 0) {
456
+ files = await resolveFileGlobs(directory, buildConfig.files);
457
+ }
458
+
459
+ const fileList: SnapshotFileInfo[] = Array.from(files.values()).map((f) => ({
460
+ path: f.path,
461
+ size: f.size,
462
+ }));
463
+ const totalSize = fileList.reduce((sum, f) => sum + f.size, 0);
464
+
465
+ const fileHashes = new Map<string, string>();
466
+ for (const file of files.values()) {
467
+ const fullPath = join(directory, file.path);
468
+ const bunFile = Bun.file(fullPath);
469
+ const content = await bunFile.arrayBuffer();
470
+ const hash = createHash('sha256').update(Buffer.from(content)).digest('hex');
471
+ fileHashes.set(file.path, hash);
472
+ }
473
+
474
+ const contentHash = await generateContentHash({
475
+ runtime: buildConfig.runtime,
476
+ description: finalDescription,
477
+ dependencies: buildConfig.dependencies,
478
+ files: fileList,
479
+ fileHashes,
480
+ env: finalEnv,
481
+ });
482
+
483
+ if (dryRun) {
484
+ if (!options.json) {
485
+ tui.info(`${tui.bold('Dry Run')} - No upload will be performed`);
486
+ console.log('');
487
+ tui.table(
488
+ [
489
+ {
490
+ Name: finalName,
491
+ Description: finalDescription ?? '-',
492
+ Runtime: buildConfig.runtime,
493
+ Tag: opts.tag ?? 'latest',
494
+ Size: tui.formatBytes(totalSize),
495
+ Files: fileList.length.toFixed(),
496
+ },
497
+ ],
498
+ ['Name', 'Description', 'Runtime', 'Tag', 'Size', 'Files'],
499
+ { layout: 'vertical', padStart: ' ' }
500
+ );
501
+
502
+ if (buildConfig.dependencies && buildConfig.dependencies.length > 0) {
503
+ console.log('');
504
+ tui.info('Dependencies:');
505
+ for (const dep of buildConfig.dependencies) {
506
+ console.log(` ${tui.muted('•')} ${dep}`);
507
+ }
508
+ }
509
+
510
+ if (finalEnv && Object.keys(finalEnv).length > 0) {
511
+ console.log('');
512
+ tui.info('Environment:');
513
+ for (const key of Object.keys(finalEnv)) {
514
+ console.log(` ${tui.muted('•')} ${key}=${tui.maskSecret(finalEnv[key])}`);
515
+ }
516
+ }
517
+
518
+ if (fileList.length > 0) {
519
+ console.log('');
520
+ tui.info('Files:');
521
+ printFileTree(fileList);
522
+ }
523
+ }
524
+
525
+ return {
526
+ snapshotId: '',
527
+ name: finalName ?? '',
528
+ tag: opts.tag ?? 'latest',
529
+ runtime: buildConfig.runtime,
530
+ sizeBytes: totalSize,
531
+ fileCount: fileList.length,
532
+ createdAt: new Date().toISOString(),
533
+ userMetadata: finalMetadata,
534
+ };
535
+ }
536
+
537
+ const tempDir = join(tmpdir(), `snapshot-build-${randomUUID()}`);
538
+ const archivePath = join(tempDir, 'snapshot.tar.gz');
539
+
540
+ try {
541
+ await Bun.write(join(tempDir, '.placeholder'), '');
542
+
543
+ if (files.size > 0) {
544
+ await tui.spinner({
545
+ message: 'Creating archive...',
546
+ type: 'simple',
547
+ callback: async () => {
548
+ await createTarGzArchive(directory, files, archivePath);
549
+ },
550
+ });
551
+ } else {
552
+ await tar.create(
553
+ {
554
+ gzip: true,
555
+ file: archivePath,
556
+ cwd: tempDir,
557
+ },
558
+ ['.placeholder']
559
+ );
560
+ }
561
+
562
+ const archiveFile = Bun.file(archivePath);
563
+ const archiveSize = archiveFile.size;
564
+
565
+ const client = getCatalystAPIClient(logger, auth, region);
566
+
567
+ const initResult = await tui.spinner({
568
+ message: 'Initializing snapshot build...',
569
+ clearOnSuccess: true,
570
+ callback: async () => {
571
+ return await snapshotBuildInit(client, {
572
+ runtime: buildConfig.runtime,
573
+ name: finalName,
574
+ tag: opts.tag,
575
+ description: finalDescription,
576
+ contentHash,
577
+ force: opts.force,
578
+ orgId,
579
+ });
580
+ },
581
+ });
582
+
583
+ if (initResult.unchanged) {
584
+ if (!options.json) {
585
+ tui.success(`Snapshot unchanged ${tui.bold(initResult.existingId!)}`);
586
+ console.log('');
587
+ tui.table(
588
+ [
589
+ {
590
+ Name: finalName,
591
+ Tag: opts.tag ?? 'latest',
592
+ },
593
+ ],
594
+ ['Name', 'Tag'],
595
+ { layout: 'vertical', padStart: ' ' }
596
+ );
597
+ }
598
+
599
+ return {
600
+ snapshotId: initResult.existingId!,
601
+ name: initResult.existingName!,
602
+ tag: initResult.existingTag ?? undefined,
603
+ runtime: buildConfig.runtime,
604
+ sizeBytes: totalSize,
605
+ fileCount: fileList.length,
606
+ createdAt: new Date().toISOString(),
607
+ unchanged: true,
608
+ userMetadata: finalMetadata,
609
+ };
610
+ }
611
+
612
+ await tui.spinner({
613
+ message: 'Uploading snapshot...',
614
+ type: 'progress',
615
+ clearOnSuccess: true,
616
+ callback: async (updateProgress) => {
617
+ const archiveBuffer = await archiveFile.arrayBuffer();
618
+ const response = await fetch(initResult.uploadUrl!, {
619
+ method: 'PUT',
620
+ headers: {
621
+ 'Content-Type': 'application/gzip',
622
+ 'Content-Length': String(archiveSize),
623
+ },
624
+ body: archiveBuffer,
625
+ });
626
+
627
+ if (!response.ok) {
628
+ throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
629
+ }
630
+
631
+ updateProgress(100);
632
+ },
633
+ });
634
+
635
+ const snapshot = await tui.spinner({
636
+ message: 'Finalizing snapshot...',
637
+ clearOnSuccess: true,
638
+ callback: async () => {
639
+ return await snapshotBuildFinalize(client, {
640
+ snapshotId: initResult.snapshotId!,
641
+ sizeBytes: totalSize,
642
+ fileCount: fileList.length,
643
+ files: fileList,
644
+ dependencies: buildConfig.dependencies,
645
+ env: finalEnv,
646
+ metadata: finalMetadata,
647
+ orgId,
648
+ });
649
+ },
650
+ });
651
+
652
+ if (!options.json) {
653
+ tui.success(`Created snapshot ${tui.bold(snapshot.snapshotId)}`);
654
+ console.log('');
655
+
656
+ tui.table(
657
+ [
658
+ {
659
+ Name: snapshot.name,
660
+ Description: snapshot.description ?? '-',
661
+ Runtime: buildConfig.runtime,
662
+ Tag: snapshot.tag ?? 'latest',
663
+ Size: tui.formatBytes(snapshot.sizeBytes),
664
+ Files: snapshot.fileCount.toFixed(),
665
+ Created: snapshot.createdAt,
666
+ },
667
+ ],
668
+ ['Name', 'Description', 'Runtime', 'Tag', 'Size', 'Files', 'Created'],
669
+ { layout: 'vertical', padStart: ' ' }
670
+ );
671
+
672
+ if (buildConfig.dependencies && buildConfig.dependencies.length > 0) {
673
+ console.log('');
674
+ tui.info('Dependencies:');
675
+ for (const dep of buildConfig.dependencies) {
676
+ console.log(` ${tui.muted('•')} ${dep}`);
677
+ }
678
+ }
679
+
680
+ if (finalEnv && Object.keys(finalEnv).length > 0) {
681
+ console.log('');
682
+ tui.info('Environment:');
683
+ for (const key of Object.keys(finalEnv)) {
684
+ console.log(` ${tui.muted('•')} ${key}=${tui.maskSecret(finalEnv[key])}`);
685
+ }
686
+ }
687
+
688
+ if (finalMetadata && Object.keys(finalMetadata).length > 0) {
689
+ console.log('');
690
+ tui.info('Metadata:');
691
+ for (const key of Object.keys(finalMetadata)) {
692
+ console.log(` ${tui.muted('•')} ${key}=${finalMetadata[key]}`);
693
+ }
694
+ }
695
+
696
+ if (snapshot.files && snapshot.files.length > 0) {
697
+ console.log('');
698
+ tui.info('Files:');
699
+ printFileTree(snapshot.files);
700
+ }
701
+ }
702
+
703
+ return {
704
+ snapshotId: snapshot.snapshotId,
705
+ name: snapshot.name,
706
+ tag: snapshot.tag ?? undefined,
707
+ runtime: buildConfig.runtime,
708
+ sizeBytes: snapshot.sizeBytes,
709
+ fileCount: snapshot.fileCount,
710
+ createdAt: snapshot.createdAt,
711
+ userMetadata: snapshot.userMetadata ?? undefined,
712
+ };
713
+ } finally {
714
+ try {
715
+ await rm(tempDir, { recursive: true, force: true });
716
+ } catch {
717
+ // Ignore cleanup errors
718
+ }
719
+ }
720
+ },
721
+ });
722
+
723
+ export default buildSubcommand;