@akash-chowdhury-24/deployhub 2.0.0 → 2.0.3

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.
@@ -2,7 +2,7 @@ import inquirer from 'inquirer';
2
2
  import chalk from 'chalk';
3
3
  import fs from 'fs-extra';
4
4
  import path from 'path';
5
- import { saveConfig } from '../core/config.js';
5
+ import { saveConfig, appendEnv } from '../core/config.js';
6
6
  import { detectFrontend, detectBackend } from '../detectors/index.js';
7
7
  import {
8
8
  getFrontendInfo,
@@ -20,6 +20,14 @@ import {
20
20
  } from '../utils/github-actions.js';
21
21
  import { printAuthorFooter } from '../utils/author.js';
22
22
  import { generateNginxConfig } from '../utils/nginx.js';
23
+ import {
24
+ promptServerDeployment,
25
+ buildServerEnvEntry,
26
+ getDockerEnvSecrets,
27
+ SSH_BASED,
28
+ } from '../deployment/init-prompts.js';
29
+ import { printDeploymentNextSteps } from '../deployment/deployment-env.js';
30
+ import { confirmValueIfContainsSpaces } from '../deployment/init-helpers.js';
23
31
 
24
32
  const FRONTEND_CHOICES = [
25
33
  { name: 'React', value: 'react' },
@@ -151,136 +159,6 @@ async function promptBuildSettings(defaults, side) {
151
159
  return inquirer.prompt(questions);
152
160
  }
153
161
 
154
- const SERVER_DEPLOY_TYPES = [
155
- 'ssh',
156
- 'docker',
157
- 'ec2',
158
- 'azure-vm',
159
- 'gcp-vm',
160
- 'kubernetes',
161
- ];
162
-
163
- /**
164
- * @param {string} projectName
165
- * @param {'frontend'|'backend'|'both'} projectType
166
- * @param {Record<string, unknown>|null} backendConfig
167
- * @param {Record<string, unknown>|null} singleConfig
168
- */
169
- async function promptServerDeployment(projectName, projectType, backendConfig, singleConfig) {
170
- return inquirer.prompt([
171
- {
172
- type: 'list',
173
- name: 'deployType',
174
- message: 'Deployment type:',
175
- choices: SERVER_DEPLOY_TYPES,
176
- },
177
- {
178
- type: 'input',
179
- name: 'envName',
180
- message: 'Environment name:',
181
- default: 'production',
182
- },
183
- {
184
- type: 'input',
185
- name: 'host',
186
- message: 'Host (for SSH-based targets):',
187
- when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
188
- },
189
- {
190
- type: 'input',
191
- name: 'user',
192
- message: 'SSH user:',
193
- when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
194
- },
195
- {
196
- type: 'input',
197
- name: 'deployPath',
198
- message: 'Deploy path:',
199
- default: `/var/www/${projectName}`,
200
- when: (a) =>
201
- ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
202
- projectType !== 'both',
203
- },
204
- {
205
- type: 'input',
206
- name: 'frontendDeployPath',
207
- message: 'Frontend deploy path:',
208
- default: `/var/www/${projectName}/public`,
209
- when: (a) =>
210
- ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
211
- projectType === 'both',
212
- },
213
- {
214
- type: 'input',
215
- name: 'backendDeployPath',
216
- message: 'Backend deploy path:',
217
- default: `/var/www/${projectName}/api`,
218
- when: (a) =>
219
- ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
220
- projectType === 'both',
221
- },
222
- {
223
- type: 'input',
224
- name: 'appName',
225
- message: 'App name (for PM2):',
226
- default: projectType === 'both' ? `${projectName}-api` : projectName,
227
- when: (a) =>
228
- ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType) &&
229
- (projectType === 'backend' || projectType === 'both'),
230
- },
231
- {
232
- type: 'input',
233
- name: 'healthUrl',
234
- message: 'Health check URL (optional):',
235
- },
236
- ]);
237
- }
238
-
239
- /**
240
- * @param {Awaited<ReturnType<typeof promptServerDeployment>>} deployAnswers
241
- * @param {'frontend'|'backend'|'both'} projectType
242
- * @param {string} projectName
243
- * @param {Record<string, unknown>|null} backendConfig
244
- * @param {Record<string, unknown>|null} singleConfig
245
- */
246
- function buildServerEnvEntry(
247
- deployAnswers,
248
- projectType,
249
- projectName,
250
- backendConfig,
251
- singleConfig
252
- ) {
253
- /** @type {Record<string, unknown>} */
254
- const envEntry = {
255
- deploymentType: 'server',
256
- type: deployAnswers.deployType,
257
- host: deployAnswers.host || '',
258
- user: deployAnswers.user || '',
259
- };
260
-
261
- if (projectType === 'both') {
262
- envEntry.frontendDeployPath =
263
- deployAnswers.frontendDeployPath || `/var/www/${projectName}/public`;
264
- envEntry.backendDeployPath =
265
- deployAnswers.backendDeployPath || `/var/www/${projectName}/api`;
266
- envEntry.appName = deployAnswers.appName || `${projectName}-api`;
267
- envEntry.framework = backendConfig?.framework || 'express';
268
- envEntry.path = envEntry.backendDeployPath;
269
- envEntry.backendDeploymentType = 'server';
270
- } else if (projectType === 'backend') {
271
- envEntry.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
272
- envEntry.path = envEntry.deployPath;
273
- envEntry.appName = deployAnswers.appName || projectName;
274
- envEntry.framework = singleConfig?.framework || 'express';
275
- envEntry.port = singleConfig?.port || 3000;
276
- } else {
277
- envEntry.deployPath = deployAnswers.deployPath || `/var/www/${projectName}`;
278
- envEntry.path = envEntry.deployPath;
279
- }
280
-
281
- return envEntry;
282
- }
283
-
284
162
  /**
285
163
  * @param {Record<string, unknown>} config
286
164
  * @param {Record<string, Record<string, unknown>>} environments
@@ -290,7 +168,8 @@ async function generateProjectScaffold(config, environments, cwd) {
290
168
  const envList = Object.values(environments);
291
169
 
292
170
  const usesFrontendSsh = envList.some(
293
- (env) => env.type === 'ssh' || env.deploymentType === 'server'
171
+ (env) =>
172
+ SSH_BASED.includes(env.type) || env.deploymentType === 'server'
294
173
  );
295
174
 
296
175
  const isFrontendProject =
@@ -343,7 +222,7 @@ export function registerInitCommand(program) {
343
222
  const defaultName = path.basename(cwd) || 'my-app';
344
223
  const cliSource = DEFAULT_NPM_CLI_SOURCE;
345
224
 
346
- const { projectName } = await inquirer.prompt([
225
+ let { projectName } = await inquirer.prompt([
347
226
  {
348
227
  type: 'input',
349
228
  name: 'projectName',
@@ -352,6 +231,8 @@ export function registerInitCommand(program) {
352
231
  },
353
232
  ]);
354
233
 
234
+ projectName = await confirmValueIfContainsSpaces(projectName, 'project name');
235
+
355
236
  const { projectType } = await inquirer.prompt([
356
237
  {
357
238
  type: 'list',
@@ -431,15 +312,19 @@ export function registerInitCommand(program) {
431
312
  /** @type {string[]} */
432
313
  const deploy = [];
433
314
  let healthUrl = '';
315
+ /** @type {string|undefined} */
316
+ let primaryDeployType;
317
+ /** @type {Record<string, string>|null} */
318
+ let dockerEnvToAppend = null;
434
319
 
435
320
  if (answers.configureDeploy) {
436
321
  if (projectType === 'backend') {
437
322
  const deployAnswers = await promptServerDeployment(
438
323
  projectName,
439
324
  projectType,
440
- backendConfig,
441
- singleConfig
325
+ backendConfig
442
326
  );
327
+ primaryDeployType = deployAnswers.deployType;
443
328
  deploy.push(deployAnswers.envName);
444
329
  environments[deployAnswers.envName] = buildServerEnvEntry(
445
330
  deployAnswers,
@@ -453,13 +338,14 @@ export function registerInitCommand(program) {
453
338
  } else if (singleConfig?.port) {
454
339
  healthUrl = `http://localhost:${singleConfig.port}/health`;
455
340
  }
341
+ dockerEnvToAppend = getDockerEnvSecrets(deployAnswers);
456
342
  } else if (projectType === 'frontend') {
457
343
  const deployAnswers = await promptServerDeployment(
458
344
  projectName,
459
345
  projectType,
460
- backendConfig,
461
- singleConfig
346
+ backendConfig
462
347
  );
348
+ primaryDeployType = deployAnswers.deployType;
463
349
  deploy.push(deployAnswers.envName);
464
350
  environments[deployAnswers.envName] = buildServerEnvEntry(
465
351
  deployAnswers,
@@ -471,16 +357,15 @@ export function registerInitCommand(program) {
471
357
  if (deployAnswers.healthUrl) {
472
358
  healthUrl = deployAnswers.healthUrl;
473
359
  }
360
+ dockerEnvToAppend = getDockerEnvSecrets(deployAnswers);
474
361
  } else {
475
- const { envName } = await inquirer.prompt([
476
- {
477
- type: 'input',
478
- name: 'envName',
479
- message: 'Environment name:',
480
- default: 'production',
481
- },
482
- ]);
483
- deploy.push(envName);
362
+ const backendDeployAnswers = await promptServerDeployment(
363
+ projectName,
364
+ 'both',
365
+ backendConfig
366
+ );
367
+ primaryDeployType = backendDeployAnswers.deployType;
368
+ deploy.push(backendDeployAnswers.envName);
484
369
 
485
370
  /** @type {Record<string, unknown>} */
486
371
  const envEntry = {
@@ -489,76 +374,25 @@ export function registerInitCommand(program) {
489
374
  deploymentType: 'server',
490
375
  };
491
376
 
492
- console.log(chalk.gray('\nBackend will be deployed to a self-hosted server.'));
493
- const backendDeployAnswers = await inquirer.prompt([
494
- {
495
- type: 'list',
496
- name: 'deployType',
497
- message: 'Backend deployment type:',
498
- choices: SERVER_DEPLOY_TYPES,
499
- default: 'ssh',
500
- },
501
- {
502
- type: 'input',
503
- name: 'host',
504
- message: 'Host (for SSH-based targets):',
505
- when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
506
- },
507
- {
508
- type: 'input',
509
- name: 'user',
510
- message: 'SSH user:',
511
- when: (a) => ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(a.deployType),
512
- },
513
- {
514
- type: 'input',
515
- name: 'backendDeployPath',
516
- message: 'Backend deploy path:',
517
- default: `/var/www/${projectName}/api`,
518
- },
519
- {
520
- type: 'input',
521
- name: 'appName',
522
- message: 'App name (for PM2):',
523
- default: `${projectName}-api`,
524
- },
525
- ]);
526
-
527
- Object.assign(envEntry, {
528
- type: backendDeployAnswers.deployType,
529
- host: backendDeployAnswers.host || '',
530
- user: backendDeployAnswers.user || '',
531
- backendDeployPath:
532
- backendDeployAnswers.backendDeployPath || `/var/www/${projectName}/api`,
533
- appName: backendDeployAnswers.appName || `${projectName}-api`,
534
- framework: backendConfig?.framework || 'express',
535
- path: backendDeployAnswers.backendDeployPath || `/var/www/${projectName}/api`,
536
- });
537
-
538
- const { frontendDeployPath } = await inquirer.prompt([
539
- {
540
- type: 'input',
541
- name: 'frontendDeployPath',
542
- message: 'Frontend deploy path:',
543
- default: `/var/www/${projectName}/public`,
544
- },
545
- ]);
546
- envEntry.frontendDeployPath = frontendDeployPath;
547
-
548
- const { healthUrlInput } = await inquirer.prompt([
549
- {
550
- type: 'input',
551
- name: 'healthUrlInput',
552
- message: 'Health check URL (optional):',
553
- default: healthUrl || '',
554
- },
555
- ]);
556
- if (healthUrlInput) healthUrl = healthUrlInput;
557
- else if (!healthUrl && backendConfig?.port) {
377
+ Object.assign(
378
+ envEntry,
379
+ buildServerEnvEntry(
380
+ backendDeployAnswers,
381
+ 'both',
382
+ projectName,
383
+ backendConfig,
384
+ singleConfig
385
+ )
386
+ );
387
+
388
+ if (backendDeployAnswers.healthUrl) {
389
+ healthUrl = backendDeployAnswers.healthUrl;
390
+ } else if (backendConfig?.port) {
558
391
  healthUrl = `http://localhost:${backendConfig.port}/health`;
559
392
  }
560
393
 
561
- environments[envName] = envEntry;
394
+ environments[backendDeployAnswers.envName] = envEntry;
395
+ dockerEnvToAppend = getDockerEnvSecrets(backendDeployAnswers);
562
396
  }
563
397
  }
564
398
 
@@ -611,6 +445,9 @@ export function registerInitCommand(program) {
611
445
  }
612
446
 
613
447
  await saveConfig(config, cwd);
448
+ if (dockerEnvToAppend) {
449
+ await appendEnv(dockerEnvToAppend, cwd);
450
+ }
614
451
  await addDeployhubToPackageJson(cliSource, cwd);
615
452
  await writeWorkflowFile(
616
453
  config.storage,
@@ -635,22 +472,28 @@ export function registerInitCommand(program) {
635
472
  const secrets = getRequiredSecrets(config.storage, deploy, environments, config);
636
473
 
637
474
  console.log('');
638
- console.log(chalk.green.bold('✓ DeployHub initialized successfully!'));
475
+ if (primaryDeployType) {
476
+ console.log(chalk.green.bold(`✔ Config generated for ${primaryDeployType} deployment.`));
477
+ printDeploymentNextSteps(primaryDeployType, secrets);
478
+ } else {
479
+ console.log(chalk.green.bold('✓ DeployHub initialized successfully!'));
480
+ console.log('');
481
+ console.log(chalk.bold('Next steps:'));
482
+ console.log(' 1. Copy .env.example to .env and fill in credentials');
483
+ if (secrets.length > 0) {
484
+ console.log(' 2. Add these secrets to GitHub (Settings → Secrets):');
485
+ secrets.forEach((s) => console.log(` • ${s}`));
486
+ }
487
+ console.log(` ${secrets.length > 0 ? '3' : '2'}. Run ${chalk.cyan('deployhub doctor')} to verify your setup`);
488
+ console.log(` ${secrets.length > 0 ? '4' : '3'}. Push to main — GitHub Actions will run ${chalk.cyan('deployhub build')} automatically`);
489
+ }
490
+
639
491
  console.log('');
640
492
  console.log(chalk.bold('Generated files:'));
641
493
  console.log(' • deployhub.config.json');
642
494
  console.log(' • .github/workflows/deployhub.yml');
643
495
  console.log(' • .env.example');
644
496
  console.log('');
645
- console.log(chalk.bold('Next steps:'));
646
- console.log(' 1. Copy .env.example to .env and fill in credentials');
647
- if (secrets.length > 0) {
648
- console.log(' 2. Add these secrets to GitHub (Settings → Secrets):');
649
- secrets.forEach((s) => console.log(` • ${s}`));
650
- }
651
- console.log(` ${secrets.length > 0 ? '3' : '2'}. Run ${chalk.cyan('deployhub doctor')} to verify your setup`);
652
- console.log(` ${secrets.length > 0 ? '4' : '3'}. Push to main — GitHub Actions will run ${chalk.cyan('deployhub build')} automatically`);
653
- console.log('');
654
497
  printAuthorFooter();
655
498
  });
656
499
  }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { loadConfig, loadEnv } from '../core/config.js';
3
- import { rollbackToVersion } from '../rollback/engine.js';
3
+ import { rollbackToVersion } from '../utils/rollback/engine.js';
4
4
  import axios from 'axios';
5
5
 
6
6
  /**
@@ -22,6 +22,21 @@ const EnvironmentSchema = z.object({
22
22
  path: z.string().optional(),
23
23
  deployPath: z.string().optional(),
24
24
  keyPath: z.string().optional(),
25
+ sshPort: z.number().optional(),
26
+ ec2InstanceId: z.string().optional(),
27
+ awsRegion: z.string().optional(),
28
+ azureSubscriptionId: z.string().optional(),
29
+ azureResourceGroup: z.string().optional(),
30
+ azureVmName: z.string().optional(),
31
+ gcpProjectId: z.string().optional(),
32
+ gcpZone: z.string().optional(),
33
+ gcpInstanceName: z.string().optional(),
34
+ kubeconfig: z.string().optional(),
35
+ kubeContext: z.string().optional(),
36
+ kubeNamespace: z.string().optional(),
37
+ dockerImageName: z.string().optional(),
38
+ dockerRegistryUrl: z.string().optional(),
39
+ dockerHost: z.string().optional(),
25
40
  appName: z.string().optional(),
26
41
  framework: z.string().optional(),
27
42
  port: z.number().optional(),