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.
- package/.agents/AGENTS.md +21 -2
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +220 -8
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/FLAGS.md +118 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/LOGGING.md +93 -0
- package/.agents/METRICS.md +127 -0
- package/.agents/MIGRATION_JOBS.md +95 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +38 -3
- package/.agents/QUEUE.md +42 -0
- package/.agents/RATE_LIMIT.md +147 -0
- package/.agents/RESILIENCE.md +235 -0
- package/.agents/SCALE_OUT.md +109 -0
- package/.agents/SECRETS.md +117 -0
- package/AGENTS.md +16 -0
- package/README.md +2 -2
- package/bin/commands/add-aws.js +396 -0
- package/bin/commands/add-ci.js +523 -0
- package/bin/commands/add-cloudflare.js +240 -0
- package/bin/commands/add-cloudrun.js +381 -0
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/add-migration-job.js +396 -0
- package/bin/commands/assets-sync.js +209 -0
- package/bin/commands/client-generate.js +52 -0
- package/bin/commands/console.js +53 -0
- package/bin/commands/db-migrate.js +47 -2
- package/bin/commands/db-studio.js +84 -0
- package/bin/commands/deploy-check.js +242 -13
- package/bin/commands/dev.js +1 -1
- package/bin/commands/export.js +87 -0
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/secrets-pull.js +208 -0
- package/bin/commands/start.js +1 -1
- package/bin/utils/db.js +7 -1
- package/bin/utils/project.js +2 -2
- package/bin/webspresso.js +22 -0
- package/core/app.js +421 -0
- package/core/auth/authenticate.js +93 -0
- package/core/auth/hash.js +5 -4
- package/core/auth/index.js +34 -4
- package/core/auth/jwt.js +5 -54
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +53 -10
- package/core/auth/middleware.js +16 -68
- package/core/auth/oauth.js +8 -17
- package/core/auth/redis-session.js +1008 -0
- package/core/auth/sessions.js +156 -0
- package/core/auth/totp.js +2 -1
- package/core/cache/byte-size.js +123 -0
- package/core/cache/cdn.js +296 -0
- package/core/cache/index.js +33 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +354 -0
- package/core/commands/registry.js +6 -20
- package/core/console/commands.js +148 -0
- package/core/console/context.js +260 -0
- package/core/console/eval.js +70 -0
- package/core/console/index.js +22 -0
- package/core/console/repl.js +102 -0
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +49 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +9 -7
- package/core/events/signal.js +7 -0
- package/core/export/discovery.js +191 -0
- package/core/export/exporter.js +277 -0
- package/core/export/index.js +24 -0
- package/core/flags/evaluator.js +252 -0
- package/core/flags/hasher.js +37 -0
- package/core/flags/index.js +21 -0
- package/core/flags/manager.js +263 -0
- package/core/flags/store.js +126 -0
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/http/adapter.js +74 -0
- package/core/http/express-adapter.js +46 -0
- package/core/http/hono-adapter.js +531 -0
- package/core/http/index.js +41 -0
- package/core/http/pipeline.js +485 -0
- package/core/index.js +336 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +325 -0
- package/core/limits/normalize.js +222 -0
- package/core/logger/formatters.js +264 -0
- package/core/logger/index.js +260 -0
- package/core/logger/trace.js +137 -0
- package/core/metrics/collectors.js +125 -0
- package/core/metrics/index.js +45 -0
- package/core/metrics/middleware.js +230 -0
- package/core/metrics/registry.js +366 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/d1.js +265 -0
- package/core/orm/index.js +176 -13
- package/core/orm/json-fields.js +22 -8
- package/core/orm/migrate-cloud.js +194 -0
- package/core/orm/model.js +10 -0
- package/core/orm/pool.js +97 -0
- package/core/orm/query-builder.js +72 -9
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/cloudflare.js +131 -0
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/adapters/pubsub.js +400 -0
- package/core/queue/adapters/sqs.js +419 -0
- package/core/queue/adapters/wait-until.js +77 -0
- package/core/queue/index.js +8 -0
- package/core/queue/manager.js +66 -41
- package/core/rate-limit/index.js +22 -0
- package/core/rate-limit/limiter.js +219 -0
- package/core/rate-limit/stores/memory.js +129 -0
- package/core/rate-limit/stores/redis.js +151 -0
- package/core/rate-limit/utils.js +112 -0
- package/core/resilience/circuit-breaker.js +422 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/secrets/index.js +113 -0
- package/core/secrets/manager.js +308 -0
- package/core/secrets/providers/aws.js +288 -0
- package/core/secrets/providers/env.js +44 -0
- package/core/secrets/providers/gcp.js +307 -0
- package/core/secrets/providers/memory.js +76 -0
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +129 -19
- package/core/stream/index.js +19 -0
- package/core/stream/text-stream.js +326 -0
- package/core/studio/index.js +43 -0
- package/core/studio/inspector.js +258 -0
- package/core/studio/server.js +325 -0
- package/core/studio/ui.js +681 -0
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +118 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +6 -5
- package/core/utils/fs.js +144 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +1549 -2
- package/index.js +272 -2
- package/package.json +3 -2
- package/plugins/admin-panel/core/api-extensions.js +2 -5
- package/plugins/admin-panel/index.js +13 -6
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -3
- package/plugins/api-key/index.js +3 -2
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +6 -15
- package/plugins/audit-log/index.js +3 -1
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +3 -2
- package/plugins/cors.js +3 -2
- package/plugins/csrf/index.js +9 -12
- package/plugins/csv/index.js +8 -5
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/email/index.js +11 -9
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +5 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/polar/src/api.js +2 -1
- package/plugins/polar/src/webhooks.js +5 -6
- package/plugins/queue/index.js +18 -2
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/redirect/index.js +16 -9
- package/plugins/rls/index.js +170 -0
- package/plugins/seo-checker/index.js +2 -2
- package/plugins/site-analytics/index.js +3 -1
- package/plugins/site-analytics/tracking.js +2 -2
- package/plugins/sitemap.js +3 -2
- package/plugins/swagger.js +12 -5
- package/plugins/upload/gcs-provider.js +148 -0
- package/plugins/upload/index.js +136 -77
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/upload/s3-provider.js +257 -0
- package/plugins/webhooks/crypto.js +6 -11
- package/plugins/webhooks/dispatcher.js +8 -8
- package/plugins/webhooks/index.js +33 -23
- package/plugins/webhooks/matcher.js +7 -11
- package/plugins/webhooks/sender.js +8 -11
- package/plugins/xlsx/index.js +7 -5
- package/src/api/api-loader.js +12 -95
- package/src/api/define-api.js +1 -0
- package/src/app-context.js +41 -5
- package/src/client-runtime/constants.js +10 -0
- package/src/client-runtime/mount.js +2 -3
- package/src/dev/banner.js +6 -2
- package/src/dev/client-gen.js +458 -0
- package/src/discovery/scan-routes.js +5 -2
- package/src/file-router.js +3 -3
- package/src/helpers.js +212 -24
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +13 -154
- package/src/plugin-manager.js +99 -18
- package/src/server.js +335 -133
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +95 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +15 -1
- package/src/services/validator.js +38 -20
- 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=
|
|
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 ||
|
|
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=
|
|
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 =
|
|
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:
|
|
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 || '
|
|
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';
|