@akash-chowdhury-24/deployhub 1.0.16 → 2.0.2

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 (32) hide show
  1. package/README.md +264 -59
  2. package/package.json +4 -3
  3. package/src/commands/doctor.js +168 -170
  4. package/src/commands/init.js +80 -371
  5. package/src/commands/rollback.js +1 -1
  6. package/src/core/config.js +17 -13
  7. package/src/core/stages.js +0 -11
  8. package/src/deployment/deployment-env.js +625 -0
  9. package/src/deployment/index.js +0 -72
  10. package/src/deployment/init-helpers.js +389 -0
  11. package/src/deployment/init-prompts.js +432 -0
  12. package/src/deployment/providers/azure-vm.js +85 -2
  13. package/src/deployment/providers/docker.js +101 -8
  14. package/src/deployment/providers/ec2.js +88 -2
  15. package/src/deployment/providers/gcp-vm.js +89 -2
  16. package/src/deployment/providers/kubernetes.js +104 -7
  17. package/src/deployment/providers/ssh.js +21 -4
  18. package/src/utils/github-actions.js +28 -131
  19. package/src/utils/rollback/engine.js +69 -0
  20. package/src/deployment/providers/platforms/_shared.js +0 -167
  21. package/src/deployment/providers/platforms/aws-amplify.js +0 -367
  22. package/src/deployment/providers/platforms/azure-static-web-apps.js +0 -68
  23. package/src/deployment/providers/platforms/cloudflare-pages.js +0 -103
  24. package/src/deployment/providers/platforms/firebase-app-hosting.js +0 -95
  25. package/src/deployment/providers/platforms/firebase-hosting.js +0 -99
  26. package/src/deployment/providers/platforms/index.js +0 -44
  27. package/src/deployment/providers/platforms/netlify.js +0 -102
  28. package/src/deployment/providers/platforms/vercel.js +0 -92
  29. package/src/rollback/engine.js +0 -102
  30. package/src/utils/firebase-config-generator.js +0 -35
  31. package/src/utils/init-platform.js +0 -236
  32. package/src/utils/platform-env.js +0 -132
@@ -7,11 +7,13 @@ import { loadConfig, loadEnv } from '../core/config.js';
7
7
  import { testProvider } from '../storage/index.js';
8
8
  import { getDeploymentProvider } from '../deployment/index.js';
9
9
  import { PROVIDER_ENV_MAP } from '../utils/github-actions.js';
10
- import { PLATFORM_ENV_MAP, PLATFORM_CLI_MAP } from '../utils/platform-env.js';
11
- import { createPlatformProvider } from '../deployment/providers/platforms/index.js';
12
- import { isCliInstalled } from '../deployment/providers/platforms/_shared.js';
13
10
  import { printDoctorFooter } from '../utils/author.js';
14
11
  import { createLocalProvider } from '../storage/providers/local.js';
12
+ import {
13
+ getDeploymentEnvKeys,
14
+ getDeploymentSecretKeys,
15
+ } from '../deployment/deployment-env.js';
16
+ import { testSshConnectivity, validateSshKeyForDoctor, testSshHostReachability } from '../deployment/init-helpers.js';
15
17
 
16
18
  /**
17
19
  * @typedef {{ name: string, pass: boolean, message: string }} CheckResult
@@ -51,14 +53,165 @@ function resolveBackendFramework(config) {
51
53
  return config.backend?.framework || config.framework || 'express';
52
54
  }
53
55
 
56
+ /** @type {Set<string>} */
57
+ const SSH_DEPLOY_TYPES = new Set(['ssh', 'ec2', 'azure-vm', 'gcp-vm']);
58
+
54
59
  /**
55
60
  * @param {import('../core/config.js').DeployHubConfig} config
56
61
  * @param {string} envName
62
+ * @param {Record<string, unknown>} envConfig
57
63
  * @returns {Promise<CheckResult[]>}
58
64
  */
59
- async function runBackendProcessChecks(config, envName) {
65
+ async function runDeploymentChecks(config, envName, envConfig) {
66
+ const deployType = envConfig.type;
67
+ if (!deployType || typeof deployType !== 'string') return [];
68
+
69
+ /** @type {CheckResult[]} */
70
+ const checks = [];
71
+ const requiredKeys = getDeploymentEnvKeys(deployType, config);
72
+
73
+ checks.push(
74
+ await runCheck(`${deployType} env vars`, async () => {
75
+ const missing = requiredKeys.filter((k) => {
76
+ if (k === 'SSH_KEY_PATH') {
77
+ return !process.env.SSH_KEY_PATH && !process.env.SSH_KEY;
78
+ }
79
+ return !process.env[k];
80
+ });
81
+ if (missing.length > 0) {
82
+ return {
83
+ name: `${deployType} env vars`,
84
+ pass: false,
85
+ message: `Missing required variables: ${missing.join(', ')} — copy .env.example to .env and fill in values (see inline comments).`,
86
+ };
87
+ }
88
+ return {
89
+ name: `${deployType} env vars`,
90
+ pass: true,
91
+ message: 'All required deployment variables present',
92
+ };
93
+ })
94
+ );
95
+
96
+ if (SSH_DEPLOY_TYPES.has(deployType)) {
97
+ const host = envConfig.host || process.env.SSH_HOST;
98
+ const user = envConfig.user || process.env.SSH_USER;
99
+ const keyPath = envConfig.keyPath || process.env.SSH_KEY_PATH;
100
+ const sshPort = Number(process.env.SSH_SSH_PORT || envConfig.sshPort) || 22;
101
+
102
+ checks.push(
103
+ await runCheck('SSH key', async () => {
104
+ const result = await validateSshKeyForDoctor(
105
+ keyPath ? String(keyPath) : undefined,
106
+ process.env.SSH_KEY
107
+ );
108
+ return {
109
+ name: 'SSH key',
110
+ pass: result.ok,
111
+ message: result.message,
112
+ };
113
+ })
114
+ );
115
+
116
+ checks.push(
117
+ await runCheck('SSH host reachability', async () => {
118
+ if (!host) {
119
+ return {
120
+ name: 'SSH host reachability',
121
+ pass: false,
122
+ message: 'SSH_HOST is required — set it in .env to your server IP or hostname.',
123
+ };
124
+ }
125
+ const result = await testSshHostReachability(String(host), sshPort);
126
+ return {
127
+ name: 'SSH host reachability',
128
+ pass: result.ok,
129
+ message: result.message,
130
+ };
131
+ })
132
+ );
133
+
134
+ const isBackend = config.projectType === 'backend' || config.projectType === 'both';
135
+ if (isBackend) {
136
+ const backendChecks = await runBackendProcessChecks(config, envName, deployType);
137
+ checks.push(...backendChecks);
138
+ }
139
+ }
140
+
141
+ if (deployType === 'docker') {
142
+ checks.push(
143
+ await runCheck('Docker daemon', async () => {
144
+ try {
145
+ const provider = getDeploymentProvider('docker', config, envName);
146
+ await provider.testConnection();
147
+ return { name: 'Docker daemon', pass: true, message: 'Docker daemon reachable' };
148
+ } catch (err) {
149
+ const msg = err instanceof Error ? err.message : String(err);
150
+ return {
151
+ name: 'Docker daemon',
152
+ pass: false,
153
+ message: `Docker not reachable — ${msg}. Install Docker or set DOCKER_HOST for a remote daemon.`,
154
+ };
155
+ }
156
+ })
157
+ );
158
+ }
159
+
160
+ if (deployType === 'kubernetes') {
161
+ checks.push(
162
+ await runCheck('Kubernetes cluster', async () => {
163
+ try {
164
+ const provider = getDeploymentProvider('kubernetes', config, envName);
165
+ await provider.testConnection();
166
+ const ctx = process.env.KUBE_CONTEXT || 'current';
167
+ const ns = process.env.KUBE_NAMESPACE || config.project || 'default';
168
+ return {
169
+ name: 'Kubernetes cluster',
170
+ pass: true,
171
+ message: `kubectl cluster-info OK (context: ${ctx}, namespace: ${ns})`,
172
+ };
173
+ } catch (err) {
174
+ const msg = err instanceof Error ? err.message : String(err);
175
+ return {
176
+ name: 'Kubernetes cluster',
177
+ pass: false,
178
+ message: `kubectl cluster-info failed — ${msg}. Check KUBECONFIG path and KUBE_CONTEXT.`,
179
+ };
180
+ }
181
+ })
182
+ );
183
+ }
184
+
185
+ if (deployType === 'ec2' && process.env.EC2_INSTANCE_ID) {
186
+ checks.push(
187
+ await runCheck('EC2 API', async () => {
188
+ const missing = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION'].filter(
189
+ (k) => !process.env[k]
190
+ );
191
+ if (missing.length > 0) {
192
+ return {
193
+ name: 'EC2 API',
194
+ pass: false,
195
+ message: `EC2_INSTANCE_ID is set but missing: ${missing.join(', ')} — needed for dynamic IP lookup.`,
196
+ };
197
+ }
198
+ return { name: 'EC2 API', pass: true, message: 'AWS credentials present for EC2 lookup' };
199
+ })
200
+ );
201
+ }
202
+
203
+ return checks;
204
+ }
205
+
206
+ /**
207
+ * @param {import('../core/config.js').DeployHubConfig} config
208
+ * @param {string} envName
209
+ * @param {string} [deployType]
210
+ * @returns {Promise<CheckResult[]>}
211
+ */
212
+ async function runBackendProcessChecks(config, envName, deployType = 'ssh') {
60
213
  const framework = resolveBackendFramework(config);
61
- const provider = getDeploymentProvider('ssh', config, envName);
214
+ const provider = getDeploymentProvider(deployType, config, envName);
62
215
 
63
216
  if (!provider.runRemoteCheck) {
64
217
  return [];
@@ -163,132 +316,6 @@ async function runBackendProcessChecks(config, envName) {
163
316
  return checks;
164
317
  }
165
318
 
166
- /**
167
- * @param {import('../core/config.js').DeployHubConfig} config
168
- * @param {string} envName
169
- * @param {string} [cwd]
170
- * @returns {Promise<CheckResult[]>}
171
- */
172
- async function runPlatformChecks(config, envName, cwd = process.cwd()) {
173
- const env = config.environments[envName];
174
- const platform = env?.platform;
175
- if (!platform) return [];
176
-
177
- const cli = PLATFORM_CLI_MAP[platform];
178
- const envKeys = PLATFORM_ENV_MAP[platform] || [];
179
- /** @type {CheckResult[]} */
180
- const checks = [];
181
-
182
- if (cli?.binary) {
183
- const cliLabel = cli.binary.charAt(0).toUpperCase() + cli.binary.slice(1);
184
- checks.push(
185
- await runCheck(`${cliLabel} CLI`, async () => {
186
- const installed = await isCliInstalled(cli.binary);
187
- if (installed) {
188
- return { name: `${cliLabel} CLI`, pass: true, message: `${cli.binary} CLI installed` };
189
- }
190
- return {
191
- name: `${cliLabel} CLI`,
192
- pass: false,
193
- message: `not found — run: ${cli.globalInstall || `npm install -g ${cli.install}`}`,
194
- };
195
- })
196
- );
197
- }
198
-
199
- for (const key of envKeys) {
200
- const isToken = key.includes('TOKEN') || key.includes('KEY');
201
- checks.push(
202
- await runCheck(key, async () => {
203
- if (!process.env[key]) {
204
- return { name: key, pass: false, message: 'not set in .env' };
205
- }
206
- if (isToken && key === envKeys[0]) {
207
- try {
208
- const provider = createPlatformProvider(platform, config, envName);
209
- if (provider.testConnection) {
210
- await provider.testConnection();
211
- return { name: key, pass: true, message: 'token valid' };
212
- }
213
- } catch (err) {
214
- return {
215
- name: key,
216
- pass: false,
217
- message: err instanceof Error ? err.message : 'token invalid',
218
- };
219
- }
220
- }
221
- return { name: key, pass: true, message: 'present' };
222
- })
223
- );
224
- }
225
-
226
- if (platform === 'vercel') {
227
- checks.push(
228
- await runCheck('Vercel project link', async () => {
229
- const vercelJson = path.join(cwd, '.vercel', 'project.json');
230
- if (await fs.pathExists(vercelJson)) {
231
- return { name: 'Vercel project link', pass: true, message: '.vercel/project.json found' };
232
- }
233
- return {
234
- name: 'Vercel project link',
235
- pass: false,
236
- message: 'not found — run: vercel link',
237
- };
238
- })
239
- );
240
- }
241
-
242
- if (platform === 'firebase-hosting') {
243
- checks.push(
244
- await runCheck('firebase.json', async () => {
245
- if (await fs.pathExists(path.join(cwd, 'firebase.json'))) {
246
- return { name: 'firebase.json', pass: true, message: 'found' };
247
- }
248
- return {
249
- name: 'firebase.json',
250
- pass: false,
251
- message: 'missing — run deployhub init or create manually',
252
- };
253
- })
254
- );
255
- }
256
-
257
- try {
258
- const provider = createPlatformProvider(platform, config, envName);
259
- if (provider.testConnection && envKeys.every((k) => process.env[k])) {
260
- checks.push(
261
- await runCheck(`${platform} connection`, async () => {
262
- await provider.testConnection();
263
- if (platform === 'netlify') {
264
- return { name: 'NETLIFY_SITE_ID', pass: true, message: 'site found' };
265
- }
266
- if (platform === 'cloudflare-pages') {
267
- return { name: 'CF project', pass: true, message: 'project exists' };
268
- }
269
- if (platform === 'aws-amplify') {
270
- return { name: 'AMPLIFY_APP_ID', pass: true, message: 'app found in AWS' };
271
- }
272
- if (platform.startsWith('firebase')) {
273
- return { name: 'Firebase project', pass: true, message: 'project ID valid' };
274
- }
275
- return { name: `${platform} connection`, pass: true, message: 'connected' };
276
- })
277
- );
278
- }
279
- } catch (err) {
280
- if (envKeys.every((k) => process.env[k])) {
281
- checks.push({
282
- name: `${platform} connection`,
283
- pass: false,
284
- message: err instanceof Error ? err.message : String(err),
285
- });
286
- }
287
- }
288
-
289
- return checks;
290
- }
291
-
292
319
  /**
293
320
  * @param {import('commander').Command} program
294
321
  */
@@ -467,32 +494,8 @@ export function registerDoctorCommand(program) {
467
494
  const env = config.environments[envName];
468
495
  if (!env) continue;
469
496
 
470
- if (env.deploymentType === 'platform' || env.frontendDeploymentType === 'platform') {
471
- const platformChecks = await runPlatformChecks(config, envName, cwd);
472
- results.push(...platformChecks);
473
- }
474
-
475
- if (env.type && ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(env.type)) {
476
- results.push(
477
- await runCheck('SSH target', async () => {
478
- const provider = getDeploymentProvider(env.type, config, envName);
479
- await provider.testConnection();
480
- const host = env.host || process.env.SSH_HOST;
481
- return {
482
- name: 'SSH target',
483
- pass: true,
484
- message: `Can reach ${host || 'host'}`,
485
- };
486
- })
487
- );
488
-
489
- const isBackend =
490
- config.projectType === 'backend' || config.projectType === 'both';
491
- if (isBackend && env.type === 'ssh') {
492
- const backendChecks = await runBackendProcessChecks(config, envName);
493
- results.push(...backendChecks);
494
- }
495
- }
497
+ const deployChecks = await runDeploymentChecks(config, envName, env);
498
+ results.push(...deployChecks);
496
499
  }
497
500
 
498
501
  results.push(
@@ -539,22 +542,17 @@ export function registerDoctorCommand(program) {
539
542
  }
540
543
  for (const envName of config.deploy || []) {
541
544
  const env = config.environments[envName];
542
- if (!env) continue;
543
-
544
- if (env.deploymentType === 'platform' || env.frontendDeploymentType === 'platform') {
545
- const platform = env.platform;
546
- if (platform) {
547
- const keys = PLATFORM_ENV_MAP[platform] || [];
548
- required.push(...keys);
549
- }
550
- } else if (env.type) {
551
- const keys = PROVIDER_ENV_MAP[env.type] || [];
552
- required.push(...keys);
553
- }
545
+ if (!env?.type) continue;
546
+ required.push(...getDeploymentSecretKeys(env.type, config));
554
547
  }
555
548
 
556
549
  const unique = [...new Set(required)];
557
- const missing = unique.filter((k) => !process.env[k]);
550
+ const missing = unique.filter((k) => {
551
+ if (k === 'SSH_KEY') {
552
+ return !process.env.SSH_KEY && !process.env.SSH_KEY_PATH;
553
+ }
554
+ return !process.env[k];
555
+ });
558
556
  if (missing.length > 0) {
559
557
  return {
560
558
  name: 'Secrets',