@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.
- package/LICENSE.md +21 -0
- package/README.md +321 -0
- package/bin/cli.ts +133 -0
- package/bin/commands/analytics.ts +328 -0
- package/bin/commands/api.ts +379 -0
- package/bin/commands/assets.ts +221 -0
- package/bin/commands/audit.ts +501 -0
- package/bin/commands/backup.ts +682 -0
- package/bin/commands/cache.ts +294 -0
- package/bin/commands/cdn.ts +281 -0
- package/bin/commands/config.ts +202 -0
- package/bin/commands/container.ts +105 -0
- package/bin/commands/cost.ts +208 -0
- package/bin/commands/database.ts +401 -0
- package/bin/commands/deploy.ts +674 -0
- package/bin/commands/domain.ts +397 -0
- package/bin/commands/email.ts +423 -0
- package/bin/commands/environment.ts +285 -0
- package/bin/commands/events.ts +424 -0
- package/bin/commands/firewall.ts +145 -0
- package/bin/commands/function.ts +116 -0
- package/bin/commands/generate.ts +280 -0
- package/bin/commands/git.ts +139 -0
- package/bin/commands/iam.ts +464 -0
- package/bin/commands/index.ts +48 -0
- package/bin/commands/init.ts +120 -0
- package/bin/commands/logs.ts +148 -0
- package/bin/commands/network.ts +579 -0
- package/bin/commands/notify.ts +489 -0
- package/bin/commands/queue.ts +407 -0
- package/bin/commands/scheduler.ts +370 -0
- package/bin/commands/secrets.ts +54 -0
- package/bin/commands/server.ts +629 -0
- package/bin/commands/shared.ts +97 -0
- package/bin/commands/ssl.ts +138 -0
- package/bin/commands/stack.ts +325 -0
- package/bin/commands/status.ts +385 -0
- package/bin/commands/storage.ts +450 -0
- package/bin/commands/team.ts +96 -0
- package/bin/commands/tunnel.ts +489 -0
- package/bin/commands/utils.ts +202 -0
- package/build.ts +15 -0
- package/cloud +2 -0
- package/package.json +99 -0
- package/src/aws/acm.ts +768 -0
- package/src/aws/application-autoscaling.ts +845 -0
- package/src/aws/bedrock.ts +4074 -0
- package/src/aws/client.ts +878 -0
- package/src/aws/cloudformation.ts +896 -0
- package/src/aws/cloudfront.ts +1531 -0
- package/src/aws/cloudwatch-logs.ts +154 -0
- package/src/aws/comprehend.ts +839 -0
- package/src/aws/connect.ts +1056 -0
- package/src/aws/deploy-imap.ts +384 -0
- package/src/aws/dynamodb.ts +340 -0
- package/src/aws/ec2.ts +1385 -0
- package/src/aws/ecr.ts +621 -0
- package/src/aws/ecs.ts +615 -0
- package/src/aws/elasticache.ts +301 -0
- package/src/aws/elbv2.ts +942 -0
- package/src/aws/email.ts +928 -0
- package/src/aws/eventbridge.ts +248 -0
- package/src/aws/iam.ts +1689 -0
- package/src/aws/imap-server.ts +2100 -0
- package/src/aws/index.ts +213 -0
- package/src/aws/kendra.ts +1097 -0
- package/src/aws/lambda.ts +786 -0
- package/src/aws/opensearch.ts +158 -0
- package/src/aws/personalize.ts +977 -0
- package/src/aws/polly.ts +559 -0
- package/src/aws/rds.ts +888 -0
- package/src/aws/rekognition.ts +846 -0
- package/src/aws/route53-domains.ts +359 -0
- package/src/aws/route53.ts +1046 -0
- package/src/aws/s3.ts +2318 -0
- package/src/aws/scheduler.ts +571 -0
- package/src/aws/secrets-manager.ts +769 -0
- package/src/aws/ses.ts +1081 -0
- package/src/aws/setup-phone.ts +104 -0
- package/src/aws/setup-sms.ts +580 -0
- package/src/aws/sms.ts +1735 -0
- package/src/aws/smtp-server.ts +531 -0
- package/src/aws/sns.ts +758 -0
- package/src/aws/sqs.ts +382 -0
- package/src/aws/ssm.ts +807 -0
- package/src/aws/sts.ts +92 -0
- package/src/aws/support.ts +391 -0
- package/src/aws/test-imap.ts +86 -0
- package/src/aws/textract.ts +780 -0
- package/src/aws/transcribe.ts +108 -0
- package/src/aws/translate.ts +641 -0
- package/src/aws/voice.ts +1379 -0
- package/src/config.ts +35 -0
- package/src/deploy/index.ts +7 -0
- package/src/deploy/static-site-external-dns.ts +906 -0
- package/src/deploy/static-site.ts +1125 -0
- package/src/dns/godaddy.ts +412 -0
- package/src/dns/index.ts +183 -0
- package/src/dns/porkbun.ts +362 -0
- package/src/dns/route53-adapter.ts +414 -0
- package/src/dns/types.ts +114 -0
- package/src/dns/validator.ts +369 -0
- package/src/generators/index.ts +5 -0
- package/src/generators/infrastructure.ts +1660 -0
- package/src/index.ts +163 -0
- package/src/push/apns.ts +452 -0
- package/src/push/fcm.ts +506 -0
- package/src/push/index.ts +58 -0
- package/src/ssl/acme-client.ts +478 -0
- package/src/ssl/index.ts +7 -0
- package/src/ssl/letsencrypt.ts +747 -0
- package/src/types.ts +2 -0
- package/src/utils/cli.ts +398 -0
- package/src/validation/index.ts +5 -0
- package/src/validation/template.ts +405 -0
- package/test/index.test.ts +128 -0
- 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
|
+
}
|