@akash-chowdhury-24/deployhub 2.0.20 → 2.0.22

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.
@@ -19,6 +19,7 @@ import {
19
19
  getEnvSettings,
20
20
  resolveDefaultEnvironmentName,
21
21
  } from '../core/config.js';
22
+ import { detectDjangoWsgiPackageDir } from '../utils/python-app-target.js';
22
23
 
23
24
  /**
24
25
  * @param {string} cwd
@@ -71,21 +72,26 @@ function resolveArtifactType(config) {
71
72
  */
72
73
  function resolveBuildSettings(config) {
73
74
  if (config.projectType === 'both' && config.backend) {
75
+ const buildCommand = config.backend.buildCommand ?? null;
74
76
  return {
75
77
  buildOutput: config.backend.buildOutput || '.',
76
78
  framework: config.backend.framework,
77
79
  startCommand: config.backend.startCommand || null,
78
80
  port: config.backend.port || 3000,
79
- buildCommand: config.backend.buildCommand ?? null,
81
+ buildCommand,
80
82
  };
81
83
  }
82
84
 
85
+ const buildCommand = config.buildCommand ?? null;
86
+ const isBackend = config.projectType === 'backend';
83
87
  return {
84
- buildOutput: config.buildOutput || 'dist',
88
+ // Backend packages are source trees (or compiled jars/binaries under a known
89
+ // output dir when set). Never default backends to frontend-style "dist".
90
+ buildOutput: config.buildOutput || (isBackend ? '.' : 'dist'),
85
91
  framework: config.framework || 'node',
86
92
  startCommand: config.startCommand || null,
87
93
  port: config.port || 3000,
88
- buildCommand: config.buildCommand ?? null,
94
+ buildCommand,
89
95
  };
90
96
  }
91
97
 
@@ -194,14 +200,47 @@ async function stageBackendArtifact(cwd, stagingDir, config) {
194
200
 
195
201
  if (['express', 'nestjs', 'fastify', 'koa', 'nextjs'].includes(framework)) {
196
202
  await copyIfExists(cwd, stagingDir, 'package.json');
197
- } else if (['fastapi', 'django', 'flask'].includes(framework)) {
203
+ await copyIfExists(cwd, stagingDir, 'package-lock.json');
204
+ } else if (['fastapi', 'django', 'flask', 'python'].includes(framework)) {
205
+ // Source-only packages: root entrypoints + manifests (no dist/).
198
206
  await copyIfExists(cwd, stagingDir, 'requirements.txt');
199
- if (framework === 'django' && (await fs.pathExists(path.join(cwd, 'manage.py')))) {
200
- await copyIfExists(cwd, stagingDir, 'manage.py');
207
+ await copyIfExists(cwd, stagingDir, 'pyproject.toml');
208
+ await copyIfExists(cwd, stagingDir, 'Pipfile');
209
+ await copyIfExists(cwd, stagingDir, 'Pipfile.lock');
210
+ await copyIfExists(cwd, stagingDir, 'setup.py');
211
+ await copyIfExists(cwd, stagingDir, 'manage.py');
212
+ await copyIfExists(cwd, stagingDir, 'main.py');
213
+ await copyIfExists(cwd, stagingDir, 'app.py');
214
+ await copyIfExists(cwd, stagingDir, 'wsgi.py');
215
+ await copyIfExists(cwd, stagingDir, 'asgi.py');
216
+ await copyDirectoryIfExists(cwd, stagingDir, 'app');
217
+ await copyDirectoryIfExists(cwd, stagingDir, 'apps');
218
+ // Django project package often sits next to manage.py (e.g. config/, mysite/).
219
+ // `config/` is already copied above for all backends; also copy the detected
220
+ // package that owns wsgi.py when it is not config/app/apps.
221
+ if (framework === 'django') {
222
+ await copyDirectoryIfExists(cwd, stagingDir, 'templates');
223
+ await copyDirectoryIfExists(cwd, stagingDir, 'static');
224
+ const wsgiPkg = detectDjangoWsgiPackageDir(cwd);
225
+ if (
226
+ wsgiPkg &&
227
+ !['app', 'apps', 'config', 'templates', 'static', 'src'].includes(wsgiPkg)
228
+ ) {
229
+ await copyDirectoryIfExists(cwd, stagingDir, wsgiPkg);
230
+ }
201
231
  }
202
- } else if (['laravel', 'symfony'].includes(framework)) {
232
+ } else if (['laravel', 'symfony', 'php'].includes(framework)) {
203
233
  await copyIfExists(cwd, stagingDir, 'composer.json');
204
234
  await copyIfExists(cwd, stagingDir, 'composer.lock');
235
+ await copyIfExists(cwd, stagingDir, 'artisan');
236
+ await copyDirectoryIfExists(cwd, stagingDir, 'app');
237
+ await copyDirectoryIfExists(cwd, stagingDir, 'bootstrap');
238
+ await copyDirectoryIfExists(cwd, stagingDir, 'public');
239
+ await copyDirectoryIfExists(cwd, stagingDir, 'routes');
240
+ await copyDirectoryIfExists(cwd, stagingDir, 'database');
241
+ await copyDirectoryIfExists(cwd, stagingDir, 'resources');
242
+ await copyDirectoryIfExists(cwd, stagingDir, 'storage');
243
+ await copyDirectoryIfExists(cwd, stagingDir, 'bin');
205
244
  } else if (framework === 'spring') {
206
245
  await copyIfExists(cwd, stagingDir, 'pom.xml');
207
246
  const targetDir = path.join(cwd, 'target');
@@ -226,11 +265,19 @@ async function stageBackendArtifact(cwd, stagingDir, config) {
226
265
  await copyIfExists(cwd, stagingDir, 'Gemfile');
227
266
  await copyIfExists(cwd, stagingDir, 'Gemfile.lock');
228
267
  await copyIfExists(cwd, stagingDir, 'config.ru');
268
+ await copyIfExists(cwd, stagingDir, 'Rakefile');
269
+ await copyDirectoryIfExists(cwd, stagingDir, 'app');
270
+ await copyDirectoryIfExists(cwd, stagingDir, 'bin');
271
+ await copyDirectoryIfExists(cwd, stagingDir, 'lib');
272
+ await copyDirectoryIfExists(cwd, stagingDir, 'db');
273
+ await copyDirectoryIfExists(cwd, stagingDir, 'public');
229
274
  } else {
230
275
  await copyIfExists(cwd, stagingDir, 'package.json');
231
276
  await copyIfExists(cwd, stagingDir, 'requirements.txt');
232
277
  }
233
278
 
279
+ // Only copy a named build-output dir when it actually exists (compiled langs).
280
+ // Skip '.' / 'src' — source is already staged above; never invent an empty dist/.
234
281
  if (settings.buildOutput && settings.buildOutput !== '.' && settings.buildOutput !== 'src') {
235
282
  const built = path.join(cwd, settings.buildOutput);
236
283
  if (await fs.pathExists(built) && !['target', 'bin', 'publish'].includes(settings.buildOutput)) {
@@ -309,6 +356,7 @@ export async function createArtifact(config, deployedTargets = [], cwd = process
309
356
  projectType: artifactType,
310
357
  framework: settings.framework,
311
358
  buildOutput: settings.buildOutput,
359
+ buildCommand: settings.buildCommand,
312
360
  startCommand: settings.startCommand,
313
361
  port: settings.port,
314
362
  generatedBy: getGeneratedByMetadata(),
@@ -320,6 +368,8 @@ export async function createArtifact(config, deployedTargets = [], cwd = process
320
368
  metadata.frontend = frontend;
321
369
  metadata.backend = {
322
370
  framework: settings.framework,
371
+ buildOutput: settings.buildOutput,
372
+ buildCommand: settings.buildCommand,
323
373
  startCommand: settings.startCommand,
324
374
  port: settings.port,
325
375
  };
@@ -0,0 +1,46 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * Format an unexpected CLI error for the user — message only, never a stack
5
+ * or pkg snapshot paths like `C:\snapshot\...` / `/snapshot/...`.
6
+ *
7
+ * @param {unknown} err
8
+ * @returns {string}
9
+ */
10
+ export function formatFatalCliError(err) {
11
+ const raw = err instanceof Error ? err.message : String(err);
12
+ const message = raw
13
+ .split(/\r?\n/)
14
+ .map((line) => line.trim())
15
+ .filter((line) => line && !/[/\\]snapshot[/\\]/i.test(line) && !/^\s*at\s+/.test(line))
16
+ .join(' ')
17
+ .trim() || 'Unknown error';
18
+
19
+ return (
20
+ `Unexpected error: ${message}\n` +
21
+ ` If this persists, open an issue with the command you ran.`
22
+ );
23
+ }
24
+
25
+ /**
26
+ * @param {unknown} err
27
+ * @param {(msg: string) => void} [write]
28
+ */
29
+ export function reportFatalCliError(err, write = console.error) {
30
+ write(chalk.red(formatFatalCliError(err)));
31
+ }
32
+
33
+ /**
34
+ * Last-resort handlers so unexpected failures never dump a raw Node stack
35
+ * (especially ugly under pkg: `C:\\snapshot\\DeployHub\\...`).
36
+ */
37
+ export function installCliFatalHandlers() {
38
+ process.on('uncaughtException', (err) => {
39
+ reportFatalCliError(err);
40
+ process.exit(1);
41
+ });
42
+ process.on('unhandledRejection', (reason) => {
43
+ reportFatalCliError(reason instanceof Error ? reason : new Error(String(reason)));
44
+ process.exit(1);
45
+ });
46
+ }
package/src/cli/index.js CHANGED
@@ -17,7 +17,12 @@ import { registerSyncWorkflowsCommand } from '../commands/sync-workflows.js';
17
17
  import { registerSyncK8sPortsCommand } from '../commands/sync-k8s-ports.js';
18
18
  import { registerEnvCommand } from '../commands/env.js';
19
19
  import { formatVersionOutput, printBanner, shouldShowBanner } from '../utils/author.js';
20
+ import {
21
+ installCliFatalHandlers,
22
+ reportFatalCliError,
23
+ } from './fatal-error.js';
20
24
 
25
+ installCliFatalHandlers();
21
26
  loadEnv();
22
27
 
23
28
  const program = new Command();
@@ -48,4 +53,7 @@ registerSyncWorkflowsCommand(program);
48
53
  registerSyncK8sPortsCommand(program);
49
54
  registerEnvCommand(program);
50
55
 
51
- program.parse();
56
+ program.parseAsync(process.argv).catch((err) => {
57
+ reportFatalCliError(err);
58
+ process.exit(1);
59
+ });
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { loadConfig, loadEnv } from '../core/config.js';
2
+ import { loadEnv } from '../core/config.js';
3
+ import { loadConfigOrExit } from '../core/load-config-or-exit.js';
3
4
  import {
4
5
  createArtifact,
5
6
  listLocalArtifacts,
@@ -25,7 +26,7 @@ export function registerArtifactCommand(program) {
25
26
  .description('Create artifact from current build output')
26
27
  .action(async () => {
27
28
  loadEnv();
28
- const config = await loadConfig();
29
+ const config = await loadConfigOrExit();
29
30
  const result = await createArtifact(config, [], process.cwd());
30
31
  console.log(chalk.green(`Artifact created: ${result.artifactDir}`));
31
32
  });
@@ -36,7 +37,7 @@ export function registerArtifactCommand(program) {
36
37
  .option('--remote', 'Also list builds from remote history.json')
37
38
  .action(async (opts) => {
38
39
  loadEnv();
39
- const config = await loadConfig();
40
+ const config = await loadConfigOrExit();
40
41
  const artifacts = await listLocalArtifacts();
41
42
 
42
43
  console.log(chalk.bold('\nLocal artifacts:\n'));
@@ -91,7 +92,7 @@ export function registerArtifactCommand(program) {
91
92
  .description('Download and extract an artifact by buildId or legacy semver')
92
93
  .action(async (versionOrBuildId) => {
93
94
  loadEnv();
94
- const config = await loadConfig();
95
+ const config = await loadConfigOrExit();
95
96
  const cwd = process.cwd();
96
97
  const needle = String(versionOrBuildId).replace(/^v/i, '');
97
98
 
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { loadConfig, loadEnv } from '../core/config.js';
2
+ import { loadEnv } from '../core/config.js';
3
+ import { loadConfigOrExit } from '../core/load-config-or-exit.js';
3
4
  import { runPipeline } from '../core/pipeline.js';
4
5
  import { buildPipelineStages } from '../core/stages.js';
5
6
 
@@ -13,14 +14,7 @@ export function registerBuildCommand(program) {
13
14
  .action(async () => {
14
15
  loadEnv();
15
16
  const cwd = process.cwd();
16
-
17
- let config;
18
- try {
19
- config = await loadConfig(cwd);
20
- } catch (err) {
21
- console.error(chalk.red(err instanceof Error ? err.message : String(err)));
22
- process.exit(1);
23
- }
17
+ const config = await loadConfigOrExit(cwd);
24
18
 
25
19
  /** @type {Record<string, unknown>} */
26
20
  const state = {};
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { loadConfig, loadEnv } from '../core/config.js';
2
+ import { loadEnv } from '../core/config.js';
3
+ import { loadConfigOrExit } from '../core/load-config-or-exit.js';
3
4
  import { resolveEnvTargets } from '../core/environments.js';
4
5
  import { runPipeline } from '../core/pipeline.js';
5
6
  import { listLocalArtifacts } from '../artifact/engine.js';
@@ -26,7 +27,7 @@ export function registerDeployCommand(program) {
26
27
  .action(async (opts) => {
27
28
  loadEnv();
28
29
  const cwd = process.cwd();
29
- const config = await loadConfig(cwd);
30
+ const config = await loadConfigOrExit(cwd);
30
31
  const log = createLogger('deploy');
31
32
 
32
33
  let targets;
@@ -4,7 +4,8 @@ import fs from 'fs-extra';
4
4
  import path from 'path';
5
5
  import os from 'os';
6
6
  import axios from 'axios';
7
- import { loadConfig, loadEnv, getEnvMethod, getEnvSettings } from '../core/config.js';
7
+ import { loadEnv, getEnvMethod, getEnvSettings } from '../core/config.js';
8
+ import { loadConfigOrExit } from '../core/load-config-or-exit.js';
8
9
  import {
9
10
  isEnvEnabled,
10
11
  resolveDefaultEnvironmentName,
@@ -612,6 +613,41 @@ async function runBackendProcessChecks(config, envName, deployType = 'ssh') {
612
613
  }
613
614
 
614
615
  if (PYTHON_FRAMEWORKS.has(framework)) {
616
+ checks.push(
617
+ await runCheck('Python 3', async () => {
618
+ const result = await provider.runRemoteCheck('command -v python3');
619
+ if (result.pass) {
620
+ return { name: 'Python 3', pass: true, message: 'python3 found on PATH' };
621
+ }
622
+ return {
623
+ name: 'Python 3',
624
+ pass: false,
625
+ message:
626
+ 'python3 not found on PATH — install it on the server ' +
627
+ '(Amazon Linux: sudo yum install -y python3; Ubuntu: sudo apt-get install -y python3)',
628
+ };
629
+ })
630
+ );
631
+
632
+ checks.push(
633
+ await runCheck('pip', async () => {
634
+ // Deploy runs `pip install -r requirements.txt`; accept pip3 or pip.
635
+ const result = await provider.runRemoteCheck(
636
+ 'command -v pip3 >/dev/null 2>&1 || command -v pip >/dev/null 2>&1'
637
+ );
638
+ if (result.pass) {
639
+ return { name: 'pip', pass: true, message: 'pip3 or pip found on PATH' };
640
+ }
641
+ return {
642
+ name: 'pip',
643
+ pass: false,
644
+ message:
645
+ 'pip/pip3 not found on PATH — install it on the server ' +
646
+ '(Amazon Linux: sudo yum install -y python3-pip; Ubuntu: sudo apt-get install -y python3-pip)',
647
+ };
648
+ })
649
+ );
650
+
615
651
  checks.push(
616
652
  await runCheck('gunicorn', async () => {
617
653
  const result = await provider.runRemoteCheck('which gunicorn || gunicorn --version');
@@ -705,6 +741,8 @@ export function registerDoctorCommand(program) {
705
741
  .action(async (opts) => {
706
742
  loadEnv();
707
743
  const cwd = process.cwd();
744
+ // Doctor requires a project config — fail cleanly before any checks (no null.storage crash).
745
+ const config = await loadConfigOrExit(cwd);
708
746
  /** @type {CheckResult[]} */
709
747
  const results = [];
710
748
  /** @type {Set<string>} names of checks that must not fail the summary */
@@ -746,17 +784,6 @@ export function registerDoctorCommand(program) {
746
784
 
747
785
  results.push(
748
786
  await runCheck('Build command', async () => {
749
- let config;
750
- try {
751
- config = await loadConfig(cwd);
752
- } catch {
753
- return {
754
- name: 'Build command',
755
- pass: false,
756
- message: 'deployhub.config.json not found — run deployhub init',
757
- };
758
- }
759
-
760
787
  if (config.projectType === 'backend' && !config.buildCommand) {
761
788
  return {
762
789
  name: 'Build command',
@@ -794,14 +821,7 @@ export function registerDoctorCommand(program) {
794
821
  })
795
822
  );
796
823
 
797
- let config = null;
798
- try {
799
- config = await loadConfig(cwd);
800
- } catch {
801
- // handled above
802
- }
803
-
804
- if (config) {
824
+ {
805
825
  for (const provider of config.storage || []) {
806
826
  const label = provider.charAt(0).toUpperCase() + provider.slice(1);
807
827
  if (provider === 'aws') {
@@ -946,10 +966,6 @@ export function registerDoctorCommand(program) {
946
966
 
947
967
  results.push(
948
968
  await runCheck('Secrets', async () => {
949
- if (!config) {
950
- return { name: 'Secrets', pass: false, message: 'No config found' };
951
- }
952
-
953
969
  /** @type {string[]} */
954
970
  const required = [];
955
971
  for (const provider of config.storage || []) {
@@ -984,7 +1000,7 @@ export function registerDoctorCommand(program) {
984
1000
  );
985
1001
 
986
1002
  // Explicit CI-prefixed secret reminders for non-grandfathered environments
987
- if (config) {
1003
+ {
988
1004
  const inCi = process.env.GITHUB_ACTIONS === 'true' || process.env.CI === 'true';
989
1005
  for (const envName of Object.keys(config.environments || {})) {
990
1006
  const env = config.environments[envName];
@@ -1,10 +1,10 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import {
4
- loadConfig,
5
4
  loadEnv,
6
5
  saveConfig,
7
6
  } from '../core/config.js';
7
+ import { loadConfigOrExit } from '../core/load-config-or-exit.js';
8
8
  import {
9
9
  getEnabledEnvironmentNames,
10
10
  getEnvMethod,
@@ -72,7 +72,7 @@ export function registerEnvCommand(program) {
72
72
  .description('List configured environments')
73
73
  .action(async () => {
74
74
  loadEnv();
75
- const config = await loadConfig();
75
+ const config = await loadConfigOrExit();
76
76
  const names = Object.keys(config.environments || {});
77
77
  if (names.length === 0) {
78
78
  console.log(chalk.yellow('No environments configured. Run: deployhub env add <name>'));
@@ -117,7 +117,7 @@ export function registerEnvCommand(program) {
117
117
  .action(async (rawName, opts) => {
118
118
  loadEnv();
119
119
  const cwd = process.cwd();
120
- const config = await loadConfig(cwd);
120
+ const config = await loadConfigOrExit(cwd);
121
121
  const log = createLogger('env');
122
122
 
123
123
  const nameCheck = validateEnvironmentName(rawName, Object.keys(config.environments || {}));
@@ -233,7 +233,7 @@ export function registerEnvCommand(program) {
233
233
  .action(async (name) => {
234
234
  loadEnv();
235
235
  const cwd = process.cwd();
236
- const config = await loadConfig(cwd);
236
+ const config = await loadConfigOrExit(cwd);
237
237
  if (!config.environments[name]) {
238
238
  console.error(chalk.red(`Environment "${name}" not found.`));
239
239
  process.exit(1);
@@ -251,7 +251,7 @@ export function registerEnvCommand(program) {
251
251
  .action(async (name) => {
252
252
  loadEnv();
253
253
  const cwd = process.cwd();
254
- const config = await loadConfig(cwd);
254
+ const config = await loadConfigOrExit(cwd);
255
255
  if (!config.environments[name]) {
256
256
  console.error(chalk.red(`Environment "${name}" not found.`));
257
257
  process.exit(1);
@@ -269,7 +269,7 @@ export function registerEnvCommand(program) {
269
269
  .action(async (name) => {
270
270
  loadEnv();
271
271
  const cwd = process.cwd();
272
- const config = await loadConfig(cwd);
272
+ const config = await loadConfigOrExit(cwd);
273
273
  if (!config.environments[name]) {
274
274
  console.error(chalk.red(`Environment "${name}" not found.`));
275
275
  process.exit(1);
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { loadConfig, loadEnv } from '../core/config.js';
2
+ import { loadEnv } from '../core/config.js';
3
+ import { loadConfigOrExit } from '../core/load-config-or-exit.js';
3
4
  import { resolveEnvTargets } from '../core/environments.js';
4
5
  import {
5
6
  rollbackToVersion,
@@ -23,7 +24,7 @@ export function registerRollbackCommand(program) {
23
24
  )
24
25
  .action(async (versionOrBuildId, opts) => {
25
26
  loadEnv();
26
- const config = await loadConfig();
27
+ const config = await loadConfigOrExit();
27
28
  const log = createLogger('rollback');
28
29
 
29
30
  let targets;
@@ -93,14 +93,8 @@ export function registerStorageCommand(program) {
93
93
  .description('List configured storage providers and status')
94
94
  .action(async () => {
95
95
  loadEnv();
96
- let config;
97
- try {
98
- const { loadConfig } = await import('../core/config.js');
99
- config = await loadConfig();
100
- } catch {
101
- console.error(chalk.red('Run deployhub init first'));
102
- process.exit(1);
103
- }
96
+ const { loadConfigOrExit } = await import('../core/load-config-or-exit.js');
97
+ const config = await loadConfigOrExit();
104
98
 
105
99
  const results = await testAllProviders(config.storage);
106
100
  console.log(chalk.bold('\nStorage Providers:\n'));
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { loadConfig, loadEnv } from '../core/config.js';
2
+ import { loadEnv } from '../core/config.js';
3
+ import { loadConfigOrExit } from '../core/load-config-or-exit.js';
3
4
  import { resolveContainerPort } from '../utils/dockerfile-expose.js';
4
5
  import {
5
6
  getDefaultKubernetesManifestPaths,
@@ -23,7 +24,7 @@ export function registerSyncK8sPortsCommand(program) {
23
24
  .action(async () => {
24
25
  loadEnv();
25
26
  const cwd = process.cwd();
26
- const config = await loadConfig(cwd);
27
+ const config = await loadConfigOrExit(cwd);
27
28
 
28
29
  const { deploymentPath, servicePath } = getDefaultKubernetesManifestPaths(cwd);
29
30
  const hasDeployment = await fs.pathExists(deploymentPath);
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { loadConfig, loadEnv } from '../core/config.js';
2
+ import { loadEnv } from '../core/config.js';
3
+ import { loadConfigOrExit } from '../core/load-config-or-exit.js';
3
4
  import { getEnabledEnvironmentNames } from '../core/environments.js';
4
5
  import {
5
6
  writeWorkflowFile,
@@ -20,7 +21,7 @@ export function registerSyncWorkflowsCommand(program) {
20
21
  .action(async () => {
21
22
  loadEnv();
22
23
  const cwd = process.cwd();
23
- const config = await loadConfig(cwd);
24
+ const config = await loadConfigOrExit(cwd);
24
25
 
25
26
  const storage = config.storage || [];
26
27
  const environments = config.environments || {};
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { loadConfig, loadEnv } from '../core/config.js';
2
+ import { loadEnv } from '../core/config.js';
3
+ import { loadConfigOrExit } from '../core/load-config-or-exit.js';
3
4
  import { resolveEnvTargets } from '../core/environments.js';
4
5
  import {
5
6
  anyEnvHasResolvableHealthCheckUrl,
@@ -86,7 +87,7 @@ export function registerVerifyCommand(program) {
86
87
  )
87
88
  .action(async (opts) => {
88
89
  loadEnv();
89
- const config = await loadConfig();
90
+ const config = await loadConfigOrExit();
90
91
 
91
92
  let outcome;
92
93
  try {
@@ -0,0 +1,37 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig } from './config.js';
3
+
4
+ /**
5
+ * @param {unknown} err
6
+ * @returns {boolean}
7
+ */
8
+ export function isConfigMissingError(err) {
9
+ return err instanceof Error && /Config not found/i.test(err.message);
10
+ }
11
+
12
+ /**
13
+ * User-facing missing-config message (no stack, no snapshot paths).
14
+ */
15
+ export function printMissingConfigError() {
16
+ console.error(chalk.red('✗ No deployhub.config.json found in this directory.'));
17
+ console.error(chalk.red(" Run 'deployhub init' first to set up your project."));
18
+ }
19
+
20
+ /**
21
+ * Load config or print a clean error and exit (never throws to the caller).
22
+ * @param {string} [cwd]
23
+ * @returns {Promise<import('./config.js').DeployHubConfig>}
24
+ */
25
+ export async function loadConfigOrExit(cwd = process.cwd()) {
26
+ try {
27
+ return await loadConfig(cwd);
28
+ } catch (err) {
29
+ if (isConfigMissingError(err)) {
30
+ printMissingConfigError();
31
+ } else {
32
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
33
+ }
34
+ process.exit(1);
35
+ throw err;
36
+ }
37
+ }
@@ -144,6 +144,8 @@ export function buildPipelineStages(config, cwd, state) {
144
144
  return;
145
145
  }
146
146
 
147
+ // Adapters themselves null-guard buildCommand; this stage always
148
+ // invokes build() so interpreted backends can log the skip cleanly.
147
149
  const adapter = getAdapter(ctx.config.framework, ctx.config, ctx.cwd);
148
150
  await adapter.build();
149
151
  },
@@ -24,6 +24,32 @@ export const SERVER_DEPLOY_TYPES = [
24
24
 
25
25
  const SSH_BASED = ['ssh', 'ec2', 'azure-vm', 'gcp-vm'];
26
26
 
27
+ const NODE_PM2_FRAMEWORKS = new Set([
28
+ 'express',
29
+ 'nestjs',
30
+ 'fastify',
31
+ 'koa',
32
+ 'nextjs',
33
+ 'node',
34
+ ]);
35
+
36
+ /**
37
+ * User-facing label for the backend process identity field (`appName`).
38
+ * Node backends use PM2; other languages use DEPLOYHUB_APP + PID files.
39
+ *
40
+ * @param {string|undefined|null} framework
41
+ * @param {string} projectName
42
+ * @param {'frontend'|'backend'|'both'} projectType
43
+ * @returns {string}
44
+ */
45
+ export function backendProcessNamePromptMessage(framework, projectName, projectType) {
46
+ const backendFramework = String(framework || '').toLowerCase();
47
+ const usesPm2 = !backendFramework || NODE_PM2_FRAMEWORKS.has(backendFramework);
48
+ const example = projectType === 'both' ? `${projectName}-api` : projectName;
49
+ return usesPm2
50
+ ? `PM2 process name for your backend (e.g. ${example}):`
51
+ : `Process name for your backend (identifies this app's process on the server, e.g. ${example}):`;
52
+ }
27
53
  /**
28
54
  * Prompt for one environment's deployment method + method-specific config.
29
55
  * Shared by `deployhub init` and `deployhub env add`.
@@ -427,7 +453,11 @@ async function promptSshBasedDeployment(base, projectName, projectType, backendC
427
453
  questions.push({
428
454
  type: 'input',
429
455
  name: 'appName',
430
- message: `PM2 process name for your backend (e.g. ${projectName}-api):`,
456
+ message: backendProcessNamePromptMessage(
457
+ /** @type {string|undefined} */ (backendConfig?.framework),
458
+ projectName,
459
+ projectType
460
+ ),
431
461
  default: projectType === 'both' ? `${projectName}-api` : projectName,
432
462
  });
433
463
  }