@akash-chowdhury-24/deployhub 2.0.19 → 2.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -618,6 +618,8 @@ All JS frontends share the same install/build flow: `npm ci` → `npm run build`
618
618
  - **Install:** Composer (on CI and server).
619
619
  - **Deploy:** SSH with PHP-FPM or `php artisan` for Laravel.
620
620
 
621
+ > ⚠️ **PHP-FPM deployments restart the FPM service for the ENTIRE host on every deploy.** If you run multiple DeployHub-managed environments on the same server, deploying ANY of them will briefly interrupt in-flight requests for ALL of them. For production use with multiple environments, either use separate hosts per environment, or set up per-environment PHP-FPM pools manually (not yet automated by DeployHub).
622
+
621
623
  ### Java
622
624
 
623
625
  - **Detect:** `pom.xml` with Spring Boot.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akash-chowdhury-24/deployhub",
3
- "version": "2.0.19",
3
+ "version": "2.0.20",
4
4
  "description": "Zero-configuration deployment and artifact manager",
5
5
  "type": "module",
6
6
  "main": "./src/cli/index.js",
@@ -36,7 +36,7 @@ import {
36
36
  printDeploymentNextSteps,
37
37
  formatSecretChecklistLine,
38
38
  } from '../deployment/deployment-env.js';
39
- import { confirmValueIfContainsSpaces } from '../deployment/init-helpers.js';
39
+ import { confirmValueIfContainsSpaces, normalizeInitHealthCheckUrl } from '../deployment/init-helpers.js';
40
40
 
41
41
  const FRONTEND_CHOICES = [
42
42
  { name: 'React', value: 'react' },
@@ -405,10 +405,13 @@ export function registerInitCommand(program) {
405
405
 
406
406
  environments[name] = entry;
407
407
 
408
- if (deployAnswers.healthUrl) {
409
- healthUrl = deployAnswers.healthUrl;
410
- } else if (!healthUrl && (singleConfig?.port || backendConfig?.port)) {
411
- healthUrl = `http://localhost:${singleConfig?.port || backendConfig?.port}/health`;
408
+ // Only persist a health check URL the user actually entered.
409
+ // Never synthesize http://localhost:<port>/health — that always fails
410
+ // from GitHub Actions (remote runner deploy target) and incorrectly
411
+ // enables the verify stage when the user left the optional prompt blank.
412
+ const answered = normalizeInitHealthCheckUrl(deployAnswers.healthUrl);
413
+ if (answered) {
414
+ healthUrl = answered;
412
415
  }
413
416
 
414
417
  const secrets = getDockerEnvSecrets(deployAnswers);
@@ -32,6 +32,18 @@ export function suggestSshUser(osHint) {
32
32
  return undefined;
33
33
  }
34
34
 
35
+ /**
36
+ * Normalize the optional health-check URL from `deployhub init`.
37
+ * Blank / whitespace → empty string. Never synthesizes localhost defaults
38
+ * (those always fail from GitHub Actions runners, which are not the deploy target).
39
+ *
40
+ * @param {unknown} answer
41
+ * @returns {string}
42
+ */
43
+ export function normalizeInitHealthCheckUrl(answer) {
44
+ return typeof answer === 'string' && answer.trim() ? answer.trim() : '';
45
+ }
46
+
35
47
  /**
36
48
  * @param {string} keyPath
37
49
  * @returns {Promise<{ ok: boolean, message: string, fixed?: boolean }>}
@@ -2,6 +2,7 @@ import { execa } from 'execa';
2
2
  import { createLogger } from '../../logger/index.js';
3
3
  import { createDockerImageDeployContext } from '../../utils/docker-image-deploy.js';
4
4
  import { resolveDockerImageRefForTag } from '../../utils/docker-image.js';
5
+ import { resolveDockerContainerName } from '../../utils/docker-container-name.js';
5
6
  import { getEnvSettings, mergeMethodSettingsIntoEnv } from '../../core/environments.js';
6
7
 
7
8
  /**
@@ -15,6 +16,8 @@ export function createDockerProvider(config, envName, env = process.env) {
15
16
  const effectiveEnv = mergeMethodSettingsIntoEnv(env, settings);
16
17
  const imageOps = createDockerImageDeployContext(config, effectiveEnv, log);
17
18
  const { fullImage, getDockerEnv, ensureImageReadyForDeploy } = imageOps;
19
+ // Env-scoped like PM2/Nginx — same-daemon multi-env must not share one container name.
20
+ const containerName = resolveDockerContainerName(config, envName);
18
21
 
19
22
  /**
20
23
  * @param {string} artifactDir
@@ -36,11 +39,11 @@ export function createDockerProvider(config, envName, env = process.env) {
36
39
 
37
40
  await execa(
38
41
  'docker',
39
- ['rm', '-f', config.project],
42
+ ['rm', '-f', containerName],
40
43
  { stdio: 'pipe', env: dockerEnv }
41
44
  ).catch(() => {});
42
45
 
43
- await execa('docker', ['run', '-d', '--rm', '--name', config.project, imageRef], {
46
+ await execa('docker', ['run', '-d', '--rm', '--name', containerName, imageRef], {
44
47
  stdio: 'inherit',
45
48
  env: dockerEnv,
46
49
  });
@@ -81,7 +84,7 @@ export function createDockerProvider(config, envName, env = process.env) {
81
84
  try {
82
85
  const { stdout } = await execa(
83
86
  'docker',
84
- ['ps', '--filter', `name=${config.project}`, '--format', '{{.Status}}'],
87
+ ['ps', '--filter', `name=^/${containerName}$`, '--format', '{{.Status}}'],
85
88
  { stdio: 'pipe', env: getDockerEnv() }
86
89
  );
87
90
  return stdout.includes('Up');
@@ -8,6 +8,7 @@ import { createDockerImageDeployContext } from '../../utils/docker-image-deploy.
8
8
  import { resolveDockerImageRefForTag } from '../../utils/docker-image.js';
9
9
  import { ensureKubernetesNamespace } from '../../utils/kubernetes-namespace.js';
10
10
  import { syncKubernetesDeploymentImage } from '../../utils/kubernetes-deploy-image.js';
11
+ import { resolveKubeNamespace } from '../../utils/kube-namespace-name.js';
11
12
  import { getEnvSettings, mergeMethodSettingsIntoEnv } from '../../core/environments.js';
12
13
 
13
14
  /**
@@ -24,8 +25,9 @@ export function createKubernetesProvider(config, envName, env = process.env) {
24
25
  const kubeconfig =
25
26
  effectiveEnv.KUBECONFIG || path.join(os.homedir(), '.kube', 'config');
26
27
  const context = effectiveEnv.KUBE_CONTEXT || '';
27
- const namespace =
28
- effectiveEnv.KUBE_NAMESPACE || config.project || 'default';
28
+ // Env-scoped like Nginx/PM2 — same-cluster multi-env must not share one namespace
29
+ // when settings still default to the project name for every environment.
30
+ const namespace = resolveKubeNamespace(config, envName, effectiveEnv);
29
31
  const deploymentName = sanitizeK8sName(config.project || 'app');
30
32
 
31
33
  function getKubectlEnv() {
@@ -10,6 +10,7 @@ import {
10
10
  getNginxConfDPath,
11
11
  resolveNginxSiteName,
12
12
  } from '../../utils/nginx.js';
13
+ import { resolvePm2AppName } from '../../utils/pm2-app-name.js';
13
14
  import { shellQuote, formatRemoteCommandFailure } from '../../utils/shell-quote.js';
14
15
 
15
16
  /** @type {Set<string>} */
@@ -44,8 +45,8 @@ export function createSshProvider(config, envName, env = process.env) {
44
45
  settings.frontendDeployPath || deployPath;
45
46
  const backendDeployPath =
46
47
  settings.backendDeployPath || deployPath;
47
- const appName =
48
- settings.appName || env.SSH_APP_NAME || config.project;
48
+ // Env-scoped like Nginx site names — same-host multi-env must not share one PM2 name.
49
+ const appName = resolvePm2AppName(config, envName, env);
49
50
  const port = settings.port || config.port || Number(env.SSH_PORT) || 3000;
50
51
  const sshKey = env.SSH_KEY;
51
52
  const keyPath = settings.keyPath || env.SSH_KEY_PATH;
@@ -133,6 +134,58 @@ export function createSshProvider(config, envName, env = process.env) {
133
134
  );
134
135
  }
135
136
 
137
+ /**
138
+ * Stop a previously managed non-PM2 backend for THIS env only.
139
+ *
140
+ * PID-file kill is gated: we only signal a PID if /proc shows our
141
+ * DEPLOYHUB_APP / deployhub.app marker in cmdline or environ. A stale PID
142
+ * reused by an unrelated process is left alone (file still removed).
143
+ * Marker-based pkill remains the primary stop for live processes.
144
+ *
145
+ * @param {import('node-ssh').NodeSSH} ssh
146
+ * @param {string} targetPath
147
+ */
148
+ async function stopScopedBackendProcess(ssh, targetPath) {
149
+ const pidFile = `${targetPath}/.deployhub.pid`;
150
+ const markerEnv = `DEPLOYHUB_APP=${appName}`;
151
+ const markerJvm = `deployhub.app=${appName}`;
152
+ // Verify-then-kill: only signal a PID if /proc shows our marker in cmdline
153
+ // or environ. A stale PID reused by an unrelated process is left alone
154
+ // (the pidfile is still removed so the next start writes a fresh one).
155
+ await exec(
156
+ ssh,
157
+ `if [ -f ${sh(pidFile)} ]; then ` +
158
+ `pid="$(cat ${sh(pidFile)} 2>/dev/null | tr -cd '0-9')"; ` +
159
+ `if [ -n "$pid" ] && [ -r "/proc/$pid/cmdline" ]; then ` +
160
+ `if tr '\\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -q -F ${sh(markerEnv)} ` +
161
+ `|| tr '\\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | grep -q -F ${sh(markerJvm)} ` +
162
+ `|| { [ -r "/proc/$pid/environ" ] && tr '\\0' ' ' < "/proc/$pid/environ" 2>/dev/null | grep -q -F ${sh(markerEnv)}; }; then ` +
163
+ `kill "$pid" 2>/dev/null || true; ` +
164
+ `fi; ` +
165
+ `fi; ` +
166
+ `rm -f ${sh(pidFile)}; ` +
167
+ `fi`
168
+ );
169
+ // Marker match — covers lost PID files / processes without a readable pidfile.
170
+ await exec(ssh, `pkill -f ${sh(markerEnv)} || true`);
171
+ await exec(ssh, `pkill -f ${sh(markerJvm)} || true`);
172
+ }
173
+
174
+ /**
175
+ * Start a nohup process with DEPLOYHUB_APP marker and write PID file.
176
+ * @param {import('node-ssh').NodeSSH} ssh
177
+ * @param {string} targetPath
178
+ * @param {string} command — command body after `nohup` (no trailing &)
179
+ */
180
+ async function startScopedNohup(ssh, targetPath, command) {
181
+ const pidFile = `${targetPath}/.deployhub.pid`;
182
+ const dir = sh(targetPath);
183
+ await exec(
184
+ ssh,
185
+ `cd ${dir} && DEPLOYHUB_APP=${sh(appName)} nohup ${command} > app.log 2>&1 & echo $! > ${sh(pidFile)}`
186
+ );
187
+ }
188
+
136
189
  /**
137
190
  * @param {import('node-ssh').NodeSSH} ssh
138
191
  * @param {string} targetPath
@@ -141,6 +194,7 @@ export function createSshProvider(config, envName, env = process.env) {
141
194
  const framework = resolveFramework();
142
195
  const startCommand = resolveStartCommand();
143
196
  const dir = sh(targetPath);
197
+ const pidFile = `${targetPath}/.deployhub.pid`;
144
198
 
145
199
  if (NODE_FRAMEWORKS.has(framework)) {
146
200
  await exec(ssh, `cd ${dir} && npm install --production`);
@@ -172,25 +226,28 @@ export function createSshProvider(config, envName, env = process.env) {
172
226
  if (framework === 'django') {
173
227
  await exec(ssh, `cd ${dir} && python manage.py migrate`);
174
228
  }
229
+ await stopScopedBackendProcess(ssh, targetPath);
175
230
  if (framework === 'fastapi') {
176
- await exec(ssh, 'pkill uvicorn || true');
177
- await exec(
231
+ await startScopedNohup(
178
232
  ssh,
179
- `cd ${dir} && nohup uvicorn main:app --host 0.0.0.0 --port ${port} > app.log 2>&1 &`
233
+ targetPath,
234
+ `uvicorn main:app --host 0.0.0.0 --port ${port}`
180
235
  );
181
236
  } else {
182
- await exec(ssh, 'pkill gunicorn || true');
237
+ // gunicorn --daemon writes its own PID; still set DEPLOYHUB_APP for pkill fallback.
183
238
  const appTarget =
184
239
  framework === 'django' ? 'config.wsgi:application' : 'app:app';
185
240
  await exec(
186
241
  ssh,
187
- `cd ${dir} && nohup gunicorn ${appTarget} --bind 0.0.0.0:${port} --daemon`
242
+ `cd ${dir} && DEPLOYHUB_APP=${sh(appName)} gunicorn ${appTarget} --name ${sh(`deployhub-${appName}`)} --bind 0.0.0.0:${port} --pid ${sh(pidFile)} --daemon`
188
243
  );
189
244
  }
190
245
  return;
191
246
  }
192
247
 
193
248
  if (PHP_FRAMEWORKS.has(framework)) {
249
+ // PHP uses a host-wide `systemctl restart php*-fpm` (see README PHP warning).
250
+ // Per-env isolation is Nginx site name + deploy path — not automated FPM pools.
194
251
  await exec(ssh, `cd ${dir} && composer install --no-dev`);
195
252
  if (framework === 'laravel') {
196
253
  await exec(ssh, `cd ${dir} && php artisan migrate --force`);
@@ -202,40 +259,36 @@ export function createSshProvider(config, envName, env = process.env) {
202
259
  }
203
260
 
204
261
  if (framework === 'spring' || framework === 'java') {
205
- await exec(ssh, `cd ${dir} && pkill -f "*.jar" || true`);
206
- await exec(
262
+ await stopScopedBackendProcess(ssh, targetPath);
263
+ // -Ddeployhub.app= embeds the env-scoped identity in the JVM command line
264
+ // so pkill -f DEPLOYHUB_APP=… and the PID file both target only this env.
265
+ await startScopedNohup(
207
266
  ssh,
208
- `cd ${dir} && nohup java -jar target/*.jar > app.log 2>&1 &`
267
+ targetPath,
268
+ `java -Ddeployhub.app=${appName} -jar target/*.jar`
209
269
  );
210
270
  return;
211
271
  }
212
272
 
213
273
  if (framework === 'go') {
214
- await exec(ssh, `cd ${dir} && pkill ${sh(appName)} || true`);
215
- await exec(
216
- ssh,
217
- `cd ${dir} && nohup ./bin/app > app.log 2>&1 &`
218
- );
274
+ await stopScopedBackendProcess(ssh, targetPath);
275
+ // Binary is always ./bin/app — must NOT pkill by appName alone (that never
276
+ // matched the process) and must NOT pkill a bare "app" (cross-env collision).
277
+ await startScopedNohup(ssh, targetPath, './bin/app');
219
278
  return;
220
279
  }
221
280
 
222
281
  if (framework === 'dotnet') {
223
- await exec(ssh, `cd ${dir} && pkill -f "dotnet" || true`);
282
+ await stopScopedBackendProcess(ssh, targetPath);
224
283
  const dll = startCommand?.replace('dotnet ', '') || 'App.dll';
225
- await exec(
226
- ssh,
227
- `cd ${dir} && nohup dotnet ${dll} > app.log 2>&1 &`
228
- );
284
+ await startScopedNohup(ssh, targetPath, `dotnet ${dll}`);
229
285
  return;
230
286
  }
231
287
 
232
288
  if (framework === 'rails') {
233
289
  await exec(ssh, `cd ${dir} && bundle install --deployment`);
234
- await exec(ssh, `cd ${dir} && pkill puma || true`);
235
- await exec(
236
- ssh,
237
- `cd ${dir} && nohup bundle exec puma -p ${port} > app.log 2>&1 &`
238
- );
290
+ await stopScopedBackendProcess(ssh, targetPath);
291
+ await startScopedNohup(ssh, targetPath, `bundle exec puma -p ${port}`);
239
292
  return;
240
293
  }
241
294
 
@@ -0,0 +1,51 @@
1
+ import { isGrandfatheredNginxEnv } from './nginx.js';
2
+
3
+ /**
4
+ * Sanitize a Docker container name (alphanumeric, underscore, hyphen, period).
5
+ * @param {string} name
6
+ * @returns {string}
7
+ */
8
+ export function sanitizeDockerContainerName(name) {
9
+ return String(name || 'app')
10
+ .replace(/[^a-zA-Z0-9_.-]/g, '-')
11
+ .replace(/^[^a-zA-Z0-9]/, 'a');
12
+ }
13
+
14
+ /**
15
+ * Whether this env is the grandfathered / single-env case for container naming.
16
+ * Reuses the same grandfather rule as Nginx / PM2.
17
+ *
18
+ * @param {import('../core/config.js').DeployHubConfig} config
19
+ * @param {string} envName
20
+ * @returns {boolean}
21
+ */
22
+ export function isGrandfatheredDockerContainerEnv(config, envName) {
23
+ return isGrandfatheredNginxEnv(config, envName);
24
+ }
25
+
26
+ /**
27
+ * Resolve the Docker container `--name` for an environment.
28
+ *
29
+ * Same risk class as PM2 process names: two docker envs targeting the same
30
+ * daemon with `docker run --name ${project}` will `docker rm -f` each other.
31
+ *
32
+ * - Grandfathered / single-env: `config.project` (unchanged).
33
+ * - Additional envs: `{project}-{env}`.
34
+ *
35
+ * @param {import('../core/config.js').DeployHubConfig} config
36
+ * @param {string} envName
37
+ * @returns {string}
38
+ */
39
+ export function resolveDockerContainerName(config, envName) {
40
+ const project = sanitizeDockerContainerName(config.project || 'app');
41
+ if (isGrandfatheredDockerContainerEnv(config, envName)) {
42
+ return project;
43
+ }
44
+ return `${project}-${sanitizeDockerContainerName(envName)}`;
45
+ }
46
+
47
+ export default {
48
+ sanitizeDockerContainerName,
49
+ isGrandfatheredDockerContainerEnv,
50
+ resolveDockerContainerName,
51
+ };
@@ -0,0 +1,63 @@
1
+ import { isGrandfatheredNginxEnv } from './nginx.js';
2
+ import { getEnvSettings } from '../core/environments.js';
3
+ import { sanitizeK8sName } from './kubernetes-manifests.js';
4
+
5
+ /**
6
+ * Whether this env is the grandfathered / single-env case for namespace naming.
7
+ * @param {import('../core/config.js').DeployHubConfig} config
8
+ * @param {string} envName
9
+ * @returns {boolean}
10
+ */
11
+ export function isGrandfatheredKubeNamespaceEnv(config, envName) {
12
+ return isGrandfatheredNginxEnv(config, envName);
13
+ }
14
+
15
+ /**
16
+ * Resolve the Kubernetes namespace for an environment.
17
+ *
18
+ * Deployment *names* stay project-scoped (safe when namespaces differ). Two
19
+ * environments targeting the same cluster with the same namespace WILL collide
20
+ * on Deployment/Service objects — same risk class as PM2/Nginx on one host.
21
+ *
22
+ * - Grandfathered / single-env: settings.kubeNamespace || KUBE_NAMESPACE || project
23
+ * (unchanged).
24
+ * - Additional envs: if configured namespace is missing or equals the project /
25
+ * grandfathered namespace, auto-scope to `{project}-{env}`. An explicitly
26
+ * distinct namespace is kept.
27
+ *
28
+ * @param {import('../core/config.js').DeployHubConfig} config
29
+ * @param {string} envName
30
+ * @param {Record<string, string|undefined>} [env] — already secret-overlaid
31
+ * @returns {string}
32
+ */
33
+ export function resolveKubeNamespace(config, envName, env = process.env) {
34
+ const project = sanitizeK8sName(config.project || 'app');
35
+ const settings = getEnvSettings(config.environments?.[envName]);
36
+ const configured = String(
37
+ settings.kubeNamespace || env.KUBE_NAMESPACE || ''
38
+ ).trim();
39
+
40
+ if (isGrandfatheredKubeNamespaceEnv(config, envName)) {
41
+ return sanitizeK8sName(configured || project);
42
+ }
43
+
44
+ const grandfather =
45
+ config.unprefixedSecretEnvironment || config.defaultEnvironment || null;
46
+ let grandfatherNs = project;
47
+ if (grandfather && config.environments?.[grandfather]) {
48
+ const gfSettings = getEnvSettings(config.environments[grandfather]);
49
+ grandfatherNs = sanitizeK8sName(gfSettings.kubeNamespace || project);
50
+ }
51
+
52
+ const defaults = new Set([project, 'default', grandfatherNs]);
53
+ if (configured && !defaults.has(sanitizeK8sName(configured))) {
54
+ return sanitizeK8sName(configured);
55
+ }
56
+
57
+ return sanitizeK8sName(`${project}-${envName}`);
58
+ }
59
+
60
+ export default {
61
+ isGrandfatheredKubeNamespaceEnv,
62
+ resolveKubeNamespace,
63
+ };
@@ -0,0 +1,71 @@
1
+ import { isGrandfatheredNginxEnv } from './nginx.js';
2
+ import { getEnvSettings } from '../core/environments.js';
3
+
4
+ /**
5
+ * Sanitize a name for PM2 process naming (same charset as Nginx site names).
6
+ * @param {string} name
7
+ * @returns {string}
8
+ */
9
+ export function sanitizePm2AppName(name) {
10
+ return String(name || 'app').replace(/[^a-zA-Z0-9_-]/g, '-');
11
+ }
12
+
13
+ /**
14
+ * Whether this env is the grandfathered / single-env case for process naming.
15
+ * Reuses the same grandfather rule as Nginx site filenames.
16
+ *
17
+ * @param {import('../core/config.js').DeployHubConfig} config
18
+ * @param {string} envName
19
+ * @returns {boolean}
20
+ */
21
+ export function isGrandfatheredPm2Env(config, envName) {
22
+ return isGrandfatheredNginxEnv(config, envName);
23
+ }
24
+
25
+ /**
26
+ * Resolve the PM2 process name for an environment.
27
+ *
28
+ * Same risk class as Nginx site filenames: two backend envs on one host with
29
+ * the same PM2 name will restart/replace each other's process.
30
+ *
31
+ * - Grandfathered / single-env: `settings.appName` || `SSH_APP_NAME` || `project`
32
+ * (unchanged — existing single-env PM2 processes keep their name).
33
+ * - Additional envs: auto-scope to `{project}-{env}` when the configured name
34
+ * is missing, equals the project default, or collides with the grandfathered
35
+ * env's resolved name. An explicitly distinct `appName` / `SSH_APP_NAME` is kept.
36
+ *
37
+ * @param {import('../core/config.js').DeployHubConfig} config
38
+ * @param {string} envName
39
+ * @param {Record<string, string|undefined>} [env] — already secret-overlaid for this env
40
+ * @returns {string}
41
+ */
42
+ export function resolvePm2AppName(config, envName, env = process.env) {
43
+ const project = sanitizePm2AppName(config.project || 'app');
44
+ const settings = getEnvSettings(config.environments?.[envName]);
45
+ const configured = (settings.appName || env.SSH_APP_NAME || '').trim();
46
+
47
+ if (isGrandfatheredPm2Env(config, envName)) {
48
+ return sanitizePm2AppName(configured || project);
49
+ }
50
+
51
+ const grandfather =
52
+ config.unprefixedSecretEnvironment || config.defaultEnvironment || null;
53
+ let grandfatherName = project;
54
+ if (grandfather && config.environments?.[grandfather]) {
55
+ const gfSettings = getEnvSettings(config.environments[grandfather]);
56
+ grandfatherName = sanitizePm2AppName(gfSettings.appName || project);
57
+ }
58
+
59
+ const defaults = new Set([project, `${project}-api`, grandfatherName]);
60
+ if (configured && !defaults.has(sanitizePm2AppName(configured))) {
61
+ return sanitizePm2AppName(configured);
62
+ }
63
+
64
+ return `${project}-${sanitizePm2AppName(envName)}`;
65
+ }
66
+
67
+ export default {
68
+ sanitizePm2AppName,
69
+ isGrandfatheredPm2Env,
70
+ resolvePm2AppName,
71
+ };