@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,795 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { saveConfig } from '../core/config.js';
7
+ import { detectFrontend, detectBackend } from '../detectors/index.js';
8
+ import {
9
+ getFrontendInfo,
10
+ } from '../detectors/frontend.detector.js';
11
+ import {
12
+ getBackendInfo,
13
+ } from '../detectors/backend.detector.js';
14
+ import { getProjectVersion } from '../utils/version.js';
15
+ import {
16
+ writeWorkflowFile,
17
+ getRequiredSecrets,
18
+ guessCliGithubRepo,
19
+ addDeployhubToPackageJson,
20
+ } from '../utils/github-actions.js';
21
+ import {
22
+ promptPlatformQuestions,
23
+ appendPlatformEnvExample,
24
+ showPlatformComparison,
25
+ suggestHealthUrl,
26
+ PLATFORM_CHOICES,
27
+ } from '../utils/init-platform.js';
28
+ import { printAuthorFooter } from '../utils/author.js';
29
+ import { ensureFirebaseJson } from '../utils/firebase-config-generator.js';
30
+ import { generateNginxConfig } from '../utils/nginx.js';
31
+
32
+ const FRONTEND_CHOICES = [
33
+ { name: 'React', value: 'react' },
34
+ { name: 'Vue', value: 'vue' },
35
+ { name: 'Angular', value: 'angular' },
36
+ { name: 'Next.js', value: 'nextjs' },
37
+ { name: 'Svelte', value: 'svelte' },
38
+ { name: 'Astro', value: 'astro' },
39
+ { name: 'Vanilla JS / HTML', value: 'vanilla' },
40
+ { name: "Other (I'll configure manually)", value: 'other' },
41
+ ];
42
+
43
+ const BACKEND_CHOICES = [
44
+ { name: 'Node.js — Express', value: 'express' },
45
+ { name: 'Node.js — NestJS', value: 'nestjs' },
46
+ { name: 'Node.js — Fastify', value: 'fastify' },
47
+ { name: 'Node.js — Koa', value: 'koa' },
48
+ { name: 'Python — FastAPI', value: 'fastapi' },
49
+ { name: 'Python — Django', value: 'django' },
50
+ { name: 'Python — Flask', value: 'flask' },
51
+ { name: 'PHP — Laravel', value: 'laravel' },
52
+ { name: 'PHP — Symfony', value: 'symfony' },
53
+ { name: 'Java — Spring Boot', value: 'spring' },
54
+ { name: 'Go', value: 'go' },
55
+ { name: '.NET (ASP.NET Core)', value: 'dotnet' },
56
+ { name: 'Ruby on Rails', value: 'rails' },
57
+ { name: "Other (I'll configure manually)", value: 'other' },
58
+ ];
59
+
60
+ const PROJECT_TYPE_CHOICES = [
61
+ { name: 'Frontend only', value: 'frontend' },
62
+ { name: 'Backend only', value: 'backend' },
63
+ { name: 'Both (monorepo / fullstack)', value: 'both' },
64
+ ];
65
+
66
+ /**
67
+ * @param {'frontend'|'backend'} side
68
+ * @param {string} [cwd]
69
+ * @returns {string|null}
70
+ */
71
+ function detectSideFramework(side, cwd) {
72
+ if (side === 'frontend') {
73
+ return detectFrontend(cwd)?.framework || null;
74
+ }
75
+ return detectBackend(cwd)?.framework || null;
76
+ }
77
+
78
+ /**
79
+ * @param {string} framework
80
+ * @param {'frontend'|'backend'} side
81
+ * @param {string} [cwd]
82
+ */
83
+ function getFrameworkDefaults(framework, side, cwd = process.cwd()) {
84
+ if (framework === 'other') {
85
+ return side === 'frontend'
86
+ ? getFrontendInfo('vanilla', cwd)
87
+ : getBackendInfo('express', cwd);
88
+ }
89
+ return side === 'frontend'
90
+ ? getFrontendInfo(framework, cwd)
91
+ : getBackendInfo(framework, cwd);
92
+ }
93
+
94
+ /**
95
+ * @param {'frontend'|'backend'} side
96
+ * @param {string|null} detectedFramework
97
+ */
98
+ async function promptFramework(side, detectedFramework) {
99
+ const choices = side === 'frontend' ? FRONTEND_CHOICES : BACKEND_CHOICES;
100
+ const detected = detectedFramework && choices.some((c) => c.value === detectedFramework)
101
+ ? detectedFramework
102
+ : choices[0].value;
103
+
104
+ const { framework } = await inquirer.prompt([
105
+ {
106
+ type: 'list',
107
+ name: 'framework',
108
+ message:
109
+ side === 'frontend'
110
+ ? 'Select your framework:'
111
+ : 'Select your language / framework:',
112
+ choices,
113
+ default: detected,
114
+ },
115
+ ]);
116
+
117
+ return framework;
118
+ }
119
+
120
+ /**
121
+ * @param {ReturnType<typeof getFrameworkDefaults>} defaults
122
+ * @param {'frontend'|'backend'} side
123
+ */
124
+ async function promptBuildSettings(defaults, side) {
125
+ const questions = [];
126
+
127
+ if (side === 'frontend' || defaults.buildCommand) {
128
+ questions.push({
129
+ type: 'input',
130
+ name: 'buildCommand',
131
+ message: 'Build command:',
132
+ default: defaults.buildCommand || 'npm run build',
133
+ when: () => side === 'frontend' || defaults.buildCommand,
134
+ });
135
+ }
136
+
137
+ questions.push({
138
+ type: 'input',
139
+ name: 'buildOutput',
140
+ message: 'Build output directory:',
141
+ default: defaults.buildOutput,
142
+ });
143
+
144
+ if (side === 'backend') {
145
+ questions.push({
146
+ type: 'input',
147
+ name: 'startCommand',
148
+ message: 'Start command:',
149
+ default: defaults.startCommand || 'npm start',
150
+ });
151
+ questions.push({
152
+ type: 'number',
153
+ name: 'port',
154
+ message: 'Default port:',
155
+ default: defaults.port || 3000,
156
+ });
157
+ }
158
+
159
+ return inquirer.prompt(questions);
160
+ }
161
+
162
+ const SERVER_DEPLOY_TYPES = [
163
+ 'ssh',
164
+ 'docker',
165
+ 'ec2',
166
+ 'azure-vm',
167
+ 'gcp-vm',
168
+ 'kubernetes',
169
+ ];
170
+
171
+ /**
172
+ * @param {string} projectName
173
+ * @param {'frontend'|'backend'|'both'} projectType
174
+ * @param {Record<string, unknown>|null} backendConfig
175
+ * @param {Record<string, unknown>|null} singleConfig
176
+ */
177
+ async function promptServerDeployment(projectName, projectType, backendConfig, singleConfig) {
178
+ return inquirer.prompt([
179
+ {
180
+ type: 'list',
181
+ name: 'deployType',
182
+ message: 'Deployment type:',
183
+ choices: SERVER_DEPLOY_TYPES,
184
+ },
185
+ {
186
+ type: 'input',
187
+ name: 'envName',
188
+ message: 'Environment name:',
189
+ default: 'production',
190
+ },
191
+ {
192
+ type: 'input',
193
+ name: 'host',
194
+ message: 'Host (for SSH-based targets):',
195
+ when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
196
+ },
197
+ {
198
+ type: 'input',
199
+ name: 'user',
200
+ message: 'SSH user:',
201
+ when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
202
+ },
203
+ {
204
+ type: 'input',
205
+ name: 'deployPath',
206
+ message: 'Deploy path:',
207
+ default: `/var/www/${projectName}`,
208
+ when: (a) =>
209
+ ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
210
+ projectType !== 'both',
211
+ },
212
+ {
213
+ type: 'input',
214
+ name: 'frontendDeployPath',
215
+ message: 'Frontend deploy path:',
216
+ default: `/var/www/${projectName}/public`,
217
+ when: (a) =>
218
+ ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
219
+ projectType === 'both',
220
+ },
221
+ {
222
+ type: 'input',
223
+ name: 'backendDeployPath',
224
+ message: 'Backend deploy path:',
225
+ default: `/var/www/${projectName}/api`,
226
+ when: (a) =>
227
+ ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
228
+ projectType === 'both',
229
+ },
230
+ {
231
+ type: 'input',
232
+ name: 'appName',
233
+ message: 'App name (for PM2):',
234
+ default: projectType === 'both' ? `${projectName}-api` : projectName,
235
+ when: (a) =>
236
+ ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
237
+ (projectType === 'backend' || projectType === 'both'),
238
+ },
239
+ {
240
+ type: 'input',
241
+ name: 'healthUrl',
242
+ message: 'Health check URL (optional):',
243
+ },
244
+ ]);
245
+ }
246
+
247
+ /**
248
+ * @param {Awaited<ReturnType<typeof promptServerDeployment>>} deployAnswers
249
+ * @param {'frontend'|'backend'|'both'} projectType
250
+ * @param {string} projectName
251
+ * @param {Record<string, unknown>|null} backendConfig
252
+ * @param {Record<string, unknown>|null} singleConfig
253
+ */
254
+ function buildServerEnvEntry(
255
+ deployAnswers,
256
+ projectType,
257
+ projectName,
258
+ backendConfig,
259
+ singleConfig
260
+ ) {
261
+ /** @type {Record<string, unknown>} */
262
+ const envEntry = {
263
+ deploymentType: 'server',
264
+ type: deployAnswers.deployType,
265
+ host: deployAnswers.host || '',
266
+ user: deployAnswers.user || '',
267
+ };
268
+
269
+ if (projectType === 'both') {
270
+ envEntry.frontendDeployPath =
271
+ deployAnswers.frontendDeployPath || `/var/www/${projectName}/public`;
272
+ envEntry.backendDeployPath =
273
+ deployAnswers.backendDeployPath || `/var/www/${projectName}/api`;
274
+ envEntry.appName = deployAnswers.appName || `${projectName}-api`;
275
+ envEntry.framework = backendConfig?.framework || 'express';
276
+ envEntry.path = envEntry.backendDeployPath;
277
+ envEntry.backendDeploymentType = 'server';
278
+ } else if (projectType === 'backend') {
279
+ envEntry.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
280
+ envEntry.path = envEntry.deployPath;
281
+ envEntry.appName = deployAnswers.appName || projectName;
282
+ envEntry.framework = singleConfig?.framework || 'express';
283
+ envEntry.port = singleConfig?.port || 3000;
284
+ } else {
285
+ envEntry.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
286
+ envEntry.path = envEntry.deployPath;
287
+ }
288
+
289
+ return envEntry;
290
+ }
291
+
292
+ /**
293
+ * @param {Record<string, unknown>} config
294
+ * @param {Record<string, Record<string, unknown>>} environments
295
+ * @param {string} cwd
296
+ */
297
+ async function generateProjectScaffold(config, environments, cwd) {
298
+ const envList = Object.values(environments);
299
+
300
+ const usesFirebase = envList.some(
301
+ (env) =>
302
+ env.platform === 'firebase-hosting' || env.platform === 'firebase-app-hosting'
303
+ );
304
+
305
+ if (usesFirebase && !(await fs.pathExists(path.join(cwd, 'firebase.json')))) {
306
+ const buildOutput =
307
+ config.frontend?.buildOutput || config.buildOutput || 'dist';
308
+ await ensureFirebaseJson(buildOutput, cwd);
309
+ console.log(chalk.gray(' • firebase.json (auto-generated)'));
310
+ }
311
+
312
+ const usesFrontendSsh = envList.some((env) => {
313
+ if (env.frontendDeploymentType === 'platform') return false;
314
+ if (env.deploymentType === 'platform' && !env.type) return false;
315
+ return env.type === 'ssh' || env.deploymentType === 'server';
316
+ });
317
+
318
+ const isFrontendProject =
319
+ config.projectType === 'frontend' || config.projectType === 'both';
320
+
321
+ if (
322
+ isFrontendProject &&
323
+ usesFrontendSsh &&
324
+ !(await fs.pathExists(path.join(cwd, 'nginx.conf')))
325
+ ) {
326
+ const deployPath =
327
+ envList.find((e) => e.deployPath || e.path)?.deployPath ||
328
+ envList.find((e) => e.deployPath || e.path)?.path ||
329
+ `/var/www/${config.project}`;
330
+ const buildOutput =
331
+ config.frontend?.buildOutput || config.buildOutput || 'dist';
332
+ const nginxConf = generateNginxConfig(config.project, deployPath, buildOutput);
333
+ await fs.writeFile(path.join(cwd, 'nginx.conf'), nginxConf);
334
+ console.log(chalk.gray(' • nginx.conf (auto-generated)'));
335
+ }
336
+ }
337
+
338
+ /**
339
+ * @param {import('commander').Command} program
340
+ */
341
+ export function registerInitCommand(program) {
342
+ program
343
+ .command('init')
344
+ .description('Interactive setup for DeployHub')
345
+ .action(async () => {
346
+ const cwd = process.cwd();
347
+
348
+ if (await fs.pathExists(path.join(cwd, 'deployhub.config.json'))) {
349
+ const { overwrite } = await inquirer.prompt([
350
+ {
351
+ type: 'confirm',
352
+ name: 'overwrite',
353
+ message: 'deployhub.config.json already exists. Overwrite?',
354
+ default: false,
355
+ },
356
+ ]);
357
+ if (!overwrite) {
358
+ console.log(chalk.yellow('Init cancelled.'));
359
+ return;
360
+ }
361
+ }
362
+
363
+ const detectedFrontend = detectFrontend(cwd);
364
+ const detectedBackend = detectBackend(cwd);
365
+ const defaultName = path.basename(cwd) || 'my-app';
366
+ const defaultCliRepo = await guessCliGithubRepo(cwd);
367
+
368
+ const { projectName } = await inquirer.prompt([
369
+ {
370
+ type: 'input',
371
+ name: 'projectName',
372
+ message: 'Project name:',
373
+ default: defaultName,
374
+ },
375
+ ]);
376
+
377
+ const { projectType } = await inquirer.prompt([
378
+ {
379
+ type: 'list',
380
+ name: 'projectType',
381
+ message: 'What are you deploying?',
382
+ choices: PROJECT_TYPE_CHOICES,
383
+ default: detectedFrontend && detectedBackend
384
+ ? 'both'
385
+ : detectedBackend
386
+ ? 'backend'
387
+ : 'frontend',
388
+ },
389
+ ]);
390
+
391
+ /** @type {Record<string, unknown>} */
392
+ let frontendConfig = null;
393
+ /** @type {Record<string, unknown>} */
394
+ let backendConfig = null;
395
+ /** @type {Record<string, unknown>} */
396
+ let singleConfig = null;
397
+
398
+ if (projectType === 'frontend' || projectType === 'both') {
399
+ const fw = await promptFramework('frontend', detectedFrontend?.framework || null);
400
+ const defaults = getFrameworkDefaults(fw, 'frontend', cwd);
401
+ const settings = await promptBuildSettings(defaults, 'frontend');
402
+ frontendConfig = {
403
+ framework: fw === 'other' ? 'custom' : fw,
404
+ buildCommand: settings.buildCommand ?? defaults.buildCommand,
405
+ buildOutput: settings.buildOutput || defaults.buildOutput,
406
+ };
407
+ }
408
+
409
+ if (projectType === 'backend' || projectType === 'both') {
410
+ const fw = await promptFramework('backend', detectedBackend?.framework || null);
411
+ const defaults = getFrameworkDefaults(fw, 'backend', cwd);
412
+ const settings = await promptBuildSettings(defaults, 'backend');
413
+ backendConfig = {
414
+ framework: fw === 'other' ? 'custom' : fw,
415
+ language: defaults.language,
416
+ buildCommand: settings.buildCommand ?? defaults.buildCommand ?? null,
417
+ startCommand: settings.startCommand ?? defaults.startCommand,
418
+ buildOutput: settings.buildOutput || defaults.buildOutput,
419
+ port: settings.port ?? defaults.port,
420
+ };
421
+ }
422
+
423
+ if (projectType === 'frontend') {
424
+ singleConfig = { ...frontendConfig };
425
+ } else if (projectType === 'backend') {
426
+ singleConfig = { ...backendConfig };
427
+ }
428
+
429
+ const answers = await inquirer.prompt([
430
+ {
431
+ type: 'checkbox',
432
+ name: 'storage',
433
+ message: 'Select storage providers:',
434
+ choices: [
435
+ { name: 'Local', value: 'local', checked: true },
436
+ { name: 'AWS S3', value: 'aws' },
437
+ { name: 'Google Drive', value: 'gdrive' },
438
+ { name: 'Azure Blob', value: 'azure' },
439
+ { name: 'GCP Storage', value: 'gcp' },
440
+ { name: 'Dropbox', value: 'dropbox' },
441
+ ],
442
+ },
443
+ {
444
+ type: 'confirm',
445
+ name: 'configureDeploy',
446
+ message: 'Configure deployment?',
447
+ default: false,
448
+ },
449
+ {
450
+ type: 'input',
451
+ name: 'cliSource',
452
+ message:
453
+ 'DeployHub CLI source for GitHub Actions (github:user/repo or npm:deployhub):',
454
+ default: defaultCliRepo,
455
+ },
456
+ ]);
457
+
458
+ /** @type {Record<string, Record<string, unknown>>} */
459
+ const environments = {};
460
+ /** @type {string[]} */
461
+ const deploy = [];
462
+ let healthUrl = '';
463
+
464
+ if (answers.configureDeploy) {
465
+ const buildOutput =
466
+ frontendConfig?.buildOutput || singleConfig?.buildOutput || 'dist';
467
+
468
+ if (projectType === 'backend') {
469
+ const deployAnswers = await promptServerDeployment(
470
+ projectName,
471
+ projectType,
472
+ backendConfig,
473
+ singleConfig
474
+ );
475
+ deploy.push(deployAnswers.envName);
476
+ environments[deployAnswers.envName] = buildServerEnvEntry(
477
+ deployAnswers,
478
+ projectType,
479
+ projectName,
480
+ backendConfig,
481
+ singleConfig
482
+ );
483
+ if (deployAnswers.healthUrl) {
484
+ healthUrl = deployAnswers.healthUrl;
485
+ } else if (singleConfig?.port) {
486
+ healthUrl = `http://localhost:${singleConfig.port}/health`;
487
+ }
488
+ } else if (projectType === 'frontend') {
489
+ const { deployMethod } = await inquirer.prompt([
490
+ {
491
+ type: 'list',
492
+ name: 'deployMethod',
493
+ message: 'How do you want to deploy?',
494
+ choices: [
495
+ {
496
+ name: 'Managed platform (Vercel, Netlify, Cloudflare Pages, etc.)',
497
+ value: 'platform',
498
+ },
499
+ {
500
+ name: 'Self-hosted server (SSH, Docker, EC2, etc.)',
501
+ value: 'server',
502
+ },
503
+ ],
504
+ },
505
+ ]);
506
+
507
+ const { envName } = await inquirer.prompt([
508
+ {
509
+ type: 'input',
510
+ name: 'envName',
511
+ message: 'Environment name:',
512
+ default: 'production',
513
+ },
514
+ ]);
515
+ deploy.push(envName);
516
+
517
+ if (deployMethod === 'platform') {
518
+ showPlatformComparison();
519
+ const { platform } = await inquirer.prompt([
520
+ {
521
+ type: 'list',
522
+ name: 'platform',
523
+ message: 'Select platform:',
524
+ choices: PLATFORM_CHOICES,
525
+ },
526
+ ]);
527
+
528
+ const platformConfig = await promptPlatformQuestions(
529
+ platform,
530
+ projectName,
531
+ buildOutput,
532
+ cwd
533
+ );
534
+ await appendPlatformEnvExample(platform, cwd);
535
+
536
+ const { healthUrlInput } = await inquirer.prompt([
537
+ {
538
+ type: 'input',
539
+ name: 'healthUrlInput',
540
+ message: 'Health check URL (optional):',
541
+ default: suggestHealthUrl(platform, projectName),
542
+ },
543
+ ]);
544
+
545
+ environments[envName] = {
546
+ deploymentType: 'platform',
547
+ ...platformConfig,
548
+ };
549
+ healthUrl = healthUrlInput || suggestHealthUrl(platform, projectName);
550
+ } else {
551
+ const deployAnswers = await promptServerDeployment(
552
+ projectName,
553
+ projectType,
554
+ backendConfig,
555
+ singleConfig
556
+ );
557
+ deployAnswers.envName = envName;
558
+ environments[envName] = buildServerEnvEntry(
559
+ deployAnswers,
560
+ projectType,
561
+ projectName,
562
+ backendConfig,
563
+ singleConfig
564
+ );
565
+ if (deployAnswers.healthUrl) healthUrl = deployAnswers.healthUrl;
566
+ }
567
+ } else {
568
+ const { frontendDeployMethod } = await inquirer.prompt([
569
+ {
570
+ type: 'list',
571
+ name: 'frontendDeployMethod',
572
+ message: 'How do you want to deploy the frontend?',
573
+ choices: [
574
+ {
575
+ name: 'Managed platform (Vercel, Netlify, Cloudflare Pages, etc.)',
576
+ value: 'platform',
577
+ },
578
+ {
579
+ name: 'Self-hosted server (SSH, Docker, EC2, etc.)',
580
+ value: 'server',
581
+ },
582
+ ],
583
+ },
584
+ ]);
585
+
586
+ const { envName } = await inquirer.prompt([
587
+ {
588
+ type: 'input',
589
+ name: 'envName',
590
+ message: 'Environment name:',
591
+ default: 'production',
592
+ },
593
+ ]);
594
+ deploy.push(envName);
595
+
596
+ /** @type {Record<string, unknown>} */
597
+ const envEntry = { backendDeploymentType: 'server' };
598
+
599
+ if (frontendDeployMethod === 'platform') {
600
+ showPlatformComparison();
601
+ const { platform } = await inquirer.prompt([
602
+ {
603
+ type: 'list',
604
+ name: 'platform',
605
+ message: 'Select frontend platform:',
606
+ choices: PLATFORM_CHOICES,
607
+ },
608
+ ]);
609
+
610
+ const platformConfig = await promptPlatformQuestions(
611
+ platform,
612
+ projectName,
613
+ buildOutput,
614
+ cwd
615
+ );
616
+ await appendPlatformEnvExample(platform, cwd);
617
+
618
+ Object.assign(envEntry, {
619
+ frontendDeploymentType: 'platform',
620
+ deploymentType: 'platform',
621
+ ...platformConfig,
622
+ });
623
+ healthUrl = suggestHealthUrl(platform, projectName);
624
+ } else {
625
+ envEntry.frontendDeploymentType = 'server';
626
+ }
627
+
628
+ console.log(chalk.gray('\nBackend will be deployed to a self-hosted server.'));
629
+ const backendDeployAnswers = await inquirer.prompt([
630
+ {
631
+ type: 'list',
632
+ name: 'deployType',
633
+ message: 'Backend deployment type:',
634
+ choices: SERVER_DEPLOY_TYPES,
635
+ default: 'ssh',
636
+ },
637
+ {
638
+ type: 'input',
639
+ name: 'host',
640
+ message: 'Host (for SSH-based targets):',
641
+ when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
642
+ },
643
+ {
644
+ type: 'input',
645
+ name: 'user',
646
+ message: 'SSH user:',
647
+ when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
648
+ },
649
+ {
650
+ type: 'input',
651
+ name: 'backendDeployPath',
652
+ message: 'Backend deploy path:',
653
+ default: `/var/www/${projectName}/api`,
654
+ },
655
+ {
656
+ type: 'input',
657
+ name: 'appName',
658
+ message: 'App name (for PM2):',
659
+ default: `${projectName}-api`,
660
+ },
661
+ ]);
662
+
663
+ Object.assign(envEntry, {
664
+ type: backendDeployAnswers.deployType,
665
+ host: backendDeployAnswers.host || '',
666
+ user: backendDeployAnswers.user || '',
667
+ backendDeployPath:
668
+ backendDeployAnswers.backendDeployPath || `/var/www/${projectName}/api`,
669
+ appName: backendDeployAnswers.appName || `${projectName}-api`,
670
+ framework: backendConfig?.framework || 'express',
671
+ path: backendDeployAnswers.backendDeployPath || `/var/www/${projectName}/api`,
672
+ });
673
+
674
+ if (frontendDeployMethod === 'server') {
675
+ const { frontendDeployPath } = await inquirer.prompt([
676
+ {
677
+ type: 'input',
678
+ name: 'frontendDeployPath',
679
+ message: 'Frontend deploy path:',
680
+ default: `/var/www/${projectName}/public`,
681
+ },
682
+ ]);
683
+ envEntry.frontendDeployPath = frontendDeployPath;
684
+ }
685
+
686
+ const { healthUrlInput } = await inquirer.prompt([
687
+ {
688
+ type: 'input',
689
+ name: 'healthUrlInput',
690
+ message: 'Health check URL (optional):',
691
+ default: healthUrl || '',
692
+ },
693
+ ]);
694
+ if (healthUrlInput) healthUrl = healthUrlInput;
695
+ else if (!healthUrl && backendConfig?.port) {
696
+ healthUrl = `http://localhost:${backendConfig.port}/health`;
697
+ }
698
+
699
+ environments[envName] = envEntry;
700
+ }
701
+ }
702
+
703
+ const version = await getProjectVersion(cwd);
704
+ const hasDocker =
705
+ (detectedFrontend?.hasDocker || detectedBackend?.hasDocker) ?? false;
706
+
707
+ /** @type {Record<string, unknown>} */
708
+ const config = {
709
+ project: projectName,
710
+ version,
711
+ projectType,
712
+ artifact: true,
713
+ storage: answers.storage.length > 0 ? answers.storage : ['local'],
714
+ deploy,
715
+ environments,
716
+ healthCheck: {
717
+ url: healthUrl,
718
+ timeout: 30,
719
+ },
720
+ notifications: {
721
+ slack: false,
722
+ email: false,
723
+ webhook: false,
724
+ },
725
+ pipeline: {
726
+ test: true,
727
+ docker: hasDocker,
728
+ deploy: deploy.length > 0,
729
+ verify: !!healthUrl,
730
+ notify: false,
731
+ },
732
+ artifactRetention: 10,
733
+ cli: {
734
+ source: answers.cliSource,
735
+ },
736
+ };
737
+
738
+ if (projectType === 'both') {
739
+ config.frontend = frontendConfig;
740
+ config.backend = backendConfig;
741
+ config.framework = backendConfig?.framework;
742
+ } else {
743
+ Object.assign(config, singleConfig);
744
+ if (projectType === 'backend') {
745
+ config.docker = hasDocker;
746
+ } else {
747
+ config.docker = hasDocker;
748
+ }
749
+ }
750
+
751
+ await saveConfig(config, cwd);
752
+ await addDeployhubToPackageJson(answers.cliSource, cwd);
753
+ await writeWorkflowFile(
754
+ config.storage,
755
+ deploy,
756
+ environments,
757
+ cwd,
758
+ answers.cliSource,
759
+ config
760
+ );
761
+
762
+ await generateProjectScaffold(config, environments, cwd);
763
+
764
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
765
+ const envExampleSrc = path.resolve(__dirname, '../../.env.example');
766
+ const envExampleDest = path.join(cwd, '.env.example');
767
+ if (await fs.pathExists(envExampleSrc)) {
768
+ await fs.copy(envExampleSrc, envExampleDest);
769
+ }
770
+
771
+ const secrets = getRequiredSecrets(config.storage, deploy, environments, config);
772
+
773
+ console.log('');
774
+ console.log(chalk.green.bold('✓ DeployHub initialized successfully!'));
775
+ console.log('');
776
+ console.log(chalk.bold('Generated files:'));
777
+ console.log(' • deployhub.config.json');
778
+ console.log(' • .github/workflows/deployhub.yml');
779
+ console.log(' • .env.example');
780
+ console.log('');
781
+ console.log(chalk.bold('Next steps:'));
782
+ console.log(' 1. Push the DeployHub CLI repo to GitHub (if using github: source)');
783
+ console.log(' 2. Copy .env.example to .env and fill in credentials');
784
+ if (secrets.length > 0) {
785
+ console.log(' 3. Add these secrets to GitHub (Settings → Secrets):');
786
+ secrets.forEach((s) => console.log(` • ${s}`));
787
+ }
788
+ console.log(` ${secrets.length > 0 ? '4' : '3'}. Run ${chalk.cyan('deployhub doctor')} to verify your setup`);
789
+ console.log(` ${secrets.length > 0 ? '5' : '4'}. Push to main — GitHub Actions will run ${chalk.cyan('deployhub build')} automatically`);
790
+ console.log('');
791
+ printAuthorFooter();
792
+ });
793
+ }
794
+
795
+ export default { registerInitCommand };