@expressots/cli 3.0.0 → 4.0.0-preview.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +41 -95
  2. package/bin/cicd/cli.d.ts +6 -0
  3. package/bin/cicd/cli.js +128 -0
  4. package/bin/cicd/form.d.ts +29 -0
  5. package/bin/cicd/form.js +346 -0
  6. package/bin/cicd/generators/azure-devops.d.ts +2 -0
  7. package/bin/cicd/generators/azure-devops.js +370 -0
  8. package/bin/cicd/generators/bitbucket.d.ts +2 -0
  9. package/bin/cicd/generators/bitbucket.js +217 -0
  10. package/bin/cicd/generators/circleci.d.ts +2 -0
  11. package/bin/cicd/generators/circleci.js +274 -0
  12. package/bin/cicd/generators/github-actions.d.ts +14 -0
  13. package/bin/cicd/generators/github-actions.js +426 -0
  14. package/bin/cicd/generators/gitlab-ci.d.ts +2 -0
  15. package/bin/cicd/generators/gitlab-ci.js +237 -0
  16. package/bin/cicd/generators/index.d.ts +6 -0
  17. package/bin/cicd/generators/index.js +15 -0
  18. package/bin/cicd/generators/jenkins.d.ts +2 -0
  19. package/bin/cicd/generators/jenkins.js +248 -0
  20. package/bin/cicd/generators/template-loader.d.ts +17 -0
  21. package/bin/cicd/generators/template-loader.js +128 -0
  22. package/bin/cicd/index.d.ts +1 -0
  23. package/bin/cicd/index.js +5 -0
  24. package/bin/cli.d.ts +1 -5
  25. package/bin/cli.js +72 -7
  26. package/bin/commands/project.commands.d.ts +19 -6
  27. package/bin/commands/project.commands.js +602 -66
  28. package/bin/config/index.d.ts +5 -0
  29. package/bin/config/index.js +10 -0
  30. package/bin/config/manager.d.ts +98 -0
  31. package/bin/config/manager.js +222 -0
  32. package/bin/containerize/analyzers/bootstrap-analyzer.d.ts +46 -0
  33. package/bin/containerize/analyzers/bootstrap-analyzer.js +187 -0
  34. package/bin/containerize/analyzers/project-analyzer.d.ts +20 -0
  35. package/bin/containerize/analyzers/project-analyzer.js +150 -0
  36. package/bin/containerize/cli.d.ts +4 -0
  37. package/bin/containerize/cli.js +113 -0
  38. package/bin/containerize/form.d.ts +15 -0
  39. package/bin/containerize/form.js +152 -0
  40. package/bin/containerize/generators/ci-generator.d.ts +31 -0
  41. package/bin/containerize/generators/ci-generator.js +940 -0
  42. package/bin/containerize/generators/docker-compose-generator.d.ts +8 -0
  43. package/bin/containerize/generators/docker-compose-generator.js +187 -0
  44. package/bin/containerize/generators/dockerfile-generator.d.ts +8 -0
  45. package/bin/containerize/generators/dockerfile-generator.js +657 -0
  46. package/bin/containerize/generators/kubernetes-generator.d.ts +8 -0
  47. package/bin/containerize/generators/kubernetes-generator.js +134 -0
  48. package/bin/containerize/generators/template-loader.d.ts +36 -0
  49. package/bin/containerize/generators/template-loader.js +129 -0
  50. package/bin/containerize/index.d.ts +4 -0
  51. package/bin/containerize/index.js +13 -0
  52. package/bin/containerize/presets/preset-registry.d.ts +20 -0
  53. package/bin/containerize/presets/preset-registry.js +102 -0
  54. package/bin/costs/cli.d.ts +5 -0
  55. package/bin/costs/cli.js +185 -0
  56. package/bin/costs/form.d.ts +44 -0
  57. package/bin/costs/form.js +412 -0
  58. package/bin/costs/index.d.ts +4 -0
  59. package/bin/costs/index.js +25 -0
  60. package/bin/costs/pricing-manager.d.ts +84 -0
  61. package/bin/costs/pricing-manager.js +342 -0
  62. package/bin/costs/providers/index.d.ts +32 -0
  63. package/bin/costs/providers/index.js +153 -0
  64. package/bin/costs/sources/api-source.d.ts +10 -0
  65. package/bin/costs/sources/api-source.js +32 -0
  66. package/bin/costs/sources/index.d.ts +6 -0
  67. package/bin/costs/sources/index.js +15 -0
  68. package/bin/costs/sources/local-json-source.d.ts +23 -0
  69. package/bin/costs/sources/local-json-source.js +59 -0
  70. package/bin/costs/sources/remote-json-source.d.ts +11 -0
  71. package/bin/costs/sources/remote-json-source.js +53 -0
  72. package/bin/costs/types.d.ts +53 -0
  73. package/bin/costs/types.js +5 -0
  74. package/bin/dev/cli.d.ts +4 -0
  75. package/bin/dev/cli.js +136 -0
  76. package/bin/dev/form.d.ts +36 -0
  77. package/bin/dev/form.js +254 -0
  78. package/bin/dev/index.d.ts +1 -0
  79. package/bin/dev/index.js +5 -0
  80. package/bin/generate/cli.d.ts +1 -1
  81. package/bin/generate/cli.js +29 -2
  82. package/bin/generate/form.d.ts +5 -1
  83. package/bin/generate/form.js +3 -3
  84. package/bin/generate/templates/nonopinionated/config.tpl +12 -0
  85. package/bin/generate/templates/nonopinionated/event.tpl +10 -0
  86. package/bin/generate/templates/nonopinionated/guard.tpl +18 -0
  87. package/bin/generate/templates/nonopinionated/handler.tpl +12 -0
  88. package/bin/generate/templates/nonopinionated/interceptor.tpl +27 -0
  89. package/bin/generate/templates/opinionated/config.tpl +47 -0
  90. package/bin/generate/templates/opinionated/entity.tpl +1 -8
  91. package/bin/generate/templates/opinionated/event.tpl +15 -0
  92. package/bin/generate/templates/opinionated/guard.tpl +41 -0
  93. package/bin/generate/templates/opinionated/handler.tpl +23 -0
  94. package/bin/generate/templates/opinionated/interceptor.tpl +50 -0
  95. package/bin/generate/utils/command-utils.d.ts +20 -5
  96. package/bin/generate/utils/command-utils.js +145 -48
  97. package/bin/generate/utils/nonopininated-cmd.d.ts +10 -1
  98. package/bin/generate/utils/nonopininated-cmd.js +100 -1
  99. package/bin/generate/utils/opinionated-cmd.d.ts +10 -1
  100. package/bin/generate/utils/opinionated-cmd.js +128 -16
  101. package/bin/generate/utils/string-utils.d.ts +6 -0
  102. package/bin/generate/utils/string-utils.js +13 -1
  103. package/bin/help/cli.d.ts +1 -1
  104. package/bin/help/command-help-registry.d.ts +23 -0
  105. package/bin/help/command-help-registry.js +303 -0
  106. package/bin/help/command-help.d.ts +36 -0
  107. package/bin/help/command-help.js +56 -0
  108. package/bin/help/form.js +127 -22
  109. package/bin/help/main-help.d.ts +8 -0
  110. package/bin/help/main-help.js +126 -0
  111. package/bin/help/render.d.ts +32 -0
  112. package/bin/help/render.js +46 -0
  113. package/bin/info/cli.d.ts +1 -1
  114. package/bin/info/form.d.ts +1 -1
  115. package/bin/info/form.js +11 -11
  116. package/bin/migrate/analyzers/platform-detector.d.ts +14 -0
  117. package/bin/migrate/analyzers/platform-detector.js +116 -0
  118. package/bin/migrate/cli.d.ts +6 -0
  119. package/bin/migrate/cli.js +98 -0
  120. package/bin/migrate/form.d.ts +25 -0
  121. package/bin/migrate/form.js +348 -0
  122. package/bin/migrate/generators/compose-to-k8s.d.ts +2 -0
  123. package/bin/migrate/generators/compose-to-k8s.js +324 -0
  124. package/bin/migrate/generators/compose-to-railway.d.ts +2 -0
  125. package/bin/migrate/generators/compose-to-railway.js +138 -0
  126. package/bin/migrate/generators/compose-to-render.d.ts +2 -0
  127. package/bin/migrate/generators/compose-to-render.js +148 -0
  128. package/bin/migrate/generators/generic-migration.d.ts +9 -0
  129. package/bin/migrate/generators/generic-migration.js +221 -0
  130. package/bin/migrate/generators/heroku-to-fly.d.ts +2 -0
  131. package/bin/migrate/generators/heroku-to-fly.js +291 -0
  132. package/bin/migrate/generators/heroku-to-railway.d.ts +2 -0
  133. package/bin/migrate/generators/heroku-to-railway.js +283 -0
  134. package/bin/migrate/generators/heroku-to-render.d.ts +2 -0
  135. package/bin/migrate/generators/heroku-to-render.js +148 -0
  136. package/bin/migrate/generators/index.d.ts +7 -0
  137. package/bin/migrate/generators/index.js +17 -0
  138. package/bin/migrate/generators/template-loader.d.ts +21 -0
  139. package/bin/migrate/generators/template-loader.js +59 -0
  140. package/bin/migrate/index.d.ts +1 -0
  141. package/bin/migrate/index.js +5 -0
  142. package/bin/new/cli.d.ts +5 -1
  143. package/bin/new/cli.js +77 -14
  144. package/bin/new/form.d.ts +27 -4
  145. package/bin/new/form.js +605 -75
  146. package/bin/profile/analyzers/dockerfile-analyzer.d.ts +27 -0
  147. package/bin/profile/analyzers/dockerfile-analyzer.js +122 -0
  148. package/bin/profile/analyzers/image-analyzer.d.ts +19 -0
  149. package/bin/profile/analyzers/image-analyzer.js +85 -0
  150. package/bin/profile/cli.d.ts +4 -0
  151. package/bin/profile/cli.js +94 -0
  152. package/bin/profile/form.d.ts +56 -0
  153. package/bin/profile/form.js +401 -0
  154. package/bin/profile/index.d.ts +1 -0
  155. package/bin/profile/index.js +5 -0
  156. package/bin/profile/optimizers/index.d.ts +19 -0
  157. package/bin/profile/optimizers/index.js +137 -0
  158. package/bin/providers/add/form.d.ts +1 -1
  159. package/bin/providers/add/form.js +27 -6
  160. package/bin/providers/create/form.js +53 -3
  161. package/bin/scripts/form.js +27 -5
  162. package/bin/studio/cli.d.ts +15 -0
  163. package/bin/studio/cli.js +172 -0
  164. package/bin/studio/index.d.ts +5 -0
  165. package/bin/studio/index.js +9 -0
  166. package/bin/templates/cache.d.ts +54 -0
  167. package/bin/templates/cache.js +180 -0
  168. package/bin/templates/cli.d.ts +8 -0
  169. package/bin/templates/cli.js +294 -0
  170. package/bin/templates/fetcher.d.ts +49 -0
  171. package/bin/templates/fetcher.js +208 -0
  172. package/bin/templates/index.d.ts +11 -0
  173. package/bin/templates/index.js +37 -0
  174. package/bin/templates/manager.d.ts +116 -0
  175. package/bin/templates/manager.js +323 -0
  176. package/bin/templates/renderer.d.ts +49 -0
  177. package/bin/templates/renderer.js +204 -0
  178. package/bin/templates/types.d.ts +51 -0
  179. package/bin/templates/types.js +5 -0
  180. package/bin/utils/add-module-to-container.d.ts +14 -3
  181. package/bin/utils/add-module-to-container.js +327 -98
  182. package/bin/utils/cli-ui.d.ts +49 -3
  183. package/bin/utils/cli-ui.js +133 -13
  184. package/bin/utils/index.d.ts +4 -0
  185. package/bin/utils/index.js +4 -0
  186. package/bin/utils/input-validation.d.ts +50 -0
  187. package/bin/utils/input-validation.js +143 -0
  188. package/bin/utils/package-manager-commands.d.ts +24 -0
  189. package/bin/utils/package-manager-commands.js +50 -0
  190. package/bin/utils/safe-spawn.d.ts +35 -0
  191. package/bin/utils/safe-spawn.js +51 -0
  192. package/bin/utils/update-tsconfig-paths.d.ts +35 -0
  193. package/bin/utils/update-tsconfig-paths.js +326 -0
  194. package/package.json +165 -156
@@ -0,0 +1,401 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.showProfileReport = exports.optimizeContainer = exports.profileImage = exports.profileContainer = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const dockerfile_analyzer_1 = require("./analyzers/dockerfile-analyzer");
11
+ const image_analyzer_1 = require("./analyzers/image-analyzer");
12
+ const optimizers_1 = require("./optimizers");
13
+ const cli_ui_1 = require("../utils/cli-ui");
14
+ /**
15
+ * Profile a Dockerfile for issues and recommendations
16
+ */
17
+ async function profileContainer(options) {
18
+ (0, cli_ui_1.printSection)("🔍 ExpressoTS Container Profiler");
19
+ const cwd = process.cwd();
20
+ const dockerfilePath = path_1.default.join(cwd, options.dockerfile);
21
+ if (!fs_1.default.existsSync(dockerfilePath)) {
22
+ console.log(chalk_1.default.red(`Error: Dockerfile not found at ${dockerfilePath}`));
23
+ console.log(chalk_1.default.gray("Run 'expressots containerize' to generate a Dockerfile first."));
24
+ return;
25
+ }
26
+ console.log(chalk_1.default.yellow(`📄 Analyzing ${options.dockerfile}...\n`));
27
+ const analysis = await (0, dockerfile_analyzer_1.analyzeDockerfile)(dockerfilePath);
28
+ const result = generateProfileResult(analysis, options);
29
+ outputResult(result, options);
30
+ }
31
+ exports.profileContainer = profileContainer;
32
+ /**
33
+ * Profile a built Docker image
34
+ */
35
+ async function profileImage(options) {
36
+ (0, cli_ui_1.printSection)("🔍 ExpressoTS Image Profiler");
37
+ if (!options.target) {
38
+ console.log(chalk_1.default.red("Error: Please specify an image name."));
39
+ console.log(chalk_1.default.gray("Usage: expressots profile image <image-name>"));
40
+ return;
41
+ }
42
+ console.log(chalk_1.default.yellow(`🐳 Analyzing image: ${options.target}...\n`));
43
+ try {
44
+ const analysis = await (0, image_analyzer_1.analyzeImage)(options.target);
45
+ console.log(chalk_1.default.bold("Image Analysis:"));
46
+ console.log(` Size: ${analysis.size}`);
47
+ console.log(` Layers: ${analysis.layers}`);
48
+ console.log(` Created: ${analysis.created}`);
49
+ console.log(` OS/Arch: ${analysis.os}/${analysis.architecture}`);
50
+ if (analysis.vulnerabilities && analysis.vulnerabilities.length > 0) {
51
+ console.log(chalk_1.default.bold("\nVulnerabilities:"));
52
+ for (const vuln of analysis.vulnerabilities) {
53
+ const color = vuln.severity === "critical"
54
+ ? chalk_1.default.red
55
+ : vuln.severity === "high"
56
+ ? chalk_1.default.yellow
57
+ : chalk_1.default.gray;
58
+ console.log(` ${color(`[${vuln.severity.toUpperCase()}]`)} ${vuln.id}: ${vuln.description}`);
59
+ }
60
+ }
61
+ else {
62
+ console.log(chalk_1.default.green("\n✓ No vulnerabilities found"));
63
+ }
64
+ }
65
+ catch (error) {
66
+ console.log(chalk_1.default.red(`Error analyzing image: ${error}`));
67
+ console.log(chalk_1.default.gray("Make sure Docker is running and the image exists."));
68
+ }
69
+ }
70
+ exports.profileImage = profileImage;
71
+ /**
72
+ * Generate and optionally apply optimizations
73
+ */
74
+ async function optimizeContainer(options) {
75
+ (0, cli_ui_1.printSection)("⚡ ExpressoTS Container Optimizer");
76
+ const cwd = process.cwd();
77
+ const dockerfilePath = path_1.default.join(cwd, options.dockerfile);
78
+ if (!fs_1.default.existsSync(dockerfilePath)) {
79
+ console.log(chalk_1.default.red(`Error: Dockerfile not found at ${dockerfilePath}`));
80
+ return;
81
+ }
82
+ const analysis = await (0, dockerfile_analyzer_1.analyzeDockerfile)(dockerfilePath);
83
+ const optimizations = (0, optimizers_1.generateOptimizations)(analysis);
84
+ if (optimizations.length === 0) {
85
+ console.log(chalk_1.default.green("✓ No optimizations needed. Your Dockerfile looks great!"));
86
+ return;
87
+ }
88
+ console.log(chalk_1.default.bold(`Found ${optimizations.length} optimization(s):\n`));
89
+ for (const opt of optimizations) {
90
+ const priorityColor = opt.priority === "high"
91
+ ? chalk_1.default.red
92
+ : opt.priority === "medium"
93
+ ? chalk_1.default.yellow
94
+ : chalk_1.default.gray;
95
+ console.log(` ${priorityColor(`[${opt.priority.toUpperCase()}]`)} ${opt.title}`);
96
+ console.log(chalk_1.default.gray(` ${opt.description}`));
97
+ console.log(chalk_1.default.green(` Impact: ${opt.impact}`));
98
+ console.log();
99
+ }
100
+ if (options.autoFix) {
101
+ console.log(chalk_1.default.yellow("🔧 Applying safe optimizations..."));
102
+ const applied = await (0, optimizers_1.applyOptimizations)(dockerfilePath, optimizations);
103
+ console.log(chalk_1.default.green(`✓ Applied ${applied} optimization(s)`));
104
+ }
105
+ else {
106
+ console.log(chalk_1.default.gray("Tip: Use --auto-fix to automatically apply safe optimizations"));
107
+ }
108
+ }
109
+ exports.optimizeContainer = optimizeContainer;
110
+ /**
111
+ * Show a comprehensive profile report
112
+ */
113
+ async function showProfileReport(options) {
114
+ (0, cli_ui_1.printSection)("📊 ExpressoTS Container Profile Report");
115
+ const cwd = process.cwd();
116
+ const dockerfilePath = path_1.default.join(cwd, options.dockerfile);
117
+ if (!fs_1.default.existsSync(dockerfilePath)) {
118
+ console.log(chalk_1.default.red(`Error: Dockerfile not found at ${dockerfilePath}`));
119
+ return;
120
+ }
121
+ const analysis = await (0, dockerfile_analyzer_1.analyzeDockerfile)(dockerfilePath);
122
+ const result = generateProfileResult(analysis, options);
123
+ // Generate report based on format
124
+ if (options.format === "json") {
125
+ const json = JSON.stringify(result, null, 2);
126
+ if (options.output) {
127
+ fs_1.default.writeFileSync(options.output, json, "utf-8");
128
+ console.log(chalk_1.default.green(`✓ Report saved to ${options.output}`));
129
+ }
130
+ else {
131
+ console.log(json);
132
+ }
133
+ }
134
+ else if (options.format === "html") {
135
+ const html = generateHtmlReport(result);
136
+ const outputPath = options.output || "container-report.html";
137
+ fs_1.default.writeFileSync(outputPath, html, "utf-8");
138
+ console.log(chalk_1.default.green(`✓ HTML report saved to ${outputPath}`));
139
+ }
140
+ else {
141
+ outputResult(result, options);
142
+ }
143
+ }
144
+ exports.showProfileReport = showProfileReport;
145
+ /**
146
+ * Generate profile result from analysis
147
+ */
148
+ function generateProfileResult(analysis, options) {
149
+ const issues = [];
150
+ const recommendations = [];
151
+ // Check for security issues
152
+ if (!analysis.hasNonRootUser) {
153
+ issues.push({
154
+ id: "SEC001",
155
+ severity: "high",
156
+ category: "security",
157
+ message: "Container runs as root user",
158
+ fix: "Add 'USER node' or create a non-root user",
159
+ });
160
+ recommendations.push({
161
+ priority: "high",
162
+ category: "Security",
163
+ title: "Add non-root user",
164
+ description: "Running containers as root is a security risk",
165
+ impact: "Reduces attack surface if container is compromised",
166
+ });
167
+ }
168
+ if (!analysis.hasHealthCheck) {
169
+ issues.push({
170
+ id: "PERF001",
171
+ severity: "medium",
172
+ category: "performance",
173
+ message: "No HEALTHCHECK instruction found",
174
+ fix: "Add HEALTHCHECK instruction for orchestration support",
175
+ });
176
+ recommendations.push({
177
+ priority: "medium",
178
+ category: "Performance",
179
+ title: "Add health check",
180
+ description: "Health checks enable better orchestration",
181
+ impact: "Faster failure detection and recovery",
182
+ });
183
+ }
184
+ if (!analysis.hasMultiStage) {
185
+ issues.push({
186
+ id: "SIZE001",
187
+ severity: "medium",
188
+ category: "size",
189
+ message: "Not using multi-stage build",
190
+ fix: "Use multi-stage build to reduce final image size",
191
+ });
192
+ recommendations.push({
193
+ priority: "medium",
194
+ category: "Size",
195
+ title: "Use multi-stage build",
196
+ description: "Multi-stage builds reduce image size significantly",
197
+ impact: "Can reduce image size by 50-80%",
198
+ });
199
+ }
200
+ // Check for best practices
201
+ if (analysis.hasNpmInstallWithoutCi) {
202
+ issues.push({
203
+ id: "BP001",
204
+ severity: "low",
205
+ category: "best-practice",
206
+ message: "Using 'npm install' instead of 'npm ci'",
207
+ fix: "Replace 'npm install' with 'npm ci' for reproducible builds",
208
+ });
209
+ }
210
+ if (!analysis.hasDockerignore) {
211
+ issues.push({
212
+ id: "SIZE002",
213
+ severity: "low",
214
+ category: "size",
215
+ message: "No .dockerignore file found",
216
+ fix: "Create .dockerignore to exclude unnecessary files",
217
+ });
218
+ }
219
+ if (analysis.hasCurlOrWgetWithoutCleanup) {
220
+ issues.push({
221
+ id: "SIZE003",
222
+ severity: "low",
223
+ category: "size",
224
+ message: "Downloaded files may not be cleaned up",
225
+ fix: "Clean up downloaded files in the same RUN layer",
226
+ });
227
+ }
228
+ // Calculate score
229
+ let score = 100;
230
+ for (const issue of issues) {
231
+ switch (issue.severity) {
232
+ case "critical":
233
+ score -= 25;
234
+ break;
235
+ case "high":
236
+ score -= 15;
237
+ break;
238
+ case "medium":
239
+ score -= 10;
240
+ break;
241
+ case "low":
242
+ score -= 5;
243
+ break;
244
+ }
245
+ }
246
+ score = Math.max(0, score);
247
+ return {
248
+ score,
249
+ issues: issues.filter((i) => {
250
+ const severityOrder = { low: 0, medium: 1, high: 2, critical: 3 };
251
+ return severityOrder[i.severity] >= severityOrder[options.severity];
252
+ }),
253
+ recommendations,
254
+ metrics: {
255
+ baseImage: analysis.baseImage,
256
+ layers: analysis.layers,
257
+ hasMultiStage: analysis.hasMultiStage,
258
+ hasHealthCheck: analysis.hasHealthCheck,
259
+ hasNonRootUser: analysis.hasNonRootUser,
260
+ nodeVersion: analysis.nodeVersion,
261
+ },
262
+ };
263
+ }
264
+ /**
265
+ * Output result to console
266
+ */
267
+ function outputResult(result, options) {
268
+ // Score
269
+ const scoreColor = result.score >= 80
270
+ ? chalk_1.default.green
271
+ : result.score >= 60
272
+ ? chalk_1.default.yellow
273
+ : chalk_1.default.red;
274
+ console.log(chalk_1.default.bold(`Container Health Score: ${scoreColor(`${result.score}/100`)}\n`));
275
+ // Metrics
276
+ console.log(chalk_1.default.bold("Metrics:"));
277
+ console.log(` Base Image: ${result.metrics.baseImage || "Unknown"}`);
278
+ console.log(` Layers: ${result.metrics.layers || "Unknown"}`);
279
+ console.log(` Multi-stage: ${result.metrics.hasMultiStage ? chalk_1.default.green("✓") : chalk_1.default.red("✗")}`);
280
+ console.log(` Health Check: ${result.metrics.hasHealthCheck ? chalk_1.default.green("✓") : chalk_1.default.red("✗")}`);
281
+ console.log(` Non-root User: ${result.metrics.hasNonRootUser ? chalk_1.default.green("✓") : chalk_1.default.red("✗")}`);
282
+ // Issues
283
+ if (result.issues.length > 0) {
284
+ console.log(chalk_1.default.bold(`\nIssues (${result.issues.length}):`));
285
+ for (const issue of result.issues) {
286
+ const color = issue.severity === "critical"
287
+ ? chalk_1.default.red
288
+ : issue.severity === "high"
289
+ ? chalk_1.default.yellow
290
+ : issue.severity === "medium"
291
+ ? chalk_1.default.cyan
292
+ : chalk_1.default.gray;
293
+ console.log(` ${color(`[${issue.severity.toUpperCase()}]`)} ${issue.message}`);
294
+ if (issue.fix) {
295
+ console.log(chalk_1.default.gray(` Fix: ${issue.fix}`));
296
+ }
297
+ }
298
+ }
299
+ else {
300
+ console.log(chalk_1.default.green("\n✓ No issues found!"));
301
+ }
302
+ // Recommendations
303
+ if (result.recommendations.length > 0) {
304
+ console.log(chalk_1.default.bold(`\nRecommendations:`));
305
+ for (const rec of result.recommendations) {
306
+ const color = rec.priority === "high" ? chalk_1.default.yellow : chalk_1.default.gray;
307
+ console.log(` ${color(`[${rec.priority.toUpperCase()}]`)} ${rec.title}`);
308
+ console.log(chalk_1.default.gray(` ${rec.description}`));
309
+ }
310
+ }
311
+ console.log();
312
+ }
313
+ /**
314
+ * Generate HTML report
315
+ */
316
+ function generateHtmlReport(result) {
317
+ const scoreColor = result.score >= 80
318
+ ? "#22c55e"
319
+ : result.score >= 60
320
+ ? "#eab308"
321
+ : "#ef4444";
322
+ return `<!DOCTYPE html>
323
+ <html lang="en">
324
+ <head>
325
+ <meta charset="UTF-8">
326
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
327
+ <title>Container Profile Report - ExpressoTS</title>
328
+ <style>
329
+ * { margin: 0; padding: 0; box-sizing: border-box; }
330
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f3f4f6; padding: 2rem; }
331
+ .container { max-width: 800px; margin: 0 auto; }
332
+ .card { background: white; border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
333
+ h1 { color: #111; margin-bottom: 0.5rem; }
334
+ h2 { color: #333; margin-bottom: 1rem; font-size: 1.25rem; }
335
+ .score { font-size: 3rem; font-weight: bold; color: ${scoreColor}; }
336
+ .metric { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #e5e7eb; }
337
+ .issue { padding: 0.75rem; margin: 0.5rem 0; border-radius: 4px; }
338
+ .issue.critical { background: #fef2f2; border-left: 4px solid #ef4444; }
339
+ .issue.high { background: #fffbeb; border-left: 4px solid #f59e0b; }
340
+ .issue.medium { background: #ecfeff; border-left: 4px solid #06b6d4; }
341
+ .issue.low { background: #f9fafb; border-left: 4px solid #9ca3af; }
342
+ .check { color: #22c55e; }
343
+ .cross { color: #ef4444; }
344
+ .footer { text-align: center; color: #6b7280; margin-top: 2rem; font-size: 0.875rem; }
345
+ </style>
346
+ </head>
347
+ <body>
348
+ <div class="container">
349
+ <div class="card">
350
+ <h1>🐳 Container Profile Report</h1>
351
+ <p>Generated by ExpressoTS CLI</p>
352
+ </div>
353
+
354
+ <div class="card">
355
+ <h2>Health Score</h2>
356
+ <div class="score">${result.score}/100</div>
357
+ </div>
358
+
359
+ <div class="card">
360
+ <h2>Metrics</h2>
361
+ <div class="metric"><span>Base Image</span><span>${result.metrics.baseImage || "Unknown"}</span></div>
362
+ <div class="metric"><span>Layers</span><span>${result.metrics.layers || "Unknown"}</span></div>
363
+ <div class="metric"><span>Multi-stage Build</span><span class="${result.metrics.hasMultiStage ? "check" : "cross"}">${result.metrics.hasMultiStage ? "✓" : "✗"}</span></div>
364
+ <div class="metric"><span>Health Check</span><span class="${result.metrics.hasHealthCheck ? "check" : "cross"}">${result.metrics.hasHealthCheck ? "✓" : "✗"}</span></div>
365
+ <div class="metric"><span>Non-root User</span><span class="${result.metrics.hasNonRootUser ? "check" : "cross"}">${result.metrics.hasNonRootUser ? "✓" : "✗"}</span></div>
366
+ </div>
367
+
368
+ <div class="card">
369
+ <h2>Issues (${result.issues.length})</h2>
370
+ ${result.issues.length === 0
371
+ ? '<p style="color: #22c55e;">✓ No issues found!</p>'
372
+ : result.issues
373
+ .map((i) => `
374
+ <div class="issue ${i.severity}">
375
+ <strong>[${i.severity.toUpperCase()}]</strong> ${i.message}
376
+ ${i.fix ? `<br><small style="color: #6b7280;">Fix: ${i.fix}</small>` : ""}
377
+ </div>
378
+ `)
379
+ .join("")}
380
+ </div>
381
+
382
+ <div class="card">
383
+ <h2>Recommendations (${result.recommendations.length})</h2>
384
+ ${result.recommendations
385
+ .map((r) => `
386
+ <div class="issue low">
387
+ <strong>${r.title}</strong><br>
388
+ ${r.description}<br>
389
+ <small style="color: #22c55e;">Impact: ${r.impact}</small>
390
+ </div>
391
+ `)
392
+ .join("")}
393
+ </div>
394
+
395
+ <div class="footer">
396
+ Generated by ExpressoTS CLI • ${new Date().toISOString()}
397
+ </div>
398
+ </div>
399
+ </body>
400
+ </html>`;
401
+ }
@@ -0,0 +1 @@
1
+ export { profileCommand } from "./cli";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.profileCommand = void 0;
4
+ var cli_1 = require("./cli");
5
+ Object.defineProperty(exports, "profileCommand", { enumerable: true, get: function () { return cli_1.profileCommand; } });
@@ -0,0 +1,19 @@
1
+ import type { DockerfileAnalysis } from "../analyzers/dockerfile-analyzer";
2
+ export interface Optimization {
3
+ id: string;
4
+ priority: "low" | "medium" | "high";
5
+ category: string;
6
+ title: string;
7
+ description: string;
8
+ impact: string;
9
+ autoFixable: boolean;
10
+ fix?: (content: string) => string;
11
+ }
12
+ /**
13
+ * Generate optimization recommendations based on Dockerfile analysis
14
+ */
15
+ export declare function generateOptimizations(analysis: DockerfileAnalysis): Optimization[];
16
+ /**
17
+ * Apply auto-fixable optimizations to Dockerfile
18
+ */
19
+ export declare function applyOptimizations(dockerfilePath: string, optimizations: Optimization[]): Promise<number>;
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.applyOptimizations = exports.generateOptimizations = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ /**
9
+ * Generate optimization recommendations based on Dockerfile analysis
10
+ */
11
+ function generateOptimizations(analysis) {
12
+ const optimizations = [];
13
+ // Multi-stage build
14
+ if (!analysis.hasMultiStage) {
15
+ optimizations.push({
16
+ id: "OPT001",
17
+ priority: "high",
18
+ category: "Size",
19
+ title: "Use multi-stage build",
20
+ description: "Multi-stage builds significantly reduce final image size by separating build and runtime dependencies.",
21
+ impact: "Can reduce image size by 50-80%",
22
+ autoFixable: false,
23
+ });
24
+ }
25
+ // Non-root user
26
+ if (!analysis.hasNonRootUser) {
27
+ optimizations.push({
28
+ id: "OPT002",
29
+ priority: "high",
30
+ category: "Security",
31
+ title: "Run as non-root user",
32
+ description: "Running containers as root is a security risk. Create and use a non-root user.",
33
+ impact: "Reduces attack surface if container is compromised",
34
+ autoFixable: false,
35
+ });
36
+ }
37
+ // Health check
38
+ if (!analysis.hasHealthCheck) {
39
+ optimizations.push({
40
+ id: "OPT003",
41
+ priority: "medium",
42
+ category: "Reliability",
43
+ title: "Add HEALTHCHECK instruction",
44
+ description: "Health checks allow orchestrators to detect and restart unhealthy containers.",
45
+ impact: "Faster failure detection, better uptime",
46
+ autoFixable: true,
47
+ fix: (content) => {
48
+ // Add health check before CMD
49
+ const cmdMatch = content.match(/^CMD\s+.*/m);
50
+ if (cmdMatch) {
51
+ const healthCheck = `HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \\
52
+ CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
53
+
54
+ `;
55
+ return content.replace(cmdMatch[0], healthCheck + cmdMatch[0]);
56
+ }
57
+ return content;
58
+ },
59
+ });
60
+ }
61
+ // npm ci instead of npm install
62
+ if (analysis.hasNpmInstallWithoutCi) {
63
+ optimizations.push({
64
+ id: "OPT004",
65
+ priority: "low",
66
+ category: "Best Practice",
67
+ title: "Use npm ci instead of npm install",
68
+ description: "npm ci provides faster, more reliable, and reproducible builds.",
69
+ impact: "Faster builds, reproducible dependencies",
70
+ autoFixable: true,
71
+ fix: (content) => {
72
+ return content.replace(/npm install(?!\s+[-\w])/g, "npm ci");
73
+ },
74
+ });
75
+ }
76
+ // .dockerignore
77
+ if (!analysis.hasDockerignore) {
78
+ optimizations.push({
79
+ id: "OPT005",
80
+ priority: "medium",
81
+ category: "Size",
82
+ title: "Add .dockerignore file",
83
+ description: "Exclude unnecessary files from the build context to speed up builds and reduce image size.",
84
+ impact: "Faster builds, smaller images",
85
+ autoFixable: false,
86
+ });
87
+ }
88
+ // Combine RUN commands
89
+ if (analysis.layers > 10) {
90
+ optimizations.push({
91
+ id: "OPT006",
92
+ priority: "low",
93
+ category: "Size",
94
+ title: "Combine RUN commands",
95
+ description: `You have ${analysis.layers} RUN layers. Consider combining related commands to reduce layers.`,
96
+ impact: "Slightly smaller image, faster pulls",
97
+ autoFixable: false,
98
+ });
99
+ }
100
+ // Check base image
101
+ if (analysis.baseImage &&
102
+ !analysis.baseImage.includes("alpine") &&
103
+ !analysis.baseImage.includes("slim")) {
104
+ optimizations.push({
105
+ id: "OPT007",
106
+ priority: "medium",
107
+ category: "Size",
108
+ title: "Consider using Alpine or slim base image",
109
+ description: `Current base image: ${analysis.baseImage}. Alpine images are typically 5-10x smaller.`,
110
+ impact: "Significantly smaller image size",
111
+ autoFixable: false,
112
+ });
113
+ }
114
+ return optimizations;
115
+ }
116
+ exports.generateOptimizations = generateOptimizations;
117
+ /**
118
+ * Apply auto-fixable optimizations to Dockerfile
119
+ */
120
+ async function applyOptimizations(dockerfilePath, optimizations) {
121
+ let content = fs_1.default.readFileSync(dockerfilePath, "utf-8");
122
+ let appliedCount = 0;
123
+ for (const opt of optimizations) {
124
+ if (opt.autoFixable && opt.fix) {
125
+ const newContent = opt.fix(content);
126
+ if (newContent !== content) {
127
+ content = newContent;
128
+ appliedCount++;
129
+ }
130
+ }
131
+ }
132
+ if (appliedCount > 0) {
133
+ fs_1.default.writeFileSync(dockerfilePath, content, "utf-8");
134
+ }
135
+ return appliedCount;
136
+ }
137
+ exports.applyOptimizations = applyOptimizations;
@@ -1,2 +1,2 @@
1
- export declare function addProvider(packageName: string, version?: string, isDevDependency?: boolean): Promise<void>;
1
+ export declare function addProvider(packageName: string, version?: string | false, isDevDependency?: boolean): Promise<void>;
2
2
  export declare function removeProvider(packageName: string): Promise<void>;
@@ -5,10 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.removeProvider = exports.addProvider = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
- const node_child_process_1 = require("node:child_process");
9
8
  const node_fs_1 = __importDefault(require("node:fs"));
10
9
  const node_process_1 = require("node:process");
11
10
  const cli_ui_1 = require("../../utils/cli-ui");
11
+ const input_validation_1 = require("../../utils/input-validation");
12
+ const safe_spawn_1 = require("../../utils/safe-spawn");
12
13
  const PACKAGE_MANAGERS = {
13
14
  npm: {
14
15
  install: "install",
@@ -38,11 +39,14 @@ function detectPackageManager() {
38
39
  }
39
40
  async function execProcess({ command, args, directory, }) {
40
41
  return new Promise((resolve, reject) => {
41
- const isWindows = process.platform === "win32";
42
- const execCommand = isWindows ? `${command}.cmd` : command;
43
- const processRunner = (0, node_child_process_1.spawn)(execCommand, args, {
42
+ // `safeSpawn` (cross-spawn) resolves the Windows `.cmd` shim and
43
+ // applies cmd.exe-aware escaping for every argv entry. Combined
44
+ // with the `isValidPackageName` / `isValidVersion` guards on the
45
+ // caller side, this prevents command injection via the package
46
+ // name or version specifier (which can legitimately contain
47
+ // `>`, `<`, `|`, etc. in a semver range).
48
+ const processRunner = (0, safe_spawn_1.safeSpawn)(command, args, {
44
49
  cwd: directory,
45
- shell: true,
46
50
  });
47
51
  console.log(chalk_1.default.bold.blue(`Executing: ${command} ${args.join(" ")}`));
48
52
  console.log(chalk_1.default.yellow("-------------------------------------------------"));
@@ -66,6 +70,20 @@ async function execProcess({ command, args, directory, }) {
66
70
  });
67
71
  }
68
72
  async function addProvider(packageName, version, isDevDependency = false) {
73
+ if (!(0, input_validation_1.isValidPackageName)(packageName)) {
74
+ (0, cli_ui_1.printError)(`Invalid package name: ${JSON.stringify(packageName)}`, "add-package");
75
+ return;
76
+ }
77
+ // yargs assigns `false` for the version flag when the user omits
78
+ // it (see add/cli.ts). Treat that and "latest" as "no suffix".
79
+ let versionSuffix = "";
80
+ if (typeof version === "string" && version !== "latest") {
81
+ if (!(0, input_validation_1.isValidVersion)(version)) {
82
+ (0, cli_ui_1.printError)(`Invalid version specifier: ${JSON.stringify(version)}`, "add-package");
83
+ return;
84
+ }
85
+ versionSuffix = `@${version}`;
86
+ }
69
87
  const packageManager = detectPackageManager();
70
88
  if (!packageManager) {
71
89
  (0, cli_ui_1.printError)("No package manager found in the project", "add-package");
@@ -75,7 +93,6 @@ async function addProvider(packageName, version, isDevDependency = false) {
75
93
  const command = isDevDependency
76
94
  ? pkgManagerConfig.addDev
77
95
  : pkgManagerConfig.install;
78
- const versionSuffix = version && version !== "latest" ? `@${version}` : "";
79
96
  console.log(`${isDevDependency ? "Adding devDependency" : "Installing"} ${packageName}...`);
80
97
  await execProcess({
81
98
  command: packageManager,
@@ -85,6 +102,10 @@ async function addProvider(packageName, version, isDevDependency = false) {
85
102
  }
86
103
  exports.addProvider = addProvider;
87
104
  async function removeProvider(packageName) {
105
+ if (!(0, input_validation_1.isValidPackageName)(packageName)) {
106
+ (0, cli_ui_1.printError)(`Invalid package name: ${JSON.stringify(packageName)}`, "remove-package");
107
+ return;
108
+ }
88
109
  const packageManager = detectPackageManager();
89
110
  if (!packageManager) {
90
111
  (0, cli_ui_1.printError)("No package manager found in the project", "remove-package");