@hed-hog/cli 0.0.49 → 0.0.50

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.
@@ -23,6 +23,7 @@ const promises_1 = require("fs/promises");
23
23
  const inquirer_1 = require("inquirer");
24
24
  const ora = require("ora");
25
25
  const pathModule = require("path");
26
+ const yaml_1 = require("yaml");
26
27
  const to_pascal_case_1 = require("../../functions/to-pascal-case");
27
28
  const database_service_1 = require("../database/database.service");
28
29
  const hedhog_service_1 = require("../hedhog/hedhog.service");
@@ -160,7 +161,7 @@ let DeveloperService = class DeveloperService {
160
161
  }
161
162
  console.log(chalk.green('\nāœ“ All required tools are available!\n'));
162
163
  // Start deployment configuration wizard
163
- const config = await this.runDeploymentWizard();
164
+ const config = await this.runDeploymentWizard(path);
164
165
  if (!config) {
165
166
  console.log(chalk.yellow('\nDeployment configuration cancelled.\n'));
166
167
  return;
@@ -547,17 +548,307 @@ let DeveloperService = class DeveloperService {
547
548
  return [];
548
549
  }
549
550
  }
550
- async runDeploymentWizard() {
551
+ extractContainerPort(ports) {
552
+ for (const portEntry of ports || []) {
553
+ if (typeof portEntry === 'number' && portEntry > 0) {
554
+ return portEntry;
555
+ }
556
+ if (typeof portEntry === 'string') {
557
+ const segment = portEntry.includes(':')
558
+ ? portEntry.split(':').pop() || ''
559
+ : portEntry;
560
+ const normalized = segment.split('/')[0];
561
+ const parsed = Number.parseInt(normalized, 10);
562
+ if (Number.isInteger(parsed) && parsed > 0) {
563
+ return parsed;
564
+ }
565
+ }
566
+ if (portEntry && typeof portEntry === 'object') {
567
+ const target = Number.parseInt(String(portEntry.target ?? ''), 10);
568
+ if (Number.isInteger(target) && target > 0) {
569
+ return target;
570
+ }
571
+ }
572
+ }
573
+ return undefined;
574
+ }
575
+ getDefaultAppPort(app) {
576
+ if (app === 'api')
577
+ return 3000;
578
+ if (app === 'admin')
579
+ return 80;
580
+ return 80;
581
+ }
582
+ extractDockerfileExposedPort(dockerfileContent) {
583
+ const exposeRegex = /^\s*EXPOSE\s+([^\r\n#]+)$/gim;
584
+ let match;
585
+ while ((match = exposeRegex.exec(dockerfileContent)) !== null) {
586
+ const declaration = (match[1] || '').trim();
587
+ if (!declaration)
588
+ continue;
589
+ const tokens = declaration.split(/\s+/);
590
+ for (const token of tokens) {
591
+ const normalized = token.split('/')[0];
592
+ const parsed = Number.parseInt(normalized, 10);
593
+ if (Number.isInteger(parsed) && parsed > 0) {
594
+ return parsed;
595
+ }
596
+ }
597
+ }
598
+ return undefined;
599
+ }
600
+ async getAppPortsFromDockerfiles(projectPath, apps) {
601
+ const appPorts = {};
602
+ for (const app of apps) {
603
+ const dockerfilePath = pathModule.join(projectPath, 'apps', app, 'Dockerfile');
604
+ if (!(0, fs_1.existsSync)(dockerfilePath)) {
605
+ appPorts[app] = this.getDefaultAppPort(app);
606
+ continue;
607
+ }
608
+ try {
609
+ const dockerfileContent = await (0, promises_1.readFile)(dockerfilePath, 'utf8');
610
+ appPorts[app] =
611
+ this.extractDockerfileExposedPort(dockerfileContent) ||
612
+ this.getDefaultAppPort(app);
613
+ }
614
+ catch {
615
+ appPorts[app] = this.getDefaultAppPort(app);
616
+ }
617
+ }
618
+ return appPorts;
619
+ }
620
+ async getDockerComposeServices(projectPath) {
621
+ const candidates = ['docker-compose.yaml', 'docker-compose.yml'];
622
+ for (const fileName of candidates) {
623
+ const filePath = pathModule.join(projectPath, fileName);
624
+ if (!(0, fs_1.existsSync)(filePath)) {
625
+ continue;
626
+ }
627
+ try {
628
+ const content = await (0, promises_1.readFile)(filePath, 'utf8');
629
+ const parsed = (0, yaml_1.parse)(content);
630
+ const servicesObject = parsed?.services;
631
+ if (!servicesObject || typeof servicesObject !== 'object') {
632
+ return { fileName, services: [] };
633
+ }
634
+ const services = Object.entries(servicesObject)
635
+ .map(([name, serviceConfig]) => {
636
+ const image = typeof serviceConfig?.image === 'string'
637
+ ? serviceConfig.image.trim()
638
+ : '';
639
+ if (!image) {
640
+ return null;
641
+ }
642
+ const containerPort = this.extractContainerPort(Array.isArray(serviceConfig?.ports) ? serviceConfig.ports : []);
643
+ return {
644
+ name,
645
+ image,
646
+ containerPort,
647
+ };
648
+ })
649
+ .filter((service) => service != null);
650
+ return { fileName, services };
651
+ }
652
+ catch (error) {
653
+ this.log(chalk.yellow(`Could not parse ${fileName}. Skipping compose services detection.`));
654
+ return null;
655
+ }
656
+ }
657
+ return null;
658
+ }
659
+ async getAvailableApps(projectPath) {
660
+ const appsDir = pathModule.join(projectPath, 'apps');
661
+ const fallbackApps = ['api', 'admin'];
662
+ if (!(0, fs_1.existsSync)(appsDir)) {
663
+ return fallbackApps;
664
+ }
665
+ try {
666
+ const entries = await (0, promises_1.readdir)(appsDir, { withFileTypes: true });
667
+ const detectedApps = entries
668
+ .filter((entry) => entry.isDirectory())
669
+ .map((entry) => entry.name)
670
+ .filter((name) => /^[a-z0-9][a-z0-9-]*$/i.test(name));
671
+ if (detectedApps.length === 0) {
672
+ return fallbackApps;
673
+ }
674
+ const preferred = ['api', 'admin'];
675
+ return [
676
+ ...preferred.filter((app) => detectedApps.includes(app)),
677
+ ...detectedApps
678
+ .filter((app) => !preferred.includes(app))
679
+ .sort((a, b) => a.localeCompare(b)),
680
+ ];
681
+ }
682
+ catch {
683
+ return fallbackApps;
684
+ }
685
+ }
686
+ async detectAppFramework(projectPath, app) {
687
+ const packageJsonPath = pathModule.join(projectPath, 'apps', app, 'package.json');
688
+ try {
689
+ if (!(0, fs_1.existsSync)(packageJsonPath)) {
690
+ if (app === 'api')
691
+ return 'nestjs';
692
+ if (app === 'admin')
693
+ return 'nextjs';
694
+ return 'node';
695
+ }
696
+ const content = await (0, promises_1.readFile)(packageJsonPath, 'utf8');
697
+ const pkg = JSON.parse(content);
698
+ const deps = {
699
+ ...(pkg?.dependencies || {}),
700
+ ...(pkg?.devDependencies || {}),
701
+ };
702
+ const scripts = pkg?.scripts || {};
703
+ const scriptText = Object.values(scripts).join(' ').toLowerCase();
704
+ if (deps.next)
705
+ return 'nextjs';
706
+ if (deps['@nestjs/core'] || deps['@nestjs/common'])
707
+ return 'nestjs';
708
+ if (deps['@angular/core'])
709
+ return 'angular';
710
+ if (deps.vite || scriptText.includes(' vite'))
711
+ return 'vitejs';
712
+ if (deps.vue)
713
+ return 'vue';
714
+ if (app === 'api')
715
+ return 'nestjs';
716
+ if (app === 'admin')
717
+ return 'nextjs';
718
+ return 'node';
719
+ }
720
+ catch {
721
+ if (app === 'api')
722
+ return 'nestjs';
723
+ if (app === 'admin')
724
+ return 'nextjs';
725
+ return 'node';
726
+ }
727
+ }
728
+ getInstallCommand() {
729
+ return [
730
+ 'if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; ',
731
+ 'elif [ -f yarn.lock ]; then yarn install --frozen-lockfile; ',
732
+ 'elif [ -f package-lock.json ]; then npm ci; ',
733
+ 'else npm install; fi',
734
+ ].join('');
735
+ }
736
+ getBuildCommand(app) {
737
+ return [
738
+ `if [ -f pnpm-lock.yaml ]; then pnpm --filter ${app} build; `,
739
+ `elif [ -f yarn.lock ]; then yarn workspace ${app} build; `,
740
+ `else npm run build --workspace=${app}; fi`,
741
+ ].join('');
742
+ }
743
+ getStartCommand(app) {
744
+ return [
745
+ `if [ -f pnpm-lock.yaml ]; then pnpm --filter ${app} start; `,
746
+ `elif [ -f yarn.lock ]; then yarn workspace ${app} start; `,
747
+ `else npm run start --workspace=${app}; fi`,
748
+ ].join('');
749
+ }
750
+ generateBasicAppDockerfile(app, framework) {
751
+ const installCmd = this.getInstallCommand();
752
+ const buildCmd = this.getBuildCommand(app);
753
+ const startCmd = this.getStartCommand(app);
754
+ if (framework === 'nestjs') {
755
+ return `FROM node:20-alpine AS builder
756
+ WORKDIR /app
757
+ RUN corepack enable
758
+ COPY . .
759
+ RUN ${installCmd}
760
+ RUN ${buildCmd}
761
+
762
+ FROM node:20-alpine
763
+ WORKDIR /app
764
+ RUN corepack enable
765
+ COPY . .
766
+ RUN ${installCmd}
767
+ COPY --from=builder /app/apps/${app}/dist ./apps/${app}/dist
768
+ ENV NODE_ENV=production
769
+ ENV PORT=3000
770
+ EXPOSE 3000
771
+ CMD ["sh", "-c", "${startCmd}"]
772
+ `;
773
+ }
774
+ if (framework === 'nextjs') {
775
+ return `FROM node:20-alpine AS builder
776
+ WORKDIR /app
777
+ RUN corepack enable
778
+ COPY . .
779
+ RUN ${installCmd}
780
+ RUN ${buildCmd}
781
+
782
+ FROM node:20-alpine
783
+ WORKDIR /app
784
+ RUN corepack enable
785
+ COPY . .
786
+ RUN ${installCmd}
787
+ ENV NODE_ENV=production
788
+ ENV PORT=3000
789
+ EXPOSE 3000
790
+ CMD ["sh", "-c", "${startCmd}"]
791
+ `;
792
+ }
793
+ if (framework === 'vitejs' ||
794
+ framework === 'angular' ||
795
+ framework === 'vue') {
796
+ return `FROM node:20-alpine AS builder
797
+ WORKDIR /app
798
+ RUN corepack enable
799
+ COPY . .
800
+ RUN ${installCmd}
801
+ RUN ${buildCmd}
802
+
803
+ FROM node:20-alpine
804
+ WORKDIR /app
805
+ RUN npm install -g serve
806
+ COPY --from=builder /app/apps/${app} ./apps/${app}
807
+ COPY --from=builder /app/dist ./dist
808
+ ENV NODE_ENV=production
809
+ EXPOSE 80
810
+ CMD ["sh", "-c", "DIR=apps/${app}/dist; [ -d dist/${app}/browser ] && DIR=dist/${app}/browser; [ -d dist/${app} ] && DIR=dist/${app}; serve -s $DIR -l 80"]
811
+ `;
812
+ }
813
+ return `FROM node:20-alpine
814
+ WORKDIR /app
815
+ RUN corepack enable
816
+ COPY . .
817
+ RUN ${installCmd}
818
+ RUN ${buildCmd}
819
+ ENV NODE_ENV=production
820
+ EXPOSE 3000
821
+ CMD ["sh", "-c", "${startCmd}"]
822
+ `;
823
+ }
824
+ async ensureAppDockerfiles(projectPath, apps) {
825
+ const createdFiles = [];
826
+ for (const app of apps) {
827
+ const dockerfilePath = pathModule.join(projectPath, 'apps', app, 'Dockerfile');
828
+ if ((0, fs_1.existsSync)(dockerfilePath)) {
829
+ continue;
830
+ }
831
+ const framework = await this.detectAppFramework(projectPath, app);
832
+ const dockerfile = this.generateBasicAppDockerfile(app, framework);
833
+ await (0, promises_1.writeFile)(dockerfilePath, dockerfile, 'utf8');
834
+ createdFiles.push(pathModule.join('apps', app, 'Dockerfile'));
835
+ this.log(chalk.green(`Created basic Dockerfile for app ${app} (detected: ${framework})`));
836
+ }
837
+ return createdFiles;
838
+ }
839
+ async runDeploymentWizard(path) {
551
840
  console.log(chalk.blue.bold('\nšŸš€ Deployment Configuration Wizard\n'));
552
841
  console.log(chalk.gray('This wizard will help you set up CI/CD for your project.\n'));
553
842
  // Gather context information
554
843
  const spinner = ora('Gathering environment information...').start();
555
- const gitRepoName = await this.getGitRepoName(process.cwd());
556
- const currentDir = pathModule.basename(process.cwd());
844
+ const gitRepoName = await this.getGitRepoName(path);
845
+ const currentDir = pathModule.basename(path);
557
846
  const defaultAppName = gitRepoName || currentDir;
558
847
  const currentContext = await this.getCurrentKubeContext();
559
848
  const availableNamespaces = await this.getKubeNamespaces();
560
849
  const availableClusters = await this.getKubeClusters();
850
+ const composeData = await this.getDockerComposeServices(path);
851
+ const availableApps = await this.getAvailableApps(path);
561
852
  spinner.stop();
562
853
  if (currentContext) {
563
854
  console.log(chalk.gray(`Current kubectl context: ${chalk.cyan(currentContext)}\n`));
@@ -565,6 +856,14 @@ let DeveloperService = class DeveloperService {
565
856
  if (availableNamespaces.length > 0) {
566
857
  console.log(chalk.gray(`Available namespaces: ${chalk.cyan(availableNamespaces.join(', '))}\n`));
567
858
  }
859
+ if (composeData) {
860
+ if (composeData.services.length > 0) {
861
+ console.log(chalk.gray(`Detected services in ${composeData.fileName}: ${chalk.cyan(composeData.services.map((s) => s.name).join(', '))}\n`));
862
+ }
863
+ else {
864
+ console.log(chalk.gray(`Detected ${composeData.fileName}, but no image-based services were found.\n`));
865
+ }
866
+ }
568
867
  const answers = await inquirer_1.default.prompt([
569
868
  {
570
869
  type: 'list',
@@ -586,6 +885,23 @@ let DeveloperService = class DeveloperService {
586
885
  ],
587
886
  default: 'github-actions',
588
887
  },
888
+ {
889
+ type: 'input',
890
+ name: 'deployBranch',
891
+ message: 'Which branch should trigger automatic deployment?',
892
+ default: 'production',
893
+ validate: (input) => {
894
+ const branch = String(input || '').trim();
895
+ if (!branch) {
896
+ return 'Branch name is required';
897
+ }
898
+ if (/\s/.test(branch)) {
899
+ return 'Branch name cannot contain spaces';
900
+ }
901
+ return true;
902
+ },
903
+ when: (answers) => answers.cicd === 'github-actions',
904
+ },
589
905
  {
590
906
  type: 'list',
591
907
  name: 'clusterSelection',
@@ -682,10 +998,27 @@ let DeveloperService = class DeveloperService {
682
998
  type: 'checkbox',
683
999
  name: 'apps',
684
1000
  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
- ],
1001
+ choices: availableApps.map((app) => ({
1002
+ name: app === 'api'
1003
+ ? 'API (Backend)'
1004
+ : app === 'admin'
1005
+ ? 'Admin (Frontend)'
1006
+ : `${app} (Detected app)`,
1007
+ value: app,
1008
+ checked: app === 'api' || app === 'admin',
1009
+ })),
1010
+ validate: (selected) => (selected || []).length > 0 || 'Select at least one app',
1011
+ },
1012
+ {
1013
+ type: 'checkbox',
1014
+ name: 'selectedInfraServices',
1015
+ message: 'Select additional docker-compose services to provision via Helm charts:',
1016
+ choices: (composeData?.services || []).map((service) => ({
1017
+ name: `${service.name} (${service.image})`,
1018
+ value: service.name,
1019
+ checked: true,
1020
+ })),
1021
+ when: () => (composeData?.services || []).length > 0,
689
1022
  },
690
1023
  {
691
1024
  type: 'confirm',
@@ -728,9 +1061,41 @@ let DeveloperService = class DeveloperService {
728
1061
  }
729
1062
  // Store if namespace needs to be created
730
1063
  const createNamespace = answers.namespaceSelection === 'new';
1064
+ const parseReplicaCount = (value) => {
1065
+ const parsed = Number.parseInt(String(value), 10);
1066
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 2;
1067
+ };
1068
+ const replicaAnswers = await inquirer_1.default.prompt((answers.apps || []).map((app) => ({
1069
+ type: 'input',
1070
+ name: `replicas_${app}`,
1071
+ message: app === 'api'
1072
+ ? 'How many replicas should API (Backend) have?'
1073
+ : app === 'admin'
1074
+ ? 'How many replicas should Admin (Frontend) have?'
1075
+ : `How many replicas should ${app} have?`,
1076
+ default: 2,
1077
+ validate: (input) => {
1078
+ const value = Number.parseInt(String(input), 10);
1079
+ if (!Number.isInteger(value) || value < 1) {
1080
+ return 'Please enter a whole number greater than or equal to 1';
1081
+ }
1082
+ return true;
1083
+ },
1084
+ })));
1085
+ const appReplicas = {};
1086
+ (answers.apps || []).forEach((app) => {
1087
+ appReplicas[app] = parseReplicaCount(replicaAnswers[`replicas_${app}`]);
1088
+ });
1089
+ const selectedInfraServices = new Set((answers.selectedInfraServices || []));
1090
+ const infraServices = (composeData?.services || []).filter((service) => selectedInfraServices.has(service.name));
1091
+ const appPorts = await this.getAppPortsFromDockerfiles(path, answers.apps || []);
731
1092
  return {
732
1093
  ...answers,
733
1094
  createNamespace,
1095
+ appReplicas,
1096
+ appPorts,
1097
+ infraServices,
1098
+ composeFileName: composeData?.fileName,
734
1099
  };
735
1100
  }
736
1101
  async generateDeploymentFiles(path, config) {
@@ -740,12 +1105,14 @@ let DeveloperService = class DeveloperService {
740
1105
  // Create k8s directory
741
1106
  const k8sDir = pathModule.join(path, 'k8s');
742
1107
  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
1108
  // Generate .dockerignore in root
748
1109
  await this.generateDockerignore(path);
1110
+ // Create basic app Dockerfiles when missing
1111
+ const createdDockerfiles = await this.ensureAppDockerfiles(path, config.apps || []);
1112
+ if (createdDockerfiles.length > 0) {
1113
+ config.createdDockerfiles = createdDockerfiles;
1114
+ config.appPorts = await this.getAppPortsFromDockerfiles(path, config.apps || []);
1115
+ }
749
1116
  // Generate GitHub Actions workflow
750
1117
  await this.generateGitHubActionsWorkflow(workflowsDir, config);
751
1118
  // Generate Kubernetes manifests for each app
@@ -760,93 +1127,62 @@ let DeveloperService = class DeveloperService {
760
1127
  if (config.setupSSL) {
761
1128
  await this.generateCertManagerIssuer(k8sDir, config);
762
1129
  }
763
- // Generate Helm chart configuration (optional)
764
- await this.generateHelmChart(path, config);
1130
+ // Generate optional Helm charts for detected docker-compose services
1131
+ await this.generateComposeServiceHelmCharts(path, config);
765
1132
  // Generate README with deployment instructions
766
1133
  await this.generateDeploymentReadme(path, config);
767
1134
  }
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
- }
1135
+ getDefaultServicePort(serviceName) {
1136
+ const name = serviceName.toLowerCase();
1137
+ if (name.includes('postgres'))
1138
+ return 5432;
1139
+ if (name.includes('mysql'))
1140
+ return 3306;
1141
+ if (name.includes('redis'))
1142
+ return 6379;
1143
+ if (name.includes('rabbitmq'))
1144
+ return 5672;
1145
+ return 80;
1146
+ }
1147
+ normalizeYamlContent(content, fileLabel) {
776
1148
  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}`));
1149
+ const parsed = (0, yaml_1.parse)(content);
1150
+ return (0, yaml_1.stringify)(parsed, { lineWidth: 0 });
781
1151
  }
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}`));
1152
+ catch {
1153
+ this.log(chalk.yellow(`Could not normalize YAML for ${fileLabel}; using rendered content as-is.`));
1154
+ return content;
788
1155
  }
789
1156
  }
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
- `;
1157
+ async generateComposeServiceHelmCharts(path, config) {
1158
+ const infraServices = (config.infraServices || []);
1159
+ if (infraServices.length === 0) {
1160
+ return;
814
1161
  }
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
- `;
1162
+ const servicesRoot = pathModule.join(path, 'helm', 'services');
1163
+ await (0, promises_1.mkdir)(servicesRoot, { recursive: true });
1164
+ for (const service of infraServices) {
1165
+ const chartDir = pathModule.join(servicesRoot, service.name);
1166
+ const templatesDir = pathModule.join(chartDir, 'templates');
1167
+ await (0, promises_1.mkdir)(templatesDir, { recursive: true });
1168
+ const servicePort = service.containerPort || this.getDefaultServicePort(service.name);
1169
+ const chartTemplatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', 'helm.service.chart.yaml.ejs');
1170
+ const valuesTemplatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', 'helm.service.values.yaml.ejs');
1171
+ const deploymentTemplatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', 'helm.service.deployment.yaml.ejs');
1172
+ const serviceTemplatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', 'helm.service.service.yaml.ejs');
1173
+ const chartYaml = await (0, ejs_1.render)(await (0, promises_1.readFile)(chartTemplatePath, 'utf8'), {
1174
+ config,
1175
+ service,
1176
+ servicePort,
1177
+ });
1178
+ const valuesYaml = await (0, ejs_1.render)(await (0, promises_1.readFile)(valuesTemplatePath, 'utf8'), { config, service, servicePort });
1179
+ const deploymentTemplate = await (0, ejs_1.render)(await (0, promises_1.readFile)(deploymentTemplatePath, 'utf8'), { config, service, servicePort });
1180
+ const serviceTemplate = await (0, ejs_1.render)(await (0, promises_1.readFile)(serviceTemplatePath, 'utf8'), { config, service, servicePort });
1181
+ await (0, promises_1.writeFile)(pathModule.join(chartDir, 'Chart.yaml'), this.normalizeYamlContent(chartYaml, `helm/services/${service.name}/Chart.yaml`), 'utf8');
1182
+ await (0, promises_1.writeFile)(pathModule.join(chartDir, 'values.yaml'), this.normalizeYamlContent(valuesYaml, `helm/services/${service.name}/values.yaml`), 'utf8');
1183
+ await (0, promises_1.writeFile)(pathModule.join(templatesDir, 'deployment.yaml'), deploymentTemplate, 'utf8');
1184
+ await (0, promises_1.writeFile)(pathModule.join(templatesDir, 'service.yaml'), serviceTemplate, 'utf8');
839
1185
  }
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
1186
  }
851
1187
  async generateDockerignore(path) {
852
1188
  const dockerignorePath = pathModule.join(path, '.dockerignore');
@@ -888,59 +1224,9 @@ temp
888
1224
  }
889
1225
  }
890
1226
  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
- `;
1227
+ const templatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', 'workflow.deploy.yml.ejs');
1228
+ const templateContent = await (0, promises_1.readFile)(templatePath, 'utf8');
1229
+ const workflowContent = await (0, ejs_1.render)(templateContent, { config });
944
1230
  const workflowPath = pathModule.join(dir, 'deploy.yml');
945
1231
  await (0, promises_1.writeFile)(workflowPath, workflowContent, 'utf8');
946
1232
  this.log(chalk.green(`Created GitHub Actions workflow: ${workflowPath}`));
@@ -948,430 +1234,34 @@ ${config.apps
948
1234
  async generateKubernetesManifests(dir, app, config) {
949
1235
  const appDir = pathModule.join(dir, app);
950
1236
  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');
1237
+ const deploymentTemplatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', 'k8s.deployment.yaml.ejs');
1238
+ const deploymentTemplate = await (0, promises_1.readFile)(deploymentTemplatePath, 'utf8');
1239
+ const deploymentContent = await (0, ejs_1.render)(deploymentTemplate, { config, app });
1240
+ await (0, promises_1.writeFile)(pathModule.join(appDir, 'deployment.yaml'), this.normalizeYamlContent(deploymentContent, `k8s/${app}/deployment.yaml`), 'utf8');
1241
+ const serviceTemplatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', 'k8s.service.yaml.ejs');
1242
+ const serviceTemplate = await (0, promises_1.readFile)(serviceTemplatePath, 'utf8');
1243
+ const serviceContent = await (0, ejs_1.render)(serviceTemplate, { config, app });
1244
+ await (0, promises_1.writeFile)(pathModule.join(appDir, 'service.yaml'), this.normalizeYamlContent(serviceContent, `k8s/${app}/service.yaml`), 'utf8');
1017
1245
  this.log(chalk.green(`Created Kubernetes manifests for ${app}`));
1018
1246
  }
1019
1247
  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');
1248
+ const templatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', 'k8s.ingress.yaml.ejs');
1249
+ const templateContent = await (0, promises_1.readFile)(templatePath, 'utf8');
1250
+ const ingressContent = await (0, ejs_1.render)(templateContent, { config });
1251
+ await (0, promises_1.writeFile)(pathModule.join(dir, 'ingress.yaml'), this.normalizeYamlContent(ingressContent, 'k8s/ingress.yaml'), 'utf8');
1063
1252
  this.log(chalk.green('Created Ingress manifest'));
1064
1253
  }
1065
1254
  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');
1255
+ const templatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', 'k8s.cert-manager-issuer.yaml.ejs');
1256
+ const templateContent = await (0, promises_1.readFile)(templatePath, 'utf8');
1257
+ const issuerContent = await (0, ejs_1.render)(templateContent, { config });
1258
+ await (0, promises_1.writeFile)(pathModule.join(dir, 'cert-manager-issuer.yaml'), this.normalizeYamlContent(issuerContent, 'k8s/cert-manager-issuer.yaml'), 'utf8');
1082
1259
  this.log(chalk.green('Created cert-manager ClusterIssuer'));
1083
1260
  }
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
1261
  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
- `;
1262
+ const templatePath = pathModule.join(__dirname, '..', '..', 'templates', 'deployment', 'DEPLOYMENT.md.ejs');
1263
+ const templateContent = await (0, promises_1.readFile)(templatePath, 'utf8');
1264
+ const readmeContent = await (0, ejs_1.render)(templateContent, { config });
1375
1265
  await (0, promises_1.writeFile)(pathModule.join(path, 'DEPLOYMENT.md'), readmeContent, 'utf8');
1376
1266
  this.log(chalk.green('Created DEPLOYMENT.md'));
1377
1267
  }
@@ -1379,6 +1269,10 @@ ${config.setupSSL
1379
1269
  console.log(chalk.blue.bold('\nšŸ“‹ Deployment Configuration Summary\n'));
1380
1270
  console.log(chalk.white('Provider: ') + chalk.cyan(config.provider));
1381
1271
  console.log(chalk.white('CI/CD: ') + chalk.cyan(config.cicd));
1272
+ if (config.cicd === 'github-actions') {
1273
+ console.log(chalk.white('Deploy Branch: ') +
1274
+ chalk.cyan(config.deployBranch || 'production'));
1275
+ }
1382
1276
  console.log(chalk.white('Cluster: ') + chalk.cyan(config.clusterName));
1383
1277
  console.log(chalk.white('Namespace: ') +
1384
1278
  chalk.cyan(config.namespace) +
@@ -1389,34 +1283,55 @@ ${config.setupSSL
1389
1283
  console.log(chalk.white('Domain: ') + chalk.cyan(config.domain));
1390
1284
  }
1391
1285
  console.log(chalk.white('Apps: ') + chalk.cyan(config.apps.join(', ')));
1286
+ console.log(chalk.white('Replicas: ') +
1287
+ chalk.cyan(config.apps
1288
+ .map((app) => `${app}: ${config.appReplicas?.[app] ?? 2}`)
1289
+ .join(', ')));
1290
+ console.log(chalk.white('Ports: ') +
1291
+ chalk.cyan(config.apps
1292
+ .map((app) => `${app}: ${config.appPorts?.[app] ?? this.getDefaultAppPort(app)}`)
1293
+ .join(', ')));
1392
1294
  console.log(chalk.white('Ingress: ') +
1393
1295
  chalk.cyan(config.setupIngress ? 'Yes' : 'No'));
1394
1296
  console.log(chalk.white('SSL/TLS: ') +
1395
1297
  chalk.cyan(config.setupSSL ? 'Yes' : 'No'));
1396
1298
  console.log(chalk.green.bold('\nāœ“ Generated Files:\n'));
1397
1299
  console.log(chalk.gray(' .dockerignore'));
1398
- config.apps.forEach((app) => {
1399
- console.log(chalk.gray(` apps/${app}/Dockerfile`));
1400
- });
1401
1300
  console.log(chalk.gray(' .github/workflows/deploy.yml'));
1402
1301
  config.apps.forEach((app) => {
1403
1302
  console.log(chalk.gray(` k8s/${app}/deployment.yaml`));
1404
1303
  console.log(chalk.gray(` k8s/${app}/service.yaml`));
1405
1304
  });
1305
+ (config.createdDockerfiles || []).forEach((dockerfilePath) => {
1306
+ console.log(chalk.gray(` ${dockerfilePath}`));
1307
+ });
1406
1308
  if (config.setupIngress) {
1407
1309
  console.log(chalk.gray(' k8s/ingress.yaml'));
1408
1310
  }
1409
1311
  if (config.setupSSL) {
1410
1312
  console.log(chalk.gray(' k8s/cert-manager-issuer.yaml'));
1411
1313
  }
1412
- console.log(chalk.gray(` helm/${config.appName}/Chart.yaml`));
1413
- console.log(chalk.gray(` helm/${config.appName}/values.yaml`));
1314
+ if ((config.infraServices || []).length > 0) {
1315
+ config.infraServices.forEach((service) => {
1316
+ console.log(chalk.gray(` helm/services/${service.name}/Chart.yaml`));
1317
+ console.log(chalk.gray(` helm/services/${service.name}/values.yaml`));
1318
+ console.log(chalk.gray(` helm/services/${service.name}/templates/deployment.yaml`));
1319
+ console.log(chalk.gray(` helm/services/${service.name}/templates/service.yaml`));
1320
+ });
1321
+ }
1414
1322
  console.log(chalk.gray(' DEPLOYMENT.md'));
1415
1323
  console.log(chalk.blue.bold('\nšŸ“– Next Steps:\n'));
1416
1324
  console.log(chalk.white('1. Review the generated files in your project'));
1417
1325
  console.log(chalk.white('2. Read DEPLOYMENT.md for setup instructions'));
1418
1326
  console.log(chalk.white('3. Configure GitHub secrets (see DEPLOYMENT.md)'));
1419
- console.log(chalk.white('4. Push to main branch to trigger deployment'));
1327
+ console.log(chalk.white(`4. Push to ${config.deployBranch || 'production'} branch to trigger deployment`));
1328
+ if ((config.infraServices || []).length > 0) {
1329
+ console.log('');
1330
+ console.log(chalk.blue.bold('ℹ Additional service charts were generated only (not applied):'));
1331
+ config.infraServices.forEach((service) => {
1332
+ console.log(chalk.white(` helm upgrade --install ${service.name} ./helm/services/${service.name} --namespace ${config.namespace} --create-namespace`));
1333
+ });
1334
+ }
1420
1335
  console.log('');
1421
1336
  }
1422
1337
  async syncPublish(path, verbose = false) {