@hed-hog/cli 0.0.35 → 0.0.37

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.
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.DeveloperService = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const chalk = require("chalk");
18
+ const child_process_1 = require("child_process");
18
19
  const crypto_1 = require("crypto");
19
20
  const ejs_1 = require("ejs");
20
21
  const fs_1 = require("fs");
@@ -51,6 +52,1373 @@ let DeveloperService = class DeveloperService {
51
52
  process.emitWarning = originalEmitWarning;
52
53
  };
53
54
  }
55
+ refreshEnvironmentPath() {
56
+ try {
57
+ if (process.platform === 'win32') {
58
+ // On Windows, re-read PATH from system environment registry
59
+ const pathCmd = `powershell -NoProfile -Command "[System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')"`;
60
+ const newPath = (0, child_process_1.execSync)(pathCmd, { encoding: 'utf8' }).trim();
61
+ if (newPath) {
62
+ process.env.PATH = newPath;
63
+ }
64
+ }
65
+ else if (process.platform === 'darwin') {
66
+ // On macOS, try to refresh PATH
67
+ try {
68
+ const newPath = (0, child_process_1.execSync)('eval "$(launchctl getenv PATH)" && echo $PATH', {
69
+ encoding: 'utf8',
70
+ shell: '/bin/bash',
71
+ }).trim();
72
+ if (newPath) {
73
+ process.env.PATH = newPath;
74
+ }
75
+ }
76
+ catch {
77
+ // Silent fail - macOS might not have launchctl env
78
+ }
79
+ }
80
+ else {
81
+ // On Linux, source bashrc and get PATH
82
+ try {
83
+ const newPath = (0, child_process_1.execSync)('bash -ic "echo $PATH"', {
84
+ encoding: 'utf8',
85
+ }).trim();
86
+ if (newPath) {
87
+ process.env.PATH = newPath;
88
+ }
89
+ }
90
+ catch {
91
+ // Silent fail
92
+ }
93
+ }
94
+ }
95
+ catch (error) {
96
+ // Silent fail - if refresh doesn't work, tools check will handle it
97
+ this.log('Environment refresh attempt completed');
98
+ }
99
+ }
100
+ async deployConfig(path, verbose = false) {
101
+ const restoreWarnings = this.suppressWarnings();
102
+ this.verbose = verbose;
103
+ path = await this.getRootPath(path);
104
+ const spinner = ora('Checking deployment configuration...').start();
105
+ try {
106
+ spinner.text = 'Checking required tools...';
107
+ // Check all required tools
108
+ const toolsStatus = await this.checkRequiredTools();
109
+ spinner.stop();
110
+ // Display tools status
111
+ console.log(chalk.blue.bold('\nšŸ”§ Required Tools Status:\n'));
112
+ const missingTools = [];
113
+ for (const [tool, status] of Object.entries(toolsStatus)) {
114
+ if (status.installed) {
115
+ console.log(chalk.green(` āœ“ ${tool.padEnd(15)} ${status.version || 'installed'}`));
116
+ }
117
+ else {
118
+ console.log(chalk.red(` āœ— ${tool.padEnd(15)} not installed`));
119
+ missingTools.push(tool);
120
+ }
121
+ }
122
+ // Handle missing tools
123
+ if (missingTools.length > 0) {
124
+ console.log(chalk.yellow('\nāš ļø Some required tools are missing.\n'));
125
+ const { installMissing } = await inquirer_1.default.prompt([
126
+ {
127
+ type: 'confirm',
128
+ name: 'installMissing',
129
+ message: 'Would you like help installing and configuring the missing tools?',
130
+ default: true,
131
+ },
132
+ ]);
133
+ if (installMissing) {
134
+ await this.helpInstallTools(missingTools);
135
+ // Refresh environment variables to pick up newly installed tools
136
+ this.refreshEnvironmentPath();
137
+ // Re-check tools after installation
138
+ console.log(chalk.blue('\nšŸ”„ Re-checking tools...\n'));
139
+ const newToolsStatus = await this.checkRequiredTools();
140
+ const stillMissing = Object.entries(newToolsStatus)
141
+ .filter(([_, status]) => !status.installed)
142
+ .map(([tool]) => tool);
143
+ if (stillMissing.length > 0) {
144
+ console.log(chalk.red('\nāŒ The following tools are still not available:'));
145
+ stillMissing.forEach((tool) => console.log(chalk.red(` • ${tool}`)));
146
+ console.log(chalk.yellow('\nPlease install them manually and run this command again.\n'));
147
+ return;
148
+ }
149
+ }
150
+ else {
151
+ console.log(chalk.yellow('\nāš ļø Cannot proceed without required tools.'));
152
+ console.log(chalk.blue('\nPlease install the following tools:\n'));
153
+ missingTools.forEach((tool) => {
154
+ console.log(chalk.white(` ${tool}:`));
155
+ this.printInstallInstructions(tool);
156
+ console.log('');
157
+ });
158
+ return;
159
+ }
160
+ }
161
+ console.log(chalk.green('\nāœ“ All required tools are available!\n'));
162
+ // Start deployment configuration wizard
163
+ const config = await this.runDeploymentWizard();
164
+ if (!config) {
165
+ console.log(chalk.yellow('\nDeployment configuration cancelled.\n'));
166
+ return;
167
+ }
168
+ // Generate deployment files
169
+ spinner.start('Generating deployment configuration files...');
170
+ await this.generateDeploymentFiles(path, config);
171
+ spinner.succeed(chalk.green('Deployment configuration completed successfully!'));
172
+ // Display summary
173
+ this.displayDeploymentSummary(config);
174
+ }
175
+ catch (error) {
176
+ spinner.fail('Failed to configure deployment.');
177
+ console.error(chalk.red('Error configuring deployment:'), error);
178
+ throw error;
179
+ }
180
+ finally {
181
+ restoreWarnings();
182
+ spinner.stop();
183
+ }
184
+ }
185
+ async checkRequiredTools() {
186
+ const tools = {
187
+ kubectl: this.checkKubectl.bind(this),
188
+ doctl: this.checkDoctl.bind(this),
189
+ 'gh cli': this.checkGhCli.bind(this),
190
+ helm: this.checkHelm.bind(this),
191
+ };
192
+ const results = {};
193
+ for (const [name, checkFn] of Object.entries(tools)) {
194
+ try {
195
+ const version = await checkFn();
196
+ results[name] = { installed: true, version };
197
+ }
198
+ catch {
199
+ results[name] = { installed: false };
200
+ }
201
+ }
202
+ return results;
203
+ }
204
+ async checkKubectl() {
205
+ const result = await this.runner.executeCommand(runner_service_1.ProgramName.KUBECTL, ['version', '--client'], {}, true);
206
+ // Extract version from output (works for both old and new kubectl versions)
207
+ const match = result.stdout.match(/Client Version: (v[\d.]+)|GitVersion:"(v[\d.]+)"/);
208
+ return match ? match[1] || match[2] : result.stdout.split('\n')[0].trim();
209
+ }
210
+ async checkDoctl() {
211
+ const result = await this.runner.executeCommand(runner_service_1.ProgramName.DOCTL, ['version'], {}, true);
212
+ return result.stdout.trim();
213
+ }
214
+ async checkGhCli() {
215
+ const result = await this.runner.executeCommand(runner_service_1.ProgramName.GH, ['--version'], {}, true);
216
+ const versionLine = result.stdout.split('\n')[0];
217
+ return versionLine.trim();
218
+ }
219
+ async checkHelm() {
220
+ const result = await this.runner.executeCommand(runner_service_1.ProgramName.HELM, ['version', '--short'], {}, true);
221
+ // Extract version (handles both --short and regular output)
222
+ const versionMatch = result.stdout.match(/v[\d.]+/);
223
+ return versionMatch ? versionMatch[0] : result.stdout.trim();
224
+ }
225
+ async checkPackageManager(manager) {
226
+ try {
227
+ if (manager === 'choco') {
228
+ await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', 'choco --version'], {}, true);
229
+ return true;
230
+ }
231
+ else if (manager === 'winget') {
232
+ await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', 'winget --version'], {}, true);
233
+ return true;
234
+ }
235
+ else if (manager === 'scoop') {
236
+ await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', 'scoop --version'], {}, true);
237
+ return true;
238
+ }
239
+ else if (manager === 'brew') {
240
+ await this.runner.executeCommand(runner_service_1.ProgramName.BREW, ['--version'], {}, true);
241
+ return true;
242
+ }
243
+ else if (manager === 'apt') {
244
+ await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', 'apt --version'], {}, true);
245
+ return true;
246
+ }
247
+ else if (manager === 'snap') {
248
+ await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', 'snap --version'], {}, true);
249
+ return true;
250
+ }
251
+ return false;
252
+ }
253
+ catch {
254
+ return false;
255
+ }
256
+ }
257
+ async getAvailablePackageManager() {
258
+ const platform = process.platform;
259
+ if (platform === 'win32') {
260
+ // Check for Chocolatey first (more reliable for dev tools)
261
+ if (await this.checkPackageManager('choco')) {
262
+ return 'choco';
263
+ }
264
+ // Then check for Scoop (developer-friendly, no admin required)
265
+ if (await this.checkPackageManager('scoop')) {
266
+ return 'scoop';
267
+ }
268
+ // Finally check for winget
269
+ if (await this.checkPackageManager('winget')) {
270
+ return 'winget';
271
+ }
272
+ return null;
273
+ }
274
+ else if (platform === 'darwin') {
275
+ if (await this.checkPackageManager('brew')) {
276
+ return 'brew';
277
+ }
278
+ return null;
279
+ }
280
+ else if (platform === 'linux') {
281
+ // Check for apt first (most common on Ubuntu/Debian)
282
+ if (await this.checkPackageManager('apt')) {
283
+ return 'apt';
284
+ }
285
+ // Then check for snap (universal package manager)
286
+ if (await this.checkPackageManager('snap')) {
287
+ return 'snap';
288
+ }
289
+ return null;
290
+ }
291
+ return null;
292
+ }
293
+ async installChocolatey() {
294
+ console.log(chalk.blue('\nšŸ“¦ Installing Chocolatey...\n'));
295
+ try {
296
+ // Install Chocolatey using the official installation script
297
+ await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, [
298
+ '-NoProfile',
299
+ '-ExecutionPolicy',
300
+ 'Bypass',
301
+ '-Command',
302
+ "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))",
303
+ ], {}, false);
304
+ console.log(chalk.green('\nāœ“ Chocolatey installed successfully!\n'));
305
+ return true;
306
+ }
307
+ catch (error) {
308
+ console.log(chalk.red('\nāœ— Failed to install Chocolatey.\n'));
309
+ console.log(chalk.yellow('Please install it manually from: https://chocolatey.org/install\n'));
310
+ return false;
311
+ }
312
+ }
313
+ async helpInstallTools(missingTools) {
314
+ console.log(chalk.blue('\nšŸ“¦ Installing and configuring tools...\n'));
315
+ // Check for available package manager
316
+ let packageManager = await this.getAvailablePackageManager();
317
+ // If on Windows and no package manager found, offer to install Chocolatey
318
+ if (process.platform === 'win32' && !packageManager) {
319
+ console.log(chalk.yellow('āš ļø No package manager detected (Chocolatey or winget).\n'));
320
+ const { installChoco } = await inquirer_1.default.prompt([
321
+ {
322
+ type: 'confirm',
323
+ name: 'installChoco',
324
+ message: 'Would you like to install Chocolatey to manage package installations?',
325
+ default: true,
326
+ },
327
+ ]);
328
+ if (installChoco) {
329
+ const success = await this.installChocolatey();
330
+ if (success) {
331
+ packageManager = 'choco';
332
+ }
333
+ else {
334
+ console.log(chalk.yellow('\nCannot proceed with automatic installation without a package manager.\n'));
335
+ console.log(chalk.blue('Please install tools manually:\n'));
336
+ for (const tool of missingTools) {
337
+ console.log(chalk.white(` ${tool}:`));
338
+ this.printInstallInstructions(tool);
339
+ console.log('');
340
+ }
341
+ return;
342
+ }
343
+ }
344
+ else {
345
+ console.log(chalk.yellow('\nCannot proceed with automatic installation without a package manager.\n'));
346
+ console.log(chalk.blue('Please install tools manually:\n'));
347
+ for (const tool of missingTools) {
348
+ console.log(chalk.white(` ${tool}:`));
349
+ this.printInstallInstructions(tool);
350
+ console.log('');
351
+ }
352
+ return;
353
+ }
354
+ }
355
+ if (packageManager) {
356
+ console.log(chalk.gray(`Using package manager: ${chalk.cyan(packageManager)}\n`));
357
+ }
358
+ for (const tool of missingTools) {
359
+ const { installNow } = await inquirer_1.default.prompt([
360
+ {
361
+ type: 'confirm',
362
+ name: 'installNow',
363
+ message: `Install ${tool} now?`,
364
+ default: true,
365
+ },
366
+ ]);
367
+ if (installNow) {
368
+ try {
369
+ await this.installTool(tool, packageManager);
370
+ console.log(chalk.green(`\nāœ“ ${tool} installed successfully!\n`));
371
+ }
372
+ catch (error) {
373
+ console.log(chalk.yellow(`\nāš ļø Could not install ${tool} automatically.`));
374
+ console.log(chalk.blue('\nPlease install it manually:\n'));
375
+ this.printInstallInstructions(tool);
376
+ console.log('');
377
+ }
378
+ }
379
+ else {
380
+ console.log(chalk.blue(`\nTo install ${tool} manually:\n`));
381
+ this.printInstallInstructions(tool);
382
+ console.log('');
383
+ }
384
+ }
385
+ }
386
+ async installTool(tool, packageManager) {
387
+ const platform = process.platform;
388
+ // Package names mapping for different package managers
389
+ const packageNames = {
390
+ kubectl: {
391
+ choco: 'kubernetes-cli',
392
+ winget: 'Kubernetes.kubectl',
393
+ scoop: 'kubectl',
394
+ brew: 'kubectl',
395
+ apt: 'kubectl',
396
+ snap: 'kubectl',
397
+ },
398
+ doctl: {
399
+ choco: 'doctl',
400
+ winget: 'DigitalOcean.Doctl',
401
+ scoop: 'doctl',
402
+ brew: 'doctl',
403
+ apt: 'doctl',
404
+ snap: 'doctl',
405
+ },
406
+ 'gh cli': {
407
+ choco: 'gh',
408
+ winget: 'GitHub.cli',
409
+ scoop: 'gh',
410
+ brew: 'gh',
411
+ apt: 'gh',
412
+ snap: 'gh',
413
+ },
414
+ helm: {
415
+ choco: 'kubernetes-helm',
416
+ winget: 'Helm.Helm',
417
+ scoop: 'helm',
418
+ brew: 'helm',
419
+ apt: 'helm',
420
+ snap: 'helm',
421
+ },
422
+ };
423
+ const packageName = packageNames[tool]?.[packageManager];
424
+ if (!packageName) {
425
+ throw new Error(`Unknown tool: ${tool} for package manager: ${packageManager}`);
426
+ }
427
+ if (platform === 'win32') {
428
+ if (packageManager === 'choco') {
429
+ await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', `choco install ${packageName} -y`], {}, false);
430
+ }
431
+ else if (packageManager === 'winget') {
432
+ await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, [
433
+ '-Command',
434
+ `winget install --id ${packageName} --silent --accept-package-agreements --accept-source-agreements`,
435
+ ], {}, false);
436
+ }
437
+ else if (packageManager === 'scoop') {
438
+ await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', `scoop install ${packageName}`], {}, false);
439
+ }
440
+ else {
441
+ throw new Error(`Unsupported package manager: ${packageManager}`);
442
+ }
443
+ }
444
+ else if (platform === 'darwin') {
445
+ if (packageManager === 'brew') {
446
+ await this.runner.executeCommand(runner_service_1.ProgramName.BREW, ['install', packageName], {}, false);
447
+ }
448
+ else {
449
+ throw new Error(`Unsupported package manager for macOS: ${packageManager}`);
450
+ }
451
+ }
452
+ else if (platform === 'linux') {
453
+ if (packageManager === 'apt') {
454
+ await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', `sudo apt update && sudo apt install -y ${packageName}`], {}, false);
455
+ }
456
+ else if (packageManager === 'snap') {
457
+ await this.runner.executeCommand(runner_service_1.ProgramName.POWERSHELL, ['-Command', `sudo snap install ${packageName} --classic`], {}, false);
458
+ }
459
+ else {
460
+ throw new Error(`Unsupported package manager for Linux: ${packageManager}`);
461
+ }
462
+ }
463
+ else {
464
+ throw new Error('Automatic installation not supported on this platform');
465
+ }
466
+ }
467
+ printInstallInstructions(tool) {
468
+ const platform = process.platform;
469
+ switch (tool) {
470
+ case 'kubectl':
471
+ console.log(chalk.gray(' Windows (Chocolatey): choco install kubernetes-cli'));
472
+ console.log(chalk.gray(' Windows (Scoop): scoop install kubectl'));
473
+ console.log(chalk.gray(' Windows (winget): winget install Kubernetes.kubectl'));
474
+ console.log(chalk.gray(' macOS (Homebrew): brew install kubectl'));
475
+ console.log(chalk.gray(' Linux (apt): sudo apt install kubectl'));
476
+ console.log(chalk.gray(' Linux (snap): sudo snap install kubectl --classic'));
477
+ break;
478
+ case 'doctl':
479
+ console.log(chalk.gray(' Windows (Chocolatey): choco install doctl'));
480
+ console.log(chalk.gray(' Windows (Scoop): scoop install doctl'));
481
+ console.log(chalk.gray(' Windows (winget): winget install DigitalOcean.Doctl'));
482
+ console.log(chalk.gray(' macOS (Homebrew): brew install doctl'));
483
+ console.log(chalk.gray(' Linux (snap): sudo snap install doctl'));
484
+ console.log(chalk.gray(' Linux (manual): https://docs.digitalocean.com/reference/doctl/how-to/install/'));
485
+ break;
486
+ case 'gh cli':
487
+ console.log(chalk.gray(' Windows (Chocolatey): choco install gh'));
488
+ console.log(chalk.gray(' Windows (Scoop): scoop install gh'));
489
+ console.log(chalk.gray(' Windows (winget): winget install GitHub.cli'));
490
+ console.log(chalk.gray(' macOS (Homebrew): brew install gh'));
491
+ console.log(chalk.gray(' Linux (apt): sudo apt install gh'));
492
+ console.log(chalk.gray(' Linux (snap): sudo snap install gh'));
493
+ break;
494
+ case 'helm':
495
+ console.log(chalk.gray(' Windows (Chocolatey): choco install kubernetes-helm'));
496
+ console.log(chalk.gray(' Windows (Scoop): scoop install helm'));
497
+ console.log(chalk.gray(' Windows (winget): winget install Helm.Helm'));
498
+ console.log(chalk.gray(' macOS (Homebrew): brew install helm'));
499
+ console.log(chalk.gray(' Linux (snap): sudo snap install helm --classic'));
500
+ console.log(chalk.gray(' Linux (manual): https://helm.sh/docs/intro/install/'));
501
+ break;
502
+ }
503
+ }
504
+ async getGitRepoName(path) {
505
+ try {
506
+ const result = await this.runner.executeCommand(runner_service_1.ProgramName.GIT, ['config', '--get', 'remote.origin.url'], { cwd: path }, true);
507
+ const url = result.stdout.trim();
508
+ // Extract repo name from git URL
509
+ const match = url.match(/\/([^\/]+?)(\.git)?$/);
510
+ return match ? match[1] : null;
511
+ }
512
+ catch {
513
+ return null;
514
+ }
515
+ }
516
+ async getCurrentKubeContext() {
517
+ try {
518
+ const result = await this.runner.executeCommand(runner_service_1.ProgramName.KUBECTL, ['config', 'current-context'], {}, true);
519
+ return result.stdout.trim();
520
+ }
521
+ catch {
522
+ return null;
523
+ }
524
+ }
525
+ async getKubeNamespaces() {
526
+ try {
527
+ const result = await this.runner.executeCommand(runner_service_1.ProgramName.KUBECTL, ['get', 'namespaces', '-o', 'jsonpath={.items[*].metadata.name}'], {}, true);
528
+ return result.stdout
529
+ .trim()
530
+ .split(/\s+/)
531
+ .filter((n) => n.length > 0);
532
+ }
533
+ catch {
534
+ return [];
535
+ }
536
+ }
537
+ async getKubeClusters() {
538
+ try {
539
+ const result = await this.runner.executeCommand(runner_service_1.ProgramName.KUBECTL, ['config', 'get-clusters'], {}, true);
540
+ return result.stdout
541
+ .trim()
542
+ .split('\n')
543
+ .slice(1) // Skip header
544
+ .filter((c) => c.length > 0);
545
+ }
546
+ catch {
547
+ return [];
548
+ }
549
+ }
550
+ async runDeploymentWizard() {
551
+ console.log(chalk.blue.bold('\nšŸš€ Deployment Configuration Wizard\n'));
552
+ console.log(chalk.gray('This wizard will help you set up CI/CD for your project.\n'));
553
+ // Gather context information
554
+ const spinner = ora('Gathering environment information...').start();
555
+ const gitRepoName = await this.getGitRepoName(process.cwd());
556
+ const currentDir = pathModule.basename(process.cwd());
557
+ const defaultAppName = gitRepoName || currentDir;
558
+ const currentContext = await this.getCurrentKubeContext();
559
+ const availableNamespaces = await this.getKubeNamespaces();
560
+ const availableClusters = await this.getKubeClusters();
561
+ spinner.stop();
562
+ if (currentContext) {
563
+ console.log(chalk.gray(`Current kubectl context: ${chalk.cyan(currentContext)}\n`));
564
+ }
565
+ if (availableNamespaces.length > 0) {
566
+ console.log(chalk.gray(`Available namespaces: ${chalk.cyan(availableNamespaces.join(', '))}\n`));
567
+ }
568
+ const answers = await inquirer_1.default.prompt([
569
+ {
570
+ type: 'list',
571
+ name: 'provider',
572
+ message: 'Select your Kubernetes provider:',
573
+ choices: [
574
+ { name: 'Digital Ocean Kubernetes', value: 'digitalocean' },
575
+ { name: 'Other (Coming soon)', value: 'other', disabled: true },
576
+ ],
577
+ default: 'digitalocean',
578
+ },
579
+ {
580
+ type: 'list',
581
+ name: 'cicd',
582
+ message: 'Select your CI/CD platform:',
583
+ choices: [
584
+ { name: 'GitHub Actions', value: 'github-actions' },
585
+ { name: 'Other (Coming soon)', value: 'other', disabled: true },
586
+ ],
587
+ default: 'github-actions',
588
+ },
589
+ {
590
+ type: 'list',
591
+ name: 'clusterSelection',
592
+ message: 'How would you like to specify your cluster?',
593
+ choices: availableClusters.length > 0
594
+ ? [
595
+ { name: 'Use current context', value: 'current' },
596
+ { name: 'Select from available clusters', value: 'select' },
597
+ { name: 'Enter cluster name manually', value: 'manual' },
598
+ ]
599
+ : [{ name: 'Enter cluster name manually', value: 'manual' }],
600
+ default: availableClusters.length > 0 ? 'current' : 'manual',
601
+ when: () => availableClusters.length > 0 || !currentContext,
602
+ },
603
+ {
604
+ type: 'list',
605
+ name: 'clusterName',
606
+ message: 'Select your Kubernetes cluster:',
607
+ choices: availableClusters,
608
+ when: (answers) => answers.clusterSelection === 'select',
609
+ },
610
+ {
611
+ type: 'input',
612
+ name: 'clusterName',
613
+ message: 'Enter your Kubernetes cluster name:',
614
+ validate: (input) => input.length > 0 || 'Cluster name is required',
615
+ when: (answers) => answers.clusterSelection === 'manual',
616
+ },
617
+ {
618
+ type: 'list',
619
+ name: 'namespaceSelection',
620
+ message: 'How would you like to configure the namespace?',
621
+ choices: availableNamespaces.length > 0
622
+ ? [
623
+ { name: 'Use existing namespace', value: 'existing' },
624
+ { name: 'Create new namespace', value: 'new' },
625
+ ]
626
+ : [{ name: 'Create new namespace', value: 'new' }],
627
+ default: availableNamespaces.length > 0 ? 'existing' : 'new',
628
+ },
629
+ {
630
+ type: 'list',
631
+ name: 'namespace',
632
+ message: 'Select an existing namespace:',
633
+ choices: availableNamespaces,
634
+ default: availableNamespaces.includes('production')
635
+ ? 'production'
636
+ : availableNamespaces[0],
637
+ when: (answers) => answers.namespaceSelection === 'existing' &&
638
+ availableNamespaces.length > 0,
639
+ },
640
+ {
641
+ type: 'input',
642
+ name: 'namespace',
643
+ message: 'Enter the new namespace name:',
644
+ default: 'production',
645
+ validate: (input) => {
646
+ if (input.length === 0)
647
+ return 'Namespace name is required';
648
+ if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(input)) {
649
+ return 'Namespace must be lowercase alphanumeric and may contain hyphens';
650
+ }
651
+ return true;
652
+ },
653
+ when: (answers) => answers.namespaceSelection === 'new',
654
+ },
655
+ {
656
+ type: 'input',
657
+ name: 'appName',
658
+ message: 'Enter your application name:',
659
+ default: defaultAppName,
660
+ validate: (input) => {
661
+ if (input.length === 0)
662
+ return 'Application name is required';
663
+ if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(input)) {
664
+ return 'Application name must be lowercase alphanumeric and may contain hyphens';
665
+ }
666
+ return true;
667
+ },
668
+ },
669
+ {
670
+ type: 'input',
671
+ name: 'containerRegistry',
672
+ message: 'Enter your container registry:',
673
+ default: (answers) => `registry.digitalocean.com/${answers.appName}`,
674
+ validate: (input) => input.length > 0 || 'Container registry is required',
675
+ },
676
+ {
677
+ type: 'input',
678
+ name: 'domain',
679
+ message: 'Enter your domain (optional, press Enter to skip):',
680
+ },
681
+ {
682
+ type: 'checkbox',
683
+ name: 'apps',
684
+ message: 'Select which apps to deploy:',
685
+ choices: [
686
+ { name: 'API (Backend)', value: 'api', checked: true },
687
+ { name: 'Admin (Frontend)', value: 'admin', checked: true },
688
+ ],
689
+ },
690
+ {
691
+ type: 'confirm',
692
+ name: 'setupIngress',
693
+ message: 'Would you like to set up Ingress for external access?',
694
+ default: true,
695
+ when: (answers) => answers.domain && answers.domain.length > 0,
696
+ },
697
+ {
698
+ type: 'confirm',
699
+ name: 'setupSSL',
700
+ message: 'Would you like to set up SSL/TLS with cert-manager?',
701
+ default: true,
702
+ when: (answers) => answers.setupIngress,
703
+ },
704
+ {
705
+ type: 'input',
706
+ name: 'email',
707
+ message: 'Enter your email for SSL certificate notifications:',
708
+ validate: (input) => {
709
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
710
+ return emailRegex.test(input) || 'Please enter a valid email address';
711
+ },
712
+ when: (answers) => answers.setupSSL,
713
+ },
714
+ {
715
+ type: 'confirm',
716
+ name: 'confirmGeneration',
717
+ message: 'Generate deployment configuration files now?',
718
+ default: true,
719
+ },
720
+ ]);
721
+ if (!answers.confirmGeneration) {
722
+ return null;
723
+ }
724
+ // Set cluster name from current context if using current
725
+ if (answers.clusterSelection === 'current' && currentContext) {
726
+ // Extract cluster name from context (format may vary)
727
+ answers.clusterName = currentContext;
728
+ }
729
+ // Store if namespace needs to be created
730
+ const createNamespace = answers.namespaceSelection === 'new';
731
+ return {
732
+ ...answers,
733
+ createNamespace,
734
+ };
735
+ }
736
+ async generateDeploymentFiles(path, config) {
737
+ // Create .github/workflows directory
738
+ const workflowsDir = pathModule.join(path, '.github', 'workflows');
739
+ await (0, promises_1.mkdir)(workflowsDir, { recursive: true });
740
+ // Create k8s directory
741
+ const k8sDir = pathModule.join(path, 'k8s');
742
+ await (0, promises_1.mkdir)(k8sDir, { recursive: true });
743
+ // Generate Dockerfiles and .dockerignore for each app
744
+ for (const app of config.apps) {
745
+ await this.generateDockerfile(path, app, config);
746
+ }
747
+ // Generate .dockerignore in root
748
+ await this.generateDockerignore(path);
749
+ // Generate GitHub Actions workflow
750
+ await this.generateGitHubActionsWorkflow(workflowsDir, config);
751
+ // Generate Kubernetes manifests for each app
752
+ for (const app of config.apps) {
753
+ await this.generateKubernetesManifests(k8sDir, app, config);
754
+ }
755
+ // Generate Ingress if requested
756
+ if (config.setupIngress) {
757
+ await this.generateIngressManifest(k8sDir, config);
758
+ }
759
+ // Generate cert-manager ClusterIssuer if SSL is requested
760
+ if (config.setupSSL) {
761
+ await this.generateCertManagerIssuer(k8sDir, config);
762
+ }
763
+ // Generate Helm chart configuration (optional)
764
+ await this.generateHelmChart(path, config);
765
+ // Generate README with deployment instructions
766
+ await this.generateDeploymentReadme(path, config);
767
+ }
768
+ async generateDockerfile(path, app, config) {
769
+ const templatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', `${app}.Dockerfile.ejs`);
770
+ const dockerfilePath = pathModule.join(path, 'apps', app, 'Dockerfile');
771
+ // Check if Dockerfile already exists
772
+ if ((0, fs_1.existsSync)(dockerfilePath)) {
773
+ this.log(chalk.yellow(`Dockerfile already exists for ${app}, skipping...`));
774
+ return;
775
+ }
776
+ try {
777
+ const templateContent = await (0, promises_1.readFile)(templatePath, 'utf8');
778
+ const renderedContent = await (0, ejs_1.render)(templateContent, { config, app });
779
+ await (0, promises_1.writeFile)(dockerfilePath, renderedContent, 'utf8');
780
+ this.log(chalk.green(`Created Dockerfile for ${app}`));
781
+ }
782
+ catch (error) {
783
+ this.log(chalk.yellow(`Could not create Dockerfile from template for ${app}, creating basic version...`));
784
+ // Create a basic Dockerfile if template doesn't exist
785
+ const basicDockerfile = this.generateBasicDockerfile(app);
786
+ await (0, promises_1.writeFile)(dockerfilePath, basicDockerfile, 'utf8');
787
+ this.log(chalk.green(`Created basic Dockerfile for ${app}`));
788
+ }
789
+ }
790
+ generateBasicDockerfile(app) {
791
+ if (app === 'api') {
792
+ return `# Dockerfile for API
793
+ FROM node:18-alpine AS builder
794
+ WORKDIR /app
795
+ COPY package.json pnpm-lock.yaml ./
796
+ COPY apps/api/package.json ./apps/api/
797
+ RUN npm install -g pnpm
798
+ RUN pnpm install --frozen-lockfile
799
+ COPY . .
800
+ RUN pnpm --filter api build
801
+
802
+ FROM node:18-alpine
803
+ WORKDIR /app
804
+ COPY package.json pnpm-lock.yaml ./
805
+ COPY apps/api/package.json ./apps/api/
806
+ RUN npm install -g pnpm
807
+ RUN pnpm install --frozen-lockfile --prod
808
+ COPY --from=builder /app/apps/api/dist ./apps/api/dist
809
+ ENV NODE_ENV=production
810
+ ENV PORT=3000
811
+ EXPOSE 3000
812
+ CMD ["node", "apps/api/dist/main.js"]
813
+ `;
814
+ }
815
+ else if (app === 'admin') {
816
+ return `# Dockerfile for Admin
817
+ FROM node:18-alpine AS builder
818
+ WORKDIR /app
819
+ COPY package.json pnpm-lock.yaml ./
820
+ COPY apps/admin/package.json ./apps/admin/
821
+ RUN npm install -g pnpm
822
+ RUN pnpm install --frozen-lockfile
823
+ COPY . .
824
+ RUN pnpm --filter admin build
825
+
826
+ FROM node:18-alpine
827
+ WORKDIR /app
828
+ RUN npm install -g pnpm
829
+ COPY --from=builder /app/apps/admin/.next ./apps/admin/.next
830
+ COPY --from=builder /app/apps/admin/public ./apps/admin/public
831
+ COPY --from=builder /app/apps/admin/package.json ./apps/admin/
832
+ COPY --from=builder /app/apps/admin/node_modules ./apps/admin/node_modules
833
+ ENV NODE_ENV=production
834
+ ENV PORT=80
835
+ EXPOSE 80
836
+ WORKDIR /app/apps/admin
837
+ CMD ["pnpm", "start"]
838
+ `;
839
+ }
840
+ return `# Dockerfile for ${app}
841
+ FROM node:18-alpine
842
+ WORKDIR /app
843
+ COPY . .
844
+ RUN npm install -g pnpm
845
+ RUN pnpm install --frozen-lockfile
846
+ RUN pnpm build
847
+ EXPOSE 3000
848
+ CMD ["pnpm", "start"]
849
+ `;
850
+ }
851
+ async generateDockerignore(path) {
852
+ const dockerignorePath = pathModule.join(path, '.dockerignore');
853
+ // Check if .dockerignore already exists
854
+ if ((0, fs_1.existsSync)(dockerignorePath)) {
855
+ this.log(chalk.yellow('.dockerignore already exists, skipping...'));
856
+ return;
857
+ }
858
+ const templatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', '.dockerignore.ejs');
859
+ try {
860
+ const templateContent = await (0, promises_1.readFile)(templatePath, 'utf8');
861
+ await (0, promises_1.writeFile)(dockerignorePath, templateContent, 'utf8');
862
+ this.log(chalk.green('Created .dockerignore'));
863
+ }
864
+ catch (error) {
865
+ // Create basic .dockerignore if template doesn't exist
866
+ const basicDockerignore = `node_modules
867
+ dist
868
+ build
869
+ .next
870
+ .env
871
+ .env.local
872
+ .git
873
+ .github
874
+ k8s
875
+ helm
876
+ *.md
877
+ test
878
+ tests
879
+ coverage
880
+ .vscode
881
+ .idea
882
+ *.log
883
+ tmp
884
+ temp
885
+ `;
886
+ await (0, promises_1.writeFile)(dockerignorePath, basicDockerignore, 'utf8');
887
+ this.log(chalk.green('Created basic .dockerignore'));
888
+ }
889
+ }
890
+ async generateGitHubActionsWorkflow(dir, config) {
891
+ const workflowContent = `name: Deploy to Kubernetes
892
+
893
+ onon:
894
+ push:
895
+ branches:
896
+ - main
897
+ - production
898
+ workflow_dispatch:
899
+
900
+ env:
901
+ REGISTRY: ${config.containerRegistry}
902
+ CLUSTER_NAME: ${config.clusterName}
903
+ NAMESPACE: ${config.namespace}
904
+
905
+ jobs:
906
+ ${config.apps
907
+ .map((app) => ` deploy-${app}:
908
+ name: Deploy ${app.toUpperCase()}
909
+ runs-on: ubuntu-latest
910
+
911
+ steps:
912
+ - name: Checkout code
913
+ uses: actions/checkout@v4
914
+
915
+ - name: Install doctl
916
+ uses: digitalocean/action-doctl@v2
917
+ with:
918
+ token: \${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
919
+
920
+ - name: Log in to Container Registry
921
+ run: doctl registry login
922
+
923
+ - name: Build and push Docker image
924
+ run: |
925
+ docker build -t \${{ env.REGISTRY }}/${config.appName}-${app}:\${{ github.sha }} \\
926
+ -f apps/${app}/Dockerfile .
927
+ docker push \${{ env.REGISTRY }}/${config.appName}-${app}:\${{ github.sha }}
928
+ docker tag \${{ env.REGISTRY }}/${config.appName}-${app}:\${{ github.sha }} \\
929
+ \${{ env.REGISTRY }}/${config.appName}-${app}:latest
930
+ docker push \${{ env.REGISTRY }}/${config.appName}-${app}:latest
931
+
932
+ - name: Save DigitalOcean kubeconfig
933
+ run: doctl kubernetes cluster kubeconfig save \${{ env.CLUSTER_NAME }}
934
+
935
+ - name: Deploy to Kubernetes
936
+ run: |
937
+ kubectl set image deployment/${config.appName}-${app} \\
938
+ ${config.appName}-${app}=\${{ env.REGISTRY }}/${config.appName}-${app}:\${{ github.sha }} \\
939
+ -n \${{ env.NAMESPACE }}
940
+ kubectl rollout status deployment/${config.appName}-${app} -n \${{ env.NAMESPACE }}
941
+ `)
942
+ .join('\n')}
943
+ `;
944
+ const workflowPath = pathModule.join(dir, 'deploy.yml');
945
+ await (0, promises_1.writeFile)(workflowPath, workflowContent, 'utf8');
946
+ this.log(chalk.green(`Created GitHub Actions workflow: ${workflowPath}`));
947
+ }
948
+ async generateKubernetesManifests(dir, app, config) {
949
+ const appDir = pathModule.join(dir, app);
950
+ await (0, promises_1.mkdir)(appDir, { recursive: true });
951
+ // Generate Deployment
952
+ const deploymentContent = `apiVersion: apps/v1
953
+ kind: Deployment
954
+ metadata:
955
+ name: ${config.appName}-${app}
956
+ namespace: ${config.namespace}
957
+ labels:
958
+ app: ${config.appName}-${app}
959
+ spec:
960
+ replicas: 2
961
+ selector:
962
+ matchLabels:
963
+ app: ${config.appName}-${app}
964
+ template:
965
+ metadata:
966
+ labels:
967
+ app: ${config.appName}-${app}
968
+ spec:
969
+ containers:
970
+ - name: ${config.appName}-${app}
971
+ image: ${config.containerRegistry}/${config.appName}-${app}:latest
972
+ ports:
973
+ - containerPort: ${app === 'api' ? '3000' : '80'}
974
+ env:
975
+ - name: NODE_ENV
976
+ value: "production"
977
+ resources:
978
+ requests:
979
+ memory: "256Mi"
980
+ cpu: "100m"
981
+ limits:
982
+ memory: "512Mi"
983
+ cpu: "500m"
984
+ livenessProbe:
985
+ httpGet:
986
+ path: ${app === 'api' ? '/health' : '/'}
987
+ port: ${app === 'api' ? '3000' : '80'}
988
+ initialDelaySeconds: 30
989
+ periodSeconds: 10
990
+ readinessProbe:
991
+ httpGet:
992
+ path: ${app === 'api' ? '/health' : '/'}
993
+ port: ${app === 'api' ? '3000' : '80'}
994
+ initialDelaySeconds: 10
995
+ periodSeconds: 5
996
+ `;
997
+ await (0, promises_1.writeFile)(pathModule.join(appDir, 'deployment.yaml'), deploymentContent, 'utf8');
998
+ // Generate Service
999
+ const serviceContent = `apiVersion: v1
1000
+ kind: Service
1001
+ metadata:
1002
+ name: ${config.appName}-${app}
1003
+ namespace: ${config.namespace}
1004
+ labels:
1005
+ app: ${config.appName}-${app}
1006
+ spec:
1007
+ type: ClusterIP
1008
+ ports:
1009
+ - port: ${app === 'api' ? '3000' : '80'}
1010
+ targetPort: ${app === 'api' ? '3000' : '80'}
1011
+ protocol: TCP
1012
+ name: http
1013
+ selector:
1014
+ app: ${config.appName}-${app}
1015
+ `;
1016
+ await (0, promises_1.writeFile)(pathModule.join(appDir, 'service.yaml'), serviceContent, 'utf8');
1017
+ this.log(chalk.green(`Created Kubernetes manifests for ${app}`));
1018
+ }
1019
+ async generateIngressManifest(dir, config) {
1020
+ const ingressContent = `apiVersion: networking.k8s.io/v1
1021
+ kind: Ingress
1022
+ metadata:
1023
+ name: ${config.appName}-ingress
1024
+ namespace: ${config.namespace}
1025
+ annotations:
1026
+ kubernetes.io/ingress.class: "nginx"
1027
+ ${config.setupSSL ? ` cert-manager.io/cluster-issuer: "letsencrypt-prod"` : ''}
1028
+ spec:
1029
+ ${config.setupSSL
1030
+ ? ` tls:
1031
+ - hosts:
1032
+ - ${config.domain}
1033
+ ${config.apps.includes('api') ? ` - api.${config.domain}` : ''}
1034
+ secretName: ${config.appName}-tls
1035
+ `
1036
+ : ''} rules:
1037
+ ${config.apps.includes('admin')
1038
+ ? ` - host: ${config.domain}
1039
+ http:
1040
+ paths:
1041
+ - path: /
1042
+ pathType: Prefix
1043
+ backend:
1044
+ service:
1045
+ name: ${config.appName}-admin
1046
+ port:
1047
+ number: 80
1048
+ `
1049
+ : ''}${config.apps.includes('api')
1050
+ ? ` - host: api.${config.domain}
1051
+ http:
1052
+ paths:
1053
+ - path: /
1054
+ pathType: Prefix
1055
+ backend:
1056
+ service:
1057
+ name: ${config.appName}-api
1058
+ port:
1059
+ number: 3000
1060
+ `
1061
+ : ''}`;
1062
+ await (0, promises_1.writeFile)(pathModule.join(dir, 'ingress.yaml'), ingressContent, 'utf8');
1063
+ this.log(chalk.green('Created Ingress manifest'));
1064
+ }
1065
+ async generateCertManagerIssuer(dir, config) {
1066
+ const issuerContent = `apiVersion: cert-manager.io/v1
1067
+ kind: ClusterIssuer
1068
+ metadata:
1069
+ name: letsencrypt-prod
1070
+ spec:
1071
+ acme:
1072
+ server: https://acme-v02.api.letsencrypt.org/directory
1073
+ email: ${config.email}
1074
+ privateKeySecretRef:
1075
+ name: letsencrypt-prod
1076
+ solvers:
1077
+ - http01:
1078
+ ingress:
1079
+ class: nginx
1080
+ `;
1081
+ await (0, promises_1.writeFile)(pathModule.join(dir, 'cert-manager-issuer.yaml'), issuerContent, 'utf8');
1082
+ this.log(chalk.green('Created cert-manager ClusterIssuer'));
1083
+ }
1084
+ async generateHelmChart(path, config) {
1085
+ const helmDir = pathModule.join(path, 'helm', config.appName);
1086
+ await (0, promises_1.mkdir)(helmDir, { recursive: true });
1087
+ // Generate Chart.yaml
1088
+ const chartContent = `apiVersion: v2
1089
+ name: ${config.appName}
1090
+ description: Helm chart for ${config.appName}
1091
+ type: application
1092
+ version: 1.0.0
1093
+ appVersion: "1.0.0"
1094
+ `;
1095
+ await (0, promises_1.writeFile)(pathModule.join(helmDir, 'Chart.yaml'), chartContent, 'utf8');
1096
+ // Generate values.yaml
1097
+ const valuesContent = `# Default values for ${config.appName}
1098
+ namespace: ${config.namespace}
1099
+
1100
+ registry: ${config.containerRegistry}
1101
+
1102
+ apps:
1103
+ ${config.apps
1104
+ .map((app) => ` ${app}:
1105
+ enabled: true
1106
+ replicas: 2
1107
+ image:
1108
+ repository: \${{ .Values.registry }}/${config.appName}-${app}
1109
+ tag: latest
1110
+ pullPolicy: Always
1111
+ service:
1112
+ type: ClusterIP
1113
+ port: ${app === 'api' ? '3000' : '80'}
1114
+ resources:
1115
+ requests:
1116
+ memory: "256Mi"
1117
+ cpu: "100m"
1118
+ limits:
1119
+ memory: "512Mi"
1120
+ cpu: "500m"
1121
+ `)
1122
+ .join('')}
1123
+ ${config.setupIngress
1124
+ ? `ingress:
1125
+ enabled: true
1126
+ className: nginx
1127
+ annotations:
1128
+ ${config.setupSSL ? ` cert-manager.io/cluster-issuer: letsencrypt-prod` : ''}
1129
+ hosts:
1130
+ ${config.apps.includes('admin')
1131
+ ? ` - host: ${config.domain}
1132
+ paths:
1133
+ - path: /
1134
+ pathType: Prefix
1135
+ backend:
1136
+ service:
1137
+ name: ${config.appName}-admin
1138
+ port:
1139
+ number: 80
1140
+ `
1141
+ : ''}${config.apps.includes('api')
1142
+ ? ` - host: api.${config.domain}
1143
+ paths:
1144
+ - path: /
1145
+ pathType: Prefix
1146
+ backend:
1147
+ service:
1148
+ name: ${config.appName}-api
1149
+ port:
1150
+ number: 3000
1151
+ `
1152
+ : ''}${config.setupSSL
1153
+ ? ` tls:
1154
+ - secretName: ${config.appName}-tls
1155
+ hosts:
1156
+ - ${config.domain}
1157
+ ${config.apps.includes('api') ? ` - api.${config.domain}` : ''}`
1158
+ : ''}`
1159
+ : ''}
1160
+ `;
1161
+ await (0, promises_1.writeFile)(pathModule.join(helmDir, 'values.yaml'), valuesContent, 'utf8');
1162
+ this.log(chalk.green('Created Helm chart'));
1163
+ }
1164
+ async generateDeploymentReadme(path, config) {
1165
+ const readmeContent = `# Deployment Guide
1166
+
1167
+ This project is configured for deployment to **${config.provider === 'digitalocean' ? 'Digital Ocean Kubernetes' : config.provider}** using **${config.cicd === 'github-actions' ? 'GitHub Actions' : config.cicd}**.
1168
+
1169
+ ## Prerequisites
1170
+
1171
+ Make sure you have the following tools installed:
1172
+
1173
+ - \`kubectl\` - Kubernetes CLI
1174
+ - \`doctl\` - Digital Ocean CLI
1175
+ - \`gh\` - GitHub CLI
1176
+ - \`helm\` - Kubernetes package manager
1177
+
1178
+ ## Initial Setup
1179
+
1180
+ ### 1. Configure Digital Ocean
1181
+
1182
+ \`\`\`bash
1183
+ # Authenticate with Digital Ocean
1184
+ doctl auth init
1185
+
1186
+ # Get your cluster kubeconfig
1187
+ doctl kubernetes cluster kubeconfig save ${config.clusterName}
1188
+ \`\`\`
1189
+
1190
+ ${config.createNamespace
1191
+ ? `### 2. Create Kubernetes Namespace
1192
+
1193
+ \`\`\`bash
1194
+ kubectl create namespace ${config.namespace}
1195
+ \`\`\`
1196
+
1197
+ `
1198
+ : `### 2. Verify Namespace
1199
+
1200
+ \`\`\`bash
1201
+ # Verify the namespace exists
1202
+ kubectl get namespace ${config.namespace}
1203
+ \`\`\`
1204
+
1205
+ `}### 3. Configure GitHub Secrets
1206
+
1207
+ Add the following secrets to your GitHub repository:
1208
+
1209
+ 1. \`DIGITALOCEAN_ACCESS_TOKEN\` - Your Digital Ocean API token
1210
+
1211
+ \`\`\`bash
1212
+ # Get your DO token and add it to GitHub
1213
+ gh secret set DIGITALOCEAN_ACCESS_TOKEN
1214
+ \`\`\`
1215
+
1216
+ ${config.setupSSL
1217
+ ? `### 4. Install cert-manager
1218
+
1219
+ \`\`\`bash
1220
+ # Install cert-manager for SSL certificates
1221
+ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
1222
+
1223
+ # Wait for cert-manager to be ready
1224
+ kubectl wait --for=condition=ready pod -l app.kubernetes.io/instance=cert-manager -n cert-manager --timeout=300s
1225
+
1226
+ # Apply the ClusterIssuer
1227
+ kubectl apply -f k8s/cert-manager-issuer.yaml
1228
+ \`\`\`
1229
+ `
1230
+ : ''}${config.setupIngress
1231
+ ? `### ${config.setupSSL ? '5' : '4'}. Install NGINX Ingress Controller
1232
+
1233
+ \`\`\`bash
1234
+ # Install NGINX Ingress Controller
1235
+ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
1236
+ helm repo update
1237
+
1238
+ helm install nginx-ingress ingress-nginx/ingress-nginx \\
1239
+ --namespace ingress-nginx \\
1240
+ --create-namespace \\
1241
+ --set controller.publishService.enabled=true
1242
+
1243
+ # Wait for the load balancer IP
1244
+ kubectl get service nginx-ingress-ingress-nginx-controller -n ingress-nginx --watch
1245
+ \`\`\`
1246
+
1247
+ **Important:** After the LoadBalancer gets an external IP, configure your DNS:
1248
+
1249
+ ${config.domain
1250
+ ? `- Point \`${config.domain}\` to the LoadBalancer IP
1251
+ ${config.apps.includes('api')
1252
+ ? `- Point \`api.${config.domain}\` to the LoadBalancer IP
1253
+ `
1254
+ : ''}`
1255
+ : ''}
1256
+ `
1257
+ : ''}
1258
+ ## Deployment
1259
+
1260
+ ### Using GitHub Actions (Automatic)
1261
+
1262
+ Push to the \`main\` or \`production\` branch:
1263
+
1264
+ \`\`\`bash
1265
+ git add .
1266
+ git commit -m "Deploy to production"
1267
+ git push origin main
1268
+ \`\`\`
1269
+
1270
+ The GitHub Actions workflow will automatically:
1271
+ 1. Build Docker images
1272
+ 2. Push to container registry
1273
+ 3. Deploy to Kubernetes cluster
1274
+
1275
+ ### Manual Deployment
1276
+
1277
+ #### Option 1: Using kubectl
1278
+
1279
+ \`\`\`bash
1280
+ # Apply all manifests
1281
+ ${config.apps.map((app) => `kubectl apply -f k8s/${app}/`).join('\n')}
1282
+ ${config.setupIngress ? `kubectl apply -f k8s/ingress.yaml` : ''}
1283
+ \`\`\`
1284
+
1285
+ #### Option 2: Using Helm
1286
+
1287
+ \`\`\`bash
1288
+ # Install or upgrade the release
1289
+ helm upgrade --install ${config.appName} ./helm/${config.appName} \\
1290
+ --namespace ${config.namespace} \\
1291
+ --create-namespace
1292
+ \`\`\`
1293
+
1294
+ ## Monitoring
1295
+
1296
+ ### Check Deployment Status
1297
+
1298
+ \`\`\`bash
1299
+ # Check pods
1300
+ kubectl get pods -n ${config.namespace}
1301
+
1302
+ # Check deployments
1303
+ kubectl get deployments -n ${config.namespace}
1304
+
1305
+ # Check services
1306
+ kubectl get services -n ${config.namespace}
1307
+
1308
+ ${config.setupIngress
1309
+ ? `# Check ingress
1310
+ kubectl get ingress -n ${config.namespace}
1311
+ `
1312
+ : ''}
1313
+ # View logs
1314
+ ${config.apps.map((app) => `kubectl logs -f deployment/${config.appName}-${app} -n ${config.namespace}`).join('\n')}
1315
+ \`\`\`
1316
+
1317
+ ### Scaling
1318
+
1319
+ \`\`\`bash
1320
+ # Scale a deployment
1321
+ ${config.apps.map((app) => `kubectl scale deployment/${config.appName}-${app} --replicas=3 -n ${config.namespace}`).join('\n')}
1322
+ \`\`\`
1323
+
1324
+ ## Rollback
1325
+
1326
+ \`\`\`bash
1327
+ # View rollout history
1328
+ ${config.apps.map((app) => `kubectl rollout history deployment/${config.appName}-${app} -n ${config.namespace}`).join('\n')}
1329
+
1330
+ # Rollback to previous version
1331
+ ${config.apps.map((app) => `kubectl rollout undo deployment/${config.appName}-${app} -n ${config.namespace}`).join('\n')}
1332
+ \`\`\`
1333
+
1334
+ ## Troubleshooting
1335
+
1336
+ ### View Pod Events
1337
+
1338
+ \`\`\`bash
1339
+ kubectl describe pod <pod-name> -n ${config.namespace}
1340
+ \`\`\`
1341
+
1342
+ ### View Cluster Events
1343
+
1344
+ \`\`\`bash
1345
+ kubectl get events -n ${config.namespace} --sort-by='.lastTimestamp'
1346
+ \`\`\`
1347
+
1348
+ ### Access Pod Shell
1349
+
1350
+ \`\`\`bash
1351
+ ${config.apps.map((app) => `kubectl exec -it deployment/${config.appName}-${app} -n ${config.namespace} -- /bin/sh`).join('\n')}
1352
+ \`\`\`
1353
+
1354
+ ## URLs
1355
+
1356
+ ${config.domain
1357
+ ? `- **Admin Panel:** https://${config.domain}
1358
+ ${config.apps.includes('api')
1359
+ ? `- **API:** https://api.${config.domain}
1360
+ `
1361
+ : ''}`
1362
+ : '- Configure your domain and update DNS records as described above\n'}
1363
+
1364
+ ## Further Reading
1365
+
1366
+ - [Digital Ocean Kubernetes Documentation](https://docs.digitalocean.com/products/kubernetes/)
1367
+ - [GitHub Actions Documentation](https://docs.github.com/en/actions)
1368
+ - [Kubernetes Documentation](https://kubernetes.io/docs/home/)
1369
+ - [Helm Documentation](https://helm.sh/docs/)
1370
+ ${config.setupSSL
1371
+ ? `- [cert-manager Documentation](https://cert-manager.io/docs/)
1372
+ `
1373
+ : ''}
1374
+ `;
1375
+ await (0, promises_1.writeFile)(pathModule.join(path, 'DEPLOYMENT.md'), readmeContent, 'utf8');
1376
+ this.log(chalk.green('Created DEPLOYMENT.md'));
1377
+ }
1378
+ displayDeploymentSummary(config) {
1379
+ console.log(chalk.blue.bold('\nšŸ“‹ Deployment Configuration Summary\n'));
1380
+ console.log(chalk.white('Provider: ') + chalk.cyan(config.provider));
1381
+ console.log(chalk.white('CI/CD: ') + chalk.cyan(config.cicd));
1382
+ console.log(chalk.white('Cluster: ') + chalk.cyan(config.clusterName));
1383
+ console.log(chalk.white('Namespace: ') +
1384
+ chalk.cyan(config.namespace) +
1385
+ chalk.gray(config.createNamespace ? ' (will be created)' : ' (existing)'));
1386
+ console.log(chalk.white('App Name: ') + chalk.cyan(config.appName));
1387
+ console.log(chalk.white('Registry: ') + chalk.cyan(config.containerRegistry));
1388
+ if (config.domain) {
1389
+ console.log(chalk.white('Domain: ') + chalk.cyan(config.domain));
1390
+ }
1391
+ console.log(chalk.white('Apps: ') + chalk.cyan(config.apps.join(', ')));
1392
+ console.log(chalk.white('Ingress: ') +
1393
+ chalk.cyan(config.setupIngress ? 'Yes' : 'No'));
1394
+ console.log(chalk.white('SSL/TLS: ') +
1395
+ chalk.cyan(config.setupSSL ? 'Yes' : 'No'));
1396
+ console.log(chalk.green.bold('\nāœ“ Generated Files:\n'));
1397
+ console.log(chalk.gray(' .dockerignore'));
1398
+ config.apps.forEach((app) => {
1399
+ console.log(chalk.gray(` apps/${app}/Dockerfile`));
1400
+ });
1401
+ console.log(chalk.gray(' .github/workflows/deploy.yml'));
1402
+ config.apps.forEach((app) => {
1403
+ console.log(chalk.gray(` k8s/${app}/deployment.yaml`));
1404
+ console.log(chalk.gray(` k8s/${app}/service.yaml`));
1405
+ });
1406
+ if (config.setupIngress) {
1407
+ console.log(chalk.gray(' k8s/ingress.yaml'));
1408
+ }
1409
+ if (config.setupSSL) {
1410
+ console.log(chalk.gray(' k8s/cert-manager-issuer.yaml'));
1411
+ }
1412
+ console.log(chalk.gray(` helm/${config.appName}/Chart.yaml`));
1413
+ console.log(chalk.gray(` helm/${config.appName}/values.yaml`));
1414
+ console.log(chalk.gray(' DEPLOYMENT.md'));
1415
+ console.log(chalk.blue.bold('\nšŸ“– Next Steps:\n'));
1416
+ console.log(chalk.white('1. Review the generated files in your project'));
1417
+ console.log(chalk.white('2. Read DEPLOYMENT.md for setup instructions'));
1418
+ console.log(chalk.white('3. Configure GitHub secrets (see DEPLOYMENT.md)'));
1419
+ console.log(chalk.white('4. Push to main branch to trigger deployment'));
1420
+ console.log('');
1421
+ }
54
1422
  async syncPublish(path, verbose = false) {
55
1423
  const restoreWarnings = this.suppressWarnings();
56
1424
  this.verbose = verbose;