@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.
@@ -0,0 +1,625 @@
1
+ /**
2
+ * Central source of truth for deployment method env vars, documentation,
3
+ * and post-init next steps.
4
+ */
5
+
6
+ /** @typedef {{ key: string, comment: string[], example?: string, default?: string, optionalReason?: string, when?: 'backend'|'optional'|'ci' }} EnvVarDef */
7
+
8
+ /** @type {Record<string, EnvVarDef[]>} */
9
+ export const SSH_BASE_ENV_VARS = [
10
+ {
11
+ key: 'SSH_HOST',
12
+ comment: [
13
+ 'Public IP address or domain name of the server you are deploying to.',
14
+ ],
15
+ example: '203.0.113.10',
16
+ },
17
+ {
18
+ key: 'SSH_USER',
19
+ comment: [
20
+ 'OS username for SSH login. Depends on the image you used:',
21
+ ' Ubuntu → ubuntu | Amazon Linux → ec2-user | Debian → admin',
22
+ 'Check your provider instance details if unsure.',
23
+ ],
24
+ example: 'ubuntu',
25
+ },
26
+ {
27
+ key: 'SSH_KEY_PATH',
28
+ comment: [
29
+ 'Path to your PRIVATE SSH key file (the .pem/.key you downloaded).',
30
+ 'Must be a file PATH — do not paste key contents here.',
31
+ ],
32
+ example: '~/.ssh/my-server-key.pem',
33
+ },
34
+ {
35
+ key: 'SSH_SSH_PORT',
36
+ optionalReason: 'only change from default 22 if your server uses a non-standard SSH port',
37
+ comment: ['SSH connection port on the server (not your app port).'],
38
+ default: '22',
39
+ when: 'optional',
40
+ },
41
+ {
42
+ key: 'SSH_DEPLOY_PATH',
43
+ optionalReason: 'only needed if the deploy path differs from deployhub.config.json',
44
+ comment: [
45
+ 'Remote directory where DeployHub extracts your artifact.',
46
+ 'The SSH user must have write permission here.',
47
+ ],
48
+ example: '/var/www/my-app',
49
+ when: 'optional',
50
+ },
51
+ ];
52
+
53
+ /** @type {EnvVarDef[]} */
54
+ export const SSH_BACKEND_ENV_VARS = [
55
+ {
56
+ key: 'SSH_APP_NAME',
57
+ comment: [
58
+ 'Process name used by PM2 when restarting your Node.js backend.',
59
+ ],
60
+ example: 'my-api',
61
+ when: 'backend',
62
+ },
63
+ {
64
+ key: 'SSH_PORT',
65
+ comment: [
66
+ 'TCP port your backend app listens on (used for health checks and start commands).',
67
+ ],
68
+ example: '3000',
69
+ when: 'backend',
70
+ },
71
+ ];
72
+
73
+ /** @type {EnvVarDef[]} */
74
+ export const SSH_CI_ENV_VARS = [
75
+ {
76
+ key: 'SSH_KEY',
77
+ optionalReason:
78
+ 'only required for GitHub Actions / CI — paste private key contents here; use SSH_KEY_PATH for local development',
79
+ comment: [
80
+ 'PRIVATE SSH key contents (full PEM block including BEGIN/END lines).',
81
+ ],
82
+ when: 'ci',
83
+ },
84
+ ];
85
+
86
+ /** @type {Record<string, EnvVarDef[]>} */
87
+ export const DEPLOYMENT_ENV_DEFS = {
88
+ ssh: [...SSH_BASE_ENV_VARS, ...SSH_BACKEND_ENV_VARS, ...SSH_CI_ENV_VARS],
89
+ docker: [
90
+ {
91
+ key: 'DOCKER_IMAGE_NAME',
92
+ comment: [
93
+ 'Docker image name (repository path without tag).',
94
+ ],
95
+ example: 'myorg/myapp',
96
+ },
97
+ {
98
+ key: 'DOCKER_IMAGE_TAG',
99
+ optionalReason: 'defaults to your project version if unset',
100
+ comment: ['Image tag to build and deploy.'],
101
+ example: 'latest',
102
+ when: 'optional',
103
+ },
104
+ {
105
+ key: 'DOCKER_REGISTRY_URL',
106
+ optionalReason: 'only required for private or non-Docker-Hub registries',
107
+ comment: [
108
+ 'Container registry URL. Leave empty for Docker Hub.',
109
+ 'Examples: https://index.docker.io/v1/ | https://ghcr.io',
110
+ ],
111
+ when: 'optional',
112
+ },
113
+ {
114
+ key: 'DOCKER_REGISTRY_USERNAME',
115
+ optionalReason: 'only required when pushing to a private registry',
116
+ comment: ['Registry username for pushing private images.'],
117
+ when: 'optional',
118
+ },
119
+ {
120
+ key: 'DOCKER_REGISTRY_TOKEN',
121
+ optionalReason: 'only required when pushing to a private registry',
122
+ comment: [
123
+ 'Registry password or personal access token.',
124
+ 'Docker Hub: access token. GHCR: GitHub PAT with write:packages.',
125
+ ],
126
+ when: 'optional',
127
+ },
128
+ {
129
+ key: 'DOCKER_HOST',
130
+ optionalReason: 'only required when deploying to a remote Docker daemon instead of local Docker',
131
+ comment: [
132
+ 'Remote Docker daemon address.',
133
+ 'Examples: ssh://ubuntu@203.0.113.10 | tcp://203.0.113.10:2376',
134
+ ],
135
+ when: 'optional',
136
+ },
137
+ {
138
+ key: 'DOCKER_TLS_VERIFY',
139
+ optionalReason: 'only required when DOCKER_HOST uses tcp:// with TLS',
140
+ comment: ['Set to 1 when connecting to a remote Docker daemon over TLS.'],
141
+ when: 'optional',
142
+ },
143
+ {
144
+ key: 'DOCKER_CERT_PATH',
145
+ optionalReason: 'only required when DOCKER_HOST uses tcp:// with TLS',
146
+ comment: [
147
+ 'Directory containing ca.pem, cert.pem, and key.pem for Docker TLS.',
148
+ ],
149
+ when: 'optional',
150
+ },
151
+ ],
152
+ ec2: [
153
+ ...SSH_BASE_ENV_VARS,
154
+ ...SSH_BACKEND_ENV_VARS,
155
+ ...SSH_CI_ENV_VARS,
156
+ {
157
+ key: 'EC2_INSTANCE_ID',
158
+ optionalReason:
159
+ 'only required for dynamic public IP lookup via AWS API; otherwise set SSH_HOST directly',
160
+ comment: [
161
+ 'AWS EC2 instance ID. Used to look up the public IP via AWS API.',
162
+ 'If unset, SSH_HOST must be set to the instance public IP or DNS.',
163
+ ],
164
+ example: 'i-0abc123def4567890',
165
+ when: 'optional',
166
+ },
167
+ {
168
+ key: 'AWS_ACCESS_KEY_ID',
169
+ optionalReason:
170
+ 'only required if using EC2_INSTANCE_ID for dynamic IP lookup; otherwise leave blank',
171
+ comment: [
172
+ 'AWS access key with ec2:DescribeInstances permission.',
173
+ 'Create in AWS Console → IAM → Users → Security credentials.',
174
+ ],
175
+ when: 'optional',
176
+ },
177
+ {
178
+ key: 'AWS_SECRET_ACCESS_KEY',
179
+ optionalReason:
180
+ 'only required if using EC2_INSTANCE_ID for dynamic IP lookup; otherwise leave blank',
181
+ comment: ['Secret for the AWS access key above.'],
182
+ when: 'optional',
183
+ },
184
+ {
185
+ key: 'AWS_REGION',
186
+ optionalReason:
187
+ 'only required if using EC2_INSTANCE_ID for dynamic IP lookup; otherwise leave blank',
188
+ comment: ['AWS region where your EC2 instance runs.'],
189
+ example: 'us-east-1',
190
+ when: 'optional',
191
+ },
192
+ ],
193
+ 'azure-vm': [
194
+ ...SSH_BASE_ENV_VARS,
195
+ ...SSH_BACKEND_ENV_VARS,
196
+ ...SSH_CI_ENV_VARS,
197
+ {
198
+ key: 'AZURE_SUBSCRIPTION_ID',
199
+ optionalReason:
200
+ 'only required for dynamic VM IP lookup via Azure API; otherwise set SSH_HOST directly',
201
+ comment: [
202
+ 'Azure subscription ID. Used to look up VM public IP via Azure API.',
203
+ 'Find in Azure Portal → Subscriptions, or run: az account show --query id -o tsv',
204
+ ],
205
+ when: 'optional',
206
+ },
207
+ {
208
+ key: 'AZURE_RESOURCE_GROUP',
209
+ optionalReason:
210
+ 'only required for dynamic VM IP lookup via Azure API; otherwise leave blank',
211
+ comment: ['Resource group containing your VM.'],
212
+ example: 'my-app-rg',
213
+ when: 'optional',
214
+ },
215
+ {
216
+ key: 'AZURE_VM_NAME',
217
+ optionalReason:
218
+ 'only required for dynamic VM IP lookup via Azure API; otherwise set SSH_HOST directly',
219
+ comment: [
220
+ 'Name of the Azure virtual machine.',
221
+ 'If unset, SSH_HOST must be set to the VM public IP or DNS.',
222
+ ],
223
+ when: 'optional',
224
+ },
225
+ {
226
+ key: 'AZURE_TENANT_ID',
227
+ optionalReason:
228
+ 'only required for non-interactive CI deploys (GitHub Actions) using a service principal',
229
+ comment: ['Azure AD tenant ID for service principal auth in CI.'],
230
+ when: 'optional',
231
+ },
232
+ {
233
+ key: 'AZURE_CLIENT_ID',
234
+ optionalReason:
235
+ 'only required for non-interactive CI deploys (GitHub Actions) using a service principal',
236
+ comment: ['Service principal application (client) ID for CI auth.'],
237
+ when: 'optional',
238
+ },
239
+ {
240
+ key: 'AZURE_CLIENT_SECRET',
241
+ optionalReason:
242
+ 'only required for non-interactive CI deploys (GitHub Actions) using a service principal',
243
+ comment: ['Service principal client secret for CI auth.'],
244
+ when: 'optional',
245
+ },
246
+ ],
247
+ 'gcp-vm': [
248
+ ...SSH_BASE_ENV_VARS,
249
+ ...SSH_BACKEND_ENV_VARS,
250
+ ...SSH_CI_ENV_VARS,
251
+ {
252
+ key: 'GCP_PROJECT_ID',
253
+ optionalReason:
254
+ 'only required for dynamic VM IP lookup via GCP API; otherwise set SSH_HOST directly',
255
+ comment: [
256
+ 'GCP project ID. Used to look up VM IP via Compute API.',
257
+ 'Find in GCP Console → Dashboard, or run: gcloud config get-value project',
258
+ ],
259
+ when: 'optional',
260
+ },
261
+ {
262
+ key: 'GCP_ZONE',
263
+ optionalReason:
264
+ 'only required for dynamic VM IP lookup via GCP API; otherwise leave blank',
265
+ comment: ['GCP zone where your VM runs.'],
266
+ example: 'us-central1-a',
267
+ when: 'optional',
268
+ },
269
+ {
270
+ key: 'GCP_INSTANCE_NAME',
271
+ optionalReason:
272
+ 'only required for dynamic VM IP lookup via GCP API; otherwise set SSH_HOST directly',
273
+ comment: [
274
+ 'GCP Compute Engine instance name.',
275
+ 'GCP uses project/instance SSH keys in metadata — add your public key in',
276
+ 'Console → Compute Engine → Metadata → SSH Keys.',
277
+ ],
278
+ when: 'optional',
279
+ },
280
+ {
281
+ key: 'GCP_KEY_FILE',
282
+ optionalReason:
283
+ 'only required for dynamic VM IP lookup via GCP API or non-interactive CI auth; otherwise leave blank',
284
+ comment: [
285
+ 'Path to a GCP service account JSON key file.',
286
+ 'Create in GCP Console → IAM → Service Accounts → Keys.',
287
+ ],
288
+ when: 'optional',
289
+ },
290
+ ],
291
+ kubernetes: [
292
+ {
293
+ key: 'KUBECONFIG',
294
+ optionalReason: 'defaults to ~/.kube/config if unset',
295
+ comment: ['Path to your kubeconfig file.'],
296
+ example: '~/.kube/config',
297
+ when: 'optional',
298
+ },
299
+ {
300
+ key: 'KUBE_CONTEXT',
301
+ comment: [
302
+ 'kubectl context name to deploy into.',
303
+ 'List contexts: kubectl config get-contexts',
304
+ ],
305
+ example: 'my-cluster',
306
+ },
307
+ {
308
+ key: 'KUBE_NAMESPACE',
309
+ optionalReason: 'defaults to your project name or "default" if unset',
310
+ comment: [
311
+ 'Kubernetes namespace for your deployment.',
312
+ 'Create one first if needed: kubectl create namespace my-app',
313
+ ],
314
+ example: 'my-app',
315
+ when: 'optional',
316
+ },
317
+ {
318
+ key: 'DOCKER_IMAGE_NAME',
319
+ optionalReason: 'only required if your manifests need an image override at deploy time',
320
+ comment: [
321
+ 'Container image to deploy (must match manifests or be overridden).',
322
+ ],
323
+ example: 'ghcr.io/myorg/myapp',
324
+ when: 'optional',
325
+ },
326
+ {
327
+ key: 'KUBE_IMAGE_PULL_SECRET',
328
+ optionalReason: 'only required when pulling from a private container registry',
329
+ comment: [
330
+ 'Name of a Kubernetes imagePullSecret for private registries.',
331
+ 'Create with: kubectl create secret docker-registry ...',
332
+ ],
333
+ when: 'optional',
334
+ },
335
+ ],
336
+ };
337
+
338
+ /** @type {Record<string, string[]>} */
339
+ export const DEPLOYMENT_ENV_KEYS = Object.fromEntries(
340
+ Object.entries(DEPLOYMENT_ENV_DEFS).map(([method, defs]) => [
341
+ method,
342
+ defs.map((d) => d.key),
343
+ ])
344
+ );
345
+
346
+ /**
347
+ * @param {string} deployType
348
+ * @param {import('../core/config.js').DeployHubConfig} [config]
349
+ * @returns {string[]}
350
+ */
351
+ export function getDeploymentEnvKeys(deployType, config = null) {
352
+ const defs = DEPLOYMENT_ENV_DEFS[deployType] || [];
353
+ const projectType = config?.projectType || 'frontend';
354
+ const isBackend = projectType === 'backend' || projectType === 'both';
355
+
356
+ return defs
357
+ .filter((d) => {
358
+ if (d.when === 'backend' && !isBackend) return false;
359
+ if (d.when === 'ci') return false;
360
+ if (d.when === 'optional') return false;
361
+ return true;
362
+ })
363
+ .map((d) => d.key);
364
+ }
365
+
366
+ /**
367
+ * Required secrets for GitHub Actions (includes CI-only vars like SSH_KEY).
368
+ * @param {string} deployType
369
+ * @param {import('../core/config.js').DeployHubConfig} [config]
370
+ * @returns {string[]}
371
+ */
372
+ export function getDeploymentSecretKeys(deployType, config = null) {
373
+ const defs = DEPLOYMENT_ENV_DEFS[deployType] || [];
374
+ const projectType = config?.projectType || 'frontend';
375
+ const isBackend = projectType === 'backend' || projectType === 'both';
376
+
377
+ /** @type {string[]} */
378
+ const keys = [];
379
+
380
+ for (const d of defs) {
381
+ if (d.when === 'backend' && !isBackend) continue;
382
+ if (d.when === 'optional') continue;
383
+ if (d.key === 'SSH_KEY_PATH') {
384
+ keys.push('SSH_KEY');
385
+ continue;
386
+ }
387
+ keys.push(d.key);
388
+ }
389
+
390
+ return keys;
391
+ }
392
+
393
+ /**
394
+ * @param {string} deployType
395
+ * @param {import('../core/config.js').DeployHubConfig} [config]
396
+ * @param {Record<string, Record<string, unknown>>} [environments]
397
+ * @returns {string}
398
+ */
399
+ export function generateDeploymentEnvSection(
400
+ deployType,
401
+ config = null,
402
+ environments = {}
403
+ ) {
404
+ const defs = DEPLOYMENT_ENV_DEFS[deployType] || [];
405
+ const projectType = config?.projectType || 'frontend';
406
+ const isBackend = projectType === 'backend' || projectType === 'both';
407
+
408
+ const title = DEPLOYMENT_SECTION_TITLES[deployType] || deployType;
409
+ /** @type {string[]} */
410
+ const lines = [`# ${title}`];
411
+
412
+ for (const d of defs) {
413
+ if (d.when === 'backend' && !isBackend) continue;
414
+
415
+ const isOptional = d.when === 'optional' || d.when === 'ci';
416
+ if (isOptional && d.optionalReason) {
417
+ lines.push(`# OPTIONAL — ${d.optionalReason}`);
418
+ }
419
+
420
+ for (const line of d.comment) {
421
+ lines.push(`# ${line}`);
422
+ }
423
+
424
+ if (d.example) {
425
+ lines.push(`# Example: ${d.example}`);
426
+ }
427
+
428
+ const defaultVal = d.default || getDefaultFromConfig(d.key, config, environments);
429
+ lines.push(defaultVal ? `${d.key}=${defaultVal}` : `${d.key}=`);
430
+ lines.push('');
431
+ }
432
+
433
+ return lines.join('\n').trimEnd();
434
+ }
435
+
436
+ /**
437
+ * @param {string} key
438
+ * @param {import('../core/config.js').DeployHubConfig} [config]
439
+ * @param {Record<string, Record<string, unknown>>} [environments]
440
+ */
441
+ function getDefaultFromConfig(key, config, environments) {
442
+ const envEntry = Object.values(environments || {})[0] || {};
443
+
444
+ const map = {
445
+ SSH_HOST: envEntry.host,
446
+ SSH_USER: envEntry.user,
447
+ SSH_DEPLOY_PATH: envEntry.deployPath || envEntry.path,
448
+ SSH_APP_NAME: envEntry.appName,
449
+ SSH_PORT: config?.port || config?.backend?.port,
450
+ SSH_SSH_PORT: '22',
451
+ KUBE_NAMESPACE: config?.project || 'default',
452
+ DOCKER_IMAGE_NAME: config?.project,
453
+ DOCKER_IMAGE_TAG: config?.version || 'latest',
454
+ DOCKER_REGISTRY_URL: envEntry.dockerRegistryUrl,
455
+ AWS_REGION: 'us-east-1',
456
+ };
457
+
458
+ const val = map[key];
459
+ return val !== undefined && val !== null ? String(val) : undefined;
460
+ }
461
+
462
+ /** @type {Record<string, string>} */
463
+ export const DEPLOYMENT_SECTION_TITLES = {
464
+ ssh: 'SSH Deployment',
465
+ docker: 'Docker Deployment',
466
+ ec2: 'AWS EC2 Deployment',
467
+ 'azure-vm': 'Azure VM Deployment',
468
+ 'gcp-vm': 'GCP VM Deployment',
469
+ kubernetes: 'Kubernetes Deployment',
470
+ };
471
+
472
+ /** @type {Record<string, { before: string[], automates: string[], after: string[] }>} */
473
+ export const DEPLOYMENT_GUIDE = {
474
+ ssh: {
475
+ before: [
476
+ 'A Linux server (VPS, bare metal, or VM) with SSH enabled.',
477
+ 'Docker is NOT required on the server unless your app uses it.',
478
+ 'Your app runtime installed (Node.js, Python, etc.) for backend deploys.',
479
+ 'Port 22 open in the server firewall for SSH from your IP.',
480
+ 'An SSH key pair: private key on your machine, public key in server authorized_keys.',
481
+ ],
482
+ automates: [
483
+ 'Generates deployhub.config.json and GitHub Actions workflow.',
484
+ 'Creates a complete .env.example with inline comments for every variable.',
485
+ 'Validates your SSH key file exists and fixes permissions if needed.',
486
+ 'Tests SSH connectivity during init (fail fast before first deploy).',
487
+ 'Uploads artifact, extracts to deploy path, and restarts your app.',
488
+ ],
489
+ after: [
490
+ 'Ensure port 22 (SSH) is open in your server firewall for inbound traffic from your IP.',
491
+ 'Copy .env.example to .env and fill in SSH_HOST, SSH_USER, SSH_KEY_PATH.',
492
+ 'Add GitHub Secrets: SSH_HOST, SSH_USER, SSH_KEY (paste private key contents for CI).',
493
+ 'Run deployhub doctor to verify SSH connectivity and credentials.',
494
+ 'git push origin main to trigger your first deployment.',
495
+ ],
496
+ },
497
+ docker: {
498
+ before: [
499
+ 'Docker installed locally (docker --version works).',
500
+ 'If deploying to a remote host: Docker installed on that host and reachable.',
501
+ 'A Dockerfile or docker-compose.yml in your project (or enable pipeline.docker).',
502
+ 'Registry account if pushing to a private registry (Docker Hub, GHCR, etc.).',
503
+ ],
504
+ automates: [
505
+ 'Generates config, workflow, and .env.example for registry and image settings.',
506
+ 'Tests Docker daemon connectivity during init.',
507
+ 'Builds and runs containers via docker compose during deploy.',
508
+ ],
509
+ after: [
510
+ 'Copy .env.example to .env and set DOCKER_IMAGE_NAME (and registry creds if private).',
511
+ 'If using a remote Docker host, set DOCKER_HOST and TLS cert paths in .env.',
512
+ 'Add the same values as GitHub Secrets for CI.',
513
+ 'Run deployhub doctor to verify Docker is reachable.',
514
+ 'git push origin main to trigger your first deployment.',
515
+ ],
516
+ },
517
+ ec2: {
518
+ before: [
519
+ 'An EC2 instance already launched in AWS Console (DeployHub does not create instances).',
520
+ 'A key pair downloaded (.pem file) when the instance was created.',
521
+ 'Security group with inbound SSH (port 22) from your IP.',
522
+ 'Instance runtime installed (Node.js, Python, etc.) for backend deploys.',
523
+ 'Note the instance public IP or DNS — or set EC2_INSTANCE_ID for auto lookup.',
524
+ ],
525
+ automates: [
526
+ 'Generates EC2-specific .env.example with SSH and optional AWS API vars.',
527
+ 'Validates SSH key file and tests SSH connectivity during init.',
528
+ 'Suggests default SSH user based on AMI (ubuntu, ec2-user, admin).',
529
+ 'Optionally resolves public IP from EC2_INSTANCE_ID via AWS API.',
530
+ ],
531
+ after: [
532
+ 'AWS Console → EC2 → Security Groups → your instance group → Inbound rules →',
533
+ ' Add rule: SSH, port 22, source: My IP',
534
+ 'Copy .env.example to .env — set SSH_KEY_PATH, SSH_HOST (or EC2_INSTANCE_ID + AWS creds).',
535
+ 'Add GitHub Secrets: SSH_HOST, SSH_USER, SSH_KEY, plus AWS_* if using instance ID lookup.',
536
+ 'Run deployhub doctor to verify SSH and optional AWS API access.',
537
+ 'git push origin main to trigger your first deployment.',
538
+ ],
539
+ },
540
+ 'azure-vm': {
541
+ before: [
542
+ 'An Azure VM already created in Azure Portal (DeployHub does not provision VMs).',
543
+ 'NSG (Network Security Group) rule allowing inbound SSH (port 22) from your IP.',
544
+ 'SSH public key added to the VM (Azure uses ~/.ssh/authorized_keys on the VM).',
545
+ 'App runtime installed on the VM for backend deploys.',
546
+ ],
547
+ automates: [
548
+ 'Generates Azure VM .env.example with SSH and optional Azure API vars.',
549
+ 'Auto-detects subscription ID via az CLI if installed and logged in.',
550
+ 'Validates SSH key and tests connectivity during init.',
551
+ ],
552
+ after: [
553
+ 'Azure Portal → VM → Networking → Inbound port rules → allow SSH (22) from your IP.',
554
+ 'Or: az network nsg rule create --name AllowSSH --priority 1000 --source-address-prefix YOUR_IP ...',
555
+ 'Copy .env.example to .env and fill in SSH_HOST, SSH_USER, SSH_KEY_PATH.',
556
+ 'For CI: add AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET as GitHub Secrets.',
557
+ 'Run deployhub doctor, then git push origin main.',
558
+ ],
559
+ },
560
+ 'gcp-vm': {
561
+ before: [
562
+ 'A Compute Engine VM already created (DeployHub does not create VMs).',
563
+ 'Firewall rule allowing tcp:22 (SSH) — default "default-allow-ssh" may already exist.',
564
+ 'SSH access: add your public key in GCP Console → Compute Engine → Metadata → SSH Keys',
565
+ ' (GCP uses project/instance metadata keys, not a launch-time key pair like AWS).',
566
+ 'App runtime installed on the VM for backend deploys.',
567
+ ],
568
+ automates: [
569
+ 'Generates GCP VM .env.example with SSH and optional GCP API vars.',
570
+ 'Auto-detects project ID via gcloud CLI if installed and authenticated.',
571
+ 'Validates SSH key and tests connectivity during init.',
572
+ ],
573
+ after: [
574
+ 'GCP Console → VPC network → Firewall → ensure ssh (tcp:22) is allowed from your IP.',
575
+ 'Or: gcloud compute firewall-rules create allow-ssh --allow tcp:22 --source-ranges YOUR_IP/32',
576
+ 'Add your SSH public key in Console → Compute Engine → Metadata → SSH Keys if not done.',
577
+ 'Copy .env.example to .env — set SSH_HOST, SSH_USER, SSH_KEY_PATH.',
578
+ 'Run deployhub doctor, then git push origin main.',
579
+ ],
580
+ },
581
+ kubernetes: {
582
+ before: [
583
+ 'An existing Kubernetes cluster (DeployHub does not provision clusters).',
584
+ 'kubectl installed and configured (kubectl cluster-info works).',
585
+ 'Kubernetes manifests (Deployment, Service, etc.) in your repo or artifact.',
586
+ 'Cluster access from CI: kubeconfig or cloud-specific auth for GitHub Actions.',
587
+ ],
588
+ automates: [
589
+ 'Lists available kubectl contexts during init so you pick from a menu.',
590
+ 'Auto-detects ~/.kube/config if present.',
591
+ 'Generates complete .env.example for kubeconfig, context, and namespace.',
592
+ 'Tests cluster connectivity during init.',
593
+ ],
594
+ after: [
595
+ 'Ensure your kubeconfig context points to the correct cluster.',
596
+ 'Create namespace if needed: kubectl create namespace YOUR_NAMESPACE',
597
+ 'For private registries: kubectl create secret docker-registry ... and set KUBE_IMAGE_PULL_SECRET.',
598
+ 'Copy .env.example to .env; add KUBECONFIG contents or auth secrets to GitHub Actions.',
599
+ 'Run deployhub doctor, then git push origin main.',
600
+ ],
601
+ },
602
+ };
603
+
604
+ /**
605
+ * @param {string} deployType
606
+ * @param {string[]} [extraSecrets]
607
+ */
608
+ export function printDeploymentNextSteps(deployType, extraSecrets = []) {
609
+ const guide = DEPLOYMENT_GUIDE[deployType];
610
+ if (!guide) return;
611
+
612
+ const title = DEPLOYMENT_SECTION_TITLES[deployType] || deployType;
613
+ console.log(`\n Next steps before your first deploy (${title}):`);
614
+
615
+ guide.after.forEach((step, i) => {
616
+ console.log(` ${i + 1}. ${step}`);
617
+ });
618
+
619
+ if (extraSecrets.length > 0) {
620
+ console.log('\n GitHub Secrets to add (Settings → Secrets and variables → Actions):');
621
+ for (const s of extraSecrets) {
622
+ console.log(` • ${s}`);
623
+ }
624
+ }
625
+ }