@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,450 @@
1
+ import type { CLI } from '@stacksjs/clapp'
2
+ import { readdirSync, statSync } from 'node:fs'
3
+ import { join, relative } from 'node:path'
4
+ import * as cli from '../../src/utils/cli'
5
+ import { S3Client } from '../../src/aws/s3'
6
+ import { loadValidatedConfig } from './shared'
7
+
8
+ export function registerStorageCommands(app: CLI): void {
9
+ app
10
+ .command('storage:list', 'List all S3 buckets')
11
+ .option('--region <region>', 'AWS region')
12
+ .action(async (options: { region?: string }) => {
13
+ cli.header('S3 Buckets')
14
+
15
+ try {
16
+ const config = await loadValidatedConfig()
17
+ const region = options.region || config.project.region || 'us-east-1'
18
+ const s3 = new S3Client(region)
19
+
20
+ const spinner = new cli.Spinner('Fetching buckets...')
21
+ spinner.start()
22
+
23
+ const result = await s3.listBuckets()
24
+ const buckets = result.Buckets || []
25
+
26
+ spinner.succeed(`Found ${buckets.length} bucket(s)`)
27
+
28
+ if (buckets.length === 0) {
29
+ cli.info('No S3 buckets found')
30
+ cli.info('Use `cloud storage:create` to create a new bucket')
31
+ return
32
+ }
33
+
34
+ cli.table(
35
+ ['Name', 'Created'],
36
+ buckets.map(bucket => [
37
+ bucket.Name || 'N/A',
38
+ bucket.CreationDate ? new Date(bucket.CreationDate).toLocaleDateString() : 'N/A',
39
+ ]),
40
+ )
41
+ }
42
+ catch (error: any) {
43
+ cli.error(`Failed to list buckets: ${error.message}`)
44
+ process.exit(1)
45
+ }
46
+ })
47
+
48
+ app
49
+ .command('storage:create <name>', 'Create a new S3 bucket')
50
+ .option('--region <region>', 'AWS region', { default: 'us-east-1' })
51
+ .option('--public', 'Enable public access')
52
+ .option('--versioning', 'Enable versioning')
53
+ .action(async (name: string, options: { region: string; public?: boolean; versioning?: boolean }) => {
54
+ cli.header('Create S3 Bucket')
55
+
56
+ try {
57
+ const s3 = new S3Client(options.region)
58
+
59
+ cli.info(`Bucket name: ${name}`)
60
+ cli.info(`Region: ${options.region}`)
61
+ cli.info(`Public access: ${options.public ? 'Yes' : 'No'}`)
62
+ cli.info(`Versioning: ${options.versioning ? 'Yes' : 'No'}`)
63
+
64
+ const confirmed = await cli.confirm('\nCreate this bucket?', true)
65
+ if (!confirmed) {
66
+ cli.info('Operation cancelled')
67
+ return
68
+ }
69
+
70
+ const spinner = new cli.Spinner('Creating bucket...')
71
+ spinner.start()
72
+
73
+ await s3.createBucket(name)
74
+
75
+ // Block public access by default
76
+ if (!options.public) {
77
+ spinner.text = 'Configuring public access block...'
78
+ await s3.putPublicAccessBlock(name, {
79
+ BlockPublicAcls: true,
80
+ IgnorePublicAcls: true,
81
+ BlockPublicPolicy: true,
82
+ RestrictPublicBuckets: true,
83
+ })
84
+ }
85
+
86
+ // Enable versioning if requested
87
+ if (options.versioning) {
88
+ spinner.text = 'Enabling versioning...'
89
+ await s3.putBucketVersioning(name, 'Enabled')
90
+ }
91
+
92
+ spinner.succeed('Bucket created')
93
+
94
+ cli.success(`\nBucket: ${name}`)
95
+ cli.info(`Region: ${options.region}`)
96
+ cli.info(`URL: s3://${name}`)
97
+ }
98
+ catch (error: any) {
99
+ cli.error(`Failed to create bucket: ${error.message}`)
100
+ process.exit(1)
101
+ }
102
+ })
103
+
104
+ app
105
+ .command('storage:delete <name>', 'Delete an S3 bucket')
106
+ .option('--force', 'Delete all objects first')
107
+ .action(async (name: string, options: { force?: boolean }) => {
108
+ cli.header('Delete S3 Bucket')
109
+
110
+ try {
111
+ const s3 = new S3Client('us-east-1')
112
+
113
+ cli.warn(`This will permanently delete bucket: ${name}`)
114
+ if (options.force) {
115
+ cli.warn('All objects in the bucket will be deleted!')
116
+ }
117
+
118
+ const confirmed = await cli.confirm('\nDelete this bucket?', false)
119
+ if (!confirmed) {
120
+ cli.info('Operation cancelled')
121
+ return
122
+ }
123
+
124
+ const spinner = new cli.Spinner('Deleting bucket...')
125
+ spinner.start()
126
+
127
+ if (options.force) {
128
+ spinner.text = 'Emptying bucket...'
129
+ await s3.emptyBucket(name)
130
+ }
131
+
132
+ await s3.deleteBucket(name)
133
+
134
+ spinner.succeed('Bucket deleted')
135
+ }
136
+ catch (error: any) {
137
+ cli.error(`Failed to delete bucket: ${error.message}`)
138
+ process.exit(1)
139
+ }
140
+ })
141
+
142
+ app
143
+ .command('storage:sync <source> <bucket>', 'Sync local directory to S3 bucket')
144
+ .option('--prefix <prefix>', 'S3 key prefix')
145
+ .option('--delete', 'Delete files in S3 that are not in source')
146
+ .option('--dry-run', 'Show what would be synced without making changes')
147
+ .action(async (source: string, bucket: string, options: { prefix?: string; delete?: boolean; dryRun?: boolean }) => {
148
+ cli.header('Sync to S3')
149
+
150
+ try {
151
+ const s3 = new S3Client('us-east-1')
152
+
153
+ cli.info(`Source: ${source}`)
154
+ cli.info(`Destination: s3://${bucket}/${options.prefix || ''}`)
155
+
156
+ if (options.dryRun) {
157
+ cli.info('Dry run mode - no changes will be made')
158
+ }
159
+
160
+ const spinner = new cli.Spinner('Scanning files...')
161
+ spinner.start()
162
+
163
+ // Get all local files
164
+ const localFiles: { path: string; key: string; size: number }[] = []
165
+
166
+ function scanDirectory(dir: string, baseDir: string) {
167
+ const entries = readdirSync(dir, { withFileTypes: true })
168
+ for (const entry of entries) {
169
+ const fullPath = join(dir, entry.name)
170
+ if (entry.isDirectory()) {
171
+ scanDirectory(fullPath, baseDir)
172
+ }
173
+ else {
174
+ const relativePath = relative(baseDir, fullPath)
175
+ const key = options.prefix ? `${options.prefix}/${relativePath}` : relativePath
176
+ const stats = statSync(fullPath)
177
+ localFiles.push({ path: fullPath, key, size: stats.size })
178
+ }
179
+ }
180
+ }
181
+
182
+ scanDirectory(source, source)
183
+
184
+ spinner.succeed(`Found ${localFiles.length} local file(s)`)
185
+
186
+ if (localFiles.length === 0) {
187
+ cli.info('No files to sync')
188
+ return
189
+ }
190
+
191
+ // Show preview
192
+ cli.info('\nFiles to sync:')
193
+ for (const file of localFiles.slice(0, 10)) {
194
+ cli.info(` ${file.key} (${(file.size / 1024).toFixed(2)} KB)`)
195
+ }
196
+ if (localFiles.length > 10) {
197
+ cli.info(` ... and ${localFiles.length - 10} more`)
198
+ }
199
+
200
+ if (options.dryRun) {
201
+ cli.info('\nDry run complete - no changes made')
202
+ return
203
+ }
204
+
205
+ const confirmed = await cli.confirm('\nSync these files?', true)
206
+ if (!confirmed) {
207
+ cli.info('Operation cancelled')
208
+ return
209
+ }
210
+
211
+ const uploadSpinner = new cli.Spinner('Uploading files...')
212
+ uploadSpinner.start()
213
+
214
+ let uploaded = 0
215
+ for (const file of localFiles) {
216
+ uploadSpinner.text = `Uploading ${file.key}... (${uploaded + 1}/${localFiles.length})`
217
+
218
+ const fileContent = Bun.file(file.path)
219
+ const buffer = await fileContent.arrayBuffer()
220
+
221
+ await s3.putObject({
222
+ bucket: bucket,
223
+ key: file.key,
224
+ body: Buffer.from(buffer),
225
+ contentType: getContentType(file.key),
226
+ })
227
+
228
+ uploaded++
229
+ }
230
+
231
+ uploadSpinner.succeed(`Uploaded ${uploaded} file(s)`)
232
+
233
+ // Delete remote files not in source if requested
234
+ if (options.delete) {
235
+ const deleteSpinner = new cli.Spinner('Checking for files to delete...')
236
+ deleteSpinner.start()
237
+
238
+ const remoteResult = await s3.listObjects({
239
+ bucket,
240
+ prefix: options.prefix,
241
+ })
242
+
243
+ const localKeys = new Set(localFiles.map(f => f.key))
244
+ const toDelete = remoteResult.objects
245
+ .filter((obj: any) => obj.Key && !localKeys.has(obj.Key))
246
+ .map((obj: any) => obj.Key!)
247
+
248
+ if (toDelete.length > 0) {
249
+ deleteSpinner.text = `Deleting ${toDelete.length} remote file(s)...`
250
+
251
+ for (const key of toDelete) {
252
+ await s3.deleteObject(bucket, key)
253
+ }
254
+
255
+ deleteSpinner.succeed(`Deleted ${toDelete.length} remote file(s)`)
256
+ }
257
+ else {
258
+ deleteSpinner.succeed('No files to delete')
259
+ }
260
+ }
261
+
262
+ cli.success('\nSync complete!')
263
+ }
264
+ catch (error: any) {
265
+ cli.error(`Failed to sync: ${error.message}`)
266
+ process.exit(1)
267
+ }
268
+ })
269
+
270
+ app
271
+ .command('storage:policy <bucket>', 'Show or set bucket policy')
272
+ .option('--set <file>', 'Set policy from JSON file')
273
+ .option('--public-read', 'Set a public read policy')
274
+ .option('--delete', 'Delete the bucket policy')
275
+ .action(async (bucket: string, options: { set?: string; publicRead?: boolean; delete?: boolean }) => {
276
+ cli.header('S3 Bucket Policy')
277
+
278
+ try {
279
+ const s3 = new S3Client('us-east-1')
280
+
281
+ if (options.delete) {
282
+ cli.warn(`This will delete the policy for bucket: ${bucket}`)
283
+
284
+ const confirmed = await cli.confirm('\nDelete bucket policy?', false)
285
+ if (!confirmed) {
286
+ cli.info('Operation cancelled')
287
+ return
288
+ }
289
+
290
+ const spinner = new cli.Spinner('Deleting policy...')
291
+ spinner.start()
292
+
293
+ await s3.deleteBucketPolicy(bucket)
294
+
295
+ spinner.succeed('Policy deleted')
296
+ return
297
+ }
298
+
299
+ if (options.publicRead) {
300
+ cli.warn(`This will make bucket ${bucket} publicly readable!`)
301
+
302
+ const confirmed = await cli.confirm('\nSet public read policy?', false)
303
+ if (!confirmed) {
304
+ cli.info('Operation cancelled')
305
+ return
306
+ }
307
+
308
+ const spinner = new cli.Spinner('Setting policy...')
309
+ spinner.start()
310
+
311
+ const policy = {
312
+ Version: '2012-10-17',
313
+ Statement: [
314
+ {
315
+ Sid: 'PublicReadGetObject',
316
+ Effect: 'Allow',
317
+ Principal: '*',
318
+ Action: 's3:GetObject',
319
+ Resource: `arn:aws:s3:::${bucket}/*`,
320
+ },
321
+ ],
322
+ }
323
+
324
+ await s3.putBucketPolicy(bucket, policy)
325
+
326
+ spinner.succeed('Public read policy set')
327
+ return
328
+ }
329
+
330
+ if (options.set) {
331
+ const spinner = new cli.Spinner('Setting policy...')
332
+ spinner.start()
333
+
334
+ const policyFile = Bun.file(options.set)
335
+ const policy = await policyFile.text()
336
+
337
+ await s3.putBucketPolicy(bucket, policy)
338
+
339
+ spinner.succeed('Policy set')
340
+ return
341
+ }
342
+
343
+ // Show current policy
344
+ const spinner = new cli.Spinner('Fetching policy...')
345
+ spinner.start()
346
+
347
+ try {
348
+ const result = await s3.getBucketPolicy(bucket)
349
+ spinner.succeed('Policy loaded')
350
+
351
+ cli.info('\nBucket Policy:')
352
+ console.log(JSON.stringify(result || {}, null, 2))
353
+ }
354
+ catch (err: any) {
355
+ if (err.message?.includes('NoSuchBucketPolicy')) {
356
+ spinner.succeed('No policy set')
357
+ cli.info('This bucket has no policy configured.')
358
+ }
359
+ else {
360
+ throw err
361
+ }
362
+ }
363
+ }
364
+ catch (error: any) {
365
+ cli.error(`Failed to manage policy: ${error.message}`)
366
+ process.exit(1)
367
+ }
368
+ })
369
+
370
+ app
371
+ .command('storage:ls <bucket>', 'List objects in a bucket')
372
+ .option('--prefix <prefix>', 'Filter by prefix')
373
+ .option('--limit <number>', 'Limit number of results', { default: '100' })
374
+ .action(async (bucket: string, options: { prefix?: string; limit?: string }) => {
375
+ cli.header(`Objects in ${bucket}`)
376
+
377
+ try {
378
+ const s3 = new S3Client('us-east-1')
379
+
380
+ const spinner = new cli.Spinner('Listing objects...')
381
+ spinner.start()
382
+
383
+ const result = await s3.listObjects({
384
+ bucket,
385
+ prefix: options.prefix,
386
+ maxKeys: Number.parseInt(options.limit || '100'),
387
+ })
388
+
389
+ const objects = result.objects
390
+
391
+ spinner.succeed(`Found ${objects.length} object(s)${result.nextContinuationToken ? ' (truncated)' : ''}`)
392
+
393
+ if (objects.length === 0) {
394
+ cli.info('No objects found')
395
+ return
396
+ }
397
+
398
+ cli.table(
399
+ ['Key', 'Size', 'Last Modified'],
400
+ objects.map(obj => [
401
+ obj.Key || 'N/A',
402
+ formatBytes(obj.Size || 0),
403
+ obj.LastModified ? new Date(obj.LastModified).toLocaleString() : 'N/A',
404
+ ]),
405
+ )
406
+
407
+ if (result.nextContinuationToken) {
408
+ cli.info(`\nMore objects available. Use --limit to see more.`)
409
+ }
410
+ }
411
+ catch (error: any) {
412
+ cli.error(`Failed to list objects: ${error.message}`)
413
+ process.exit(1)
414
+ }
415
+ })
416
+ }
417
+
418
+ function getContentType(filename: string): string {
419
+ const ext = filename.split('.').pop()?.toLowerCase()
420
+ const types: Record<string, string> = {
421
+ html: 'text/html',
422
+ css: 'text/css',
423
+ js: 'application/javascript',
424
+ json: 'application/json',
425
+ png: 'image/png',
426
+ jpg: 'image/jpeg',
427
+ jpeg: 'image/jpeg',
428
+ gif: 'image/gif',
429
+ svg: 'image/svg+xml',
430
+ ico: 'image/x-icon',
431
+ woff: 'font/woff',
432
+ woff2: 'font/woff2',
433
+ ttf: 'font/ttf',
434
+ eot: 'application/vnd.ms-fontobject',
435
+ pdf: 'application/pdf',
436
+ zip: 'application/zip',
437
+ xml: 'application/xml',
438
+ txt: 'text/plain',
439
+ md: 'text/markdown',
440
+ }
441
+ return types[ext || ''] || 'application/octet-stream'
442
+ }
443
+
444
+ function formatBytes(bytes: number): string {
445
+ if (bytes === 0) return '0 B'
446
+ const k = 1024
447
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
448
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
449
+ return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
450
+ }
@@ -0,0 +1,96 @@
1
+ import type { CLI } from '@stacksjs/clapp'
2
+ import * as cli from '../../src/utils/cli'
3
+
4
+ export function registerTeamCommands(app: CLI): void {
5
+ app
6
+ .command('team:add <email> <role>', 'Add team member')
7
+ .action(async (email: string, role: string) => {
8
+ cli.header('Adding Team Member')
9
+
10
+ cli.info(`Email: ${email}`)
11
+ cli.info(`Role: ${role}`)
12
+
13
+ const validRoles = ['admin', 'developer', 'viewer']
14
+ if (!validRoles.includes(role.toLowerCase())) {
15
+ cli.error(`Invalid role. Must be one of: ${validRoles.join(', ')}`)
16
+ return
17
+ }
18
+
19
+ const confirm = await cli.confirm('\nAdd this team member?', true)
20
+ if (!confirm) {
21
+ cli.info('Operation cancelled')
22
+ return
23
+ }
24
+
25
+ const spinner = new cli.Spinner('Creating IAM user and sending invitation...')
26
+ spinner.start()
27
+
28
+ // TODO: Create IAM user with appropriate policies based on role
29
+ // TODO: Send invitation email with credentials
30
+ await new Promise(resolve => setTimeout(resolve, 2000))
31
+
32
+ spinner.succeed('Team member added successfully')
33
+
34
+ cli.success('\nTeam member added!')
35
+ cli.info('An invitation email has been sent with access credentials')
36
+
37
+ cli.info('\nAccess Details:')
38
+ cli.info(` - Email: ${email}`)
39
+ cli.info(` - Role: ${role}`)
40
+ cli.info(` - Status: Pending`)
41
+ })
42
+
43
+ app
44
+ .command('team:list', 'List team members')
45
+ .action(async () => {
46
+ cli.header('Team Members')
47
+
48
+ const spinner = new cli.Spinner('Fetching team members...')
49
+ spinner.start()
50
+
51
+ // TODO: Fetch IAM users with appropriate tags
52
+ await new Promise(resolve => setTimeout(resolve, 1500))
53
+
54
+ spinner.stop()
55
+
56
+ cli.table(
57
+ ['Email', 'Role', 'Status', 'Added', 'Last Login'],
58
+ [
59
+ ['admin@example.com', 'Admin', 'Active', '2024-01-01', '2 hours ago'],
60
+ ['dev@example.com', 'Developer', 'Active', '2024-01-15', '1 day ago'],
61
+ ['viewer@example.com', 'Viewer', 'Active', '2024-02-01', '3 days ago'],
62
+ ['new@example.com', 'Developer', 'Pending', '2024-11-10', 'Never'],
63
+ ],
64
+ )
65
+
66
+ cli.info('\nTip: Use `cloud team:add` to add new team members')
67
+ cli.info('Tip: Use `cloud team:remove` to remove team members')
68
+ })
69
+
70
+ app
71
+ .command('team:remove <email>', 'Remove team member')
72
+ .action(async (email: string) => {
73
+ cli.header('Removing Team Member')
74
+
75
+ cli.info(`Email: ${email}`)
76
+
77
+ cli.warn('\nThis will revoke all access for this team member')
78
+
79
+ const confirm = await cli.confirm('Remove this team member?', false)
80
+ if (!confirm) {
81
+ cli.info('Operation cancelled')
82
+ return
83
+ }
84
+
85
+ const spinner = new cli.Spinner('Removing IAM user and access...')
86
+ spinner.start()
87
+
88
+ // TODO: Delete IAM user and associated resources
89
+ await new Promise(resolve => setTimeout(resolve, 2000))
90
+
91
+ spinner.succeed('Team member removed successfully')
92
+
93
+ cli.success('\nTeam member removed!')
94
+ cli.info('All access credentials have been revoked')
95
+ })
96
+ }