@stacksjs/ts-cloud 0.1.1

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 (117) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +321 -0
  3. package/bin/cli.ts +133 -0
  4. package/bin/commands/analytics.ts +328 -0
  5. package/bin/commands/api.ts +379 -0
  6. package/bin/commands/assets.ts +221 -0
  7. package/bin/commands/audit.ts +501 -0
  8. package/bin/commands/backup.ts +682 -0
  9. package/bin/commands/cache.ts +294 -0
  10. package/bin/commands/cdn.ts +281 -0
  11. package/bin/commands/config.ts +202 -0
  12. package/bin/commands/container.ts +105 -0
  13. package/bin/commands/cost.ts +208 -0
  14. package/bin/commands/database.ts +401 -0
  15. package/bin/commands/deploy.ts +674 -0
  16. package/bin/commands/domain.ts +397 -0
  17. package/bin/commands/email.ts +423 -0
  18. package/bin/commands/environment.ts +285 -0
  19. package/bin/commands/events.ts +424 -0
  20. package/bin/commands/firewall.ts +145 -0
  21. package/bin/commands/function.ts +116 -0
  22. package/bin/commands/generate.ts +280 -0
  23. package/bin/commands/git.ts +139 -0
  24. package/bin/commands/iam.ts +464 -0
  25. package/bin/commands/index.ts +48 -0
  26. package/bin/commands/init.ts +120 -0
  27. package/bin/commands/logs.ts +148 -0
  28. package/bin/commands/network.ts +579 -0
  29. package/bin/commands/notify.ts +489 -0
  30. package/bin/commands/queue.ts +407 -0
  31. package/bin/commands/scheduler.ts +370 -0
  32. package/bin/commands/secrets.ts +54 -0
  33. package/bin/commands/server.ts +629 -0
  34. package/bin/commands/shared.ts +97 -0
  35. package/bin/commands/ssl.ts +138 -0
  36. package/bin/commands/stack.ts +325 -0
  37. package/bin/commands/status.ts +385 -0
  38. package/bin/commands/storage.ts +450 -0
  39. package/bin/commands/team.ts +96 -0
  40. package/bin/commands/tunnel.ts +489 -0
  41. package/bin/commands/utils.ts +202 -0
  42. package/build.ts +15 -0
  43. package/cloud +2 -0
  44. package/package.json +99 -0
  45. package/src/aws/acm.ts +768 -0
  46. package/src/aws/application-autoscaling.ts +845 -0
  47. package/src/aws/bedrock.ts +4074 -0
  48. package/src/aws/client.ts +878 -0
  49. package/src/aws/cloudformation.ts +896 -0
  50. package/src/aws/cloudfront.ts +1531 -0
  51. package/src/aws/cloudwatch-logs.ts +154 -0
  52. package/src/aws/comprehend.ts +839 -0
  53. package/src/aws/connect.ts +1056 -0
  54. package/src/aws/deploy-imap.ts +384 -0
  55. package/src/aws/dynamodb.ts +340 -0
  56. package/src/aws/ec2.ts +1385 -0
  57. package/src/aws/ecr.ts +621 -0
  58. package/src/aws/ecs.ts +615 -0
  59. package/src/aws/elasticache.ts +301 -0
  60. package/src/aws/elbv2.ts +942 -0
  61. package/src/aws/email.ts +928 -0
  62. package/src/aws/eventbridge.ts +248 -0
  63. package/src/aws/iam.ts +1689 -0
  64. package/src/aws/imap-server.ts +2100 -0
  65. package/src/aws/index.ts +213 -0
  66. package/src/aws/kendra.ts +1097 -0
  67. package/src/aws/lambda.ts +786 -0
  68. package/src/aws/opensearch.ts +158 -0
  69. package/src/aws/personalize.ts +977 -0
  70. package/src/aws/polly.ts +559 -0
  71. package/src/aws/rds.ts +888 -0
  72. package/src/aws/rekognition.ts +846 -0
  73. package/src/aws/route53-domains.ts +359 -0
  74. package/src/aws/route53.ts +1046 -0
  75. package/src/aws/s3.ts +2318 -0
  76. package/src/aws/scheduler.ts +571 -0
  77. package/src/aws/secrets-manager.ts +769 -0
  78. package/src/aws/ses.ts +1081 -0
  79. package/src/aws/setup-phone.ts +104 -0
  80. package/src/aws/setup-sms.ts +580 -0
  81. package/src/aws/sms.ts +1735 -0
  82. package/src/aws/smtp-server.ts +531 -0
  83. package/src/aws/sns.ts +758 -0
  84. package/src/aws/sqs.ts +382 -0
  85. package/src/aws/ssm.ts +807 -0
  86. package/src/aws/sts.ts +92 -0
  87. package/src/aws/support.ts +391 -0
  88. package/src/aws/test-imap.ts +86 -0
  89. package/src/aws/textract.ts +780 -0
  90. package/src/aws/transcribe.ts +108 -0
  91. package/src/aws/translate.ts +641 -0
  92. package/src/aws/voice.ts +1379 -0
  93. package/src/config.ts +35 -0
  94. package/src/deploy/index.ts +7 -0
  95. package/src/deploy/static-site-external-dns.ts +906 -0
  96. package/src/deploy/static-site.ts +1125 -0
  97. package/src/dns/godaddy.ts +412 -0
  98. package/src/dns/index.ts +183 -0
  99. package/src/dns/porkbun.ts +362 -0
  100. package/src/dns/route53-adapter.ts +414 -0
  101. package/src/dns/types.ts +114 -0
  102. package/src/dns/validator.ts +369 -0
  103. package/src/generators/index.ts +5 -0
  104. package/src/generators/infrastructure.ts +1660 -0
  105. package/src/index.ts +163 -0
  106. package/src/push/apns.ts +452 -0
  107. package/src/push/fcm.ts +506 -0
  108. package/src/push/index.ts +58 -0
  109. package/src/ssl/acme-client.ts +478 -0
  110. package/src/ssl/index.ts +7 -0
  111. package/src/ssl/letsencrypt.ts +747 -0
  112. package/src/types.ts +2 -0
  113. package/src/utils/cli.ts +398 -0
  114. package/src/validation/index.ts +5 -0
  115. package/src/validation/template.ts +405 -0
  116. package/test/index.test.ts +128 -0
  117. package/tsconfig.json +18 -0
@@ -0,0 +1,379 @@
1
+ import type { CLI } from '@stacksjs/clapp'
2
+ import * as cli from '../../src/utils/cli'
3
+ import { loadValidatedConfig } from './shared'
4
+
5
+ export function registerApiCommands(app: CLI): void {
6
+ app
7
+ .command('api:list', 'List all API Gateway APIs')
8
+ .option('--region <region>', 'AWS region')
9
+ .action(async (options: { region?: string }) => {
10
+ cli.header('API Gateway APIs')
11
+
12
+ try {
13
+ const config = await loadValidatedConfig()
14
+ const region = options.region || config.project.region || 'us-east-1'
15
+
16
+ // Use Lambda client which has API Gateway methods
17
+ const { LambdaClient } = await import('../../src/aws/lambda')
18
+ const lambda = new LambdaClient(region)
19
+
20
+ const spinner = new cli.Spinner('Fetching APIs...')
21
+ spinner.start()
22
+
23
+ // Show a simplified view using CloudFormation
24
+ const { CloudFormationClient } = await import('../../src/aws/cloudformation')
25
+ const cfn = new CloudFormationClient(region)
26
+
27
+ const stacks = await cfn.listStacks(['CREATE_COMPLETE', 'UPDATE_COMPLETE'])
28
+ const apiStacks = stacks.StackSummaries.filter(s =>
29
+ s.StackName?.includes('api') || s.StackName?.includes('Api') || s.StackName?.includes('API'),
30
+ )
31
+
32
+ spinner.succeed('APIs listed')
33
+
34
+ cli.info('\nAPI-related CloudFormation stacks:')
35
+ if (apiStacks.length === 0) {
36
+ cli.info('No API Gateway stacks found')
37
+ cli.info('\nTo create an API, you can:')
38
+ cli.info(' 1. Use cloud.config.ts to define your API')
39
+ cli.info(' 2. Deploy with `cloud deploy`')
40
+ }
41
+ else {
42
+ cli.table(
43
+ ['Stack Name', 'Status', 'Created'],
44
+ apiStacks.map(stack => [
45
+ stack.StackName || 'N/A',
46
+ stack.StackStatus || 'N/A',
47
+ stack.CreationTime ? new Date(stack.CreationTime).toLocaleDateString() : 'N/A',
48
+ ]),
49
+ )
50
+ }
51
+
52
+ cli.info('\nTip: Use AWS Console or `aws apigateway get-rest-apis` for detailed API listing')
53
+ }
54
+ catch (error: any) {
55
+ cli.error(`Failed to list APIs: ${error.message}`)
56
+ process.exit(1)
57
+ }
58
+ })
59
+
60
+ app
61
+ .command('api:describe <apiId>', 'Show API Gateway API details')
62
+ .option('--region <region>', 'AWS region')
63
+ .action(async (apiId: string, options: { region?: string }) => {
64
+ cli.header(`API: ${apiId}`)
65
+
66
+ try {
67
+ const config = await loadValidatedConfig()
68
+ const region = options.region || config.project.region || 'us-east-1'
69
+
70
+ cli.info(`API ID: ${apiId}`)
71
+ cli.info(`Region: ${region}`)
72
+ cli.info('')
73
+ cli.info('For detailed API information, use AWS CLI:')
74
+ cli.info(` aws apigateway get-rest-api --rest-api-id ${apiId} --region ${region}`)
75
+ cli.info(` aws apigateway get-resources --rest-api-id ${apiId} --region ${region}`)
76
+ cli.info(` aws apigateway get-stages --rest-api-id ${apiId} --region ${region}`)
77
+ }
78
+ catch (error: any) {
79
+ cli.error(`Failed to describe API: ${error.message}`)
80
+ process.exit(1)
81
+ }
82
+ })
83
+
84
+ app
85
+ .command('api:stages <apiId>', 'List API stages')
86
+ .option('--region <region>', 'AWS region')
87
+ .action(async (apiId: string, options: { region?: string }) => {
88
+ cli.header(`API Stages: ${apiId}`)
89
+
90
+ try {
91
+ const config = await loadValidatedConfig()
92
+ const region = options.region || config.project.region || 'us-east-1'
93
+
94
+ cli.info(`API ID: ${apiId}`)
95
+ cli.info(`Region: ${region}`)
96
+ cli.info('')
97
+ cli.info('Common stages:')
98
+ cli.info(' - prod (production)')
99
+ cli.info(' - staging')
100
+ cli.info(' - dev (development)')
101
+ cli.info('')
102
+ cli.info('For detailed stage information, use AWS CLI:')
103
+ cli.info(` aws apigateway get-stages --rest-api-id ${apiId} --region ${region}`)
104
+ }
105
+ catch (error: any) {
106
+ cli.error(`Failed to list stages: ${error.message}`)
107
+ process.exit(1)
108
+ }
109
+ })
110
+
111
+ app
112
+ .command('api:deploy <apiId>', 'Deploy API to a stage')
113
+ .option('--region <region>', 'AWS region', { default: 'us-east-1' })
114
+ .option('--stage <name>', 'Stage name', { default: 'prod' })
115
+ .option('--description <text>', 'Deployment description')
116
+ .action(async (apiId: string, options: { region: string; stage: string; description?: string }) => {
117
+ cli.header('Deploy API')
118
+
119
+ try {
120
+ cli.info(`API ID: ${apiId}`)
121
+ cli.info(`Stage: ${options.stage}`)
122
+
123
+ const confirmed = await cli.confirm('\nDeploy to this stage?', true)
124
+ if (!confirmed) {
125
+ cli.info('Operation cancelled')
126
+ return
127
+ }
128
+
129
+ cli.info('')
130
+ cli.info('To deploy an API Gateway API:')
131
+ cli.info(` aws apigateway create-deployment \\`)
132
+ cli.info(` --rest-api-id ${apiId} \\`)
133
+ cli.info(` --stage-name ${options.stage} \\`)
134
+ cli.info(` --region ${options.region}`)
135
+
136
+ if (options.description) {
137
+ cli.info(` --description "${options.description}"`)
138
+ }
139
+ }
140
+ catch (error: any) {
141
+ cli.error(`Failed to deploy API: ${error.message}`)
142
+ process.exit(1)
143
+ }
144
+ })
145
+
146
+ app
147
+ .command('api:domains', 'List custom domain names')
148
+ .option('--region <region>', 'AWS region')
149
+ .action(async (options: { region?: string }) => {
150
+ cli.header('API Gateway Custom Domains')
151
+
152
+ try {
153
+ const config = await loadValidatedConfig()
154
+ const region = options.region || config.project.region || 'us-east-1'
155
+
156
+ cli.info(`Region: ${region}`)
157
+ cli.info('')
158
+ cli.info('To list custom domains, use AWS CLI:')
159
+ cli.info(` aws apigateway get-domain-names --region ${region}`)
160
+ cli.info('')
161
+ cli.info('To create a custom domain:')
162
+ cli.info(' 1. Request or import an SSL certificate in ACM')
163
+ cli.info(' 2. Create a custom domain in API Gateway')
164
+ cli.info(' 3. Create a base path mapping to your API')
165
+ cli.info(' 4. Add a DNS record pointing to the distribution')
166
+ }
167
+ catch (error: any) {
168
+ cli.error(`Failed to list domains: ${error.message}`)
169
+ process.exit(1)
170
+ }
171
+ })
172
+
173
+ app
174
+ .command('api:usage <apiId>', 'Show API usage statistics')
175
+ .option('--region <region>', 'AWS region')
176
+ .option('--stage <name>', 'Stage name', { default: 'prod' })
177
+ .option('--days <number>', 'Number of days to show', { default: '7' })
178
+ .action(async (apiId: string, options: { region?: string; stage: string; days: string }) => {
179
+ cli.header('API Usage Statistics')
180
+
181
+ try {
182
+ const config = await loadValidatedConfig()
183
+ const region = options.region || config.project.region || 'us-east-1'
184
+
185
+ cli.info(`API ID: ${apiId}`)
186
+ cli.info(`Stage: ${options.stage}`)
187
+ cli.info(`Period: Last ${options.days} days`)
188
+ cli.info('')
189
+
190
+ // Calculate date range
191
+ const endDate = new Date()
192
+ const startDate = new Date()
193
+ startDate.setDate(startDate.getDate() - Number.parseInt(options.days))
194
+
195
+ cli.info('To view API metrics in CloudWatch:')
196
+ cli.info(` aws cloudwatch get-metric-statistics \\`)
197
+ cli.info(` --namespace AWS/ApiGateway \\`)
198
+ cli.info(` --metric-name Count \\`)
199
+ cli.info(` --dimensions Name=ApiName,Value=${apiId} Name=Stage,Value=${options.stage} \\`)
200
+ cli.info(` --start-time ${startDate.toISOString()} \\`)
201
+ cli.info(` --end-time ${endDate.toISOString()} \\`)
202
+ cli.info(` --period 86400 \\`)
203
+ cli.info(` --statistics Sum \\`)
204
+ cli.info(` --region ${region}`)
205
+
206
+ cli.info('')
207
+ cli.info('Available metrics:')
208
+ cli.info(' - Count: Total API calls')
209
+ cli.info(' - Latency: Response latency')
210
+ cli.info(' - 4XXError: Client errors')
211
+ cli.info(' - 5XXError: Server errors')
212
+ cli.info(' - IntegrationLatency: Backend latency')
213
+ }
214
+ catch (error: any) {
215
+ cli.error(`Failed to get usage: ${error.message}`)
216
+ process.exit(1)
217
+ }
218
+ })
219
+
220
+ app
221
+ .command('api:export <apiId>', 'Export API specification')
222
+ .option('--region <region>', 'AWS region', { default: 'us-east-1' })
223
+ .option('--stage <name>', 'Stage name', { default: 'prod' })
224
+ .option('--format <format>', 'Export format (oas30, swagger)', { default: 'oas30' })
225
+ .option('--output <file>', 'Output file path')
226
+ .action(async (apiId: string, options: { region: string; stage: string; format: string; output?: string }) => {
227
+ cli.header('Export API Specification')
228
+
229
+ try {
230
+ cli.info(`API ID: ${apiId}`)
231
+ cli.info(`Stage: ${options.stage}`)
232
+ cli.info(`Format: ${options.format === 'oas30' ? 'OpenAPI 3.0' : 'Swagger 2.0'}`)
233
+ cli.info('')
234
+
235
+ const exportType = options.format === 'oas30' ? 'oas30' : 'swagger'
236
+
237
+ cli.info('To export the API specification:')
238
+ cli.info(` aws apigateway get-export \\`)
239
+ cli.info(` --rest-api-id ${apiId} \\`)
240
+ cli.info(` --stage-name ${options.stage} \\`)
241
+ cli.info(` --export-type ${exportType} \\`)
242
+ cli.info(` --accepts application/json \\`)
243
+ cli.info(` --region ${options.region} \\`)
244
+ cli.info(` ${options.output || 'api-spec.json'}`)
245
+ }
246
+ catch (error: any) {
247
+ cli.error(`Failed to export API: ${error.message}`)
248
+ process.exit(1)
249
+ }
250
+ })
251
+
252
+ app
253
+ .command('api:logs <apiId>', 'View API Gateway logs')
254
+ .option('--region <region>', 'AWS region')
255
+ .option('--stage <name>', 'Stage name', { default: 'prod' })
256
+ .option('--tail', 'Tail the logs')
257
+ .action(async (apiId: string, options: { region?: string; stage: string; tail?: boolean }) => {
258
+ cli.header('API Gateway Logs')
259
+
260
+ try {
261
+ const config = await loadValidatedConfig()
262
+ const region = options.region || config.project.region || 'us-east-1'
263
+
264
+ const logGroupName = `API-Gateway-Execution-Logs_${apiId}/${options.stage}`
265
+
266
+ cli.info(`Log Group: ${logGroupName}`)
267
+ cli.info('')
268
+ cli.info('To view logs:')
269
+ cli.info(` aws logs filter-log-events \\`)
270
+ cli.info(` --log-group-name "${logGroupName}" \\`)
271
+ cli.info(` --region ${region}`)
272
+
273
+ if (options.tail) {
274
+ cli.info('')
275
+ cli.info('For real-time log tailing, use:')
276
+ cli.info(` aws logs tail "${logGroupName}" --follow --region ${region}`)
277
+ }
278
+
279
+ cli.info('')
280
+ cli.info('Note: Ensure logging is enabled for the API stage.')
281
+ cli.info('You can enable it in the stage settings.')
282
+ }
283
+ catch (error: any) {
284
+ cli.error(`Failed to get logs: ${error.message}`)
285
+ process.exit(1)
286
+ }
287
+ })
288
+
289
+ app
290
+ .command('api:test <apiId> <path>', 'Test an API endpoint')
291
+ .option('--region <region>', 'AWS region')
292
+ .option('--stage <name>', 'Stage name', { default: 'prod' })
293
+ .option('--method <method>', 'HTTP method', { default: 'GET' })
294
+ .option('--body <json>', 'Request body (JSON)')
295
+ .option('--header <header>', 'Request header (can be specified multiple times)')
296
+ .action(async (apiId: string, path: string, options: {
297
+ region?: string
298
+ stage: string
299
+ method: string
300
+ body?: string
301
+ header?: string | string[]
302
+ }) => {
303
+ cli.header('Test API Endpoint')
304
+
305
+ try {
306
+ const config = await loadValidatedConfig()
307
+ const region = options.region || config.project.region || 'us-east-1'
308
+
309
+ // Build the API URL
310
+ const apiUrl = `https://${apiId}.execute-api.${region}.amazonaws.com/${options.stage}${path.startsWith('/') ? path : `/${path}`}`
311
+
312
+ cli.info(`URL: ${apiUrl}`)
313
+ cli.info(`Method: ${options.method}`)
314
+
315
+ const headers: Record<string, string> = {
316
+ 'Content-Type': 'application/json',
317
+ }
318
+
319
+ if (options.header) {
320
+ const headerList = Array.isArray(options.header) ? options.header : [options.header]
321
+ for (const h of headerList) {
322
+ const [key, ...valueParts] = h.split(':')
323
+ headers[key.trim()] = valueParts.join(':').trim()
324
+ }
325
+ }
326
+
327
+ cli.info('Headers:')
328
+ for (const [key, value] of Object.entries(headers)) {
329
+ cli.info(` ${key}: ${value}`)
330
+ }
331
+
332
+ if (options.body) {
333
+ cli.info(`Body: ${options.body}`)
334
+ }
335
+
336
+ const confirmed = await cli.confirm('\nSend request?', true)
337
+ if (!confirmed) {
338
+ cli.info('Operation cancelled')
339
+ return
340
+ }
341
+
342
+ const spinner = new cli.Spinner('Sending request...')
343
+ spinner.start()
344
+
345
+ const startTime = Date.now()
346
+
347
+ const response = await fetch(apiUrl, {
348
+ method: options.method,
349
+ headers,
350
+ body: options.body,
351
+ })
352
+
353
+ const elapsed = Date.now() - startTime
354
+ const responseBody = await response.text()
355
+
356
+ spinner.succeed(`Response received (${elapsed}ms)`)
357
+
358
+ cli.info(`\nStatus: ${response.status} ${response.statusText}`)
359
+
360
+ cli.info('\nResponse Headers:')
361
+ response.headers.forEach((value, key) => {
362
+ cli.info(` ${key}: ${value}`)
363
+ })
364
+
365
+ cli.info('\nResponse Body:')
366
+ try {
367
+ const json = JSON.parse(responseBody)
368
+ console.log(JSON.stringify(json, null, 2))
369
+ }
370
+ catch {
371
+ console.log(responseBody)
372
+ }
373
+ }
374
+ catch (error: any) {
375
+ cli.error(`Failed to test API: ${error.message}`)
376
+ process.exit(1)
377
+ }
378
+ })
379
+ }
@@ -0,0 +1,221 @@
1
+ import type { CLI } from '@stacksjs/clapp'
2
+ import { existsSync } from 'node:fs'
3
+ import * as cli from '../../src/utils/cli'
4
+ import { S3Client } from '../../src/aws/s3'
5
+ import { CloudFrontClient } from '../../src/aws/cloudfront'
6
+ import { loadValidatedConfig } from './shared'
7
+
8
+ export function registerAssetsCommands(app: CLI): void {
9
+ app
10
+ .command('assets:build', 'Build assets')
11
+ .option('--minify', 'Minify output')
12
+ .option('--compress', 'Compress output')
13
+ .action(async (options?: { minify?: boolean, compress?: boolean }) => {
14
+ cli.header('Building Assets')
15
+
16
+ const minify = options?.minify || false
17
+ const compress = options?.compress || false
18
+
19
+ cli.info('Build configuration:')
20
+ cli.info(` - Minify: ${minify ? 'Yes' : 'No'}`)
21
+ cli.info(` - Compress: ${compress ? 'Yes' : 'No'}`)
22
+
23
+ const spinner = new cli.Spinner('Building assets...')
24
+ spinner.start()
25
+
26
+ // TODO: Run build process
27
+ await new Promise(resolve => setTimeout(resolve, 4000))
28
+
29
+ spinner.succeed('Assets built successfully')
30
+
31
+ cli.success('\nBuild complete!')
32
+ cli.info('\nOutput:')
33
+ cli.info(' - JS: 2.3 MB > 456 KB (80% reduction)')
34
+ cli.info(' - CSS: 890 KB > 123 KB (86% reduction)')
35
+ cli.info(' - Images: 15.2 MB > 8.9 MB (41% reduction)')
36
+ cli.info('\nBuild directory: ./dist')
37
+ })
38
+
39
+ app
40
+ .command('assets:optimize:images', 'Optimize images')
41
+ .option('--quality <quality>', 'Image quality (1-100)', { default: '85' })
42
+ .action(async (options?: { quality?: string }) => {
43
+ const quality = options?.quality || '85'
44
+
45
+ cli.header('Optimizing Images')
46
+
47
+ cli.info(`Quality: ${quality}%`)
48
+
49
+ const spinner = new cli.Spinner('Optimizing images...')
50
+ spinner.start()
51
+
52
+ // TODO: Optimize images
53
+ await new Promise(resolve => setTimeout(resolve, 3000))
54
+
55
+ spinner.succeed('Images optimized')
56
+
57
+ cli.success('\nOptimization complete!')
58
+ cli.info('\nResults:')
59
+ cli.info(' - Processed: 127 images')
60
+ cli.info(' - Original: 15.2 MB')
61
+ cli.info(' - Optimized: 8.9 MB')
62
+ cli.info(' - Savings: 6.3 MB (41%)')
63
+ })
64
+
65
+ app
66
+ .command('images:optimize', 'Optimize and compress images')
67
+ .option('--dir <directory>', 'Directory to optimize', { default: './public/images' })
68
+ .action(async (options?: { dir?: string }) => {
69
+ const dir = options?.dir || './public/images'
70
+
71
+ cli.header('Optimizing Images')
72
+
73
+ cli.info(`Directory: ${dir}`)
74
+
75
+ const spinner = new cli.Spinner('Optimizing images...')
76
+ spinner.start()
77
+
78
+ // TODO: Optimize images in directory
79
+ await new Promise(resolve => setTimeout(resolve, 3000))
80
+
81
+ spinner.succeed('Images optimized')
82
+
83
+ cli.success('\nOptimization complete!')
84
+ cli.info('\nResults:')
85
+ cli.info(' - PNG: 45 files, 3.2 MB > 1.8 MB (44% savings)')
86
+ cli.info(' - JPG: 82 files, 12.0 MB > 7.1 MB (41% savings)')
87
+ cli.info(' - Total savings: 6.3 MB')
88
+ })
89
+
90
+ app
91
+ .command('assets:deploy', 'Deploy static assets to S3')
92
+ .option('--source <path>', 'Source directory', { default: 'dist' })
93
+ .option('--bucket <name>', 'S3 bucket name')
94
+ .option('--prefix <prefix>', 'S3 prefix/folder')
95
+ .option('--delete', 'Delete files not in source')
96
+ .option('--cache-control <value>', 'Cache-Control header', { default: 'public, max-age=31536000' })
97
+ .action(async (options?: { source?: string, bucket?: string, prefix?: string, delete?: boolean, cacheControl?: string }) => {
98
+ cli.header('Deploying Assets to S3')
99
+
100
+ try {
101
+ const config = await loadValidatedConfig()
102
+ const region = config.project.region || 'us-east-1'
103
+
104
+ const source = options?.source || 'dist'
105
+ const bucket = options?.bucket
106
+ const prefix = options?.prefix
107
+ const shouldDelete = options?.delete || false
108
+ const cacheControl = options?.cacheControl || 'public, max-age=31536000'
109
+
110
+ if (!bucket) {
111
+ cli.error('--bucket is required')
112
+ return
113
+ }
114
+
115
+ // Check if source directory exists
116
+ if (!existsSync(source)) {
117
+ cli.error(`Source directory not found: ${source}`)
118
+ return
119
+ }
120
+
121
+ cli.info(`Source: ${source}`)
122
+ cli.info(`Bucket: s3://${bucket}${prefix ? `/${prefix}` : ''}`)
123
+ cli.info(`Cache-Control: ${cacheControl}`)
124
+ if (shouldDelete) {
125
+ cli.warn('Delete mode enabled - files not in source will be removed')
126
+ }
127
+
128
+ const confirmed = await cli.confirm('\nDeploy assets now?', true)
129
+ if (!confirmed) {
130
+ cli.info('Deployment cancelled')
131
+ return
132
+ }
133
+
134
+ const s3 = new S3Client(region)
135
+
136
+ const spinner = new cli.Spinner('Uploading assets to S3...')
137
+ spinner.start()
138
+
139
+ await s3.sync({
140
+ source,
141
+ bucket,
142
+ prefix,
143
+ delete: shouldDelete,
144
+ cacheControl,
145
+ acl: 'public-read',
146
+ })
147
+
148
+ spinner.succeed('Assets deployed successfully!')
149
+
150
+ // Get bucket size
151
+ const size = await s3.getBucketSize(bucket, prefix)
152
+ const sizeInMB = (size / 1024 / 1024).toFixed(2)
153
+
154
+ cli.success(`\nDeployment complete!`)
155
+ cli.info(`Total size: ${sizeInMB} MB`)
156
+ cli.info(`\nAssets URL: https://${bucket}.s3.${region}.amazonaws.com${prefix ? `/${prefix}` : ''}`)
157
+ }
158
+ catch (error: any) {
159
+ cli.error(`Deployment failed: ${error.message}`)
160
+ }
161
+ })
162
+
163
+ app
164
+ .command('assets:invalidate', 'Invalidate CloudFront cache')
165
+ .option('--distribution <id>', 'CloudFront distribution ID')
166
+ .option('--paths <paths>', 'Paths to invalidate (comma-separated)', { default: '/*' })
167
+ .option('--wait', 'Wait for invalidation to complete')
168
+ .action(async (options?: { distribution?: string, paths?: string, wait?: boolean }) => {
169
+ cli.header('Invalidating CloudFront Cache')
170
+
171
+ try {
172
+ const distributionId = options?.distribution
173
+
174
+ if (!distributionId) {
175
+ cli.error('--distribution is required')
176
+ return
177
+ }
178
+
179
+ const pathsStr = options?.paths || '/*'
180
+ const paths = pathsStr.split(',').map(p => p.trim())
181
+ const shouldWait = options?.wait || false
182
+
183
+ cli.info(`Distribution: ${distributionId}`)
184
+ cli.info(`Paths: ${paths.join(', ')}`)
185
+
186
+ const confirmed = await cli.confirm('\nInvalidate cache now?', true)
187
+ if (!confirmed) {
188
+ cli.info('Invalidation cancelled')
189
+ return
190
+ }
191
+
192
+ const cloudfront = new CloudFrontClient()
193
+
194
+ const spinner = new cli.Spinner('Creating invalidation...')
195
+ spinner.start()
196
+
197
+ const invalidation = await cloudfront.invalidatePaths(distributionId, paths)
198
+
199
+ spinner.succeed('Invalidation created')
200
+
201
+ cli.success(`\nInvalidation ID: ${invalidation.Id}`)
202
+ cli.info(`Status: ${invalidation.Status}`)
203
+ cli.info(`Created: ${new Date(invalidation.CreateTime).toLocaleString()}`)
204
+
205
+ if (shouldWait) {
206
+ const waitSpinner = new cli.Spinner('Waiting for invalidation to complete...')
207
+ waitSpinner.start()
208
+
209
+ await cloudfront.waitForInvalidation(distributionId, invalidation.Id)
210
+
211
+ waitSpinner.succeed('Invalidation completed!')
212
+ }
213
+ else {
214
+ cli.info('\nInvalidation is in progress. Use --wait to wait for completion.')
215
+ }
216
+ }
217
+ catch (error: any) {
218
+ cli.error(`Invalidation failed: ${error.message}`)
219
+ }
220
+ })
221
+ }