@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,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
|
+
}
|