@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,674 @@
1
+ import type { CLI } from '@stacksjs/clapp'
2
+ import { existsSync, statSync, writeFileSync } from 'node:fs'
3
+ import * as cli from '../../src/utils/cli'
4
+ import { InfrastructureGenerator } from '../../src/generators/infrastructure'
5
+ import { CloudFormationClient } from '../../src/aws/cloudformation'
6
+ import { S3Client } from '../../src/aws/s3'
7
+ import { CloudFrontClient } from '../../src/aws/cloudfront'
8
+ import { ECRClient } from '../../src/aws/ecr'
9
+ import { ECSClient } from '../../src/aws/ecs'
10
+ import { validateTemplate, validateTemplateSize, validateResourceLimits } from '../../src/validation/template'
11
+ import { loadValidatedConfig } from './shared'
12
+
13
+ export function registerDeployCommands(app: CLI): void {
14
+ app
15
+ .command('deploy', 'Deploy infrastructure')
16
+ .option('--stack <name>', 'Stack name')
17
+ .option('--env <environment>', 'Environment to deploy to')
18
+ .action(async (options?: { stack?: string, env?: string }) => {
19
+ cli.header('Deploying Infrastructure')
20
+
21
+ try {
22
+ // Load configuration
23
+ const config = await loadValidatedConfig()
24
+ const environment = (options?.env || 'production') as 'production' | 'staging' | 'development'
25
+ const stackName = options?.stack || `${config.project.slug}-${environment}`
26
+ const region = config.project.region || 'us-east-1'
27
+
28
+ cli.info(`Stack: ${stackName}`)
29
+ cli.info(`Region: ${region}`)
30
+ cli.info(`Environment: ${environment}`)
31
+
32
+ // Generate CloudFormation template
33
+ cli.step('Generating CloudFormation template...')
34
+ const generator = new InfrastructureGenerator({
35
+ config,
36
+ environment,
37
+ })
38
+
39
+ generator.generate()
40
+ const templateBody = generator.toJSON()
41
+ const template = JSON.parse(templateBody)
42
+
43
+ // Validate template
44
+ cli.step('Validating template...')
45
+ const validation = validateTemplate(template)
46
+ const sizeValidation = validateTemplateSize(templateBody)
47
+ const limitsValidation = validateResourceLimits(template)
48
+
49
+ // Show errors
50
+ const allErrors = [
51
+ ...validation.errors,
52
+ ...sizeValidation.errors,
53
+ ...limitsValidation.errors,
54
+ ]
55
+
56
+ if (allErrors.length > 0) {
57
+ cli.error('Template validation failed:')
58
+ for (const error of allErrors) {
59
+ cli.error(` - ${error.path}: ${error.message}`)
60
+ }
61
+ return
62
+ }
63
+
64
+ // Show warnings
65
+ const allWarnings = [
66
+ ...validation.warnings,
67
+ ...sizeValidation.warnings,
68
+ ...limitsValidation.warnings,
69
+ ]
70
+
71
+ if (allWarnings.length > 0) {
72
+ for (const warning of allWarnings) {
73
+ cli.warn(` - ${warning.path}: ${warning.message}`)
74
+ }
75
+ }
76
+
77
+ cli.success('Template validated successfully')
78
+
79
+ // Show resource summary
80
+ const resourceCount = Object.keys(template.Resources).length
81
+ cli.info(`\nResources to deploy: ${resourceCount}`)
82
+
83
+ // Count resource types
84
+ const typeCounts: Record<string, number> = {}
85
+ for (const resource of Object.values(template.Resources)) {
86
+ const type = (resource as any).Type
87
+ typeCounts[type] = (typeCounts[type] || 0) + 1
88
+ }
89
+
90
+ for (const [type, count] of Object.entries(typeCounts).sort((a, b) => b[1] - a[1]).slice(0, 5)) {
91
+ cli.info(` - ${type}: ${count}`)
92
+ }
93
+
94
+ // Confirm deployment
95
+ const confirmed = await cli.confirm('\nDeploy now?', true)
96
+ if (!confirmed) {
97
+ cli.info('Deployment cancelled')
98
+ return
99
+ }
100
+
101
+ // Initialize CloudFormation client
102
+ const cfn = new CloudFormationClient(region)
103
+
104
+ // Check if stack exists
105
+ cli.step('Checking stack status...')
106
+ let stackExists = false
107
+ try {
108
+ const result = await cfn.describeStacks({ stackName })
109
+ stackExists = result.Stacks && result.Stacks.length > 0
110
+ }
111
+ catch (error) {
112
+ // Stack doesn't exist, that's fine
113
+ stackExists = false
114
+ }
115
+
116
+ if (stackExists) {
117
+ cli.info('Stack exists, updating...')
118
+ const updateSpinner = new cli.Spinner('Updating CloudFormation stack...')
119
+ updateSpinner.start()
120
+
121
+ try {
122
+ await cfn.updateStack({
123
+ stackName,
124
+ templateBody,
125
+ capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
126
+ tags: [
127
+ { Key: 'Project', Value: config.project.name },
128
+ { Key: 'Environment', Value: environment },
129
+ { Key: 'ManagedBy', Value: 'ts-cloud' },
130
+ ],
131
+ })
132
+
133
+ updateSpinner.succeed('Update initiated')
134
+
135
+ // Wait for completion
136
+ cli.step('Waiting for stack update to complete...')
137
+ await cfn.waitForStack(stackName, 'stack-update-complete')
138
+
139
+ cli.success('Stack updated successfully!')
140
+ }
141
+ catch (error: any) {
142
+ if (error.message.includes('No updates are to be performed')) {
143
+ updateSpinner.succeed('No changes detected')
144
+ cli.info('Stack is already up to date')
145
+ return
146
+ }
147
+ throw error
148
+ }
149
+ }
150
+ else {
151
+ cli.info('Creating new stack...')
152
+ const createSpinner = new cli.Spinner('Creating CloudFormation stack...')
153
+ createSpinner.start()
154
+
155
+ await cfn.createStack({
156
+ stackName,
157
+ templateBody,
158
+ capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
159
+ tags: [
160
+ { Key: 'Project', Value: config.project.name },
161
+ { Key: 'Environment', Value: environment },
162
+ { Key: 'ManagedBy', Value: 'ts-cloud' },
163
+ ],
164
+ })
165
+
166
+ createSpinner.succeed('Stack creation initiated')
167
+
168
+ // Wait for completion
169
+ cli.step('Waiting for stack creation to complete...')
170
+ await cfn.waitForStack(stackName, 'stack-create-complete')
171
+
172
+ cli.success('Stack created successfully!')
173
+ }
174
+
175
+ // Get stack outputs
176
+ const outputs = await cfn.getStackOutputs(stackName)
177
+
178
+ cli.box(`Deployment Complete!
179
+
180
+ Stack: ${stackName}
181
+ Region: ${region}
182
+ Environment: ${environment}
183
+ Resources: ${resourceCount}
184
+
185
+ View in console:
186
+ https://console.aws.amazon.com/cloudformation/home?region=${region}#/stacks/stackinfo?stackId=${encodeURIComponent(stackName)}`, 'green')
187
+
188
+ if (Object.keys(outputs).length > 0) {
189
+ cli.info('\nStack Outputs:')
190
+ for (const [key, value] of Object.entries(outputs)) {
191
+ cli.info(` - ${key}: ${value}`)
192
+ }
193
+ }
194
+ }
195
+ catch (error: any) {
196
+ cli.error(`Deployment failed: ${error.message}`)
197
+ if (error.stack) {
198
+ cli.info('\nStack trace:')
199
+ console.error(error.stack)
200
+ }
201
+ }
202
+ })
203
+
204
+ app
205
+ .command('deploy:server', 'Deploy EC2 infrastructure')
206
+ .option('--env <environment>', 'Environment (production, staging, development)')
207
+ .action(async (options?: { env?: string }) => {
208
+ cli.header('Deploying Server Infrastructure')
209
+
210
+ try {
211
+ const config = await loadValidatedConfig()
212
+ const environment = (options?.env || 'production') as 'production' | 'staging' | 'development'
213
+ const stackName = `${config.project.slug}-server-${environment}`
214
+ const region = config.project.region || 'us-east-1'
215
+
216
+ cli.info(`Stack: ${stackName}`)
217
+ cli.info(`Region: ${region}`)
218
+ cli.info(`Environment: ${environment}`)
219
+
220
+ cli.step('Generating EC2 server infrastructure...')
221
+
222
+ // TODO: Generate server-specific infrastructure
223
+ const spinner = new cli.Spinner('Deploying server infrastructure...')
224
+ spinner.start()
225
+
226
+ await new Promise(resolve => setTimeout(resolve, 2000))
227
+
228
+ spinner.succeed('Server infrastructure deployed successfully!')
229
+
230
+ cli.success('\nDeployment complete!')
231
+ cli.info('\nNext steps:')
232
+ cli.info(' - cloud server:list - View deployed servers')
233
+ cli.info(' - cloud server:ssh <name> - SSH into a server')
234
+ }
235
+ catch (error: any) {
236
+ cli.error(`Deployment failed: ${error.message}`)
237
+ }
238
+ })
239
+
240
+ app
241
+ .command('deploy:serverless', 'Deploy serverless infrastructure')
242
+ .option('--env <environment>', 'Environment (production, staging, development)')
243
+ .option('--function <name>', 'Deploy specific function only')
244
+ .action(async (options?: { env?: string, function?: string }) => {
245
+ cli.header('Deploying Serverless Infrastructure')
246
+
247
+ try {
248
+ const config = await loadValidatedConfig()
249
+ const environment = (options?.env || 'production') as 'production' | 'staging' | 'development'
250
+ const stackName = `${config.project.slug}-serverless-${environment}`
251
+ const region = config.project.region || 'us-east-1'
252
+
253
+ cli.info(`Stack: ${stackName}`)
254
+ cli.info(`Region: ${region}`)
255
+ cli.info(`Environment: ${environment}`)
256
+
257
+ if (options?.function) {
258
+ cli.info(`Function: ${options.function}`)
259
+ }
260
+
261
+ cli.step('Generating serverless infrastructure...')
262
+
263
+ const spinner = new cli.Spinner('Deploying serverless infrastructure...')
264
+ spinner.start()
265
+
266
+ await new Promise(resolve => setTimeout(resolve, 2000))
267
+
268
+ spinner.succeed('Serverless infrastructure deployed successfully!')
269
+
270
+ cli.success('\nDeployment complete!')
271
+ cli.info('\nNext steps:')
272
+ cli.info(' - cloud function:list - View deployed functions')
273
+ cli.info(' - cloud function:logs <name> - View function logs')
274
+ cli.info(' - cloud function:invoke <name> - Test function')
275
+ }
276
+ catch (error: any) {
277
+ cli.error(`Deployment failed: ${error.message}`)
278
+ }
279
+ })
280
+
281
+ app
282
+ .command('deploy:status', 'Check deployment status')
283
+ .option('--stack <name>', 'Stack name')
284
+ .option('--env <environment>', 'Environment')
285
+ .action(async (options?: { stack?: string, env?: string }) => {
286
+ cli.header('Deployment Status')
287
+
288
+ try {
289
+ const config = await loadValidatedConfig()
290
+ const environment = options?.env || 'production'
291
+ const stackName = options?.stack || `${config.project.slug}-${environment}`
292
+ const region = config.project.region || 'us-east-1'
293
+
294
+ cli.info(`Stack: ${stackName}`)
295
+ cli.info(`Region: ${region}`)
296
+
297
+ const spinner = new cli.Spinner('Checking deployment status...')
298
+ spinner.start()
299
+
300
+ const cfn = new CloudFormationClient(region)
301
+
302
+ // Get stack status
303
+ const result = await cfn.describeStacks({ stackName })
304
+
305
+ if (result.Stacks.length === 0) {
306
+ spinner.fail('Stack not found')
307
+ cli.warning('No deployment found for this environment')
308
+ return
309
+ }
310
+
311
+ const stack = result.Stacks[0]
312
+ spinner.succeed('Status retrieved')
313
+
314
+ cli.info(`\nStatus: ${stack.StackStatus}`)
315
+ cli.info(`Created: ${stack.CreationTime}`)
316
+ if (stack.LastUpdatedTime) {
317
+ cli.info(`Last Updated: ${stack.LastUpdatedTime}`)
318
+ }
319
+
320
+ // Show outputs
321
+ if (stack.Outputs && stack.Outputs.length > 0) {
322
+ cli.info('\nOutputs:')
323
+ for (const output of stack.Outputs) {
324
+ cli.info(` ${output.OutputKey}: ${output.OutputValue}`)
325
+ }
326
+ }
327
+ }
328
+ catch (error: any) {
329
+ cli.error(`Failed to get status: ${error.message}`)
330
+ }
331
+ })
332
+
333
+ app
334
+ .command('deploy:rollback', 'Rollback to previous version')
335
+ .option('--stack <name>', 'Stack name')
336
+ .option('--env <environment>', 'Environment')
337
+ .action(async (options?: { stack?: string, env?: string }) => {
338
+ cli.header('Rolling Back Deployment')
339
+
340
+ try {
341
+ const config = await loadValidatedConfig()
342
+ const environment = options?.env || 'production'
343
+ const stackName = options?.stack || `${config.project.slug}-${environment}`
344
+ const region = config.project.region || 'us-east-1'
345
+
346
+ cli.info(`Stack: ${stackName}`)
347
+ cli.info(`Region: ${region}`)
348
+
349
+ const confirmed = await cli.confirm('\nAre you sure you want to rollback?', false)
350
+ if (!confirmed) {
351
+ cli.info('Rollback cancelled')
352
+ return
353
+ }
354
+
355
+ const spinner = new cli.Spinner('Rolling back stack...')
356
+ spinner.start()
357
+
358
+ const cfn = new CloudFormationClient(region)
359
+
360
+ // Delete the stack
361
+ await cfn.deleteStack(stackName)
362
+
363
+ spinner.succeed('Stack deletion initiated')
364
+
365
+ // Wait for deletion
366
+ cli.step('Waiting for stack deletion...')
367
+ await cfn.waitForStack(stackName, 'stack-delete-complete')
368
+
369
+ cli.success('Stack rolled back successfully!')
370
+ }
371
+ catch (error: any) {
372
+ cli.error(`Rollback failed: ${error.message}`)
373
+ }
374
+ })
375
+
376
+ app
377
+ .command('deploy:static', 'Deploy static site (S3 + CloudFront invalidation)')
378
+ .option('--source <path>', 'Source directory', { default: 'dist' })
379
+ .option('--bucket <name>', 'S3 bucket name')
380
+ .option('--distribution <id>', 'CloudFront distribution ID')
381
+ .option('--prefix <prefix>', 'S3 prefix/folder')
382
+ .option('--delete', 'Delete files not in source')
383
+ .option('--cache-control <value>', 'Cache-Control header', { default: 'public, max-age=31536000' })
384
+ .option('--no-invalidate', 'Skip CloudFront invalidation')
385
+ .option('--wait', 'Wait for invalidation to complete')
386
+ .action(async (options?: {
387
+ source?: string
388
+ bucket?: string
389
+ distribution?: string
390
+ prefix?: string
391
+ delete?: boolean
392
+ cacheControl?: string
393
+ invalidate?: boolean
394
+ wait?: boolean
395
+ }) => {
396
+ cli.header('Deploying Static Site')
397
+
398
+ try {
399
+ const config = await loadValidatedConfig()
400
+ const region = config.project.region || 'us-east-1'
401
+
402
+ const source = options?.source || 'dist'
403
+ const bucket = options?.bucket
404
+ const distributionId = options?.distribution
405
+ const prefix = options?.prefix
406
+ const shouldDelete = options?.delete || false
407
+ const cacheControl = options?.cacheControl || 'public, max-age=31536000'
408
+ const shouldInvalidate = options?.invalidate !== false
409
+ const shouldWait = options?.wait || false
410
+
411
+ if (!bucket) {
412
+ cli.error('--bucket is required')
413
+ return
414
+ }
415
+
416
+ // Check if source directory exists
417
+ if (!existsSync(source)) {
418
+ cli.error(`Source directory not found: ${source}`)
419
+ return
420
+ }
421
+
422
+ cli.info(`Source: ${source}`)
423
+ cli.info(`Bucket: s3://${bucket}${prefix ? `/${prefix}` : ''}`)
424
+ cli.info(`Cache-Control: ${cacheControl}`)
425
+ if (distributionId) {
426
+ cli.info(`CloudFront Distribution: ${distributionId}`)
427
+ }
428
+ if (shouldDelete) {
429
+ cli.warn('Delete mode enabled - files not in source will be removed')
430
+ }
431
+
432
+ const confirmed = await cli.confirm('\nDeploy static site now?', true)
433
+ if (!confirmed) {
434
+ cli.info('Deployment cancelled')
435
+ return
436
+ }
437
+
438
+ // Step 1: Upload to S3
439
+ const s3 = new S3Client(region)
440
+ const uploadSpinner = new cli.Spinner('Uploading files to S3...')
441
+ uploadSpinner.start()
442
+
443
+ await s3.sync({
444
+ source,
445
+ bucket,
446
+ prefix,
447
+ delete: shouldDelete,
448
+ cacheControl,
449
+ acl: 'public-read',
450
+ })
451
+
452
+ uploadSpinner.succeed('Files uploaded successfully!')
453
+
454
+ // Get bucket size
455
+ const size = await s3.getBucketSize(bucket, prefix)
456
+ const sizeInMB = (size / 1024 / 1024).toFixed(2)
457
+ cli.info(`Total size: ${sizeInMB} MB`)
458
+
459
+ // Step 2: Invalidate CloudFront (if distribution provided)
460
+ if (shouldInvalidate && distributionId) {
461
+ const cloudfront = new CloudFrontClient()
462
+ const invalidateSpinner = new cli.Spinner('Invalidating CloudFront cache...')
463
+ invalidateSpinner.start()
464
+
465
+ const invalidation = await cloudfront.invalidateAll(distributionId)
466
+ invalidateSpinner.succeed('Invalidation created')
467
+
468
+ cli.info(`Invalidation ID: ${invalidation.Id}`)
469
+
470
+ if (shouldWait) {
471
+ const waitSpinner = new cli.Spinner('Waiting for invalidation to complete...')
472
+ waitSpinner.start()
473
+ await cloudfront.waitForInvalidation(distributionId, invalidation.Id)
474
+ waitSpinner.succeed('Invalidation completed!')
475
+ }
476
+ }
477
+
478
+ cli.box(`Static Site Deployed!
479
+
480
+ Source: ${source}
481
+ Bucket: s3://${bucket}${prefix ? `/${prefix}` : ''}
482
+ Size: ${sizeInMB} MB
483
+ ${distributionId ? `Distribution: ${distributionId}` : ''}
484
+
485
+ View your site:
486
+ https://${bucket}.s3.${region}.amazonaws.com${prefix ? `/${prefix}` : ''}/index.html`, 'green')
487
+ }
488
+ catch (error: any) {
489
+ cli.error(`Deployment failed: ${error.message}`)
490
+ }
491
+ })
492
+
493
+ app
494
+ .command('deploy:container', 'Deploy container (ECR push + ECS service update)')
495
+ .option('--cluster <name>', 'ECS cluster name')
496
+ .option('--service <name>', 'ECS service name')
497
+ .option('--repository <name>', 'ECR repository name')
498
+ .option('--image <tag>', 'Docker image tag', { default: 'latest' })
499
+ .option('--dockerfile <path>', 'Dockerfile path', { default: 'Dockerfile' })
500
+ .option('--context <path>', 'Docker build context', { default: '.' })
501
+ .option('--task-definition <name>', 'Task definition family name')
502
+ .option('--force', 'Force new deployment even if no changes')
503
+ .option('--wait', 'Wait for deployment to stabilize')
504
+ .action(async (options?: {
505
+ cluster?: string
506
+ service?: string
507
+ repository?: string
508
+ image?: string
509
+ dockerfile?: string
510
+ context?: string
511
+ taskDefinition?: string
512
+ force?: boolean
513
+ wait?: boolean
514
+ }) => {
515
+ cli.header('Deploying Container')
516
+
517
+ try {
518
+ const config = await loadValidatedConfig()
519
+ const region = config.project.region || 'us-east-1'
520
+
521
+ const cluster = options?.cluster
522
+ const service = options?.service
523
+ const repository = options?.repository
524
+ const imageTag = options?.image || 'latest'
525
+ const dockerfile = options?.dockerfile || 'Dockerfile'
526
+ const context = options?.context || '.'
527
+ const forceDeployment = options?.force || false
528
+ const shouldWait = options?.wait || false
529
+
530
+ if (!cluster || !service) {
531
+ cli.error('--cluster and --service are required')
532
+ return
533
+ }
534
+
535
+ if (!repository) {
536
+ cli.error('--repository is required')
537
+ return
538
+ }
539
+
540
+ // Check if Dockerfile exists
541
+ if (!existsSync(dockerfile)) {
542
+ cli.error(`Dockerfile not found: ${dockerfile}`)
543
+ return
544
+ }
545
+
546
+ cli.info(`Cluster: ${cluster}`)
547
+ cli.info(`Service: ${service}`)
548
+ cli.info(`Repository: ${repository}`)
549
+ cli.info(`Image Tag: ${imageTag}`)
550
+ cli.info(`Dockerfile: ${dockerfile}`)
551
+
552
+ const confirmed = await cli.confirm('\nDeploy container now?', true)
553
+ if (!confirmed) {
554
+ cli.info('Deployment cancelled')
555
+ return
556
+ }
557
+
558
+ const ecr = new ECRClient(region)
559
+ const ecs = new ECSClient(region)
560
+
561
+ // Step 1: Get ECR login credentials
562
+ const loginSpinner = new cli.Spinner('Getting ECR credentials...')
563
+ loginSpinner.start()
564
+
565
+ const authResult = await ecr.getAuthorizationToken()
566
+ if (!authResult.authorizationData?.[0]) {
567
+ loginSpinner.fail('Failed to get ECR credentials')
568
+ return
569
+ }
570
+
571
+ const auth = authResult.authorizationData[0]
572
+ const registryEndpoint = auth.proxyEndpoint || ''
573
+ const registryHost = registryEndpoint.replace('https://', '')
574
+
575
+ loginSpinner.succeed('ECR credentials obtained')
576
+
577
+ // Step 2: Docker login to ECR
578
+ const dockerLoginSpinner = new cli.Spinner('Logging into ECR...')
579
+ dockerLoginSpinner.start()
580
+
581
+ const token = auth.authorizationToken || ''
582
+ const decoded = Buffer.from(token, 'base64').toString('utf8')
583
+ const password = decoded.split(':')[1]
584
+
585
+ // Run docker login
586
+ const { spawn } = await import('child_process')
587
+ const dockerLogin = spawn('docker', ['login', '--username', 'AWS', '--password-stdin', registryHost], {
588
+ stdio: ['pipe', 'pipe', 'pipe'],
589
+ })
590
+
591
+ dockerLogin.stdin.write(password)
592
+ dockerLogin.stdin.end()
593
+
594
+ await new Promise<void>((resolve, reject) => {
595
+ dockerLogin.on('close', (code) => {
596
+ if (code === 0) resolve()
597
+ else reject(new Error(`Docker login failed with code ${code}`))
598
+ })
599
+ })
600
+
601
+ dockerLoginSpinner.succeed('Logged into ECR')
602
+
603
+ // Step 3: Build Docker image
604
+ const buildSpinner = new cli.Spinner('Building Docker image...')
605
+ buildSpinner.start()
606
+
607
+ const imageUri = `${registryHost}/${repository}:${imageTag}`
608
+
609
+ const dockerBuild = spawn('docker', ['build', '-t', imageUri, '-f', dockerfile, context], {
610
+ stdio: ['pipe', 'pipe', 'pipe'],
611
+ })
612
+
613
+ await new Promise<void>((resolve, reject) => {
614
+ let stderr = ''
615
+ dockerBuild.stderr.on('data', (data) => { stderr += data.toString() })
616
+ dockerBuild.on('close', (code) => {
617
+ if (code === 0) resolve()
618
+ else reject(new Error(`Docker build failed: ${stderr}`))
619
+ })
620
+ })
621
+
622
+ buildSpinner.succeed('Docker image built')
623
+
624
+ // Step 4: Push to ECR
625
+ const pushSpinner = new cli.Spinner('Pushing image to ECR...')
626
+ pushSpinner.start()
627
+
628
+ const dockerPush = spawn('docker', ['push', imageUri], {
629
+ stdio: ['pipe', 'pipe', 'pipe'],
630
+ })
631
+
632
+ await new Promise<void>((resolve, reject) => {
633
+ let stderr = ''
634
+ dockerPush.stderr.on('data', (data) => { stderr += data.toString() })
635
+ dockerPush.on('close', (code) => {
636
+ if (code === 0) resolve()
637
+ else reject(new Error(`Docker push failed: ${stderr}`))
638
+ })
639
+ })
640
+
641
+ pushSpinner.succeed('Image pushed to ECR')
642
+
643
+ // Step 5: Update ECS service
644
+ const updateSpinner = new cli.Spinner('Updating ECS service...')
645
+ updateSpinner.start()
646
+
647
+ await ecs.updateService({
648
+ cluster,
649
+ service,
650
+ forceNewDeployment: forceDeployment,
651
+ })
652
+
653
+ updateSpinner.succeed('ECS service updated')
654
+
655
+ // Step 6: Wait for deployment (if requested)
656
+ if (shouldWait) {
657
+ const waitSpinner = new cli.Spinner('Waiting for deployment to stabilize...')
658
+ waitSpinner.start()
659
+
660
+ await ecs.waitForServiceStable(cluster, service)
661
+
662
+ waitSpinner.succeed('Deployment stabilized')
663
+ }
664
+
665
+ cli.success('\nContainer deployment complete!')
666
+ cli.info(`\nImage: ${imageUri}`)
667
+ cli.info(`Cluster: ${cluster}`)
668
+ cli.info(`Service: ${service}`)
669
+ }
670
+ catch (error: any) {
671
+ cli.error(`Deployment failed: ${error.message}`)
672
+ }
673
+ })
674
+ }