@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,328 @@
1
+ import type { CLI } from '@stacksjs/clapp'
2
+ import * as cli from '../../src/utils/cli'
3
+ import { DynamoDBClient } from '../../src/aws/dynamodb'
4
+
5
+ export function registerAnalyticsCommands(app: CLI): void {
6
+ app
7
+ .command('analytics:sites:list', 'List all analytics sites')
8
+ .option('--table <name>', 'DynamoDB table name', { default: 'ts-analytics' })
9
+ .option('--region <region>', 'AWS region', { default: 'us-east-1' })
10
+ .action(async (options: { table: string; region: string }) => {
11
+ cli.header('Analytics Sites')
12
+
13
+ try {
14
+ const dynamodb = new DynamoDBClient(options.region)
15
+
16
+ const spinner = new cli.Spinner('Fetching sites...')
17
+ spinner.start()
18
+
19
+ const result = await dynamodb.scan({
20
+ TableName: options.table,
21
+ FilterExpression: 'begins_with(pk, :pk)',
22
+ ExpressionAttributeValues: {
23
+ ':pk': { S: 'SITE#' },
24
+ },
25
+ })
26
+
27
+ spinner.succeed(`Found ${result.Items?.length || 0} site(s)`)
28
+
29
+ if (!result.Items || result.Items.length === 0) {
30
+ cli.info('No analytics sites found')
31
+ cli.info('Use `cloud analytics:sites:create` to create a new site')
32
+ return
33
+ }
34
+
35
+ const sites = result.Items.map(item => DynamoDBClient.unmarshal(item))
36
+
37
+ cli.table(
38
+ ['ID', 'Name', 'Domains', 'Active', 'Created'],
39
+ sites.map(site => [
40
+ site.siteId || 'N/A',
41
+ site.name || 'Unnamed',
42
+ (site.domains || []).join(', ') || '-',
43
+ site.isActive ? 'Yes' : 'No',
44
+ site.createdAt ? new Date(site.createdAt).toLocaleDateString() : 'N/A',
45
+ ]),
46
+ )
47
+ }
48
+ catch (error: any) {
49
+ cli.error(`Failed to list sites: ${error.message}`)
50
+ process.exit(1)
51
+ }
52
+ })
53
+
54
+ app
55
+ .command('analytics:sites:create', 'Create a new analytics site')
56
+ .option('--table <name>', 'DynamoDB table name', { default: 'ts-analytics' })
57
+ .option('--region <region>', 'AWS region', { default: 'us-east-1' })
58
+ .option('--name <name>', 'Site name')
59
+ .option('--domain <domain>', 'Site domain(s) (can be specified multiple times)')
60
+ .action(async (options: { table: string; region: string; name?: string; domain?: string | string[] }) => {
61
+ cli.header('Create Analytics Site')
62
+
63
+ try {
64
+ const dynamodb = new DynamoDBClient(options.region)
65
+
66
+ // Get site name
67
+ const name = options.name || await cli.prompt('Site name', 'My Site')
68
+
69
+ // Get domains
70
+ let domains: string[] = []
71
+ if (options.domain) {
72
+ domains = Array.isArray(options.domain) ? options.domain : [options.domain]
73
+ }
74
+ else {
75
+ const domainInput = await cli.prompt('Site domains (comma-separated)', 'example.com')
76
+ domains = domainInput.split(',').map(d => d.trim()).filter(Boolean)
77
+ }
78
+
79
+ cli.info(`\nCreating site: ${name}`)
80
+ cli.info(`Domains: ${domains.join(', ')}`)
81
+
82
+ const confirm = await cli.confirm('\nCreate this site?', true)
83
+ if (!confirm) {
84
+ cli.info('Operation cancelled')
85
+ return
86
+ }
87
+
88
+ const spinner = new cli.Spinner('Creating site...')
89
+ spinner.start()
90
+
91
+ // Generate site ID
92
+ const siteId = `site_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
93
+
94
+ const now = new Date().toISOString()
95
+
96
+ await dynamodb.putItem({
97
+ TableName: options.table,
98
+ Item: {
99
+ pk: { S: `SITE#${siteId}` },
100
+ sk: { S: `SITE#${siteId}` },
101
+ siteId: { S: siteId },
102
+ name: { S: name },
103
+ domains: { L: domains.map(d => ({ S: d })) },
104
+ isActive: { BOOL: true },
105
+ createdAt: { S: now },
106
+ updatedAt: { S: now },
107
+ },
108
+ })
109
+
110
+ spinner.succeed('Site created successfully')
111
+
112
+ cli.success(`\nSite ID: ${siteId}`)
113
+ cli.info('\nAdd the tracking script to your website:')
114
+ cli.info(` <script src="https://analytics.stacksjs.com/track.js" data-site-id="${siteId}"></script>`)
115
+ }
116
+ catch (error: any) {
117
+ cli.error(`Failed to create site: ${error.message}`)
118
+ process.exit(1)
119
+ }
120
+ })
121
+
122
+ app
123
+ .command('analytics:sites:update <siteId>', 'Update an analytics site')
124
+ .option('--table <name>', 'DynamoDB table name', { default: 'ts-analytics' })
125
+ .option('--region <region>', 'AWS region', { default: 'us-east-1' })
126
+ .option('--name <name>', 'New site name')
127
+ .option('--domain <domain>', 'Set domains (replaces existing)')
128
+ .option('--active <boolean>', 'Set site active/inactive status')
129
+ .action(async (siteId: string, options: { table: string; region: string; name?: string; domain?: string | string[]; active?: string }) => {
130
+ cli.header('Update Analytics Site')
131
+
132
+ try {
133
+ const dynamodb = new DynamoDBClient(options.region)
134
+
135
+ // First, verify the site exists
136
+ const result = await dynamodb.getItem({
137
+ TableName: options.table,
138
+ Key: {
139
+ pk: { S: `SITE#${siteId}` },
140
+ sk: { S: `SITE#${siteId}` },
141
+ },
142
+ })
143
+
144
+ if (!result.Item) {
145
+ cli.error(`Site not found: ${siteId}`)
146
+ process.exit(1)
147
+ }
148
+
149
+ const site = DynamoDBClient.unmarshal(result.Item)
150
+ cli.info(`Updating site: ${site.name || 'Unnamed'} (${siteId})`)
151
+ cli.info('')
152
+
153
+ // Build update expression
154
+ const updates: string[] = []
155
+ const expressionNames: Record<string, string> = {}
156
+ const expressionValues: Record<string, any> = {}
157
+
158
+ if (options.name) {
159
+ updates.push('#n = :name')
160
+ expressionNames['#n'] = 'name'
161
+ expressionValues[':name'] = { S: options.name }
162
+ cli.info(` Name: ${site.name} -> ${options.name}`)
163
+ }
164
+
165
+ if (options.domain !== undefined) {
166
+ const domains = Array.isArray(options.domain) ? options.domain : [options.domain]
167
+ updates.push('domains = :domains')
168
+ expressionValues[':domains'] = { L: domains.map(d => ({ S: d })) }
169
+ cli.info(` Domains: ${JSON.stringify(site.domains || [])} -> ${JSON.stringify(domains)}`)
170
+ }
171
+
172
+ if (options.active !== undefined) {
173
+ const isActive = options.active === 'true' || options.active === '1'
174
+ updates.push('isActive = :active')
175
+ expressionValues[':active'] = { BOOL: isActive }
176
+ cli.info(` Active: ${site.isActive} -> ${isActive}`)
177
+ }
178
+
179
+ if (updates.length === 0) {
180
+ cli.warn('No updates specified. Use --name, --domain, or --active options.')
181
+ return
182
+ }
183
+
184
+ // Always update updatedAt
185
+ updates.push('updatedAt = :updatedAt')
186
+ expressionValues[':updatedAt'] = { S: new Date().toISOString() }
187
+
188
+ await dynamodb.updateItem({
189
+ TableName: options.table,
190
+ Key: {
191
+ pk: { S: `SITE#${siteId}` },
192
+ sk: { S: `SITE#${siteId}` },
193
+ },
194
+ UpdateExpression: `SET ${updates.join(', ')}`,
195
+ ExpressionAttributeNames: Object.keys(expressionNames).length > 0 ? expressionNames : undefined,
196
+ ExpressionAttributeValues: expressionValues,
197
+ })
198
+
199
+ cli.info('')
200
+ cli.success('Site updated successfully')
201
+ }
202
+ catch (error: any) {
203
+ cli.error(`Failed to update site: ${error.message}`)
204
+ process.exit(1)
205
+ }
206
+ })
207
+
208
+ app
209
+ .command('analytics:query <siteId>', 'Query analytics data for a site')
210
+ .option('--table <name>', 'DynamoDB table name', { default: 'ts-analytics' })
211
+ .option('--region <region>', 'AWS region', { default: 'us-east-1' })
212
+ .option('--limit <n>', 'Max items to return', { default: '10' })
213
+ .option('--type <type>', 'Filter by type (pageview, event, etc.)')
214
+ .action(async (siteId: string, options: { table: string; region: string; limit: string; type?: string }) => {
215
+ cli.header(`Analytics Data: ${siteId}`)
216
+
217
+ try {
218
+ const dynamodb = new DynamoDBClient(options.region)
219
+
220
+ const spinner = new cli.Spinner('Querying data...')
221
+ spinner.start()
222
+
223
+ const result = await dynamodb.query({
224
+ TableName: options.table,
225
+ KeyConditionExpression: 'pk = :pk',
226
+ ExpressionAttributeValues: {
227
+ ':pk': { S: `SITE#${siteId}` },
228
+ },
229
+ ScanIndexForward: false,
230
+ Limit: parseInt(options.limit, 10),
231
+ })
232
+
233
+ spinner.succeed(`Found ${result.Items?.length || 0} items`)
234
+
235
+ if (!result.Items || result.Items.length === 0) {
236
+ cli.info('No data found for this site')
237
+ return
238
+ }
239
+
240
+ const items = result.Items.map(item => DynamoDBClient.unmarshal(item))
241
+
242
+ for (const item of items) {
243
+ cli.info('')
244
+ cli.info(`SK: ${item.sk}`)
245
+ if (item.path) cli.info(` Path: ${item.path}`)
246
+ if (item.timestamp) cli.info(` Time: ${item.timestamp}`)
247
+ if (item.visitorId) cli.info(` Visitor: ${item.visitorId}`)
248
+ if (item.browser) cli.info(` Browser: ${item.browser}`)
249
+ if (item.country) cli.info(` Country: ${item.country}`)
250
+ }
251
+ }
252
+ catch (error: any) {
253
+ cli.error(`Failed to query data: ${error.message}`)
254
+ process.exit(1)
255
+ }
256
+ })
257
+
258
+ app
259
+ .command('analytics:realtime <siteId>', 'Check realtime visitors for a site')
260
+ .option('--table <name>', 'DynamoDB table name', { default: 'ts-analytics' })
261
+ .option('--region <region>', 'AWS region', { default: 'us-east-1' })
262
+ .option('--minutes <n>', 'Minutes to look back', { default: '5' })
263
+ .action(async (siteId: string, options: { table: string; region: string; minutes: string }) => {
264
+ cli.header(`Realtime: ${siteId}`)
265
+
266
+ try {
267
+ const dynamodb = new DynamoDBClient(options.region)
268
+
269
+ const spinner = new cli.Spinner('Checking realtime...')
270
+ spinner.start()
271
+
272
+ const minutesAgo = parseInt(options.minutes, 10)
273
+ const cutoff = new Date(Date.now() - minutesAgo * 60 * 1000)
274
+
275
+ const result = await dynamodb.query({
276
+ TableName: options.table,
277
+ KeyConditionExpression: 'pk = :pk AND sk BETWEEN :start AND :end',
278
+ ExpressionAttributeValues: {
279
+ ':pk': { S: `SITE#${siteId}` },
280
+ ':start': { S: `PAGEVIEW#${cutoff.toISOString()}` },
281
+ ':end': { S: 'PAGEVIEW#Z' },
282
+ },
283
+ ScanIndexForward: false,
284
+ })
285
+
286
+ spinner.succeed(`Found ${result.Items?.length || 0} pageviews in last ${minutesAgo} minutes`)
287
+
288
+ if (!result.Items || result.Items.length === 0) {
289
+ cli.warn('No recent pageviews found')
290
+ cli.info('')
291
+ cli.info('Possible issues:')
292
+ cli.info(' 1. Tracking script not installed correctly')
293
+ cli.info(' 2. Site ID mismatch')
294
+ cli.info(' 3. CORS or network issues')
295
+ return
296
+ }
297
+
298
+ const items = result.Items.map(item => DynamoDBClient.unmarshal(item))
299
+ const uniqueVisitors = new Set(items.map(i => i.visitorId)).size
300
+
301
+ cli.info('')
302
+ cli.success(`${uniqueVisitors} unique visitor(s) online`)
303
+ cli.info('')
304
+
305
+ // Group by path
306
+ const byPath: Record<string, number> = {}
307
+ for (const item of items) {
308
+ const path = item.path || '/'
309
+ byPath[path] = (byPath[path] || 0) + 1
310
+ }
311
+
312
+ cli.info('Active pages:')
313
+ for (const [path, count] of Object.entries(byPath).sort((a, b) => b[1] - a[1]).slice(0, 5)) {
314
+ cli.info(` ${path}: ${count} view(s)`)
315
+ }
316
+
317
+ cli.info('')
318
+ cli.info('Recent pageviews:')
319
+ for (const item of items.slice(0, 5)) {
320
+ cli.info(` ${item.timestamp} - ${item.path} (${item.browser || 'Unknown'})`)
321
+ }
322
+ }
323
+ catch (error: any) {
324
+ cli.error(`Failed to check realtime: ${error.message}`)
325
+ process.exit(1)
326
+ }
327
+ })
328
+ }