webspresso 0.0.104 → 0.0.106

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 (233) hide show
  1. package/.agents/AGENTS.md +21 -2
  2. package/.agents/ARCHITECTURE.md +23 -0
  3. package/.agents/CACHE.md +147 -0
  4. package/.agents/CLI.md +220 -8
  5. package/.agents/EARLY_HINTS.md +120 -0
  6. package/.agents/FLAGS.md +118 -0
  7. package/.agents/HEALTH.md +118 -0
  8. package/.agents/LIMITS.md +109 -0
  9. package/.agents/LOGGING.md +93 -0
  10. package/.agents/METRICS.md +127 -0
  11. package/.agents/MIGRATION_JOBS.md +95 -0
  12. package/.agents/ORM.md +155 -1
  13. package/.agents/PLUGINS.md +38 -3
  14. package/.agents/QUEUE.md +42 -0
  15. package/.agents/RATE_LIMIT.md +147 -0
  16. package/.agents/RESILIENCE.md +235 -0
  17. package/.agents/SCALE_OUT.md +109 -0
  18. package/.agents/SECRETS.md +117 -0
  19. package/AGENTS.md +16 -0
  20. package/README.md +2 -2
  21. package/bin/commands/add-aws.js +396 -0
  22. package/bin/commands/add-ci.js +523 -0
  23. package/bin/commands/add-cloudflare.js +240 -0
  24. package/bin/commands/add-cloudrun.js +381 -0
  25. package/bin/commands/add-docker.js +6 -6
  26. package/bin/commands/add-migration-job.js +396 -0
  27. package/bin/commands/assets-sync.js +209 -0
  28. package/bin/commands/client-generate.js +52 -0
  29. package/bin/commands/console.js +53 -0
  30. package/bin/commands/db-migrate.js +47 -2
  31. package/bin/commands/db-studio.js +84 -0
  32. package/bin/commands/deploy-check.js +242 -13
  33. package/bin/commands/dev.js +1 -1
  34. package/bin/commands/export.js +87 -0
  35. package/bin/commands/new.js +5 -5
  36. package/bin/commands/openapi.js +1 -1
  37. package/bin/commands/secrets-pull.js +208 -0
  38. package/bin/commands/start.js +1 -1
  39. package/bin/utils/db.js +7 -1
  40. package/bin/utils/project.js +2 -2
  41. package/bin/webspresso.js +22 -0
  42. package/core/app.js +421 -0
  43. package/core/auth/authenticate.js +93 -0
  44. package/core/auth/hash.js +5 -4
  45. package/core/auth/index.js +34 -4
  46. package/core/auth/jwt.js +5 -54
  47. package/core/auth/magic-link.js +4 -5
  48. package/core/auth/manager.js +53 -10
  49. package/core/auth/middleware.js +16 -68
  50. package/core/auth/oauth.js +8 -17
  51. package/core/auth/redis-session.js +1008 -0
  52. package/core/auth/sessions.js +156 -0
  53. package/core/auth/totp.js +2 -1
  54. package/core/cache/byte-size.js +123 -0
  55. package/core/cache/cdn.js +296 -0
  56. package/core/cache/index.js +33 -0
  57. package/core/cache/lru.js +477 -0
  58. package/core/cache/tagged-lru.js +354 -0
  59. package/core/commands/registry.js +6 -20
  60. package/core/console/commands.js +148 -0
  61. package/core/console/context.js +260 -0
  62. package/core/console/eval.js +70 -0
  63. package/core/console/index.js +22 -0
  64. package/core/console/repl.js +102 -0
  65. package/core/early-hints/index.js +267 -0
  66. package/core/errors/domain.js +49 -0
  67. package/core/errors/http.js +31 -0
  68. package/core/errors/index.js +10 -0
  69. package/core/errors/normalize.js +88 -2
  70. package/core/events/discovery.js +6 -24
  71. package/core/events/dispatcher.js +9 -7
  72. package/core/events/signal.js +7 -0
  73. package/core/export/discovery.js +191 -0
  74. package/core/export/exporter.js +277 -0
  75. package/core/export/index.js +24 -0
  76. package/core/flags/evaluator.js +252 -0
  77. package/core/flags/hasher.js +37 -0
  78. package/core/flags/index.js +21 -0
  79. package/core/flags/manager.js +263 -0
  80. package/core/flags/store.js +126 -0
  81. package/core/health/index.js +41 -0
  82. package/core/health/middleware.js +213 -0
  83. package/core/health/normalize.js +140 -0
  84. package/core/health/probes.js +203 -0
  85. package/core/http/adapter.js +74 -0
  86. package/core/http/express-adapter.js +46 -0
  87. package/core/http/hono-adapter.js +531 -0
  88. package/core/http/index.js +41 -0
  89. package/core/http/pipeline.js +485 -0
  90. package/core/index.js +336 -0
  91. package/core/limits/index.js +34 -0
  92. package/core/limits/middleware.js +325 -0
  93. package/core/limits/normalize.js +222 -0
  94. package/core/logger/formatters.js +264 -0
  95. package/core/logger/index.js +260 -0
  96. package/core/logger/trace.js +137 -0
  97. package/core/metrics/collectors.js +125 -0
  98. package/core/metrics/index.js +45 -0
  99. package/core/metrics/middleware.js +230 -0
  100. package/core/metrics/registry.js +366 -0
  101. package/core/orm/batch.js +70 -0
  102. package/core/orm/cache/fingerprint.js +2 -2
  103. package/core/orm/cache/memory-provider.js +27 -110
  104. package/core/orm/d1.js +265 -0
  105. package/core/orm/index.js +176 -13
  106. package/core/orm/json-fields.js +22 -8
  107. package/core/orm/migrate-cloud.js +194 -0
  108. package/core/orm/model.js +10 -0
  109. package/core/orm/pool.js +97 -0
  110. package/core/orm/query-builder.js +72 -9
  111. package/core/orm/repository.js +318 -19
  112. package/core/orm/schema-helpers.js +8 -10
  113. package/core/orm/scopes.js +321 -1
  114. package/core/orm/transaction.js +10 -2
  115. package/core/orm/utils.js +4 -86
  116. package/core/queue/adapters/cloudflare.js +131 -0
  117. package/core/queue/adapters/database.js +4 -8
  118. package/core/queue/adapters/pubsub.js +400 -0
  119. package/core/queue/adapters/sqs.js +419 -0
  120. package/core/queue/adapters/wait-until.js +77 -0
  121. package/core/queue/index.js +8 -0
  122. package/core/queue/manager.js +66 -41
  123. package/core/rate-limit/index.js +22 -0
  124. package/core/rate-limit/limiter.js +219 -0
  125. package/core/rate-limit/stores/memory.js +129 -0
  126. package/core/rate-limit/stores/redis.js +151 -0
  127. package/core/rate-limit/utils.js +112 -0
  128. package/core/resilience/circuit-breaker.js +422 -0
  129. package/core/resilience/errors.js +63 -0
  130. package/core/resilience/index.js +53 -0
  131. package/core/resilience/registry.js +97 -0
  132. package/core/resilience/retry.js +203 -0
  133. package/core/schema-cache.js +9 -2
  134. package/core/secrets/index.js +113 -0
  135. package/core/secrets/manager.js +308 -0
  136. package/core/secrets/providers/aws.js +288 -0
  137. package/core/secrets/providers/env.js +44 -0
  138. package/core/secrets/providers/gcp.js +307 -0
  139. package/core/secrets/providers/memory.js +76 -0
  140. package/core/shutdown/http-adapter.js +4 -1
  141. package/core/shutdown/shutdown-manager.js +129 -19
  142. package/core/stream/index.js +19 -0
  143. package/core/stream/text-stream.js +326 -0
  144. package/core/studio/index.js +43 -0
  145. package/core/studio/inspector.js +258 -0
  146. package/core/studio/server.js +325 -0
  147. package/core/studio/ui.js +681 -0
  148. package/core/utils/array.js +12 -0
  149. package/core/utils/base64url.js +8 -0
  150. package/core/utils/crypto.js +118 -0
  151. package/core/utils/ensure-leading-slash.js +6 -0
  152. package/core/utils/escape-regexp.js +7 -0
  153. package/core/utils/fetch.js +6 -5
  154. package/core/utils/fs.js +144 -0
  155. package/core/utils/hmac.js +6 -0
  156. package/core/utils/json.js +98 -0
  157. package/core/utils/object.js +106 -0
  158. package/core/utils/random.js +9 -0
  159. package/core/utils/regex.js +77 -0
  160. package/core/utils/sha256.js +6 -0
  161. package/core/utils/string.js +87 -0
  162. package/core/utils/strip-trailing-slash.js +6 -0
  163. package/core/utils/time.js +96 -0
  164. package/core/utils/timeout.js +348 -0
  165. package/core/utils/url.js +106 -0
  166. package/core/utils/walk.js +7 -0
  167. package/index.d.ts +1549 -2
  168. package/index.js +272 -2
  169. package/package.json +3 -2
  170. package/plugins/admin-panel/core/api-extensions.js +2 -5
  171. package/plugins/admin-panel/index.js +13 -6
  172. package/plugins/admin-panel/modules/menu.js +1 -1
  173. package/plugins/api-key/crypto.js +3 -3
  174. package/plugins/api-key/index.js +3 -2
  175. package/plugins/api-key/scopes.js +5 -7
  176. package/plugins/audit-log/api-handlers.js +6 -15
  177. package/plugins/audit-log/index.js +3 -1
  178. package/plugins/audit-log/parse.js +3 -3
  179. package/plugins/basic-auth/index.js +3 -2
  180. package/plugins/cors.js +3 -2
  181. package/plugins/csrf/index.js +9 -12
  182. package/plugins/csv/index.js +8 -5
  183. package/plugins/dashboard/app.js +2 -2
  184. package/plugins/dashboard/index.js +5 -5
  185. package/plugins/data-exchange/record-selection.js +2 -5
  186. package/plugins/email/api-handlers.js +5 -18
  187. package/plugins/email/auth-bridge.js +3 -2
  188. package/plugins/email/index.js +11 -9
  189. package/plugins/health-check.js +2 -1
  190. package/plugins/index.js +5 -0
  191. package/plugins/lazy.js +5 -6
  192. package/plugins/mcp/discovery.js +18 -17
  193. package/plugins/polar/src/api.js +2 -1
  194. package/plugins/polar/src/webhooks.js +5 -6
  195. package/plugins/queue/index.js +18 -2
  196. package/plugins/rate-limit/index.js +3 -1
  197. package/plugins/redirect/index.js +16 -9
  198. package/plugins/rls/index.js +170 -0
  199. package/plugins/seo-checker/index.js +2 -2
  200. package/plugins/site-analytics/index.js +3 -1
  201. package/plugins/site-analytics/tracking.js +2 -2
  202. package/plugins/sitemap.js +3 -2
  203. package/plugins/swagger.js +12 -5
  204. package/plugins/upload/gcs-provider.js +148 -0
  205. package/plugins/upload/index.js +136 -77
  206. package/plugins/upload/orphan-pruner.js +4 -3
  207. package/plugins/upload/s3-provider.js +257 -0
  208. package/plugins/webhooks/crypto.js +6 -11
  209. package/plugins/webhooks/dispatcher.js +8 -8
  210. package/plugins/webhooks/index.js +33 -23
  211. package/plugins/webhooks/matcher.js +7 -11
  212. package/plugins/webhooks/sender.js +8 -11
  213. package/plugins/xlsx/index.js +7 -5
  214. package/src/api/api-loader.js +12 -95
  215. package/src/api/define-api.js +1 -0
  216. package/src/app-context.js +41 -5
  217. package/src/client-runtime/constants.js +10 -0
  218. package/src/client-runtime/mount.js +2 -3
  219. package/src/dev/banner.js +6 -2
  220. package/src/dev/client-gen.js +458 -0
  221. package/src/discovery/scan-routes.js +5 -2
  222. package/src/file-router.js +3 -3
  223. package/src/helpers.js +212 -24
  224. package/src/modules/module-discovery.js +1 -9
  225. package/src/pages/page-loader.js +13 -154
  226. package/src/plugin-manager.js +99 -18
  227. package/src/server.js +335 -133
  228. package/src/services/discovery.js +19 -40
  229. package/src/services/executor.js +95 -25
  230. package/src/services/memoize.js +3 -25
  231. package/src/services/registry.js +15 -1
  232. package/src/services/validator.js +38 -20
  233. package/src/template/precompile.js +8 -19
@@ -0,0 +1,109 @@
1
+ # Stateless Scale-Out & Database Connection Pool Management Guide
2
+
3
+ When deploying Webspresso to serverless or auto-scaling cloud environments (Google Cloud Run, AWS App Runner, ECS Fargate, AWS Lambda, Kubernetes, or PM2 Cluster), multiple container instances run concurrently and scale up/down dynamically.
4
+
5
+ To ensure seamless operation without lost user sessions or database exhaustion, Webspresso provides native zero-dependency **Stateless Scale-Out** primitives:
6
+ 1. **Distributed Redis Session Store** (`createRedisSessionStore`)
7
+ 2. **Distributed Redis Session & Device Tracker** (`createRedisSessionTracker`)
8
+ 3. **Built-in Lightweight RESP Redis Client** (`createMiniRedisClient`) & `InMemoryRedisClient`
9
+ 4. **Serverless Database Connection Pool Optimizer** (`normalizeServerlessPoolConfig`, `isServerlessEnvironment`)
10
+
11
+ ---
12
+
13
+ ## 🎯 Key Capabilities
14
+
15
+ ### 1. Distributed Redis Session Store (`createRedisSessionStore`)
16
+ - Drop-in replacement for `express-session` MemoryStore.
17
+ - Stores session data in Redis with automatic TTL expiration synced to session cookies (`maxAge`).
18
+ - Implements the complete `express-session` Store contract: `get`, `set`, `destroy`, `touch`, `all`, `length`, `clear`.
19
+ - Zero external dependencies: works with any standard client (`ioredis`, `redis` v4/v3, or the built-in `createMiniRedisClient`).
20
+ - **Auto-Detection**: Setting `REDIS_URL=redis://...` or `SESSION_STORE=redis` in your environment automatically wires the Redis session store without manual boilerplate!
21
+
22
+ ### 2. Distributed Redis Device & Session Tracker (`createRedisSessionTracker`)
23
+ - Replaces or supplements Knex database session tracking across ephemeral instances.
24
+ - Maintains user active sessions (`user:sessions:<userId>`) and device metadata (`sess:device:<sessionId>`).
25
+ - Supports remote single-session revocation, "revoke other sessions", and "force logout from all devices".
26
+
27
+ ### 3. Serverless DB Connection Pool Optimizer (`normalizeServerlessPoolConfig`)
28
+ - Prevents database connection exhaustion on managed databases (Google Cloud SQL, AWS RDS / Aurora) when container replicas scale from 1 to 50+.
29
+ - Automatically detects serverless platforms (`K_SERVICE` on Cloud Run, `AWS_EXECUTION_ENV` / `AWS_LAMBDA_FUNCTION_NAME` on AWS):
30
+ - Sets `pool.min: 0` (so idle containers release database sockets).
31
+ - Clamps `pool.max: 5` (or `2` on Lambda) per container instance.
32
+ - Reduces `acquireTimeoutMillis` from Knex's 60s default to 10s.
33
+ - Sets safe `idleTimeoutMillis: 30000`, `createTimeoutMillis: 5000`.
34
+
35
+ ---
36
+
37
+ ## 🚀 Quick Start & Usage
38
+
39
+ ### 1. Zero-Configuration Cloud Run / ECS Deployment
40
+
41
+ Simply provide `REDIS_URL` in your container environment variables:
42
+
43
+ ```bash
44
+ # In Cloud Run or AWS ECS Task Definition
45
+ REDIS_URL=redis://default:secret@memorystore-or-elasticache:6379
46
+ SESSION_SECRET=a_very_secure_random_production_secret_32_chars
47
+ ```
48
+
49
+ Webspresso automatically detects `REDIS_URL` and activates both `createRedisSessionStore` and `normalizeServerlessPoolConfig`.
50
+
51
+ ---
52
+
53
+ ### 2. Explicit Auth Session Configuration
54
+
55
+ ```javascript
56
+ // server.js
57
+ const { createApp, createRedisSessionStore } = require('webspresso');
58
+
59
+ const app = createApp({
60
+ auth: {
61
+ session: {
62
+ secret: process.env.SESSION_SECRET,
63
+ // Pass custom store or let Webspresso auto-detect REDIS_URL
64
+ store: createRedisSessionStore({
65
+ url: process.env.REDIS_URL,
66
+ prefix: 'myapp:sess:',
67
+ ttl: 86400, // 24 hours
68
+ }),
69
+ },
70
+ },
71
+ server: {
72
+ compression: true,
73
+ },
74
+ });
75
+ ```
76
+
77
+ ---
78
+
79
+ ### 3. Explicit Database Connection Pool Configuration
80
+
81
+ ```javascript
82
+ // webspresso.db.js
83
+ module.exports = {
84
+ client: 'pg',
85
+ connection: process.env.DATABASE_URL,
86
+ // Enable serverless pool optimization explicitly
87
+ serverlessPool: true,
88
+ pool: {
89
+ // Override defaults if using a pooler like PgBouncer or RDS Proxy
90
+ min: 0,
91
+ max: 5,
92
+ },
93
+ };
94
+ ```
95
+
96
+ ---
97
+
98
+ ### 4. Production Preflight Audit (`webspresso deploy:check`)
99
+
100
+ Run the preflight check before deploying to verify that your session store and connection pool are properly configured for scale-out:
101
+
102
+ ```bash
103
+ npx webspresso deploy:check
104
+ ```
105
+
106
+ The preflight check will:
107
+ - ✅ Verify distributed session store (Redis) is active in production instead of ephemeral MemoryStore.
108
+ - ⚠️ Flag database connection pools with excessively large `max` (> 10) on autoscaling serverless containers.
109
+ - ⚠️ Check that `pool.min` is set to 0 for serverless scale-to-zero workloads.
@@ -0,0 +1,117 @@
1
+ # Cloud Secrets Management Guide (`core/secrets` & `secrets:pull`)
2
+
3
+ Webspresso features an enterprise, zero-dependency Cloud Secrets Management system (`core/secrets`) designed to eliminate plain-text `.env` files and secret leakage in containerized and serverless environments (Google Cloud Run, AWS App Runner, ECS Fargate, Kubernetes).
4
+
5
+ ---
6
+
7
+ ## 🎯 Key Capabilities
8
+
9
+ 1. **Zero External Dependencies**:
10
+ - Interacts directly with cloud vault REST APIs using native Node.js standard libraries (`https`, `crypto`).
11
+ - **Google Cloud Secret Manager**: Automatically acquires OAuth2 tokens from the local GCP Metadata Server in Cloud Run/GKE, exchanges RSA-SHA256 signed JWTs from `GOOGLE_APPLICATION_CREDENTIALS`, or uses static tokens.
12
+ - **AWS Secrets Manager & SSM Parameter Store**: Employs an embedded, standards-compliant AWS Signature Version 4 (SigV4) signer with HMAC-SHA256.
13
+ 2. **In-Memory Caching with TTL & Invalidation**:
14
+ - Caches secret values in RAM with a configurable TTL (default: 5 minutes) to avoid redundant HTTP roundtrips.
15
+ - Supports single secret cache busting (`secrets.invalidate(name)`) and full cache clearing (`secrets.clearCache()`) for automated secret rotation.
16
+ 3. **JSON Secret Bundle Unpacking & Dot-Notation Subkeys**:
17
+ - Access specific fields within JSON bundles using dot-notation:
18
+ `await secrets.get('prod/app/config.DATABASE_URL')`
19
+ - Unpack entire JSON bundles into `process.env`:
20
+ `await secrets.loadBundle('prod/app/config')`
21
+ 4. **Local Developer Synchronization (`webspresso secrets:pull`)**:
22
+ - Pulls secrets from Google Cloud or AWS into a local `.env.local` file for local development without committing credentials to Git.
23
+
24
+ ---
25
+
26
+ ## 🚀 Usage in Application Bootstrap
27
+
28
+ ### 1. Hydrating Environment at Application Boot
29
+
30
+ Call `loadSecrets()` before initializing your database or server:
31
+
32
+ ```javascript
33
+ // server.js
34
+ const { createApp, loadSecrets } = require('webspresso');
35
+
36
+ async function start() {
37
+ // Load secrets from Google Cloud Secret Manager or AWS before bootstrapping
38
+ await loadSecrets({
39
+ provider: 'auto', // 'cloudrun' | 'gcp' | 'aws' | 'auto'
40
+ secrets: {
41
+ SESSION_SECRET: 'session-secret:latest',
42
+ DATABASE_URL: 'rds-database-url:latest',
43
+ },
44
+ // Or unpack a single JSON bundle holding all config:
45
+ // bundle: 'prod/webspresso/app-secrets',
46
+ });
47
+
48
+ const app = createApp({
49
+ server: { port: process.env.PORT || 3000 },
50
+ });
51
+
52
+ await app.listen();
53
+ }
54
+
55
+ start();
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 🛠️ Standalone `SecretManager` Usage
61
+
62
+ ```javascript
63
+ const { createSecretManager } = require('webspresso/core/secrets');
64
+
65
+ const secrets = createSecretManager({
66
+ provider: 'auto', // Auto-detects GCP or AWS based on environment markers
67
+ cacheTtl: '10m', // 10 minutes cache TTL
68
+ projectId: 'my-gcp-project', // optional for GCP
69
+ region: 'us-east-1', // optional for AWS
70
+ });
71
+
72
+ // 1. Fetch a single secret
73
+ const apiKey = await secrets.get('STRIPE_SECRET_KEY');
74
+
75
+ // 2. Fetch with fallback or enforce required
76
+ const optionalToken = await secrets.get('SLACK_WEBHOOK', { fallback: null });
77
+ const mustExist = await secrets.get('APP_KEY', { required: true });
78
+
79
+ // 3. Dot-notation subkey access inside JSON secret bundles
80
+ const dbPassword = await secrets.get('prod/database.password');
81
+
82
+ // 4. Invalidate cache on rotation
83
+ secrets.invalidate('STRIPE_SECRET_KEY');
84
+ ```
85
+
86
+ ---
87
+
88
+ ## 💻 CLI Synchronization Tool (`secrets:pull`)
89
+
90
+ Pulls cloud secrets into `.env.local` for local development:
91
+
92
+ ```bash
93
+ # Auto-detect cloud platform and pull expected secrets into .env.local
94
+ npx webspresso secrets:pull
95
+
96
+ # Explicitly pull from Google Cloud Run / Secret Manager
97
+ npx webspresso secrets:pull --platform cloudrun -s SESSION_SECRET DATABASE_URL
98
+
99
+ # Explicitly pull from AWS Secrets Manager
100
+ npx webspresso secrets:pull --platform aws -s SESSION_SECRET DATABASE_URL
101
+
102
+ # Unpack a JSON bundle to stdout (dry run)
103
+ npx webspresso secrets:pull --bundle prod/app/config --dry-run
104
+
105
+ # Output as JSON
106
+ npx webspresso secrets:pull -s API_KEY DB_PASS --format json -o secrets.json
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 🔒 Security Best Practices
112
+
113
+ 1. **Never commit `.env` or `.env.local`**: Ensure `.env.local` is listed in `.gitignore` and `.gcloudignore`.
114
+ 2. **Minimal IAM Permissions**:
115
+ - GCP: Grant the Cloud Run service account `roles/secretmanager.secretAccessor`.
116
+ - AWS: Grant the ECS Task role `secretsmanager:GetSecretValue` and `ssm:GetParameter`.
117
+ 3. **Use Short TTL for Rotating Secrets**: If secrets rotate frequently (e.g. daily), configure `cacheTtl: '15m'` or invalidate on rotation webhook triggers.
package/AGENTS.md CHANGED
@@ -18,6 +18,13 @@ When tasked with a feature or bugfix, **immediately consult the corresponding to
18
18
  | **Authentication (Session / JWT / Guard)** | [`.agents/AUTH.md`](.agents/AUTH.md) | `core/auth/` |
19
19
  | **Background Jobs & Worker Queue** | [`.agents/QUEUE.md`](.agents/QUEUE.md) | `jobs/` |
20
20
  | **Decoupled Events & Signals** | [`.agents/EVENTS.md`](.agents/EVENTS.md) | `core/events/`, `listeners/` |
21
+ | **Fault Tolerance / Circuit Breaker / Retry** | [`.agents/RESILIENCE.md`](.agents/RESILIENCE.md) | `core/resilience/` |
22
+ | **Stateless Scale-Out (Redis Sessions & DB Pool)** | [`.agents/SCALE_OUT.md`](.agents/SCALE_OUT.md) | `core/auth/redis-session.js`, `core/orm/pool.js` |
23
+ | **Cloud Secrets (GCP & AWS Secret Manager)** | [`.agents/SECRETS.md`](.agents/SECRETS.md) | `core/secrets/`, `bin/commands/secrets-pull.js` |
24
+ | **Cloud Metrics & Distributed Tracing** | [`.agents/METRICS.md`](.agents/METRICS.md) | `core/metrics/`, `/metrics` |
25
+ | **Cloud Migration Jobs (Zero-Downtime DB Deploy)** | [`.agents/MIGRATION_JOBS.md`](.agents/MIGRATION_JOBS.md) | `bin/commands/add-migration-job.js`, `core/orm/migrate-cloud.js` |
26
+ | **Rate Limiting Engine (Zero-Dependency & Redis)** | [`.agents/RATE_LIMIT.md`](.agents/RATE_LIMIT.md) | `core/rate-limit/` |
27
+ | **Feature Flags & Canary Splitter (`core/flags`)** | [`.agents/FLAGS.md`](.agents/FLAGS.md) | `core/flags/` |
21
28
  | **API Signatures & Types Verification** | [`index.d.ts`](index.d.ts) | **Canonical Ground Truth** |
22
29
 
23
30
  ---
@@ -57,6 +64,15 @@ Every coding agent working on Webspresso **MUST** follow this 4-step execution d
57
64
  - **Framework Exceptions & Central Error Boundary**: Built-in exception hierarchy (`WebspressoError`, `HttpError`, `ValidationError`, `SecurityError`, `RequestAbortedError`, `ConfigurationError`, `PluginError`, `RouteNotFoundError`) and custom handler hook (`app.setErrorHandler`). Async route handlers automatically propagate rejected promises. Production responses mask 500 stack traces and error details.
58
65
  - **Graceful Shutdown & Lifecycle Management**: Configured via `createApp({ server: { shutdown: { enabled: true, mode: 'graceful'|'force', timeout: 10000 } } })`. Socket connection tracking, request draining with `Connection: close`, and reverse-order plugin disposer cleanup.
59
66
  - **HTTP Response Compression**: Zero-dependency streaming zlib compression configured via `createApp({ server: { compression: true } })` with Brotli (`br`), Gzip (`gzip`), Deflate (`deflate`), threshold buffering (1 KB), and route-level opt-out (`res.compress(false)`).
67
+ - **HTTP 103 Early Hints & Critical Asset Preloading**: Zero-dependency RFC 8288 / RFC 8297 Early Hints dispatcher configured via `createApp({ earlyHints: true })` or per-page `definePage({ earlyHints })`. Emits informational 103 status code with route CSS/JS preloads before async `load()` finishes to minimize TTFB, FCP and LCP, with safe fallback and `res.earlyHints(links)` / `ctx.earlyHints(links)` helpers.
68
+ - **Configurable Payload Size Limits & DoS Protection**: Zero-dependency body limit enforcement configured via `createApp({ limits: { json: '1mb', urlencoded: '1mb', raw: false, text: false, parameterLimit: 1000, captureRawBody: false } })` or shorthand `limits: '5mb'`, with automatic HTTP 413 `PayloadTooLargeError` and HTTP 400 `BadRequestError` mapping, route overrides (`limits.routes`), streaming bypass (`limits.exclude`), and raw body capture on `req.rawBody` for webhook signatures.
69
+ - **Native Cloud & Kubernetes Health & Readiness Probes**: Zero-dependency RFC-compliant liveness (`/healthz`), readiness (`/readyz`), and combined diagnostic (`/health`) endpoints configured via `createApp({ healthCheck: true })`. Includes non-blocking event loop lag monitoring (`perf_hooks.monitorEventLoopDelay`), database/queue connectivity tests, and automatic HTTP 503 draining integration with `ShutdownManager` when pods enter graceful termination.
70
+ - **High-Performance In-Memory LRU & Tagged Cache Engine**: Zero-dependency memory-bounded caching (`core/cache`) configured via `createApp({ cache: { maxEntries: 1000, maxMemoryMb: 50, defaultTtl: '15m' } })` or shorthand `cache: true|false`. Provides O(1) LRU eviction, byte budget constraints preventing OOM, leak-free tag invalidation (`TaggedLruCache`), stampede protection (`getOrSet`), and powers ORM query caching and Zod schema caching.
71
+ - **Enterprise Resilience & Fault Tolerance (Circuit Breaker & Retry Engine)**: Zero-dependency resilience system (`core/resilience`) featuring a three-state `CircuitBreaker` (`CLOSED`, `OPEN`, `HALF_OPEN`), rolling window failure rate tracking, per-call execution timeouts, graceful fallbacks, and an exponential backoff retry engine (`retry`, `withRetry`) with randomized jitter and transient error detection (`ECONNRESET`, 429, 5xx). Seamlessly integrated into the Services Layer (`defineService({ circuitBreaker, retry })`) and exposed via `app.breakers`, `app.getBreaker()`, `req.breaker()`, and `req.retry()`.
72
+ - **Stateless Scale-Out & Database Connection Pool Management**: Zero-dependency Redis session store (`createRedisSessionStore`), distributed device tracking (`createRedisSessionTracker`), lightweight RESP Redis client, serverless DB connection pool optimizer (`normalizeServerlessPoolConfig`), and scale-out audits in `webspresso deploy:check`. Automatically activates distributed sessions when `REDIS_URL` or `SESSION_STORE=redis` is configured.
73
+ - **Enterprise Cloud Secrets Management**: Zero-dependency integration with Google Cloud Secret Manager & AWS Secrets Manager/SSM Parameter Store (`core/secrets`), in-memory caching with TTL (`cacheTtl: '5m'`), JSON secret bundle unpacking, dot-notation subkeys (`app.DB_PASSWORD`), bootstrap environment hydration (`loadSecrets()`), and local sync CLI (`webspresso secrets:pull`).
74
+ - **Cloud Metrics & Distributed Tracing**: Zero-dependency Prometheus exposition format (`/metrics`), JSON telemetry (`/metrics.json`), HTTP latency histograms, Node.js event loop/memory/pool/cache collectors, and W3C `traceparent` & `x-trace-id` propagation (`core/metrics`) configured via `createApp({ metrics: true })`.
75
+ - **Cloud Migration Jobs & Zero-Downtime Database Deploy**: Pre-deploy Cloud Run Jobs, AWS ECS Fargate one-off tasks, and Kubernetes Helm hooks (`add:migration-job`), safe retry connections, and automatic stale lock healing (`core/orm/migrate-cloud`, `webspresso db:migrate --safe`).
60
76
  - **Custom Error & 404 Pages**: Configured via `createApp({ errorPages: { notFound, serverError, timeout } })`. Supports template paths (e.g. `notFound: '404.njk'`) or custom handler functions, passing `{ fsy, locale, isDev, url, method }` to templates.
61
77
  - **Cold Start & High-Performance Optimizations**:
62
78
  - Unified single-pass route scanner (`discoverAppRoutes`).
package/README.md CHANGED
@@ -22,7 +22,7 @@ cd my-app
22
22
  npm run dev
23
23
  ```
24
24
 
25
- Open **`http://localhost:3000`** in your browser.
25
+ Open **`http://localhost:7331`** in your browser.
26
26
 
27
27
  ---
28
28
 
@@ -147,7 +147,7 @@ const { app } = createApp({
147
147
  },
148
148
  });
149
149
 
150
- const PORT = process.env.PORT || 3000;
150
+ const PORT = process.env.PORT || 7331;
151
151
  app.listen(PORT, () => {
152
152
  console.log(`Server running at http://localhost:${PORT}`);
153
153
  });
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Add AWS Command
3
+ * Generate production-ready AWS configuration (apprunner.yaml, ecs-task-definition.json, deploy-aws.sh)
4
+ * Supports AWS App Runner and AWS ECS (Fargate) container deployments
5
+ * @module bin/commands/add-aws
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { generateDockerfile, generateDockerignore } = require('./add-docker');
13
+
14
+ /**
15
+ * Normalize CPU value to ECS Fargate units (256, 512, 1024, 2048, 4096)
16
+ * @param {string|number} cpu
17
+ * @returns {string}
18
+ */
19
+ function normalizeEcsCpu(cpu) {
20
+ if (!cpu) return '512';
21
+ const str = String(cpu).trim().toLowerCase();
22
+ if (str === '0.25' || str === '0.25 vcpu') return '256';
23
+ if (str === '0.5' || str === '0.5 vcpu') return '512';
24
+ if (str === '1' || str === '1 vcpu') return '1024';
25
+ if (str === '2' || str === '2 vcpu') return '2048';
26
+ if (str === '4' || str === '4 vcpu') return '4096';
27
+ return str.replace(/[^0-9]/g, '') || '512';
28
+ }
29
+
30
+ /**
31
+ * Normalize Memory value to ECS Fargate MiB (512, 1024, 2048, etc.)
32
+ * @param {string|number} memory
33
+ * @returns {string}
34
+ */
35
+ function normalizeEcsMemory(memory) {
36
+ if (!memory) return '1024';
37
+ const str = String(memory).trim().toLowerCase();
38
+ if (str.endsWith('gb') || str.endsWith('g')) {
39
+ const num = parseFloat(str);
40
+ return String(Math.round(num * 1024));
41
+ }
42
+ if (str.endsWith('mib') || str.endsWith('mb') || str.endsWith('mi')) {
43
+ return str.replace(/[^0-9]/g, '') || '1024';
44
+ }
45
+ return str.replace(/[^0-9]/g, '') || '1024';
46
+ }
47
+
48
+ /**
49
+ * Generate apprunner.yaml for AWS App Runner
50
+ * @param {Object} options
51
+ * @param {string} options.appName
52
+ * @param {number} [options.port=7331]
53
+ * @param {string} [options.nodeVersion='nodejs20']
54
+ * @returns {string}
55
+ */
56
+ function generateAppRunnerYaml(options = {}) {
57
+ const {
58
+ port = 7331,
59
+ nodeVersion = 'nodejs20',
60
+ } = options;
61
+
62
+ return `# ==============================================================================
63
+ # AWS App Runner Configuration for Webspresso
64
+ # Fully-managed container & code runtime with auto-scaling and zero-ops TLS
65
+ # Docs: https://docs.aws.amazon.com/apprunner/latest/dg/config-file.html
66
+ # ==============================================================================
67
+
68
+ version: 1.0
69
+ runtime: ${nodeVersion}
70
+
71
+ build:
72
+ commands:
73
+ build:
74
+ - npm ci --omit=dev
75
+ - npm run build --if-present
76
+ env:
77
+ - name: NODE_ENV
78
+ value: production
79
+
80
+ run:
81
+ command: npm start
82
+ network:
83
+ port: "${port}"
84
+ env: PORT
85
+ env:
86
+ - name: NODE_ENV
87
+ value: production
88
+ - name: PORT
89
+ value: "${port}"
90
+ `;
91
+ }
92
+
93
+ /**
94
+ * Generate AWS ECS Task Definition JSON for AWS Fargate
95
+ * @param {Object} options
96
+ * @param {string} options.appName
97
+ * @param {string} [options.region='us-east-1']
98
+ * @param {string|number} [options.cpu='512']
99
+ * @param {string|number} [options.memory='1024']
100
+ * @param {number} [options.port=7331]
101
+ * @returns {string}
102
+ */
103
+ function generateEcsTaskDefinition(options = {}) {
104
+ const {
105
+ appName = 'webspresso-app',
106
+ region = 'us-east-1',
107
+ cpu = '512',
108
+ memory = '1024',
109
+ port = 7331,
110
+ } = options;
111
+
112
+ const normalizedCpu = normalizeEcsCpu(cpu);
113
+ const normalizedMemory = normalizeEcsMemory(memory);
114
+
115
+ const taskDef = {
116
+ family: appName,
117
+ networkMode: 'awsvpc',
118
+ requiresCompatibilities: ['FARGATE'],
119
+ cpu: normalizedCpu,
120
+ memory: normalizedMemory,
121
+ executionRoleArn: 'arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/ecsTaskExecutionRole',
122
+ taskRoleArn: 'arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/ecsTaskRole',
123
+ containerDefinitions: [
124
+ {
125
+ name: appName,
126
+ image: `YOUR_AWS_ACCOUNT_ID.dkr.ecr.${region}.amazonaws.com/${appName}:latest`,
127
+ essential: true,
128
+ portMappings: [
129
+ {
130
+ containerPort: port,
131
+ hostPort: port,
132
+ protocol: 'tcp',
133
+ },
134
+ ],
135
+ environment: [
136
+ { name: 'NODE_ENV', value: 'production' },
137
+ { name: 'PORT', value: String(port) },
138
+ ],
139
+ healthCheck: {
140
+ command: [
141
+ 'CMD-SHELL',
142
+ `node -e 'require("http").get("http://localhost:${port}/healthz", (r) => process.exit(r.statusCode === 200 ? 0 : 1))' || exit 1`,
143
+ ],
144
+ interval: 30,
145
+ timeout: 5,
146
+ retries: 3,
147
+ startPeriod: 15,
148
+ },
149
+ logConfiguration: {
150
+ logDriver: 'awslogs',
151
+ options: {
152
+ 'awslogs-group': `/ecs/${appName}`,
153
+ 'awslogs-region': region,
154
+ 'awslogs-stream-prefix': 'ecs',
155
+ 'awslogs-create-group': 'true',
156
+ },
157
+ },
158
+ },
159
+ ],
160
+ };
161
+
162
+ return JSON.stringify(taskDef, null, 2) + '\n';
163
+ }
164
+
165
+ /**
166
+ * Generate deploy-aws.sh script for automated deployment to ECR, App Runner, or ECS
167
+ * @param {Object} options
168
+ * @param {string} options.appName
169
+ * @param {string} [options.region='us-east-1']
170
+ * @param {number} [options.port=7331]
171
+ * @returns {string}
172
+ */
173
+ function generateAwsDeployScript(options = {}) {
174
+ const {
175
+ appName = 'webspresso-app',
176
+ region = 'us-east-1',
177
+ port = 7331,
178
+ } = options;
179
+
180
+ return `#!/usr/bin/env bash
181
+ set -euo pipefail
182
+
183
+ # ==============================================================================
184
+ # Webspresso AWS Deployment Automation Script
185
+ # Builds hardened Docker image, pushes to Amazon ECR, and updates App Runner / ECS
186
+ # Usage: ./deploy-aws.sh [APP_NAME] [REGION]
187
+ # ==============================================================================
188
+
189
+ APP_NAME="\${1:-${appName}}"
190
+ REGION="\${2:-\${AWS_REGION:-${region}}}"
191
+ PORT="\${PORT:-${port}}"
192
+
193
+ echo "☁️ Webspresso AWS Deployment Target: '\$APP_NAME' in '\$REGION'"
194
+
195
+ # 1. Verify AWS CLI
196
+ if ! command -v aws &> /dev/null; then
197
+ echo "❌ Error: 'aws' CLI is not installed or not in PATH."
198
+ echo "Please install AWS CLI: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
199
+ exit 1
200
+ fi
201
+
202
+ # 2. Verify Docker
203
+ if ! command -v docker &> /dev/null; then
204
+ echo "❌ Error: 'docker' is not installed or not running."
205
+ exit 1
206
+ fi
207
+
208
+ # 3. Retrieve AWS Account ID
209
+ AWS_ACCOUNT_ID="\$(aws sts get-caller-identity --query Account --output text 2>/dev/null || true)"
210
+ if [ -z "\$AWS_ACCOUNT_ID" ]; then
211
+ echo "❌ Error: Could not determine AWS Account ID. Are your AWS credentials configured?"
212
+ echo "Run: aws configure"
213
+ exit 1
214
+ fi
215
+
216
+ ECR_URI="\$AWS_ACCOUNT_ID.dkr.ecr.\$REGION.amazonaws.com/\$APP_NAME"
217
+ IMAGE_TAG="\$(git rev-parse --short HEAD 2>/dev/null || echo 'latest')"
218
+
219
+ echo "🔑 Authenticated as AWS Account: \$AWS_ACCOUNT_ID"
220
+ echo "📦 Target ECR Repository: \$ECR_URI"
221
+
222
+ # 4. Ensure ECR Repository exists
223
+ aws ecr describe-repositories --repository-names "\$APP_NAME" --region "\$REGION" &>/dev/null || {
224
+ echo "📁 Creating ECR repository '\$APP_NAME'..."
225
+ aws ecr create-repository \\
226
+ --repository-name "\$APP_NAME" \\
227
+ --image-scanning-configuration scanOnPush=true \\
228
+ --region "\$REGION" >/dev/null
229
+ }
230
+
231
+ # 5. Authenticate Docker with Amazon ECR
232
+ echo "🔐 Logging in to Amazon ECR..."
233
+ aws ecr get-login-password --region "\$REGION" | docker login --username AWS --password-stdin "\$AWS_ACCOUNT_ID.dkr.ecr.\$REGION.amazonaws.com"
234
+
235
+ # 6. Build production multi-stage Docker image
236
+ echo "🔨 Building Docker image..."
237
+ docker build -t "\$APP_NAME:\$IMAGE_TAG" -t "\$ECR_URI:\$IMAGE_TAG" -t "\$ECR_URI:latest" .
238
+
239
+ # 7. Push to ECR
240
+ echo "🚀 Pushing image to Amazon ECR..."
241
+ docker push "\$ECR_URI:\$IMAGE_TAG"
242
+ docker push "\$ECR_URI:latest"
243
+
244
+ echo ""
245
+ echo "✅ Image successfully built and pushed to Amazon ECR:"
246
+ echo " \$ECR_URI:\$IMAGE_TAG"
247
+ echo " \$ECR_URI:latest"
248
+ echo ""
249
+ echo "Next steps to complete deployment:"
250
+ echo " Option A (AWS App Runner):"
251
+ echo " aws apprunner create-service --service-name \$APP_NAME \\"
252
+ echo " --source-configuration ImageRepository='{ImageIdentifier=\"'\$ECR_URI:latest'\",ImageRepositoryType=\"ECR\",ImageConfiguration={Port=\"'\$PORT'\"}}'"
253
+ echo ""
254
+ echo " Option B (AWS ECS Fargate):"
255
+ echo " aws ecs register-task-definition --cli-input-json file://ecs-task-definition.json"
256
+ echo " aws ecs update-service --cluster \$APP_NAME-cluster --service \$APP_NAME-service --force-new-deployment"
257
+ echo ""
258
+ `;
259
+ }
260
+
261
+ /**
262
+ * Register command with Commander
263
+ * @param {import('commander').Command} program
264
+ */
265
+ function registerCommand(program) {
266
+ program
267
+ .command('add:aws')
268
+ .alias('aws')
269
+ .alias('add-aws')
270
+ .description('Generate production-ready AWS deployment configuration (App Runner & ECS Fargate)')
271
+ .option('-r, --region <region>', 'AWS region', 'us-east-1')
272
+ .option('--port <port>', 'Container port (Webspresso default is 7331)', (v) => parseInt(v, 10), 7331)
273
+ .option('--cpu <cpu>', 'CPU allocation for ECS/App Runner (e.g. 512, 1024 or 1 vCPU)', '512')
274
+ .option('--memory <memory>', 'Memory allocation for ECS/App Runner (e.g. 1024, 1GB)', '1024')
275
+ .option('-f, --force', 'Overwrite existing files without prompting', false)
276
+ .action(async (...actionArgs) => {
277
+ const cmd = actionArgs[actionArgs.length - 1];
278
+ const options = (cmd && typeof cmd.opts === 'function')
279
+ ? cmd.opts()
280
+ : (actionArgs.find((a) => a && typeof a === 'object') || {});
281
+
282
+ const cwd = process.cwd();
283
+ const pkgPath = path.join(cwd, 'package.json');
284
+
285
+ if (!fs.existsSync(pkgPath)) {
286
+ console.error('❌ Not a Webspresso project! package.json was not found in current directory.');
287
+ process.exit(1);
288
+ }
289
+
290
+ let pkg = {};
291
+ try {
292
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
293
+ } catch (err) {
294
+ console.error(`❌ Failed to parse package.json: ${err.message}`);
295
+ process.exit(1);
296
+ }
297
+
298
+ const appName = pkg.name ? pkg.name.replace(/[^a-zA-Z0-9_-]/g, '-') : 'webspresso-app';
299
+ const force = Boolean(options.force);
300
+ const region = options.region || 'us-east-1';
301
+ const port = options.port || 7331;
302
+ const cpu = options.cpu || '512';
303
+ const memory = options.memory || '1024';
304
+
305
+ const appRunnerPath = path.join(cwd, 'apprunner.yaml');
306
+ const ecsTaskDefPath = path.join(cwd, 'ecs-task-definition.json');
307
+ const scriptPath = path.join(cwd, 'deploy-aws.sh');
308
+ const dockerfilePath = path.join(cwd, 'Dockerfile');
309
+ const dockerignorePath = path.join(cwd, '.dockerignore');
310
+
311
+ console.log(`\n☁️ Scaffolding AWS deployment configuration for "${appName}"...`);
312
+
313
+ // 1. Ensure Dockerfile exists (needed by ECR & container deploy)
314
+ if (!fs.existsSync(dockerfilePath)) {
315
+ const dockerfileContent = generateDockerfile({ port, node: '22-alpine' });
316
+ fs.writeFileSync(dockerfilePath, dockerfileContent, 'utf8');
317
+ console.log(`✅ Created Dockerfile (configured for port ${port})`);
318
+ }
319
+
320
+ if (!fs.existsSync(dockerignorePath)) {
321
+ const dockerignoreContent = generateDockerignore();
322
+ fs.writeFileSync(dockerignorePath, dockerignoreContent, 'utf8');
323
+ console.log(`✅ Created .dockerignore`);
324
+ }
325
+
326
+ // 2. apprunner.yaml
327
+ if (fs.existsSync(appRunnerPath) && !force) {
328
+ console.log(`⚠️ ${path.basename(appRunnerPath)} already exists. Skipping (use --force to overwrite).`);
329
+ } else {
330
+ const appRunnerContent = generateAppRunnerYaml({
331
+ appName,
332
+ port,
333
+ });
334
+ fs.writeFileSync(appRunnerPath, appRunnerContent, 'utf8');
335
+ console.log(`✅ Created ${path.basename(appRunnerPath)} (AWS App Runner)`);
336
+ }
337
+
338
+ // 3. ecs-task-definition.json
339
+ if (fs.existsSync(ecsTaskDefPath) && !force) {
340
+ console.log(`⚠️ ${path.basename(ecsTaskDefPath)} already exists. Skipping (use --force to overwrite).`);
341
+ } else {
342
+ const taskDefContent = generateEcsTaskDefinition({
343
+ appName,
344
+ region,
345
+ cpu,
346
+ memory,
347
+ port,
348
+ });
349
+ fs.writeFileSync(ecsTaskDefPath, taskDefContent, 'utf8');
350
+ console.log(`✅ Created ${path.basename(ecsTaskDefPath)} (AWS ECS Fargate Task Definition)`);
351
+ }
352
+
353
+ // 4. deploy-aws.sh
354
+ if (fs.existsSync(scriptPath) && !force) {
355
+ console.log(`⚠️ ${path.basename(scriptPath)} already exists. Skipping (use --force to overwrite).`);
356
+ } else {
357
+ const scriptContent = generateAwsDeployScript({
358
+ appName,
359
+ region,
360
+ port,
361
+ });
362
+ fs.writeFileSync(scriptPath, scriptContent, { encoding: 'utf8', mode: 0o755 });
363
+ console.log(`✅ Created ${path.basename(scriptPath)} (executable)`);
364
+ }
365
+
366
+ // 5. Update package.json scripts
367
+ let modifiedPkg = false;
368
+ if (!pkg.scripts) pkg.scripts = {};
369
+ if (!pkg.scripts['aws:deploy']) {
370
+ pkg.scripts['aws:deploy'] = './deploy-aws.sh';
371
+ modifiedPkg = true;
372
+ }
373
+
374
+ if (modifiedPkg) {
375
+ try {
376
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
377
+ console.log(`✅ Added "aws:deploy" script to package.json`);
378
+ } catch (_) {}
379
+ }
380
+
381
+ console.log('\n🎉 AWS deployment configuration complete!');
382
+ console.log('\nNext steps:');
383
+ console.log(' 1. Configure AWS CLI: `aws configure`');
384
+ console.log(' 2. Review ecs-task-definition.json and apprunner.yaml');
385
+ console.log(' 3. Deploy container via ECR: `npm run aws:deploy`\n');
386
+ });
387
+ }
388
+
389
+ module.exports = {
390
+ registerCommand,
391
+ generateAppRunnerYaml,
392
+ generateEcsTaskDefinition,
393
+ generateAwsDeployScript,
394
+ normalizeEcsCpu,
395
+ normalizeEcsMemory,
396
+ };