@akash-chowdhury-24/deployhub 1.0.0

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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/install.ps1 +55 -0
  4. package/install.sh +99 -0
  5. package/package.json +86 -0
  6. package/src/adapters/dotnet.adapter.js +41 -0
  7. package/src/adapters/go.adapter.js +40 -0
  8. package/src/adapters/index.js +48 -0
  9. package/src/adapters/java.adapter.js +46 -0
  10. package/src/adapters/node.adapter.js +77 -0
  11. package/src/adapters/php.adapter.js +43 -0
  12. package/src/adapters/python.adapter.js +54 -0
  13. package/src/adapters/rails.adapter.js +66 -0
  14. package/src/artifact/engine.js +473 -0
  15. package/src/cli/index.js +45 -0
  16. package/src/commands/artifact.js +88 -0
  17. package/src/commands/build.js +44 -0
  18. package/src/commands/clean.js +50 -0
  19. package/src/commands/deploy.js +82 -0
  20. package/src/commands/doctor.js +630 -0
  21. package/src/commands/init.js +795 -0
  22. package/src/commands/logs.js +33 -0
  23. package/src/commands/rollback.js +50 -0
  24. package/src/commands/storage.js +116 -0
  25. package/src/commands/update.js +63 -0
  26. package/src/commands/verify.js +55 -0
  27. package/src/core/config.js +168 -0
  28. package/src/core/pipeline.js +77 -0
  29. package/src/core/stages.js +210 -0
  30. package/src/deployment/index.js +156 -0
  31. package/src/deployment/providers/azure-vm.js +7 -0
  32. package/src/deployment/providers/docker.js +30 -0
  33. package/src/deployment/providers/ec2.js +7 -0
  34. package/src/deployment/providers/gcp-vm.js +7 -0
  35. package/src/deployment/providers/kubernetes.js +30 -0
  36. package/src/deployment/providers/platforms/_shared.js +167 -0
  37. package/src/deployment/providers/platforms/aws-amplify.js +164 -0
  38. package/src/deployment/providers/platforms/azure-static-web-apps.js +68 -0
  39. package/src/deployment/providers/platforms/cloudflare-pages.js +103 -0
  40. package/src/deployment/providers/platforms/firebase-app-hosting.js +95 -0
  41. package/src/deployment/providers/platforms/firebase-hosting.js +99 -0
  42. package/src/deployment/providers/platforms/index.js +44 -0
  43. package/src/deployment/providers/platforms/netlify.js +102 -0
  44. package/src/deployment/providers/platforms/vercel.js +92 -0
  45. package/src/deployment/providers/ssh.js +365 -0
  46. package/src/detectors/angular.js +23 -0
  47. package/src/detectors/backend.detector.js +304 -0
  48. package/src/detectors/dotnet.js +18 -0
  49. package/src/detectors/frontend.detector.js +219 -0
  50. package/src/detectors/go.js +20 -0
  51. package/src/detectors/index.js +78 -0
  52. package/src/detectors/java.js +24 -0
  53. package/src/detectors/nextjs.js +23 -0
  54. package/src/detectors/node.js +28 -0
  55. package/src/detectors/php.js +17 -0
  56. package/src/detectors/python.js +22 -0
  57. package/src/detectors/react.js +29 -0
  58. package/src/detectors/vue.js +23 -0
  59. package/src/logger/index.js +44 -0
  60. package/src/notifications/email.js +53 -0
  61. package/src/notifications/index.js +34 -0
  62. package/src/notifications/slack.js +18 -0
  63. package/src/notifications/webhook.js +21 -0
  64. package/src/rollback/engine.js +102 -0
  65. package/src/storage/index.js +109 -0
  66. package/src/storage/providers/aws.js +96 -0
  67. package/src/storage/providers/azure.js +45 -0
  68. package/src/storage/providers/dropbox.js +49 -0
  69. package/src/storage/providers/ftp.js +69 -0
  70. package/src/storage/providers/gcp.js +45 -0
  71. package/src/storage/providers/gdrive.js +80 -0
  72. package/src/storage/providers/local.js +61 -0
  73. package/src/utils/author.js +141 -0
  74. package/src/utils/checksums.js +53 -0
  75. package/src/utils/firebase-config-generator.js +35 -0
  76. package/src/utils/github-actions.js +389 -0
  77. package/src/utils/init-platform.js +229 -0
  78. package/src/utils/nginx.js +34 -0
  79. package/src/utils/platform-env.js +132 -0
  80. package/src/utils/version.js +31 -0
@@ -0,0 +1,389 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { PLATFORM_ENV_MAP, PLATFORM_CLI_MAP } from './platform-env.js';
4
+ import { getWorkflowHeaderComment } from './author.js';
5
+
6
+ /** @typedef {'aws'|'azure'|'gcp'|'gdrive'|'dropbox'|'local'|'ftp'|'ssh'} ProviderEnvKey */
7
+
8
+ const PROVIDER_ENV_MAP = {
9
+ aws: [
10
+ 'AWS_ACCESS_KEY_ID',
11
+ 'AWS_SECRET_ACCESS_KEY',
12
+ 'AWS_BUCKET',
13
+ 'AWS_REGION',
14
+ ],
15
+ azure: ['AZURE_CONNECTION_STRING', 'AZURE_CONTAINER'],
16
+ gcp: ['GCP_PROJECT_ID', 'GCP_KEY_FILE', 'GCP_BUCKET'],
17
+ gdrive: [
18
+ 'GDRIVE_CLIENT_ID',
19
+ 'GDRIVE_CLIENT_SECRET',
20
+ 'GDRIVE_REFRESH_TOKEN',
21
+ 'GDRIVE_FOLDER_ID',
22
+ ],
23
+ dropbox: ['DROPBOX_ACCESS_TOKEN'],
24
+ local: [],
25
+ ftp: ['FTP_HOST', 'FTP_USER', 'FTP_PASSWORD'],
26
+ ssh: ['SSH_HOST', 'SSH_USER', 'SSH_KEY'],
27
+ };
28
+
29
+ const BACKEND_SSH_ENV_VARS = [
30
+ 'SSH_DEPLOY_PATH',
31
+ 'SSH_APP_NAME',
32
+ 'SSH_PORT',
33
+ ];
34
+
35
+ /**
36
+ * @param {string} cliSource
37
+ * @returns {string}
38
+ */
39
+ export function getCliInstallSpec(cliSource) {
40
+ if (!cliSource || cliSource === 'npm:deployhub') {
41
+ return 'deployhub@latest';
42
+ }
43
+ if (cliSource.startsWith('github:')) {
44
+ return cliSource;
45
+ }
46
+ if (cliSource.startsWith('file:')) {
47
+ return cliSource;
48
+ }
49
+ return cliSource;
50
+ }
51
+
52
+ /**
53
+ * @param {import('../core/config.js').DeployHubConfig} [config]
54
+ * @returns {string[]}
55
+ */
56
+ function getBackendSetupSteps(config) {
57
+ if (!config) return [];
58
+
59
+ const projectType = config.projectType || 'frontend';
60
+ if (projectType === 'frontend') return [];
61
+
62
+ /** @type {string[]} */
63
+ const steps = [];
64
+
65
+ if (projectType === 'backend' || projectType === 'both') {
66
+ const framework = config.backend?.framework || config.framework || 'express';
67
+ if (['express', 'nestjs', 'fastify', 'koa', 'nextjs'].includes(framework)) {
68
+ steps.push(` - uses: actions/setup-node@v4
69
+ with:
70
+ node-version: '20'`);
71
+ }
72
+ if (['fastapi', 'django', 'flask'].includes(framework)) {
73
+ steps.push(` - uses: actions/setup-python@v5
74
+ with:
75
+ python-version: '3.11'`);
76
+ }
77
+ if (['spring', 'java'].includes(framework)) {
78
+ steps.push(` - uses: actions/setup-java@v4
79
+ with:
80
+ distribution: 'temurin'
81
+ java-version: '17'`);
82
+ }
83
+ if (framework === 'go') {
84
+ steps.push(` - uses: actions/setup-go@v5
85
+ with:
86
+ go-version: '1.22'`);
87
+ }
88
+ if (framework === 'dotnet') {
89
+ steps.push(` - uses: actions/setup-dotnet@v4
90
+ with:
91
+ dotnet-version: '8.0.x'`);
92
+ }
93
+ if (framework === 'rails') {
94
+ steps.push(` - uses: ruby/setup-ruby@v1
95
+ with:
96
+ ruby-version: '3.2'
97
+ bundler-cache: true`);
98
+ }
99
+ }
100
+
101
+ return steps;
102
+ }
103
+
104
+ /**
105
+ * @param {Record<string, unknown>[]} environments
106
+ * @returns {string[]}
107
+ */
108
+ function getPlatformInstallSteps(environments) {
109
+ /** @type {Set<string>} */
110
+ const steps = new Set();
111
+
112
+ for (const env of environments) {
113
+ if (env.deploymentType !== 'platform' && env.frontendDeploymentType !== 'platform') {
114
+ continue;
115
+ }
116
+ const platform = env.platform;
117
+ if (!platform) continue;
118
+ const cli = PLATFORM_CLI_MAP[platform];
119
+ if (cli?.globalInstall) {
120
+ steps.add(` - name: Install ${platform} CLI\n run: ${cli.globalInstall}`);
121
+ }
122
+ }
123
+
124
+ return Array.from(steps);
125
+ }
126
+
127
+ /**
128
+ * @param {string[]} deployEnvironments
129
+ * @param {Record<string, Record<string, unknown>>} environments
130
+ */
131
+ function collectPlatformEnvVars(deployEnvironments, environments) {
132
+ /** @type {Set<string>} */
133
+ const envVars = new Set();
134
+
135
+ for (const envName of deployEnvironments) {
136
+ const env = environments[envName];
137
+ if (!env) continue;
138
+
139
+ if (env.deploymentType === 'platform' || env.frontendDeploymentType === 'platform') {
140
+ const platform = env.platform;
141
+ if (!platform) continue;
142
+ const keys = PLATFORM_ENV_MAP[platform] || [];
143
+ for (const key of keys) {
144
+ envVars.add(`${key}: \${{ secrets.${key} }}`);
145
+ }
146
+ }
147
+ }
148
+
149
+ return envVars;
150
+ }
151
+
152
+ /**
153
+ * @param {string[]} storageProviders
154
+ * @param {string[]} deployEnvironments
155
+ * @param {Record<string, { type: string }>} environments
156
+ * @param {string} [cliSource]
157
+ * @param {import('../core/config.js').DeployHubConfig} [config]
158
+ * @returns {string}
159
+ */
160
+ export function generateWorkflowYaml(
161
+ storageProviders,
162
+ deployEnvironments,
163
+ environments,
164
+ cliSource = 'npm:deployhub',
165
+ config = null
166
+ ) {
167
+ /** @type {Set<string>} */
168
+ const envVars = new Set(['DEPLOYHUB_ENV: production']);
169
+
170
+ for (const provider of storageProviders) {
171
+ const keys = PROVIDER_ENV_MAP[provider] || [];
172
+ for (const key of keys) {
173
+ envVars.add(`${key}: \${{ secrets.${key} }}`);
174
+ }
175
+ }
176
+
177
+ for (const envName of deployEnvironments) {
178
+ const env = environments[envName];
179
+ if (!env) continue;
180
+
181
+ if (env.deploymentType !== 'platform') {
182
+ const keys = PROVIDER_ENV_MAP[env.type] || [];
183
+ for (const key of keys) {
184
+ envVars.add(`${key}: \${{ secrets.${key} }}`);
185
+ }
186
+ }
187
+
188
+ if (env.type === 'ssh' && config) {
189
+ const projectType = config.projectType || 'frontend';
190
+ if (projectType === 'backend' || projectType === 'both') {
191
+ for (const key of BACKEND_SSH_ENV_VARS) {
192
+ envVars.add(`${key}: \${{ secrets.${key} }}`);
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ for (const line of collectPlatformEnvVars(deployEnvironments, environments)) {
199
+ envVars.add(line);
200
+ }
201
+
202
+ const envBlock = Array.from(envVars)
203
+ .map((line) => ` ${line}`)
204
+ .join('\n');
205
+
206
+ const installSpec = getCliInstallSpec(cliSource);
207
+ const backendSteps = getBackendSetupSteps(config);
208
+ const uniqueBackendSteps = [...new Set(backendSteps)].join('\n');
209
+
210
+ const envList = deployEnvironments
211
+ .map((name) => environments[name])
212
+ .filter(Boolean);
213
+ const platformSteps = getPlatformInstallSteps(envList).join('\n');
214
+
215
+ const projectType = config?.projectType || 'frontend';
216
+ const installDepsCommand =
217
+ projectType === 'backend' || projectType === 'both'
218
+ ? getInstallDepsCommand(config)
219
+ : 'npm install';
220
+
221
+ const workflow = `${getWorkflowHeaderComment()}name: DeployHub
222
+ on:
223
+ push:
224
+ branches: [main]
225
+ jobs:
226
+ deploy:
227
+ runs-on: ubuntu-latest
228
+ steps:
229
+ - uses: actions/checkout@v4
230
+ ${uniqueBackendSteps ? `${uniqueBackendSteps}\n` : ''} - uses: actions/setup-node@v4
231
+ with:
232
+ node-version: '20'
233
+ - name: Install project dependencies
234
+ run: ${installDepsCommand}
235
+ ${platformSteps ? `${platformSteps}\n` : ''} - name: Install DeployHub CLI
236
+ run: npm install ${installSpec} --no-save
237
+ - run: npx deployhub build
238
+ env:
239
+ ${envBlock}
240
+ `;
241
+
242
+ return workflow;
243
+ }
244
+
245
+ /**
246
+ * @param {import('../core/config.js').DeployHubConfig} [config]
247
+ * @returns {string}
248
+ */
249
+ function getInstallDepsCommand(config) {
250
+ if (!config) return 'npm install';
251
+
252
+ const framework =
253
+ config.backend?.framework || config.framework || 'express';
254
+ const language = config.backend?.language || config.language;
255
+
256
+ if (language === 'python' || ['fastapi', 'django', 'flask'].includes(framework)) {
257
+ return 'pip install -r requirements.txt';
258
+ }
259
+ if (['laravel', 'symfony'].includes(framework)) {
260
+ return 'composer install --no-interaction';
261
+ }
262
+ if (framework === 'spring' || framework === 'java') {
263
+ return 'mvn dependency:resolve || true';
264
+ }
265
+ if (framework === 'go') {
266
+ return 'go mod download';
267
+ }
268
+ if (framework === 'dotnet') {
269
+ return 'dotnet restore';
270
+ }
271
+ if (framework === 'rails') {
272
+ return 'bundle install';
273
+ }
274
+ return 'npm install';
275
+ }
276
+
277
+ /**
278
+ * @param {string[]} storageProviders
279
+ * @param {string[]} deployEnvironments
280
+ * @param {Record<string, { type: string }>} environments
281
+ * @param {string} [cwd]
282
+ * @param {string} [cliSource]
283
+ * @param {import('../core/config.js').DeployHubConfig} [config]
284
+ */
285
+ export async function writeWorkflowFile(
286
+ storageProviders,
287
+ deployEnvironments,
288
+ environments,
289
+ cwd = process.cwd(),
290
+ cliSource = 'npm:deployhub',
291
+ config = null
292
+ ) {
293
+ const workflowDir = path.join(cwd, '.github', 'workflows');
294
+ await fs.ensureDir(workflowDir);
295
+ const content = generateWorkflowYaml(
296
+ storageProviders,
297
+ deployEnvironments,
298
+ environments,
299
+ cliSource,
300
+ config
301
+ );
302
+ await fs.writeFile(path.join(workflowDir, 'deployhub.yml'), content);
303
+ }
304
+
305
+ /**
306
+ * @param {string} cliSource
307
+ * @param {string} [cwd]
308
+ */
309
+ export async function addDeployhubToPackageJson(cliSource, cwd = process.cwd()) {
310
+ const pkgPath = path.join(cwd, 'package.json');
311
+ if (!(await fs.pathExists(pkgPath))) return;
312
+
313
+ const pkg = await fs.readJson(pkgPath);
314
+ pkg.devDependencies = pkg.devDependencies || {};
315
+ pkg.devDependencies.deployhub = getCliInstallSpec(cliSource);
316
+ pkg.scripts = pkg.scripts || {};
317
+ pkg.scripts['deployhub:build'] = 'deployhub build';
318
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
319
+ }
320
+
321
+ /**
322
+ * @param {string} [cwd]
323
+ * @returns {Promise<string>}
324
+ */
325
+ export async function guessCliGithubRepo(cwd = process.cwd()) {
326
+ try {
327
+ const { execa } = await import('execa');
328
+ const { stdout } = await execa('git', ['remote', 'get-url', 'origin'], {
329
+ cwd,
330
+ stdio: 'pipe',
331
+ });
332
+ const match = stdout.match(/github\.com[:/](.+?)\/(.+?)(?:\.git)?$/);
333
+ if (match) {
334
+ return `github:${match[1]}/deployhub`;
335
+ }
336
+ } catch {
337
+ // ignore
338
+ }
339
+ return 'github:YOUR_USERNAME/deployhub';
340
+ }
341
+
342
+ /**
343
+ * @param {string[]} storageProviders
344
+ * @param {string[]} deployEnvironments
345
+ * @param {Record<string, { type: string }>} environments
346
+ * @param {import('../core/config.js').DeployHubConfig} [config]
347
+ * @returns {string[]}
348
+ */
349
+ export function getRequiredSecrets(
350
+ storageProviders,
351
+ deployEnvironments,
352
+ environments,
353
+ config = null
354
+ ) {
355
+ /** @type {Set<string>} */
356
+ const secrets = new Set();
357
+
358
+ for (const provider of storageProviders) {
359
+ const keys = PROVIDER_ENV_MAP[provider] || [];
360
+ keys.forEach((k) => secrets.add(k));
361
+ }
362
+
363
+ for (const envName of deployEnvironments) {
364
+ const env = environments[envName];
365
+ if (!env) continue;
366
+
367
+ if (env.deploymentType === 'platform' || env.frontendDeploymentType === 'platform') {
368
+ const platform = env.platform;
369
+ if (platform) {
370
+ const keys = PLATFORM_ENV_MAP[platform] || [];
371
+ keys.forEach((k) => secrets.add(k));
372
+ }
373
+ } else {
374
+ const keys = PROVIDER_ENV_MAP[env.type] || [];
375
+ keys.forEach((k) => secrets.add(k));
376
+ }
377
+
378
+ if (env.type === 'ssh' && config) {
379
+ const projectType = config.projectType || 'frontend';
380
+ if (projectType === 'backend' || projectType === 'both') {
381
+ BACKEND_SSH_ENV_VARS.forEach((k) => secrets.add(k));
382
+ }
383
+ }
384
+ }
385
+
386
+ return Array.from(secrets);
387
+ }
388
+
389
+ export { PROVIDER_ENV_MAP };
@@ -0,0 +1,229 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ import { execa } from 'execa';
6
+ import {
7
+ PLATFORM_CHOICES,
8
+ PLATFORM_COMPARISON,
9
+ getPlatformEnvExample,
10
+ } from './platform-env.js';
11
+ import { ensureFirebaseJson } from './firebase-config-generator.js';
12
+ import { appendEnv } from '../core/config.js';
13
+
14
+ /**
15
+ * @param {string} platform
16
+ * @param {string} projectName
17
+ * @param {string} buildOutput
18
+ * @param {string} [cwd]
19
+ * @returns {Promise<Record<string, unknown>>}
20
+ */
21
+ export async function promptPlatformQuestions(
22
+ platform,
23
+ projectName,
24
+ buildOutput,
25
+ cwd = process.cwd()
26
+ ) {
27
+ /** @type {Record<string, unknown>} */
28
+ const config = { platform };
29
+
30
+ switch (platform) {
31
+ case 'vercel': {
32
+ const answers = await inquirer.prompt([
33
+ {
34
+ type: 'input',
35
+ name: 'projectName',
36
+ message: 'Vercel project name:',
37
+ default: projectName,
38
+ },
39
+ {
40
+ type: 'confirm',
41
+ name: 'vercelLinked',
42
+ message: 'Have you already linked this project with Vercel?',
43
+ default: fs.existsSync(path.join(cwd, '.vercel', 'project.json')),
44
+ },
45
+ ]);
46
+ config.projectName = answers.projectName;
47
+
48
+ if (!answers.vercelLinked) {
49
+ console.log(chalk.gray('Running vercel link — follow the prompts...'));
50
+ try {
51
+ await execa('vercel', ['link'], { cwd, stdio: 'inherit' });
52
+ } catch {
53
+ console.log(
54
+ chalk.yellow(
55
+ 'Could not run vercel link automatically. Run it manually, then add VERCEL_ORG_ID and VERCEL_PROJECT_ID to .env'
56
+ )
57
+ );
58
+ }
59
+ }
60
+ break;
61
+ }
62
+
63
+ case 'netlify': {
64
+ const answers = await inquirer.prompt([
65
+ {
66
+ type: 'input',
67
+ name: 'siteId',
68
+ message: 'Netlify site ID (from Netlify dashboard):',
69
+ },
70
+ ]);
71
+ config.siteId = answers.siteId;
72
+ break;
73
+ }
74
+
75
+ case 'cloudflare-pages': {
76
+ const answers = await inquirer.prompt([
77
+ {
78
+ type: 'input',
79
+ name: 'projectName',
80
+ message: 'Cloudflare Pages project name:',
81
+ default: projectName,
82
+ },
83
+ {
84
+ type: 'input',
85
+ name: 'accountId',
86
+ message: 'Cloudflare account ID (from dashboard):',
87
+ },
88
+ ]);
89
+ config.projectName = answers.projectName;
90
+ config.accountId = answers.accountId;
91
+ break;
92
+ }
93
+
94
+ case 'aws-amplify': {
95
+ const answers = await inquirer.prompt([
96
+ {
97
+ type: 'input',
98
+ name: 'appId',
99
+ message: 'Amplify App ID (from AWS console):',
100
+ },
101
+ {
102
+ type: 'input',
103
+ name: 'region',
104
+ message: 'AWS region:',
105
+ default: 'us-east-1',
106
+ },
107
+ {
108
+ type: 'confirm',
109
+ name: 'githubConnected',
110
+ message: 'GitHub already connected to Amplify?',
111
+ default: true,
112
+ },
113
+ ]);
114
+ config.appId = answers.appId;
115
+ config.region = answers.region;
116
+ config.githubConnected = answers.githubConnected;
117
+ break;
118
+ }
119
+
120
+ case 'azure-static-web-apps': {
121
+ const answers = await inquirer.prompt([
122
+ {
123
+ type: 'input',
124
+ name: 'resourceName',
125
+ message: 'Resource name:',
126
+ default: projectName,
127
+ },
128
+ ]);
129
+ config.resourceName = answers.resourceName;
130
+ break;
131
+ }
132
+
133
+ case 'firebase-hosting': {
134
+ const answers = await inquirer.prompt([
135
+ {
136
+ type: 'input',
137
+ name: 'projectId',
138
+ message: 'Firebase project ID:',
139
+ default: projectName,
140
+ },
141
+ ]);
142
+ config.projectId = answers.projectId;
143
+ await ensureFirebaseJson(buildOutput, cwd);
144
+ console.log(chalk.green('✓ firebase.json generated'));
145
+ break;
146
+ }
147
+
148
+ case 'firebase-app-hosting': {
149
+ const answers = await inquirer.prompt([
150
+ {
151
+ type: 'input',
152
+ name: 'projectId',
153
+ message: 'Firebase project ID:',
154
+ default: projectName,
155
+ },
156
+ {
157
+ type: 'input',
158
+ name: 'backendName',
159
+ message: 'Backend name:',
160
+ default: `${projectName}-backend`,
161
+ },
162
+ ]);
163
+ config.projectId = answers.projectId;
164
+ config.backendName = answers.backendName;
165
+ break;
166
+ }
167
+
168
+ default:
169
+ break;
170
+ }
171
+
172
+ return config;
173
+ }
174
+
175
+ /**
176
+ * @param {string} platform
177
+ * @param {string} [cwd]
178
+ */
179
+ export async function appendPlatformEnvExample(platform, cwd = process.cwd()) {
180
+ const examples = getPlatformEnvExample(platform);
181
+ await appendEnv(examples, cwd);
182
+
183
+ const envExamplePath = path.join(cwd, '.env.example');
184
+ if (!(await fs.pathExists(envExamplePath))) return;
185
+
186
+ let content = await fs.readFile(envExamplePath, 'utf-8');
187
+ const sectionHeader = `\n# ${platform} deployment\n`;
188
+ const lines = Object.keys(examples)
189
+ .map((key) => `${key}=`)
190
+ .join('\n');
191
+
192
+ if (!content.includes(`${platform} deployment`)) {
193
+ content += `${sectionHeader}${lines}\n`;
194
+ await fs.writeFile(envExamplePath, content);
195
+ }
196
+ }
197
+
198
+ export function showPlatformComparison() {
199
+ console.log('');
200
+ console.log(chalk.bold('Platform comparison:'));
201
+ console.log(chalk.gray(PLATFORM_COMPARISON));
202
+ console.log('');
203
+ }
204
+
205
+ /**
206
+ * @param {string} platform
207
+ * @param {string} projectName
208
+ * @returns {string}
209
+ */
210
+ export function suggestHealthUrl(platform, projectName) {
211
+ const urls = {
212
+ vercel: `https://${projectName}.vercel.app`,
213
+ netlify: `https://${projectName}.netlify.app`,
214
+ 'cloudflare-pages': `https://${projectName}.pages.dev`,
215
+ 'firebase-hosting': `https://${projectName}.web.app`,
216
+ 'firebase-app-hosting': `https://${projectName}.web.app`,
217
+ };
218
+ return urls[platform] || '';
219
+ }
220
+
221
+ export { PLATFORM_CHOICES };
222
+
223
+ export default {
224
+ promptPlatformQuestions,
225
+ appendPlatformEnvExample,
226
+ showPlatformComparison,
227
+ suggestHealthUrl,
228
+ PLATFORM_CHOICES,
229
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Generate nginx server block config for SPA frontend deployments.
3
+ *
4
+ * @param {string} projectName
5
+ * @param {string} deployPath - Absolute deploy path on the server
6
+ * @param {string} [buildOutput='dist']
7
+ * @returns {string}
8
+ */
9
+ export function generateNginxConfig(projectName, deployPath, buildOutput = 'dist') {
10
+ const root = `${deployPath.replace(/\/$/, '')}/${buildOutput}`;
11
+
12
+ return `server {
13
+ listen 80;
14
+ server_name _;
15
+ root ${root};
16
+ index index.html;
17
+
18
+ location / {
19
+ try_files $uri $uri/ /index.html;
20
+ }
21
+ }
22
+ `;
23
+ }
24
+
25
+ /**
26
+ * @param {string} projectName
27
+ * @returns {string}
28
+ */
29
+ export function getNginxSitePath(projectName) {
30
+ const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, '-');
31
+ return `/etc/nginx/sites-available/${safeName}`;
32
+ }
33
+
34
+ export default { generateNginxConfig, getNginxSitePath };