@akash-chowdhury-24/deployhub 2.0.0 → 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.
package/README.md CHANGED
@@ -585,6 +585,207 @@ You can enable **multiple providers** — DeployHub uploads to all of them in pa
585
585
 
586
586
  ---
587
587
 
588
+ ## Choosing a deployment method
589
+
590
+ DeployHub supports six deployment targets. Pick based on what infrastructure you already have — DeployHub does not provision servers, VMs, or clusters for you.
591
+
592
+ | Method | Best for | You need already |
593
+ |--------|----------|------------------|
594
+ | **ssh** | Any Linux VPS or bare-metal server you control | Server with SSH, key pair, app runtime |
595
+ | **docker** | Containerized apps (Dockerfile or docker-compose.yml) | Docker locally or on a remote host |
596
+ | **ec2** | AWS users with an existing EC2 instance | Running EC2 instance, security group, key pair |
597
+ | **azure-vm** | Azure users with an existing virtual machine | Running Azure VM, NSG allowing SSH |
598
+ | **gcp-vm** | GCP users with an existing Compute Engine VM | Running VM, firewall rule for SSH, metadata SSH key |
599
+ | **kubernetes** | Teams with an existing K8s cluster | Cluster, kubectl access, manifests in repo |
600
+
601
+ ---
602
+
603
+ ## Deployment method guides
604
+
605
+ Each method below follows the same structure: **prerequisites** (before `deployhub init`), **what DeployHub automates**, **after init** (matches terminal output), and a **variable reference**.
606
+
607
+ ### SSH
608
+
609
+ **Prerequisites (before `deployhub init`):**
610
+ - [ ] A Linux server with SSH enabled
611
+ - [ ] Private SSH key file (.pem/.key) and public key in `authorized_keys`
612
+ - [ ] Port 22 open in firewall for your IP
613
+ - [ ] App runtime on server (Node.js, Python, etc.) for backends
614
+
615
+ **What DeployHub automates:**
616
+ - Complete `.env.example` with commented variables
617
+ - SSH key permission check (offers to `chmod 600`)
618
+ - SSH connectivity test during `init`
619
+ - Artifact upload, extract, app restart (PM2, gunicorn, etc.)
620
+
621
+ **After `init`:**
622
+ 1. Ensure port 22 is open in your server firewall
623
+ 2. Copy `.env.example` → `.env`; set `SSH_HOST`, `SSH_USER`, `SSH_KEY_PATH`
624
+ 3. Add GitHub Secrets: `SSH_HOST`, `SSH_USER`, `SSH_KEY` (paste private key for CI)
625
+ 4. Run `deployhub doctor`
626
+ 5. `git push origin main`
627
+
628
+ | Variable | Description | Example | Where to get it |
629
+ |----------|-------------|---------|-----------------|
630
+ | `SSH_HOST` | Server IP or hostname | `203.0.113.10` | Your hosting provider dashboard |
631
+ | `SSH_USER` | SSH login user | `ubuntu` | AMI/image docs (Ubuntu→ubuntu, Amazon Linux→ec2-user) |
632
+ | `SSH_KEY_PATH` | Path to private key file | `~/.ssh/my-key.pem` | Downloaded when server was created |
633
+ | `SSH_SSH_PORT` | SSH port (optional) | `22` | Server SSH config |
634
+ | `SSH_DEPLOY_PATH` | Remote deploy directory | `/var/www/my-app` | Your server layout |
635
+ | `SSH_APP_NAME` | PM2 process name (backend) | `my-api` | Your choice |
636
+ | `SSH_PORT` | App listen port (backend) | `3000` | Your app config |
637
+ | `SSH_KEY` | Private key contents (CI only) | `-----BEGIN...` | Same key as `SSH_KEY_PATH` |
638
+
639
+ ### Docker
640
+
641
+ **Prerequisites:**
642
+ - [ ] Docker installed (`docker --version` works)
643
+ - [ ] `Dockerfile` or `docker-compose.yml` in project
644
+ - [ ] Registry account if pushing private images
645
+
646
+ **What DeployHub automates:**
647
+ - `.env.example` for image name, registry, remote `DOCKER_HOST`
648
+ - Docker daemon connectivity test during `init`
649
+ - `docker compose up` or build/push/run during deploy
650
+
651
+ **After `init`:**
652
+ 1. Set `DOCKER_IMAGE_NAME` in `.env`
653
+ 2. For private registries: set `DOCKER_REGISTRY_USERNAME` and `DOCKER_REGISTRY_TOKEN`
654
+ 3. For remote Docker: set `DOCKER_HOST` (e.g. `ssh://ubuntu@203.0.113.10`)
655
+ 4. Run `deployhub doctor`, then `git push origin main`
656
+
657
+ | Variable | Description | Example | Where to get it |
658
+ |----------|-------------|---------|-----------------|
659
+ | `DOCKER_IMAGE_NAME` | Image repository path | `myorg/myapp` | Your registry naming |
660
+ | `DOCKER_IMAGE_TAG` | Image tag | `latest` | Version or `latest` |
661
+ | `DOCKER_REGISTRY_URL` | Registry URL (optional) | `https://ghcr.io` | Registry docs |
662
+ | `DOCKER_REGISTRY_USERNAME` | Registry user | `myuser` | Registry account |
663
+ | `DOCKER_REGISTRY_TOKEN` | Registry password/token | *(secret)* | Docker Hub / GHCR PAT |
664
+ | `DOCKER_HOST` | Remote daemon (optional) | `ssh://ubuntu@host` | Remote Docker setup |
665
+
666
+ ### AWS EC2
667
+
668
+ **Prerequisites:**
669
+ - [ ] EC2 instance launched in AWS Console (DeployHub does not create it)
670
+ - [ ] Key pair `.pem` downloaded at launch
671
+ - [ ] Security group: inbound SSH (22) from your IP
672
+ - [ ] App runtime on instance for backends
673
+
674
+ **What DeployHub automates:**
675
+ - EC2-specific `.env.example` (SSH + optional AWS API vars)
676
+ - SSH key validation and connectivity test
677
+ - OS user suggestion from AMI hint (ubuntu, ec2-user)
678
+ - Optional public IP lookup via `EC2_INSTANCE_ID` + AWS CLI
679
+
680
+ **After `init`:**
681
+ 1. AWS Console → EC2 → Security Groups → Inbound rules → SSH port 22 from My IP
682
+ 2. Copy `.env.example` → `.env`; set `SSH_KEY_PATH`, `SSH_HOST` (or `EC2_INSTANCE_ID` + AWS creds)
683
+ 3. GitHub Secrets: `SSH_HOST`, `SSH_USER`, `SSH_KEY`, plus `AWS_*` if using instance ID lookup
684
+ 4. Run `deployhub doctor`, then `git push origin main`
685
+
686
+ | Variable | Description | Example | Where to get it |
687
+ |----------|-------------|---------|-----------------|
688
+ | `SSH_HOST` | Instance public IP/DNS | `54.123.45.67` | EC2 Console → Instances |
689
+ | `SSH_USER` | SSH user for AMI | `ec2-user` | AMI documentation |
690
+ | `SSH_KEY_PATH` | Path to .pem key | `~/.ssh/ec2-key.pem` | Downloaded at instance launch |
691
+ | `EC2_INSTANCE_ID` | Instance ID (optional) | `i-0abc123...` | EC2 Console |
692
+ | `AWS_ACCESS_KEY_ID` | AWS key for API lookup | `AKIA...` | IAM → Users → Security credentials |
693
+ | `AWS_SECRET_ACCESS_KEY` | AWS secret | *(secret)* | Same as above |
694
+ | `AWS_REGION` | Instance region | `us-east-1` | EC2 Console top bar |
695
+
696
+ ### Azure VM
697
+
698
+ **Prerequisites:**
699
+ - [ ] Azure VM created in Portal (DeployHub does not provision it)
700
+ - [ ] NSG rule allowing inbound SSH (port 22)
701
+ - [ ] SSH public key on the VM
702
+ - [ ] App runtime for backends
703
+
704
+ **What DeployHub automates:**
705
+ - Azure VM `.env.example` with SSH + optional Azure API vars
706
+ - Auto-detects subscription ID via `az` CLI if logged in
707
+ - SSH key validation and connectivity test
708
+
709
+ **After `init`:**
710
+ 1. Azure Portal → VM → Networking → allow SSH (22) from your IP
711
+ 2. Copy `.env.example` → `.env`; set `SSH_HOST`, `SSH_USER`, `SSH_KEY_PATH`
712
+ 3. For CI: add `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` as GitHub Secrets
713
+ 4. Run `deployhub doctor`, then `git push origin main`
714
+
715
+ | Variable | Description | Example | Where to get it |
716
+ |----------|-------------|---------|-----------------|
717
+ | `SSH_HOST` | VM public IP | `20.1.2.3` | Azure Portal → VM overview |
718
+ | `SSH_USER` | SSH username | `azureuser` | Chosen at VM creation |
719
+ | `SSH_KEY_PATH` | Private key path | `~/.ssh/azure.pem` | Your key file |
720
+ | `AZURE_SUBSCRIPTION_ID` | Subscription (optional) | `uuid` | `az account show` |
721
+ | `AZURE_RESOURCE_GROUP` | Resource group | `my-app-rg` | Portal → Resource groups |
722
+ | `AZURE_VM_NAME` | VM name | `my-vm` | Portal → Virtual machines |
723
+
724
+ ### GCP VM
725
+
726
+ **Prerequisites:**
727
+ - [ ] Compute Engine VM created (DeployHub does not create it)
728
+ - [ ] Firewall rule allowing `tcp:22` (default `default-allow-ssh` may exist)
729
+ - [ ] SSH public key in **Metadata → SSH Keys** (GCP uses metadata keys, not launch key pairs like AWS)
730
+ - [ ] App runtime for backends
731
+
732
+ **What DeployHub automates:**
733
+ - GCP VM `.env.example` with SSH + optional GCP API vars
734
+ - Auto-detects project ID via `gcloud` if authenticated
735
+ - SSH key validation and connectivity test
736
+
737
+ **After `init`:**
738
+ 1. GCP Console → VPC → Firewall → ensure SSH (tcp:22) allowed from your IP
739
+ 2. Add SSH public key: Console → Compute Engine → Metadata → SSH Keys
740
+ 3. Copy `.env.example` → `.env`; set `SSH_HOST`, `SSH_USER`, `SSH_KEY_PATH`
741
+ 4. Run `deployhub doctor`, then `git push origin main`
742
+
743
+ | Variable | Description | Example | Where to get it |
744
+ |----------|-------------|---------|-----------------|
745
+ | `SSH_HOST` | External IP | `34.56.78.90` | Compute Engine → VM instances |
746
+ | `SSH_USER` | SSH username | `your_google_username` | GCP OS Login or metadata |
747
+ | `SSH_KEY_PATH` | Private key path | `~/.ssh/gcp-key` | Your local key pair |
748
+ | `SSH_SSH_PORT` | SSH connection port (optional) | `22` | Server SSH config |
749
+ | `SSH_DEPLOY_PATH` | Remote deploy directory (optional) | `/var/www/my-app` | Your server layout |
750
+ | `SSH_APP_NAME` | PM2 process name (backend) | `my-api` | Your choice |
751
+ | `SSH_PORT` | App listen port (backend) | `3000` | Your app config |
752
+ | `SSH_KEY` | Private key contents (CI only) | `-----BEGIN...` | Same key as `SSH_KEY_PATH` |
753
+ | `GCP_PROJECT_ID` | Project ID (optional) | `my-project-123` | `gcloud config get-value project` |
754
+ | `GCP_ZONE` | VM zone (optional) | `us-central1-a` | VM instance details |
755
+ | `GCP_INSTANCE_NAME` | Instance name (optional) | `my-vm` | Compute Engine list |
756
+ | `GCP_KEY_FILE` | Service account JSON (optional, CI) | `/path/to/key.json` | IAM → Service Accounts → Keys |
757
+
758
+ ### Kubernetes
759
+
760
+ **Prerequisites:**
761
+ - [ ] Existing Kubernetes cluster (DeployHub does not provision clusters)
762
+ - [ ] `kubectl` installed and configured
763
+ - [ ] Kubernetes manifests (`.yaml` or `k8s/` directory) in your repo
764
+ - [ ] Cluster reachable from CI (kubeconfig secret or cloud auth)
765
+
766
+ **What DeployHub automates:**
767
+ - Lists `kubectl` contexts during `init` for easy selection
768
+ - Auto-detects `~/.kube/config`
769
+ - Complete `.env.example` for kubeconfig, context, namespace
770
+ - Cluster connectivity test during `init`
771
+
772
+ **After `init`:**
773
+ 1. Verify context: `kubectl config get-contexts`
774
+ 2. Create namespace if needed: `kubectl create namespace my-app`
775
+ 3. For private registries: create `imagePullSecret` and set `KUBE_IMAGE_PULL_SECRET`
776
+ 4. Copy `.env.example` → `.env`; add kubeconfig/auth to GitHub Secrets for CI
777
+ 5. Run `deployhub doctor`, then `git push origin main`
778
+
779
+ | Variable | Description | Example | Where to get it |
780
+ |----------|-------------|---------|-----------------|
781
+ | `KUBECONFIG` | Path to kubeconfig | `~/.kube/config` | Default kubectl config |
782
+ | `KUBE_CONTEXT` | Context name | `my-cluster` | `kubectl config get-contexts` |
783
+ | `KUBE_NAMESPACE` | Target namespace | `my-app` | `kubectl get namespaces` |
784
+ | `DOCKER_IMAGE_NAME` | Container image | `ghcr.io/org/app` | Your registry |
785
+ | `KUBE_IMAGE_PULL_SECRET` | Pull secret name | `regcred` | `kubectl create secret docker-registry` |
786
+
787
+ ---
788
+
588
789
  ## Minimal `deployhub.config.json` examples
589
790
 
590
791
  ### Storage only — React
@@ -640,7 +841,7 @@ Prefer `deployhub init` over hand-writing config — it sets adapters, workflow,
640
841
  |---------|-----|
641
842
  | `Deploy requires storage upload` | Add at least one storage provider in config |
642
843
  | AWS / GDrive check fails in `doctor` | Run `deployhub storage add <provider>` and match GitHub Secrets |
643
- | SSH deploy fails | Verify `SSH_KEY` is the **private** key; user can write to deploy path |
844
+ | SSH deploy fails | Verify `SSH_KEY_PATH` points to your private `.pem` file (or `SSH_KEY` in CI); user can write to deploy path; port 22 open |
644
845
  | Wrong output uploaded | Fix `buildOutput` in config (`dist` vs `build` vs `.next`) |
645
846
  | Tests fail in CI | Set `"pipeline": { "test": false }` temporarily, or fix tests |
646
847
  | Monorepo subfolders | Edit `buildCommand` paths in `deployhub.config.json` after init |
@@ -692,16 +893,25 @@ Add these secrets in your repository (Settings → Secrets and variables → Act
692
893
  | `DROPBOX_ACCESS_TOKEN` | Dropbox |
693
894
  | `FTP_HOST`, `FTP_USER`, `FTP_PASSWORD` | FTP storage |
694
895
 
695
- ### Server deployment (SSH, EC2, VMs)
896
+ ### Server deployment (SSH, EC2, VMs, Docker, Kubernetes)
696
897
 
697
898
  | Secret | Used for |
698
899
  |--------|----------|
699
- | `SSH_HOST` | Target server hostname |
900
+ | `SSH_HOST` | Target server hostname or IP |
700
901
  | `SSH_USER` | SSH username |
701
- | `SSH_KEY` | Private SSH key (PEM) |
902
+ | `SSH_KEY_PATH` | Local path to private key (`.env` only) |
903
+ | `SSH_KEY` | Private key contents (GitHub Actions / CI) |
904
+ | `SSH_SSH_PORT` | SSH connection port (default 22) |
702
905
  | `SSH_DEPLOY_PATH` | Remote directory (optional if set in config) |
703
906
  | `SSH_APP_NAME` | PM2 process name for backends |
704
- | `SSH_PORT` | App port on server (optional) |
907
+ | `SSH_PORT` | App port on server (backend) |
908
+ | `EC2_INSTANCE_ID`, `AWS_*` | Optional EC2 dynamic IP lookup |
909
+ | `AZURE_SUBSCRIPTION_ID`, `AZURE_RESOURCE_GROUP`, `AZURE_VM_NAME` | Optional Azure VM IP lookup |
910
+ | `GCP_PROJECT_ID`, `GCP_ZONE`, `GCP_INSTANCE_NAME`, `GCP_KEY_FILE` | Optional GCP VM IP lookup |
911
+ | `DOCKER_IMAGE_NAME`, `DOCKER_REGISTRY_*`, `DOCKER_HOST` | Docker deployment |
912
+ | `KUBECONFIG`, `KUBE_CONTEXT`, `KUBE_NAMESPACE` | Kubernetes deployment |
913
+
914
+ See [Deployment method guides](#deployment-method-guides) for full per-method variable tables with examples.
705
915
 
706
916
  ## `deployhub doctor` Output
707
917
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akash-chowdhury-24/deployhub",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Zero-configuration deployment and artifact manager",
5
5
  "type": "module",
6
6
  "main": "./src/cli/index.js",
@@ -35,8 +35,9 @@
35
35
  "scripts": {
36
36
  "start": "node src/cli/index.js",
37
37
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
38
- "prepublishOnly": "npm test",
39
- "build:bundle": "node scripts/build-bundle.mjs",
38
+ "verify:bundle-sources": "node scripts/verify-bundle-sources.mjs",
39
+ "prepublishOnly": "npm test && npm run verify:bundle-sources",
40
+ "build:bundle": "node scripts/verify-bundle-sources.mjs && node scripts/build-bundle.mjs",
40
41
  "build:binaries": "npm run build:bundle && node scripts/prefetch-pkg-cache.mjs && pkg dist/deployhub.cjs --targets node20-linux-x64,node20-macos-x64,node20-macos-arm64,node20-win-x64 --output dist/deployhub --compress GZip"
41
42
  },
42
43
  "pkg": {
@@ -9,6 +9,11 @@ import { getDeploymentProvider } from '../deployment/index.js';
9
9
  import { PROVIDER_ENV_MAP } from '../utils/github-actions.js';
10
10
  import { printDoctorFooter } from '../utils/author.js';
11
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';
12
17
 
13
18
  /**
14
19
  * @typedef {{ name: string, pass: boolean, message: string }} CheckResult
@@ -48,14 +53,165 @@ function resolveBackendFramework(config) {
48
53
  return config.backend?.framework || config.framework || 'express';
49
54
  }
50
55
 
56
+ /** @type {Set<string>} */
57
+ const SSH_DEPLOY_TYPES = new Set(['ssh', 'ec2', 'azure-vm', 'gcp-vm']);
58
+
51
59
  /**
52
60
  * @param {import('../core/config.js').DeployHubConfig} config
53
61
  * @param {string} envName
62
+ * @param {Record<string, unknown>} envConfig
54
63
  * @returns {Promise<CheckResult[]>}
55
64
  */
56
- 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') {
57
213
  const framework = resolveBackendFramework(config);
58
- const provider = getDeploymentProvider('ssh', config, envName);
214
+ const provider = getDeploymentProvider(deployType, config, envName);
59
215
 
60
216
  if (!provider.runRemoteCheck) {
61
217
  return [];
@@ -338,27 +494,8 @@ export function registerDoctorCommand(program) {
338
494
  const env = config.environments[envName];
339
495
  if (!env) continue;
340
496
 
341
- if (env.type && ['ssh', 'ec2', 'azure-vm', 'gcp-vm'].includes(env.type)) {
342
- results.push(
343
- await runCheck('SSH target', async () => {
344
- const provider = getDeploymentProvider(env.type, config, envName);
345
- await provider.testConnection();
346
- const host = env.host || process.env.SSH_HOST;
347
- return {
348
- name: 'SSH target',
349
- pass: true,
350
- message: `Can reach ${host || 'host'}`,
351
- };
352
- })
353
- );
354
-
355
- const isBackend =
356
- config.projectType === 'backend' || config.projectType === 'both';
357
- if (isBackend && env.type === 'ssh') {
358
- const backendChecks = await runBackendProcessChecks(config, envName);
359
- results.push(...backendChecks);
360
- }
361
- }
497
+ const deployChecks = await runDeploymentChecks(config, envName, env);
498
+ results.push(...deployChecks);
362
499
  }
363
500
 
364
501
  results.push(
@@ -405,16 +542,17 @@ export function registerDoctorCommand(program) {
405
542
  }
406
543
  for (const envName of config.deploy || []) {
407
544
  const env = config.environments[envName];
408
- if (!env) continue;
409
-
410
- if (env.type) {
411
- const keys = PROVIDER_ENV_MAP[env.type] || [];
412
- required.push(...keys);
413
- }
545
+ if (!env?.type) continue;
546
+ required.push(...getDeploymentSecretKeys(env.type, config));
414
547
  }
415
548
 
416
549
  const unique = [...new Set(required)];
417
- 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
+ });
418
556
  if (missing.length > 0) {
419
557
  return {
420
558
  name: 'Secrets',