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,53 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - console Command
5
+ * Launches interactive REPL console with preloaded framework context
6
+ * @module bin/commands/console
7
+ */
8
+
9
+ const { startConsole, evaluateInConsole } = require('../../core/console');
10
+
11
+ function registerCommand(program) {
12
+ program
13
+ .command('console')
14
+ .alias('c')
15
+ .description('Start interactive Webspresso REPL console with preloaded db, models, and services')
16
+ .option('-e, --eval <code-string>', 'Evaluate JavaScript code and print result, then exit')
17
+ .option('-E, --env <environment>', 'Environment name (default: development)')
18
+ .option('-c, --config <path>', 'Path to database config file (optional)')
19
+ .option('--no-history', 'Disable command history persistence')
20
+ .option('--no-banner', 'Do not display startup banner')
21
+ .action(async (options) => {
22
+ try {
23
+ if (options.eval) {
24
+ const { output, error } = await evaluateInConsole(options.eval, {
25
+ configPath: options.config,
26
+ env: options.env,
27
+ });
28
+ if (error) {
29
+ console.error(error.stack || error.message);
30
+ process.exit(1);
31
+ }
32
+ if (output !== undefined && output !== 'undefined') {
33
+ console.log(output);
34
+ }
35
+ process.exit(0);
36
+ } else {
37
+ await startConsole({
38
+ configPath: options.config,
39
+ env: options.env,
40
+ history: options.history,
41
+ banner: options.banner,
42
+ });
43
+ }
44
+ } catch (err) {
45
+ console.error('❌ Failed to start Webspresso console:', err.message);
46
+ process.exit(1);
47
+ }
48
+ });
49
+ }
50
+
51
+ module.exports = {
52
+ registerCommand,
53
+ };
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const { loadDbConfig, createDbInstance } = require('../utils/db');
7
+ const { runCloudMigration, freeStaleMigrationLock, getMigrationStatus } = require('../../core/orm/migrate-cloud');
7
8
 
8
9
  function registerCommand(program) {
9
10
  program
@@ -11,15 +12,59 @@ function registerCommand(program) {
11
12
  .description('Run pending database migrations')
12
13
  .option('-e, --env <environment>', 'Environment (development, production)', 'development')
13
14
  .option('-c, --config <path>', 'Path to database config file')
15
+ .option('--safe', 'Run in cloud-safe mode with connectivity retries and stale lock auto-healing')
16
+ .option('--dry-run', 'Preview pending migrations without applying them')
17
+ .option('--unlock-if-stale', 'Automatically release stale migration locks from aborted jobs')
18
+ .option('--force-unlock', 'Force release migration table lock immediately')
19
+ .option('--retries <n>', 'Database connection retry attempts', (v) => parseInt(v, 10), 5)
14
20
  .action(async (options) => {
15
21
  const { config, path: configPath } = loadDbConfig(options.config);
16
22
  console.log(`\n📦 Using config: ${configPath}`);
17
23
  console.log(` Environment: ${options.env}\n`);
18
24
 
19
25
  const knex = await createDbInstance(config, options.env);
20
-
26
+ const migrationConfig = config.migrations || {};
27
+
21
28
  try {
22
- const migrationConfig = config.migrations || {};
29
+ if (options.forceUnlock) {
30
+ console.log('🔓 Releasing migration lock table...');
31
+ await freeStaleMigrationLock(knex, migrationConfig);
32
+ console.log('✅ Migration lock freed.\n');
33
+ return;
34
+ }
35
+
36
+ if (options.safe || options.dryRun || options.unlockIfStale) {
37
+ const result = await runCloudMigration(knex, {
38
+ migrationConfig,
39
+ dryRun: Boolean(options.dryRun),
40
+ unlockIfStale: Boolean(options.unlockIfStale || options.safe),
41
+ waitForDb: Boolean(options.safe),
42
+ retries: options.retries || 5,
43
+ logger: (msg) => console.log(` → ${msg}`),
44
+ });
45
+
46
+ if (result.dryRun) {
47
+ if (result.pendingBefore.length === 0) {
48
+ console.log('✅ Dry-run: Database is up to date. No pending migrations.\n');
49
+ } else {
50
+ console.log(`\n🔍 Dry-run: ${result.pendingBefore.length} migration(s) pending:`);
51
+ for (const m of result.pendingBefore) {
52
+ console.log(` ○ ${m}`);
53
+ }
54
+ console.log('');
55
+ }
56
+ return;
57
+ }
58
+
59
+ if (result.applied.length === 0) {
60
+ console.log('✅ Already up to date.\n');
61
+ } else {
62
+ console.log(`\n✅ Done. ${result.applied.length} migration(s) completed in ${result.durationMs}ms.\n`);
63
+ }
64
+ return;
65
+ }
66
+
67
+ // Standard knex migration run
23
68
  const [batch, migrations] = await knex.migrate.latest(migrationConfig);
24
69
 
25
70
  if (migrations.length === 0) {
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI DB Studio Command
5
+ * Launches visual database browser and SQL console
6
+ * @module bin/commands/db-studio
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { loadDbConfig, createDbInstance } = require('../utils/db');
12
+ const { startStudio } = require('../../core/studio');
13
+
14
+ function registerCommand(program) {
15
+ program
16
+ .command('db:studio')
17
+ .alias('studio')
18
+ .description('Open interactive visual database browser and SQL runner in your browser')
19
+ .option('-p, --port <port>', 'Port to listen on', '4567')
20
+ .option('-H, --host <host>', 'Host to bind to', '127.0.0.1')
21
+ .option('-e, --env <environment>', 'Environment (development, production)', 'development')
22
+ .option('-c, --config <path>', 'Path to database config file')
23
+ .option('--open', 'Automatically open browser', true)
24
+ .option('--no-open', 'Do not open browser')
25
+ .action(async (options = {}) => {
26
+ const cwd = process.cwd();
27
+ const pkgPath = path.join(cwd, 'package.json');
28
+ if (!fs.existsSync(pkgPath)) {
29
+ console.error('❌ Not a Webspresso project! Run this command in your project directory.');
30
+ process.exit(1);
31
+ }
32
+
33
+ let dbConfig;
34
+ let configPath;
35
+
36
+ try {
37
+ const loaded = loadDbConfig(options.config);
38
+ dbConfig = loaded.config;
39
+ configPath = loaded.path;
40
+ } catch (err) {
41
+ console.error('❌ Database configuration could not be loaded:', err.message);
42
+ process.exit(1);
43
+ }
44
+
45
+ console.log(`\n📦 Using config: ${path.relative(cwd, configPath)} (${options.env})`);
46
+ const knex = await createDbInstance(dbConfig, options.env);
47
+
48
+ try {
49
+ const port = parseInt(options.port, 10) || 4567;
50
+ const host = options.host || '127.0.0.1';
51
+ const open = options.open !== false;
52
+
53
+ await startStudio({
54
+ knex,
55
+ port,
56
+ host,
57
+ open,
58
+ silent: false,
59
+ });
60
+
61
+ // Keep process open until interrupted
62
+ const cleanup = async () => {
63
+ console.log('\n🛑 Stopping DB Studio...');
64
+ try {
65
+ await knex.destroy();
66
+ } catch (_) {}
67
+ process.exit(0);
68
+ };
69
+
70
+ process.on('SIGINT', cleanup);
71
+ process.on('SIGTERM', cleanup);
72
+ } catch (err) {
73
+ console.error('❌ Failed to start DB Studio:', err.message);
74
+ try {
75
+ await knex.destroy();
76
+ } catch (_) {}
77
+ process.exit(1);
78
+ }
79
+ });
80
+ }
81
+
82
+ module.exports = {
83
+ registerCommand,
84
+ };
@@ -10,6 +10,26 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
12
12
 
13
+ function resolveProjectDbConfig(dir, targetEnv) {
14
+ for (const filename of ['webspresso.db.js', 'knexfile.js']) {
15
+ const full = path.resolve(dir || process.cwd(), filename);
16
+ if (fs.existsSync(full)) {
17
+ try {
18
+ if (fs.statSync(full).isFile()) {
19
+ const loaded = require(full);
20
+ const resolved = (loaded && typeof loaded === 'object') ? (loaded[targetEnv] || loaded) : null;
21
+ if (resolved && typeof resolved === 'object' && resolved.client) {
22
+ return resolved;
23
+ }
24
+ }
25
+ } catch {
26
+ // ignore
27
+ }
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+
13
33
  /**
14
34
  * Known weak secret patterns that should never be used in production
15
35
  */
@@ -65,10 +85,14 @@ async function runDeployCheck(options = {}) {
65
85
  const successes = [];
66
86
  const warnings = [];
67
87
  const errors = [];
88
+ const remediations = [];
68
89
 
69
- const addPass = (check, message) => successes.push({ check, message });
70
- const addWarn = (check, message) => warnings.push({ check, message });
71
- const addError = (check, message) => errors.push({ check, message });
90
+ const addPass = (check, message) => successes.push({ check, title: check, message });
91
+ const addWarn = (check, message) => warnings.push({ check, title: check, message });
92
+ const addError = (check, message) => errors.push({ check, title: check, message });
93
+ const addRemediation = (action) => {
94
+ if (!remediations.includes(action)) remediations.push(action);
95
+ };
72
96
 
73
97
  // Load environment variables from process.env and local .env files
74
98
  const localEnv = {
@@ -77,6 +101,25 @@ async function runDeployCheck(options = {}) {
77
101
  ...process.env,
78
102
  };
79
103
 
104
+ // ----------------------------------------------------
105
+ // Target Platform Detection
106
+ // ----------------------------------------------------
107
+ let targetPlatform = (options.platform || 'auto').toLowerCase();
108
+ const isPlatformAuto = targetPlatform === 'auto';
109
+ if (isPlatformAuto) {
110
+ if (localEnv.K_SERVICE || fs.existsSync(path.join(cwd, 'cloudbuild.yaml')) || fs.existsSync(path.join(cwd, '.gcloudignore'))) {
111
+ targetPlatform = 'cloudrun';
112
+ } else if (localEnv.AWS_EXECUTION_ENV || localEnv.AWS_LAMBDA_FUNCTION_NAME || fs.existsSync(path.join(cwd, 'apprunner.yaml')) || fs.existsSync(path.join(cwd, 'ecs-task-definition.json'))) {
113
+ targetPlatform = 'aws';
114
+ } else if (fs.existsSync(path.join(cwd, 'wrangler.toml'))) {
115
+ targetPlatform = 'cloudflare';
116
+ } else if (fs.existsSync(path.join(cwd, 'Dockerfile'))) {
117
+ targetPlatform = 'docker';
118
+ } else {
119
+ targetPlatform = 'generic';
120
+ }
121
+ }
122
+
80
123
  // ----------------------------------------------------
81
124
  // 1. Project & Server Basics
82
125
  // ----------------------------------------------------
@@ -194,22 +237,63 @@ async function runDeployCheck(options = {}) {
194
237
  const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith('.js') || f.endsWith('.ts'));
195
238
  addPass('Migrations', `Found ${files.length} migration file(s) in migrations/`);
196
239
 
197
- // Try verifying migration status if db config exists
198
- const dbConfig = resolveDbConfigIfExists(cwd, targetEnv);
240
+ // Try verifying migration status and connection pool if db config exists
241
+ const dbConfig = resolveProjectDbConfig(cwd, targetEnv);
199
242
  if (dbConfig) {
243
+ // Check connection pool for serverless/container scale-out
244
+ const isSqlite = dbConfig.client === 'better-sqlite3' || dbConfig.client === 'sqlite3';
245
+ if (!isSqlite && (dbConfig.client === 'pg' || dbConfig.client === 'mysql2' || dbConfig.client === 'mysql')) {
246
+ const pool = dbConfig.pool || {};
247
+ if (pool.max > 10) {
248
+ addWarn('DB Connection Pool', `DB pool max is ${pool.max}. Large pools per container can exhaust database connections when autoscaling. Consider max <= 5 or an external pooler.`);
249
+ } else if (pool.serverless || (pool.min === 0 && (pool.max || 5) <= 5)) {
250
+ addPass('DB Connection Pool', `Database connection pool configured for serverless scaling (min: ${pool.min ?? 0}, max: ${pool.max ?? 5})`);
251
+ }
252
+
253
+ if ((localEnv.K_SERVICE || localEnv.AWS_EXECUTION_ENV || pool.serverless) && pool.min > 0) {
254
+ addWarn('DB Connection Pool', `DB pool min is ${pool.min}. For serverless scale-to-zero, min: 0 is recommended so idle containers release connections.`);
255
+ }
256
+ }
257
+
200
258
  try {
201
- const db = createDbInstance(dbConfig);
202
- const status = await db.knex.migrate.status();
203
- if (status > 0) {
204
- addWarn('Pending Migrations', `There are ${status} pending database migration(s) to run (run: npx webspresso db:migrate)`);
205
- } else if (status === 0) {
206
- addPass('Pending Migrations', 'All database migrations are applied up to date');
259
+ const db = createDbInstance(dbConfig, targetEnv);
260
+ const knexInstance = db.knex || db;
261
+ if (knexInstance?.migrate?.status) {
262
+ const status = await knexInstance.migrate.status();
263
+ if (status > 0) {
264
+ addWarn('Pending Migrations', `There are ${status} pending database migration(s) to run (run: npx webspresso db:migrate)`);
265
+ } else if (status === 0) {
266
+ addPass('Pending Migrations', 'All database migrations are applied up to date');
267
+ }
268
+ }
269
+ if (knexInstance && typeof knexInstance.destroy === 'function') {
270
+ await knexInstance.destroy();
207
271
  }
208
- await db.knex.destroy();
209
272
  } catch (err) {
210
273
  addWarn('Database Connectivity', `Could not check migration status: ${err.message}`);
211
274
  }
212
275
  }
276
+
277
+ // Check cloud migration job setup for Cloud Run or AWS
278
+ if (targetPlatform === 'cloudrun') {
279
+ const hasJob = fs.existsSync(path.join(cwd, 'cloudrun-job.yaml')) ||
280
+ fs.existsSync(path.join(cwd, 'deploy-cloudrun-migration.sh')) ||
281
+ (fs.existsSync(path.join(cwd, 'cloudbuild.yaml')) && fs.readFileSync(path.join(cwd, 'cloudbuild.yaml'), 'utf8').includes('run jobs'));
282
+ if (hasJob) {
283
+ addPass('Cloud Migration Strategy', 'Cloud Run migration job configuration found');
284
+ } else {
285
+ addWarn('Cloud Migration Strategy', 'Running migrations inside scaled web containers can cause race conditions. Scaffold an isolated Cloud Run Job: npx webspresso add:migration-job --platform cloudrun');
286
+ }
287
+ } else if (targetPlatform === 'aws') {
288
+ const hasEcsTask = fs.existsSync(path.join(cwd, 'ecs-migration-task.json')) ||
289
+ fs.existsSync(path.join(cwd, 'deploy-aws-migration.sh')) ||
290
+ (fs.existsSync(path.join(cwd, 'apprunner.yaml')) && fs.readFileSync(path.join(cwd, 'apprunner.yaml'), 'utf8').includes('db:migrate'));
291
+ if (hasEcsTask) {
292
+ addPass('Cloud Migration Strategy', 'AWS ECS Task or App Runner pre-deploy migration configuration found');
293
+ } else {
294
+ addWarn('Cloud Migration Strategy', 'Recommend running migrations as an isolated ECS Task or App Runner pre-run: npx webspresso add:migration-job --platform aws');
295
+ }
296
+ }
213
297
  } else {
214
298
  addPass('Migrations', 'No migrations directory (skipping migration status check)');
215
299
  }
@@ -249,13 +333,146 @@ async function runDeployCheck(options = {}) {
249
333
  }
250
334
  }
251
335
 
336
+ // ----------------------------------------------------
337
+ // 8. Stateless Scale-Out & Session Store
338
+ // ----------------------------------------------------
339
+ const hasRedis = Boolean(localEnv.REDIS_URL || localEnv.SESSION_STORE === 'redis');
340
+ if (hasRedis) {
341
+ addPass('Distributed Session Store', `Distributed session store configured for scale-out (${localEnv.SESSION_STORE || 'Redis'})`);
342
+ } else if (targetEnv === 'production' && serverCode && (serverCode.includes('auth') || serverCode.includes('session'))) {
343
+ addWarn('Distributed Session Store', 'In-memory session store detected in production. For stateless scale-out (Cloud Run, AWS ECS/Fargate, Kubernetes), configure a distributed store via REDIS_URL=redis://... or SESSION_STORE=redis.');
344
+ }
345
+
346
+ // ----------------------------------------------------
347
+ // 9. Platform-Specific Cloud Preflight
348
+ // ----------------------------------------------------
349
+ const dbConfig = resolveProjectDbConfig(cwd, targetEnv);
350
+ const isSqlite = !dbConfig || dbConfig.client === 'better-sqlite3' || dbConfig.client === 'sqlite3';
351
+
352
+ // 9.1 Ephemeral Storage (Database & Uploads)
353
+ if (isSqlite && (targetPlatform === 'cloudrun' || targetPlatform === 'aws')) {
354
+ const pName = targetPlatform === 'cloudrun' ? 'Google Cloud Run' : 'AWS (App Runner / ECS)';
355
+ const dbRec = targetPlatform === 'cloudrun' ? 'Google Cloud SQL (PostgreSQL/MySQL)' : 'AWS RDS / Aurora';
356
+ addWarn(
357
+ 'Ephemeral Storage (Database)',
358
+ `${pName} uses an ephemeral container filesystem. Local SQLite databases will be lost on scale-down or redeploy. Migrate to ${dbRec}.`
359
+ );
360
+ addRemediation(`Configure managed database: Use ${dbRec} via webspresso.db.js or DATABASE_URL`);
361
+ }
362
+
363
+ if (serverCode && (serverCode.includes("storage: 'local'") || serverCode.includes('uploadPlugin({') || serverCode.includes('uploadPlugin()'))) {
364
+ const hasCloudStorage = serverCode.includes("storage: 'gcs'") || serverCode.includes("storage: 's3'") || serverCode.includes("storage: 'cloudflare'");
365
+ if (!hasCloudStorage && (targetPlatform === 'cloudrun' || targetPlatform === 'aws')) {
366
+ const pStorage = targetPlatform === 'cloudrun' ? 'Google Cloud Storage (GCS)' : 'AWS S3';
367
+ addWarn(
368
+ 'Ephemeral Storage (Uploads)',
369
+ `Local disk file upload storage detected. Container restarts on ${targetPlatform === 'cloudrun' ? 'Cloud Run' : 'AWS'} will cause uploaded files to be lost. Configure ${pStorage}.`
370
+ );
371
+ addRemediation(`Configure ${pStorage}: uploadPlugin({ storage: '${targetPlatform === 'cloudrun' ? 'gcs' : 's3'}' })`);
372
+ }
373
+ }
374
+
375
+ // 9.2 Shutdown Timeout Alignment
376
+ if (targetPlatform === 'cloudrun') {
377
+ if (serverCode) {
378
+ const match = serverCode.match(/timeout:\s*(\d+)/);
379
+ if (match) {
380
+ const timeoutMs = parseInt(match[1], 10);
381
+ if (timeoutMs > 10000) {
382
+ addWarn('Shutdown Timeout Alignment', `Webspresso shutdown timeout (${timeoutMs}ms) exceeds Cloud Run default SIGTERM deadline (10000ms). Container may be forcibly SIGKILLed before draining.`);
383
+ addRemediation('Set shutdown timeout <= 10000ms in createApp({ server: { shutdown: { timeout: 9000 } } })');
384
+ } else {
385
+ addPass('Shutdown Timeout Alignment', `Shutdown timeout (${timeoutMs}ms) matches Cloud Run 10s SIGTERM deadline`);
386
+ }
387
+ } else {
388
+ addPass('Shutdown Timeout Alignment', 'Default shutdown timeout (10s) compatible with Cloud Run SIGTERM deadline');
389
+ }
390
+ }
391
+ } else if (targetPlatform === 'aws') {
392
+ addPass('Shutdown Timeout Alignment', 'Default shutdown timeout (10s) well within AWS ECS 30s stopTimeout');
393
+ }
394
+
395
+ // 9.3 Platform Artifacts & Secrets Inspection
396
+ if (targetPlatform === 'cloudrun') {
397
+ const gcloudignorePath = path.join(cwd, '.gcloudignore');
398
+ if (fs.existsSync(gcloudignorePath)) {
399
+ const gignore = fs.readFileSync(gcloudignorePath, 'utf8');
400
+ if (!gignore.includes('.env')) {
401
+ addError('Google Cloud Ignore', '.env is NOT excluded in .gcloudignore! Secrets may leak into Google Cloud Build.');
402
+ addRemediation('Add .env to .gcloudignore');
403
+ } else {
404
+ addPass('Google Cloud Ignore', '.gcloudignore present and properly excludes .env');
405
+ }
406
+ } else {
407
+ addWarn('Google Cloud Ignore', '.gcloudignore not found (recommend running: npx webspresso add:cloudrun)');
408
+ addRemediation('Generate Cloud Run configuration: npx webspresso add:cloudrun');
409
+ }
410
+
411
+ const cloudbuildPath = path.join(cwd, 'cloudbuild.yaml');
412
+ if (fs.existsSync(cloudbuildPath)) {
413
+ addPass('Cloud Build Pipeline', 'cloudbuild.yaml CI/CD pipeline found');
414
+ } else {
415
+ addWarn('Cloud Build Pipeline', 'cloudbuild.yaml not found (run: npx webspresso add:cloudrun)');
416
+ }
417
+
418
+ if (serverCode) {
419
+ if (serverCode.includes('process.env.PORT') || serverCode.includes('PORT')) {
420
+ addPass('Cloud Run PORT Binding', 'Binds to dynamic process.env.PORT (required for Cloud Run 8080)');
421
+ } else {
422
+ addWarn('Cloud Run PORT Binding', 'Ensure server listens on process.env.PORT (Cloud Run assigns port 8080)');
423
+ addRemediation('Listen on process.env.PORT: app.listen(process.env.PORT || 3000)');
424
+ }
425
+ }
426
+ } else if (targetPlatform === 'aws') {
427
+ const apprunnerPath = path.join(cwd, 'apprunner.yaml');
428
+ const ecsTaskPath = path.join(cwd, 'ecs-task-definition.json');
429
+
430
+ if (fs.existsSync(apprunnerPath)) {
431
+ addPass('AWS App Runner Config', 'apprunner.yaml found');
432
+ }
433
+ if (fs.existsSync(ecsTaskPath)) {
434
+ try {
435
+ const ecsTask = JSON.parse(fs.readFileSync(ecsTaskPath, 'utf8'));
436
+ if (ecsTask.containerDefinitions?.[0]?.logConfiguration?.logDriver === 'awslogs') {
437
+ addPass('AWS CloudWatch Logs', 'CloudWatch logDriver (awslogs) configured in ECS task definition');
438
+ }
439
+ addPass('AWS ECS Task Definition', 'ecs-task-definition.json found and valid JSON');
440
+ } catch (err) {
441
+ addError('AWS ECS Task Definition', `ecs-task-definition.json is invalid JSON: ${err.message}`);
442
+ }
443
+ }
444
+
445
+ if (!fs.existsSync(apprunnerPath) && !fs.existsSync(ecsTaskPath)) {
446
+ addWarn('AWS Configuration', 'Neither apprunner.yaml nor ecs-task-definition.json found');
447
+ addRemediation('Generate AWS configuration: npx webspresso add:aws');
448
+ }
449
+ } else if (targetPlatform === 'cloudflare') {
450
+ const wranglerPath = path.join(cwd, 'wrangler.toml');
451
+ if (fs.existsSync(wranglerPath)) {
452
+ const wrangler = fs.readFileSync(wranglerPath, 'utf8');
453
+ if (wrangler.includes('nodejs_compat')) {
454
+ addPass('Cloudflare Worker Config', 'wrangler.toml found with nodejs_compat flag');
455
+ } else {
456
+ addWarn('Cloudflare Worker Config', 'wrangler.toml missing compatibility_flags = ["nodejs_compat"]');
457
+ addRemediation('Add compatibility_flags = ["nodejs_compat"] to wrangler.toml');
458
+ }
459
+ } else {
460
+ addWarn('Cloudflare Worker Config', 'wrangler.toml not found (run: npx webspresso add:cloudflare)');
461
+ addRemediation('Generate Cloudflare Worker files: npx webspresso add:cloudflare');
462
+ }
463
+ }
464
+
252
465
  const passed = errors.length === 0;
253
466
 
254
467
  return {
255
468
  passed,
469
+ platform: targetPlatform,
470
+ isPlatformAuto,
256
471
  successes,
472
+ passes: successes,
257
473
  warnings,
258
474
  errors,
475
+ remediations,
259
476
  };
260
477
  }
261
478
 
@@ -269,6 +486,7 @@ function registerCommand(program) {
269
486
  .alias('check:deploy')
270
487
  .description('Run production preflight check (secrets, container hardening, database, and optimizations)')
271
488
  .option('-e, --env <environment>', 'Target environment to evaluate (default: production)', 'production')
489
+ .option('-p, --platform <platform>', 'Target cloud platform (cloudrun, aws, cloudflare, docker, or auto)', 'auto')
272
490
  .option('--strict', 'Fail with exit code 1 if any warnings are detected', false)
273
491
  .option('--json', 'Output preflight results as JSON', false)
274
492
  .action(async (...actionArgs) => {
@@ -280,6 +498,7 @@ function registerCommand(program) {
280
498
  const result = await runDeployCheck({
281
499
  cwd: process.cwd(),
282
500
  env: options.env || 'production',
501
+ platform: options.platform || 'auto',
283
502
  });
284
503
 
285
504
  if (options.json) {
@@ -291,7 +510,9 @@ function registerCommand(program) {
291
510
  }
292
511
 
293
512
  console.log('\n🚀 Webspresso Production Deployment Preflight');
294
- console.log('=============================================\n');
513
+ console.log('=============================================');
514
+ console.log(`Target Platform : ${result.platform.toUpperCase()} ${result.isPlatformAuto ? '(auto-detected)' : ''}`);
515
+ console.log(`Environment : ${options.env || 'production'}\n`);
295
516
 
296
517
  if (result.successes.length > 0) {
297
518
  console.log('Passed Checks:');
@@ -317,6 +538,14 @@ function registerCommand(program) {
317
538
  console.log('');
318
539
  }
319
540
 
541
+ if (result.remediations && result.remediations.length > 0) {
542
+ console.log('Recommended Actions:');
543
+ for (const rem of result.remediations) {
544
+ console.log(` 👉 ${rem}`);
545
+ }
546
+ console.log('');
547
+ }
548
+
320
549
  console.log('---------------------------------------------');
321
550
  console.log(`Summary: ${result.successes.length} passed, ${result.warnings.length} warning(s), ${result.errors.length} error(s).\n`);
322
551
 
@@ -31,7 +31,7 @@ function registerCommand(program) {
31
31
  program
32
32
  .command('dev')
33
33
  .description('Start development server')
34
- .option('-p, --port <port>', 'Port number', '3000')
34
+ .option('-p, --port <port>', 'Port number', '7331')
35
35
  .option('--no-css', 'Skip CSS watch (if Tailwind is set up)')
36
36
  .option('--no-js', 'Skip client JS watch (if esbuild / watch:js is set up)')
37
37
  .action((options) => {
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI Export Command
5
+ * Prerender pages into static HTML files for static hosting
6
+ * @module bin/commands/export
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { exportStaticSite } = require('../../core/export');
12
+
13
+ function registerCommand(program) {
14
+ program
15
+ .command('export')
16
+ .description('Export and prerender application pages into static HTML files for Cloudflare Pages, S3, or Vercel')
17
+ .option('-o, --out <dir>', 'Output directory for exported files', 'dist')
18
+ .option('-b, --base-url <url>', 'Base URL for canonical links and sitemap.xml')
19
+ .option('-c, --clean', 'Clean output directory before export', true)
20
+ .option('--no-clean', 'Do not clean output directory before export')
21
+ .option('--no-assets', 'Skip copying public static assets')
22
+ .option('--no-clean-urls', 'Generate "path.html" instead of "path/index.html"')
23
+ .option('--sitemap', 'Generate sitemap.xml for prerendered routes', false)
24
+ .option('--concurrency <n>', 'Number of concurrent render requests', '5')
25
+ .option('--silent', 'Suppress log output', false)
26
+ .action(async (options = {}) => {
27
+ const cwd = process.cwd();
28
+ const pkgPath = path.join(cwd, 'package.json');
29
+ if (!fs.existsSync(pkgPath)) {
30
+ console.error('❌ Not a Webspresso project! Run this command in your project directory.');
31
+ process.exit(1);
32
+ }
33
+
34
+ const outDir = options.out || 'dist';
35
+ const clean = options.clean !== false;
36
+ const cleanUrls = options.cleanUrls !== false;
37
+ const copyAssets = options.assets !== false;
38
+ const sitemap = options.sitemap || Boolean(options.baseUrl);
39
+ const baseUrl = options.baseUrl;
40
+ const concurrency = parseInt(options.concurrency, 10) || 5;
41
+ const silent = options.silent === true;
42
+
43
+ // Check if project has a custom server.js
44
+ let serverConfig = {};
45
+ const serverJsPath = path.join(cwd, 'server.js');
46
+ if (fs.existsSync(serverJsPath)) {
47
+ try {
48
+ // If server.js exports a function or config object
49
+ const srv = require(serverJsPath);
50
+ if (srv && typeof srv === 'object') {
51
+ serverConfig = srv;
52
+ }
53
+ } catch (_) {}
54
+ }
55
+
56
+ try {
57
+ const result = await exportStaticSite({
58
+ cwd,
59
+ outDir,
60
+ clean,
61
+ cleanUrls,
62
+ copyAssets,
63
+ sitemap,
64
+ baseUrl,
65
+ concurrency,
66
+ silent,
67
+ serverConfig,
68
+ });
69
+
70
+ if (result.errors && result.errors.length > 0) {
71
+ console.error(`⚠️ Static export completed with ${result.errors.length} error(s).`);
72
+ process.exitCode = 1;
73
+ } else {
74
+ if (!silent) {
75
+ console.log(`✅ Static export succeeded: ${result.rendered} page(s) ready in "${outDir}".`);
76
+ }
77
+ }
78
+ } catch (err) {
79
+ console.error('❌ Static export failed:', err.message);
80
+ process.exit(1);
81
+ }
82
+ });
83
+ }
84
+
85
+ module.exports = {
86
+ registerCommand,
87
+ };
@@ -312,10 +312,10 @@ module.exports = { loadEnv };
312
312
 
313
313
  const envSchema = z.object({
314
314
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
315
- PORT: z.coerce.number().int().positive().default(3000),
315
+ PORT: z.coerce.number().int().positive().default(7331),
316
316
  DEFAULT_LOCALE: z.string().min(1).default('en'),
317
317
  SUPPORTED_LOCALES: z.string().min(1).default('en,de'),
318
- BASE_URL: z.string().url().default('http://localhost:3000'),
318
+ BASE_URL: z.string().url().default('http://localhost:7331'),
319
319
  DATABASE_URL: z.string().optional(),
320
320
  });
321
321
 
@@ -382,11 +382,11 @@ app.listen(env.PORT, () => {
382
382
 
383
383
  // Create .env.example (see config/load-env.js for merge order)
384
384
  let envExample = `# Copy to .env and adjust. Optional overrides: .env.local, .env.<NODE_ENV>, .env.<NODE_ENV>.local
385
- PORT=3000
385
+ PORT=7331
386
386
  NODE_ENV=development
387
387
  DEFAULT_LOCALE=en
388
388
  SUPPORTED_LOCALES=en,de
389
- BASE_URL=http://localhost:3000
389
+ BASE_URL=http://localhost:7331
390
390
  `;
391
391
 
392
392
  if (useDatabase && databaseType) {
@@ -645,7 +645,7 @@ cp .env.example .env
645
645
  npm run dev
646
646
  \`\`\`
647
647
 
648
- Visit http://localhost:3000
648
+ Visit http://localhost:7331
649
649
 
650
650
  ## Configuration
651
651