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,240 @@
1
+ /**
2
+ * Add Cloudflare Command
3
+ * Generate production-ready wrangler.toml and worker.js for Cloudflare Workers / Pages
4
+ * @module bin/commands/add-cloudflare
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ /**
13
+ * Generate wrangler.toml content
14
+ * @param {Object} options
15
+ * @param {string} options.appName
16
+ * @param {boolean} [options.d1=true]
17
+ * @param {boolean} [options.kv=false]
18
+ * @param {boolean} [options.queue=false]
19
+ * @param {boolean} [options.assets=true]
20
+ * @returns {string}
21
+ */
22
+ function generateWranglerToml({ appName, d1 = true, kv = false, queue = false, assets = true }) {
23
+ const lines = [
24
+ `# Cloudflare Workers configuration for Webspresso`,
25
+ `name = "${appName}"`,
26
+ `main = "worker.js"`,
27
+ `compatibility_date = "2024-09-23"`,
28
+ `compatibility_flags = ["nodejs_compat"]`,
29
+ ``,
30
+ ];
31
+
32
+ if (assets) {
33
+ lines.push(
34
+ `# Static asset serving via Workers Assets / Pages`,
35
+ `[assets]`,
36
+ `directory = "./public"`,
37
+ `binding = "ASSETS"`,
38
+ ``
39
+ );
40
+ }
41
+
42
+ if (d1) {
43
+ lines.push(
44
+ `# Cloudflare D1 Serverless SQL Database`,
45
+ `[[d1_databases]]`,
46
+ `binding = "DB"`,
47
+ `database_name = "${appName}-db"`,
48
+ `database_id = "YOUR_D1_DATABASE_ID_HERE"`,
49
+ `migrations_dir = "./migrations"`,
50
+ ``
51
+ );
52
+ }
53
+
54
+ if (kv) {
55
+ lines.push(
56
+ `# Cloudflare KV for fast distributed session storage`,
57
+ `[[kv_namespaces]]`,
58
+ `binding = "SESSIONS"`,
59
+ `id = "YOUR_KV_NAMESPACE_ID_HERE"`,
60
+ ``
61
+ );
62
+ }
63
+
64
+ if (queue) {
65
+ lines.push(
66
+ `# Cloudflare Queues for guaranteed background jobs`,
67
+ `[[queues.producers]]`,
68
+ `binding = "QUEUE"`,
69
+ `queue = "${appName}-queue"`,
70
+ ``,
71
+ `[[queues.consumers]]`,
72
+ `queue = "${appName}-queue"`,
73
+ `max_batch_size = 10`,
74
+ `max_batch_timeout = 5`,
75
+ ``
76
+ );
77
+ }
78
+
79
+ lines.push(
80
+ `[observability]`,
81
+ `enabled = true`,
82
+ ``
83
+ );
84
+
85
+ return lines.join('\n');
86
+ }
87
+
88
+ /**
89
+ * Generate worker.js edge bridge entry point
90
+ * @param {Object} options
91
+ * @param {boolean} [options.d1=true]
92
+ * @param {boolean} [options.queue=false]
93
+ * @returns {string}
94
+ */
95
+ function generateWorkerJs({ d1 = true, queue = false }) {
96
+ const d1Snippet = d1
97
+ ? ` // Connect Cloudflare D1 to Webspresso ORM if binding is available
98
+ let db;
99
+ if (env.DB) {
100
+ db = createDatabase({ client: 'd1', d1: env.DB });
101
+ }
102
+ `
103
+ : '';
104
+
105
+ const queueConsumer = queue
106
+ ? `
107
+ /**
108
+ * Process background jobs from Cloudflare Queues
109
+ */
110
+ async queue(batch, env, ctx) {
111
+ if (app.queue && typeof app.queue.processBatch === 'function') {
112
+ await app.queue.processBatch(batch);
113
+ }
114
+ },`
115
+ : '';
116
+
117
+ return `/**
118
+ * Cloudflare Workers Edge Entry Point for Webspresso
119
+ * Generated via \`npx webspresso add:cloudflare\`
120
+ */
121
+
122
+ import { createApp${d1 ? ', createDatabase' : ''} } from 'webspresso';
123
+
124
+ // Initialize Webspresso app with high-performance Hono Edge adapter
125
+ const app = createApp({
126
+ adapter: 'hono',
127
+ });
128
+
129
+ export default {
130
+ /**
131
+ * Handle incoming HTTP requests
132
+ */
133
+ async fetch(request, env, ctx) {
134
+ ${d1Snippet} return app.handle(request, {
135
+ env,
136
+ ctx,${d1 ? '\n db,' : ''}
137
+ waitUntil: (promise) => ctx.waitUntil(promise),
138
+ });
139
+ },${queueConsumer}
140
+ };
141
+ `;
142
+ }
143
+
144
+ /**
145
+ * Register command with Commander
146
+ * @param {import('commander').Command} program
147
+ */
148
+ function registerCommand(program) {
149
+ program
150
+ .command('add:cloudflare')
151
+ .alias('cloudflare')
152
+ .alias('add-cloudflare')
153
+ .description('Generate production-ready wrangler.toml and worker.js for Cloudflare Workers')
154
+ .option('--no-d1', 'Do not configure Cloudflare D1 database binding')
155
+ .option('--kv', 'Configure Cloudflare KV for session storage', false)
156
+ .option('--queue', 'Configure Cloudflare Queues for background tasks', false)
157
+ .option('-f, --force', 'Overwrite existing files without prompting', false)
158
+ .action(async (...actionArgs) => {
159
+ const cmd = actionArgs[actionArgs.length - 1];
160
+ const options = (cmd && typeof cmd.opts === 'function')
161
+ ? cmd.opts()
162
+ : (actionArgs.find((a) => a && typeof a === 'object') || {});
163
+
164
+ const cwd = process.cwd();
165
+ const pkgPath = path.join(cwd, 'package.json');
166
+
167
+ if (!fs.existsSync(pkgPath)) {
168
+ console.error('❌ Not a Webspresso project! package.json was not found in current directory.');
169
+ process.exit(1);
170
+ }
171
+
172
+ let pkg = {};
173
+ try {
174
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
175
+ } catch (err) {
176
+ console.error(`❌ Failed to parse package.json: ${err.message}`);
177
+ process.exit(1);
178
+ }
179
+
180
+ const appName = pkg.name ? pkg.name.replace(/[^a-zA-Z0-9_-]/g, '-') : 'webspresso-app';
181
+ const force = Boolean(options.force);
182
+ const d1 = options.d1 !== false;
183
+ const kv = Boolean(options.kv);
184
+ const queue = Boolean(options.queue);
185
+
186
+ const wranglerPath = path.join(cwd, 'wrangler.toml');
187
+ const workerPath = path.join(cwd, 'worker.js');
188
+
189
+ console.log(`\n☁️ Scaffolding Cloudflare Workers configuration for "${appName}"...`);
190
+
191
+ // 1. wrangler.toml
192
+ if (fs.existsSync(wranglerPath) && !force) {
193
+ console.log(`⚠️ ${path.basename(wranglerPath)} already exists. Skipping (use --force to overwrite).`);
194
+ } else {
195
+ const tomlContent = generateWranglerToml({ appName, d1, kv, queue, assets: true });
196
+ fs.writeFileSync(wranglerPath, tomlContent, 'utf8');
197
+ console.log(`✅ Created ${path.basename(wranglerPath)}`);
198
+ }
199
+
200
+ // 2. worker.js
201
+ if (fs.existsSync(workerPath) && !force) {
202
+ console.log(`⚠️ ${path.basename(workerPath)} already exists. Skipping (use --force to overwrite).`);
203
+ } else {
204
+ const workerContent = generateWorkerJs({ d1, queue });
205
+ fs.writeFileSync(workerPath, workerContent, 'utf8');
206
+ console.log(`✅ Created ${path.basename(workerPath)}`);
207
+ }
208
+
209
+ // 3. Update package.json scripts
210
+ let modifiedPkg = false;
211
+ if (!pkg.scripts) pkg.scripts = {};
212
+ if (!pkg.scripts['cf:dev']) {
213
+ pkg.scripts['cf:dev'] = 'wrangler dev';
214
+ modifiedPkg = true;
215
+ }
216
+ if (!pkg.scripts['cf:deploy']) {
217
+ pkg.scripts['cf:deploy'] = 'wrangler deploy';
218
+ modifiedPkg = true;
219
+ }
220
+
221
+ if (modifiedPkg) {
222
+ try {
223
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
224
+ console.log(`✅ Added "cf:dev" and "cf:deploy" scripts to package.json`);
225
+ } catch (_) {}
226
+ }
227
+
228
+ console.log('\n🎉 Cloudflare Workers setup complete!');
229
+ console.log('\nNext steps:');
230
+ console.log(' 1. Run `npx wrangler dev` to test locally on Cloudflare Workerd isolate');
231
+ console.log(' 2. Create your D1 database: `npx wrangler d1 create ' + appName + '-db`');
232
+ console.log(' 3. Deploy to the edge: `npx wrangler deploy`\n');
233
+ });
234
+ }
235
+
236
+ module.exports = {
237
+ registerCommand,
238
+ generateWranglerToml,
239
+ generateWorkerJs,
240
+ };
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Add Cloud Run Command
3
+ * Generate production-ready Google Cloud Run configuration (cloudbuild.yaml, .gcloudignore, deploy-cloudrun.sh)
4
+ * @module bin/commands/add-cloudrun
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { generateDockerfile, generateDockerignore } = require('./add-docker');
12
+
13
+ /**
14
+ * Generate cloudbuild.yaml for Google Cloud Build CI/CD
15
+ * @param {Object} options
16
+ * @param {string} options.appName
17
+ * @param {string} [options.region='us-central1']
18
+ * @param {string} [options.memory='512Mi']
19
+ * @param {string} [options.cpu='1']
20
+ * @param {number} [options.port=8080]
21
+ * @param {number} [options.minInstances=0]
22
+ * @param {number} [options.maxInstances=10]
23
+ * @param {string} [options.cloudSql]
24
+ * @returns {string}
25
+ */
26
+ function generateCloudBuildYaml(options = {}) {
27
+ const {
28
+ appName = 'webspresso-app',
29
+ region = 'us-central1',
30
+ memory = '512Mi',
31
+ cpu = '1',
32
+ port = 8080,
33
+ minInstances = 0,
34
+ maxInstances = 10,
35
+ cloudSql = '',
36
+ } = options;
37
+
38
+ const cloudSqlArgs = cloudSql
39
+ ? `\n - '--add-cloudsql-instances'\n - '${cloudSql}'`
40
+ : '';
41
+
42
+ return `# ==============================================================================
43
+ # Google Cloud Build Pipeline for Webspresso
44
+ # Builds hardened multi-stage Docker image and deploys to Google Cloud Run
45
+ # ==============================================================================
46
+
47
+ steps:
48
+ # 1. Build production container using multi-stage Dockerfile with cache
49
+ - name: 'gcr.io/cloud-builders/docker'
50
+ id: 'build-image'
51
+ args:
52
+ - 'build'
53
+ - '-t'
54
+ - '$_AR_LOCATION-docker.pkg.dev/$PROJECT_ID/$_AR_REPOSITORY/$_SERVICE_NAME:$COMMIT_SHA'
55
+ - '-t'
56
+ - '$_AR_LOCATION-docker.pkg.dev/$PROJECT_ID/$_AR_REPOSITORY/$_SERVICE_NAME:latest'
57
+ - '--cache-from'
58
+ - '$_AR_LOCATION-docker.pkg.dev/$PROJECT_ID/$_AR_REPOSITORY/$_SERVICE_NAME:latest'
59
+ - '.'
60
+
61
+ # 2. Push image to Google Artifact Registry
62
+ - name: 'gcr.io/cloud-builders/docker'
63
+ id: 'push-image'
64
+ args:
65
+ - 'push'
66
+ - '$_AR_LOCATION-docker.pkg.dev/$PROJECT_ID/$_AR_REPOSITORY/$_SERVICE_NAME:$COMMIT_SHA'
67
+
68
+ # 3. Deploy to Serverless Google Cloud Run
69
+ - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
70
+ id: 'deploy-cloud-run'
71
+ entrypoint: gcloud
72
+ args:
73
+ - 'run'
74
+ - 'deploy'
75
+ - '$_SERVICE_NAME'
76
+ - '--image'
77
+ - '$_AR_LOCATION-docker.pkg.dev/$PROJECT_ID/$_AR_REPOSITORY/$_SERVICE_NAME:$COMMIT_SHA'
78
+ - '--region'
79
+ - '$_REGION'
80
+ - '--platform'
81
+ - 'managed'
82
+ - '--allow-unauthenticated'
83
+ - '--port'
84
+ - '$_PORT'
85
+ - '--memory'
86
+ - '$_MEMORY'
87
+ - '--cpu'
88
+ - '$_CPU'
89
+ - '--min-instances'
90
+ - '$_MIN_INSTANCES'
91
+ - '--max-instances'
92
+ - '$_MAX_INSTANCES'
93
+ - '--execution-environment'
94
+ - 'gen2'
95
+ - '--set-env-vars'
96
+ - 'NODE_ENV=production,PORT=$_PORT'${cloudSqlArgs}
97
+
98
+ substitutions:
99
+ _SERVICE_NAME: '${appName}'
100
+ _REGION: '${region}'
101
+ _AR_LOCATION: '${region}'
102
+ _AR_REPOSITORY: 'webspresso'
103
+ _PORT: '${port}'
104
+ _MEMORY: '${memory}'
105
+ _CPU: '${cpu}'
106
+ _MIN_INSTANCES: '${minInstances}'
107
+ _MAX_INSTANCES: '${maxInstances}'
108
+
109
+ images:
110
+ - '$_AR_LOCATION-docker.pkg.dev/$PROJECT_ID/$_AR_REPOSITORY/$_SERVICE_NAME:$COMMIT_SHA'
111
+ - '$_AR_LOCATION-docker.pkg.dev/$PROJECT_ID/$_AR_REPOSITORY/$_SERVICE_NAME:latest'
112
+
113
+ options:
114
+ logging: CLOUD_LOGGING_ONLY
115
+ `;
116
+ }
117
+
118
+ /**
119
+ * Generate .gcloudignore file content
120
+ * @returns {string}
121
+ */
122
+ function generateGCloudIgnore() {
123
+ return `# ==============================================================================
124
+ # Google Cloud Ignore for Webspresso
125
+ # Prevents sensitive files and dev artifacts from uploading to Cloud Build
126
+ # ==============================================================================
127
+
128
+ .gcloudignore
129
+ .git
130
+ .gitignore
131
+ .github
132
+
133
+ # Dependencies and caches
134
+ node_modules
135
+ npm-debug.log*
136
+ yarn-debug.log*
137
+ yarn-error.log*
138
+ pnpm-debug.log*
139
+
140
+ # Environment & Secrets (Never upload secrets into container build context)
141
+ .env
142
+ .env.*
143
+ !.env.example
144
+
145
+ # Local SQLite databases (Ephemeral on Cloud Run)
146
+ *.sqlite
147
+ *.sqlite3
148
+ *.db
149
+ *.db-journal
150
+ *.db-wal
151
+ *.db-shm
152
+ data/*.sqlite*
153
+
154
+ # Test, coverage & documentation
155
+ tests
156
+ coverage
157
+ playwright-report
158
+ test-results
159
+ docs
160
+ .agents
161
+
162
+ # IDE files
163
+ .idea
164
+ .vscode
165
+ *.swp
166
+ .DS_Store
167
+ `;
168
+ }
169
+
170
+ /**
171
+ * Generate deploy-cloudrun.sh one-line deployment shell script
172
+ * @param {Object} options
173
+ * @param {string} options.appName
174
+ * @param {string} [options.region='us-central1']
175
+ * @param {string} [options.memory='512Mi']
176
+ * @param {string} [options.cpu='1']
177
+ * @param {number} [options.port=8080]
178
+ * @param {number} [options.minInstances=0]
179
+ * @param {number} [options.maxInstances=10]
180
+ * @param {string} [options.cloudSql]
181
+ * @returns {string}
182
+ */
183
+ function generateDeployScript(options = {}) {
184
+ const {
185
+ appName = 'webspresso-app',
186
+ region = 'us-central1',
187
+ memory = '512Mi',
188
+ cpu = '1',
189
+ port = 8080,
190
+ minInstances = 0,
191
+ maxInstances = 10,
192
+ cloudSql = '',
193
+ } = options;
194
+
195
+ const cloudSqlFlag = cloudSql ? ` \\\n --add-cloudsql-instances "${cloudSql}"` : '';
196
+
197
+ return `#!/usr/bin/env bash
198
+ set -euo pipefail
199
+
200
+ # ==============================================================================
201
+ # Webspresso Google Cloud Run Quick Deploy Script
202
+ # Usage: ./deploy-cloudrun.sh [SERVICE_NAME] [REGION]
203
+ # ==============================================================================
204
+
205
+ SERVICE_NAME="\${1:-${appName}}"
206
+ REGION="\${2:-${region}}"
207
+ PORT="\${PORT:-${port}}"
208
+ MEMORY="\${MEMORY:-${memory}}"
209
+ CPU="\${CPU:-${cpu}}"
210
+ MIN_INSTANCES="\${MIN_INSTANCES:-${minInstances}}"
211
+ MAX_INSTANCES="\${MAX_INSTANCES:-${maxInstances}}"
212
+
213
+ echo "🚀 Deploying '\$SERVICE_NAME' to Google Cloud Run in '\$REGION'..."
214
+
215
+ gcloud run deploy "\$SERVICE_NAME" \\
216
+ --source . \\
217
+ --region "\$REGION" \\
218
+ --platform managed \\
219
+ --allow-unauthenticated \\
220
+ --port "\$PORT" \\
221
+ --memory "\$MEMORY" \\
222
+ --cpu "\$CPU" \\
223
+ --min-instances "\$MIN_INSTANCES" \\
224
+ --max-instances "\$MAX_INSTANCES" \\
225
+ --execution-environment gen2 \\
226
+ --set-env-vars "NODE_ENV=production,PORT=\$PORT"${cloudSqlFlag}
227
+
228
+ echo "✅ Webspresso app '\$SERVICE_NAME' deployed successfully to Cloud Run!"
229
+ `;
230
+ }
231
+
232
+ /**
233
+ * Register command with Commander
234
+ * @param {import('commander').Command} program
235
+ */
236
+ function registerCommand(program) {
237
+ program
238
+ .command('add:cloudrun')
239
+ .alias('cloudrun')
240
+ .alias('add-cloudrun')
241
+ .description('Generate production-ready Google Cloud Run deployment configuration')
242
+ .option('-r, --region <region>', 'Google Cloud region', 'us-central1')
243
+ .option('--memory <memory>', 'Container memory allocation', '512Mi')
244
+ .option('--cpu <cpu>', 'Container CPU allocation', '1')
245
+ .option('--port <port>', 'Container port (Cloud Run defaults to 8080)', (v) => parseInt(v, 10), 8080)
246
+ .option('--min-instances <num>', 'Minimum container instances', (v) => parseInt(v, 10), 0)
247
+ .option('--max-instances <num>', 'Maximum container instances', (v) => parseInt(v, 10), 10)
248
+ .option('--cloud-sql <instance>', 'Cloud SQL instance connection name (e.g. project:region:instance)', '')
249
+ .option('-f, --force', 'Overwrite existing files without prompting', false)
250
+ .action(async (...actionArgs) => {
251
+ const cmd = actionArgs[actionArgs.length - 1];
252
+ const options = (cmd && typeof cmd.opts === 'function')
253
+ ? cmd.opts()
254
+ : (actionArgs.find((a) => a && typeof a === 'object') || {});
255
+
256
+ const cwd = process.cwd();
257
+ const pkgPath = path.join(cwd, 'package.json');
258
+
259
+ if (!fs.existsSync(pkgPath)) {
260
+ console.error('❌ Not a Webspresso project! package.json was not found in current directory.');
261
+ process.exit(1);
262
+ }
263
+
264
+ let pkg = {};
265
+ try {
266
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
267
+ } catch (err) {
268
+ console.error(`❌ Failed to parse package.json: ${err.message}`);
269
+ process.exit(1);
270
+ }
271
+
272
+ const appName = pkg.name ? pkg.name.replace(/[^a-zA-Z0-9_-]/g, '-') : 'webspresso-app';
273
+ const force = Boolean(options.force);
274
+ const region = options.region || 'us-central1';
275
+ const memory = options.memory || '512Mi';
276
+ const cpu = String(options.cpu || '1');
277
+ const port = options.port || 8080;
278
+ const minInstances = options.minInstances ?? 0;
279
+ const maxInstances = options.maxInstances ?? 10;
280
+ const cloudSql = options.cloudSql || '';
281
+
282
+ const cloudbuildPath = path.join(cwd, 'cloudbuild.yaml');
283
+ const gcloudignorePath = path.join(cwd, '.gcloudignore');
284
+ const scriptPath = path.join(cwd, 'deploy-cloudrun.sh');
285
+ const dockerfilePath = path.join(cwd, 'Dockerfile');
286
+ const dockerignorePath = path.join(cwd, '.dockerignore');
287
+
288
+ console.log(`\n☁️ Scaffolding Google Cloud Run configuration for "${appName}"...`);
289
+
290
+ // 1. Ensure Dockerfile exists (needed by Cloud Run / Cloud Build)
291
+ if (!fs.existsSync(dockerfilePath)) {
292
+ const dockerfileContent = generateDockerfile({ port, node: '22-alpine' });
293
+ fs.writeFileSync(dockerfilePath, dockerfileContent, 'utf8');
294
+ console.log(`✅ Created Dockerfile (configured for port ${port})`);
295
+ }
296
+
297
+ if (!fs.existsSync(dockerignorePath)) {
298
+ const dockerignoreContent = generateDockerignore();
299
+ fs.writeFileSync(dockerignorePath, dockerignoreContent, 'utf8');
300
+ console.log(`✅ Created .dockerignore`);
301
+ }
302
+
303
+ // 2. cloudbuild.yaml
304
+ if (fs.existsSync(cloudbuildPath) && !force) {
305
+ console.log(`⚠️ ${path.basename(cloudbuildPath)} already exists. Skipping (use --force to overwrite).`);
306
+ } else {
307
+ const yamlContent = generateCloudBuildYaml({
308
+ appName,
309
+ region,
310
+ memory,
311
+ cpu,
312
+ port,
313
+ minInstances,
314
+ maxInstances,
315
+ cloudSql,
316
+ });
317
+ fs.writeFileSync(cloudbuildPath, yamlContent, 'utf8');
318
+ console.log(`✅ Created ${path.basename(cloudbuildPath)}`);
319
+ }
320
+
321
+ // 3. .gcloudignore
322
+ if (fs.existsSync(gcloudignorePath) && !force) {
323
+ console.log(`⚠️ ${path.basename(gcloudignorePath)} already exists. Skipping (use --force to overwrite).`);
324
+ } else {
325
+ const ignoreContent = generateGCloudIgnore();
326
+ fs.writeFileSync(gcloudignorePath, ignoreContent, 'utf8');
327
+ console.log(`✅ Created ${path.basename(gcloudignorePath)}`);
328
+ }
329
+
330
+ // 4. deploy-cloudrun.sh
331
+ if (fs.existsSync(scriptPath) && !force) {
332
+ console.log(`⚠️ ${path.basename(scriptPath)} already exists. Skipping (use --force to overwrite).`);
333
+ } else {
334
+ const scriptContent = generateDeployScript({
335
+ appName,
336
+ region,
337
+ memory,
338
+ cpu,
339
+ port,
340
+ minInstances,
341
+ maxInstances,
342
+ cloudSql,
343
+ });
344
+ fs.writeFileSync(scriptPath, scriptContent, { encoding: 'utf8', mode: 0o755 });
345
+ console.log(`✅ Created ${path.basename(scriptPath)} (executable)`);
346
+ }
347
+
348
+ // 5. Update package.json scripts
349
+ let modifiedPkg = false;
350
+ if (!pkg.scripts) pkg.scripts = {};
351
+ if (!pkg.scripts['gcp:deploy']) {
352
+ pkg.scripts['gcp:deploy'] = './deploy-cloudrun.sh';
353
+ modifiedPkg = true;
354
+ }
355
+ if (!pkg.scripts['gcp:build']) {
356
+ pkg.scripts['gcp:build'] = 'gcloud builds submit --config=cloudbuild.yaml';
357
+ modifiedPkg = true;
358
+ }
359
+
360
+ if (modifiedPkg) {
361
+ try {
362
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
363
+ console.log(`✅ Added "gcp:deploy" and "gcp:build" scripts to package.json`);
364
+ } catch (_) {}
365
+ }
366
+
367
+ console.log('\n🎉 Google Cloud Run setup complete!');
368
+ console.log('\nNext steps:');
369
+ console.log(' 1. Authenticate with GCP: `gcloud auth login`');
370
+ console.log(' 2. Set active project: `gcloud config set project YOUR_PROJECT_ID`');
371
+ console.log(' 3. Deploy directly: `npm run gcp:deploy` or `gcloud run deploy`');
372
+ console.log(' 4. Or build via Cloud Build: `npm run gcp:build`\n');
373
+ });
374
+ }
375
+
376
+ module.exports = {
377
+ registerCommand,
378
+ generateCloudBuildYaml,
379
+ generateGCloudIgnore,
380
+ generateDeployScript,
381
+ };
@@ -56,13 +56,13 @@ function detectDatabase(cwd, pkg = {}) {
56
56
  * Generate production-grade multi-stage Dockerfile
57
57
  * @param {Object} options
58
58
  * @param {string} [options.node='22-alpine']
59
- * @param {number} [options.port=3000]
59
+ * @param {number} [options.port=7331]
60
60
  * @param {boolean} [options.hasBuildScript=false]
61
61
  * @returns {string}
62
62
  */
63
63
  function generateDockerfile(options = {}) {
64
64
  const nodeImage = normalizeNodeVersion(options.node || '22-alpine');
65
- const port = options.port || 3000;
65
+ const port = options.port || 7331;
66
66
  const isAlpine = nodeImage.includes('alpine');
67
67
 
68
68
  return `# ==========================================
@@ -198,7 +198,7 @@ public/uploads/*
198
198
  * @param {string} options.appName
199
199
  * @param {'postgres'|'mysql'|'sqlite'|'none'} options.db
200
200
  * @param {boolean} [options.redis=false]
201
- * @param {number} [options.port=3000]
201
+ * @param {number} [options.port=7331]
202
202
  * @returns {string}
203
203
  */
204
204
  function generateDockerCompose(options = {}) {
@@ -206,7 +206,7 @@ function generateDockerCompose(options = {}) {
206
206
  appName = 'webspresso-app',
207
207
  db = 'sqlite',
208
208
  redis = false,
209
- port = 3000,
209
+ port = 7331,
210
210
  } = options;
211
211
 
212
212
  let dbService = '';
@@ -338,7 +338,7 @@ function registerCommand(program) {
338
338
  .option('-n, --node <version>', 'Node.js base image version (default: 22-alpine)', '22-alpine')
339
339
  .option('-d, --db <type>', 'Database service to configure: postgres, mysql, sqlite, none (auto-detected by default)')
340
340
  .option('-r, --redis', 'Include Redis service in docker-compose.yml', false)
341
- .option('-p, --port <port>', 'Application container port (default: 3000)', '3000')
341
+ .option('-p, --port <port>', 'Application container port (default: 7331)', '7331')
342
342
  .option('-f, --force', 'Overwrite existing Dockerfile and docker-compose.yml without prompting', false)
343
343
  .action(async (...actionArgs) => {
344
344
  const cmd = actionArgs[actionArgs.length - 1];
@@ -363,7 +363,7 @@ function registerCommand(program) {
363
363
  }
364
364
 
365
365
  const appName = pkg.name ? pkg.name.replace(/[^a-zA-Z0-9_-]/g, '-') : 'webspresso-app';
366
- const port = parseInt(options.port || '3000', 10) || 3000;
366
+ const port = parseInt(options.port || '7331', 10) || 7331;
367
367
  const dbType = options.db || detectDatabase(cwd, pkg);
368
368
  const isRedis = Boolean(options.redis);
369
369
  const nodeVersion = options.node || '22-alpine';