@expressots/cli 3.0.0 → 4.0.0-preview.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +41 -95
  2. package/bin/cicd/cli.d.ts +6 -0
  3. package/bin/cicd/cli.js +126 -0
  4. package/bin/cicd/form.d.ts +29 -0
  5. package/bin/cicd/form.js +345 -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 -1
  25. package/bin/cli.js +18 -3
  26. package/bin/commands/project.commands.d.ts +19 -6
  27. package/bin/commands/project.commands.js +390 -61
  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 +154 -0
  40. package/bin/containerize/generators/ci-generator.d.ts +31 -0
  41. package/bin/containerize/generators/ci-generator.js +936 -0
  42. package/bin/containerize/generators/docker-compose-generator.d.ts +8 -0
  43. package/bin/containerize/generators/docker-compose-generator.js +186 -0
  44. package/bin/containerize/generators/dockerfile-generator.d.ts +8 -0
  45. package/bin/containerize/generators/dockerfile-generator.js +635 -0
  46. package/bin/containerize/generators/kubernetes-generator.d.ts +8 -0
  47. package/bin/containerize/generators/kubernetes-generator.js +133 -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 +183 -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 +134 -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.js +29 -2
  81. package/bin/generate/form.d.ts +5 -1
  82. package/bin/generate/form.js +3 -3
  83. package/bin/generate/templates/nonopinionated/config.tpl +12 -0
  84. package/bin/generate/templates/nonopinionated/event.tpl +10 -0
  85. package/bin/generate/templates/nonopinionated/guard.tpl +18 -0
  86. package/bin/generate/templates/nonopinionated/handler.tpl +12 -0
  87. package/bin/generate/templates/nonopinionated/interceptor.tpl +27 -0
  88. package/bin/generate/templates/opinionated/config.tpl +47 -0
  89. package/bin/generate/templates/opinionated/entity.tpl +1 -8
  90. package/bin/generate/templates/opinionated/event.tpl +15 -0
  91. package/bin/generate/templates/opinionated/guard.tpl +41 -0
  92. package/bin/generate/templates/opinionated/handler.tpl +23 -0
  93. package/bin/generate/templates/opinionated/interceptor.tpl +50 -0
  94. package/bin/generate/utils/command-utils.d.ts +7 -3
  95. package/bin/generate/utils/command-utils.js +95 -31
  96. package/bin/generate/utils/nonopininated-cmd.d.ts +10 -1
  97. package/bin/generate/utils/nonopininated-cmd.js +100 -1
  98. package/bin/generate/utils/opinionated-cmd.d.ts +10 -1
  99. package/bin/generate/utils/opinionated-cmd.js +112 -7
  100. package/bin/generate/utils/string-utils.d.ts +6 -0
  101. package/bin/generate/utils/string-utils.js +13 -1
  102. package/bin/help/form.js +11 -3
  103. package/bin/migrate/analyzers/platform-detector.d.ts +14 -0
  104. package/bin/migrate/analyzers/platform-detector.js +116 -0
  105. package/bin/migrate/cli.d.ts +6 -0
  106. package/bin/migrate/cli.js +96 -0
  107. package/bin/migrate/form.d.ts +25 -0
  108. package/bin/migrate/form.js +347 -0
  109. package/bin/migrate/generators/compose-to-k8s.d.ts +2 -0
  110. package/bin/migrate/generators/compose-to-k8s.js +324 -0
  111. package/bin/migrate/generators/compose-to-railway.d.ts +2 -0
  112. package/bin/migrate/generators/compose-to-railway.js +138 -0
  113. package/bin/migrate/generators/compose-to-render.d.ts +2 -0
  114. package/bin/migrate/generators/compose-to-render.js +148 -0
  115. package/bin/migrate/generators/generic-migration.d.ts +9 -0
  116. package/bin/migrate/generators/generic-migration.js +221 -0
  117. package/bin/migrate/generators/heroku-to-fly.d.ts +2 -0
  118. package/bin/migrate/generators/heroku-to-fly.js +291 -0
  119. package/bin/migrate/generators/heroku-to-railway.d.ts +2 -0
  120. package/bin/migrate/generators/heroku-to-railway.js +283 -0
  121. package/bin/migrate/generators/heroku-to-render.d.ts +2 -0
  122. package/bin/migrate/generators/heroku-to-render.js +148 -0
  123. package/bin/migrate/generators/index.d.ts +7 -0
  124. package/bin/migrate/generators/index.js +17 -0
  125. package/bin/migrate/generators/template-loader.d.ts +21 -0
  126. package/bin/migrate/generators/template-loader.js +59 -0
  127. package/bin/migrate/index.d.ts +1 -0
  128. package/bin/migrate/index.js +5 -0
  129. package/bin/new/cli.js +21 -6
  130. package/bin/new/form.d.ts +25 -4
  131. package/bin/new/form.js +285 -70
  132. package/bin/profile/analyzers/dockerfile-analyzer.d.ts +27 -0
  133. package/bin/profile/analyzers/dockerfile-analyzer.js +122 -0
  134. package/bin/profile/analyzers/image-analyzer.d.ts +19 -0
  135. package/bin/profile/analyzers/image-analyzer.js +85 -0
  136. package/bin/profile/cli.d.ts +4 -0
  137. package/bin/profile/cli.js +92 -0
  138. package/bin/profile/form.d.ts +56 -0
  139. package/bin/profile/form.js +400 -0
  140. package/bin/profile/index.d.ts +1 -0
  141. package/bin/profile/index.js +5 -0
  142. package/bin/profile/optimizers/index.d.ts +19 -0
  143. package/bin/profile/optimizers/index.js +137 -0
  144. package/bin/providers/add/form.d.ts +1 -1
  145. package/bin/providers/add/form.js +27 -6
  146. package/bin/providers/create/form.js +2 -1
  147. package/bin/scripts/form.js +27 -5
  148. package/bin/studio/cli.d.ts +15 -0
  149. package/bin/studio/cli.js +166 -0
  150. package/bin/studio/index.d.ts +5 -0
  151. package/bin/studio/index.js +9 -0
  152. package/bin/templates/cache.d.ts +54 -0
  153. package/bin/templates/cache.js +180 -0
  154. package/bin/templates/cli.d.ts +8 -0
  155. package/bin/templates/cli.js +292 -0
  156. package/bin/templates/fetcher.d.ts +49 -0
  157. package/bin/templates/fetcher.js +208 -0
  158. package/bin/templates/index.d.ts +11 -0
  159. package/bin/templates/index.js +37 -0
  160. package/bin/templates/manager.d.ts +116 -0
  161. package/bin/templates/manager.js +323 -0
  162. package/bin/templates/renderer.d.ts +49 -0
  163. package/bin/templates/renderer.js +204 -0
  164. package/bin/templates/types.d.ts +51 -0
  165. package/bin/templates/types.js +5 -0
  166. package/bin/utils/add-module-to-container.d.ts +2 -2
  167. package/bin/utils/add-module-to-container.js +15 -5
  168. package/bin/utils/cli-ui.d.ts +30 -3
  169. package/bin/utils/cli-ui.js +95 -13
  170. package/bin/utils/index.d.ts +4 -0
  171. package/bin/utils/index.js +4 -0
  172. package/bin/utils/input-validation.d.ts +50 -0
  173. package/bin/utils/input-validation.js +143 -0
  174. package/bin/utils/package-manager-commands.d.ts +24 -0
  175. package/bin/utils/package-manager-commands.js +50 -0
  176. package/bin/utils/safe-spawn.d.ts +35 -0
  177. package/bin/utils/safe-spawn.js +51 -0
  178. package/bin/utils/update-tsconfig-paths.d.ts +35 -0
  179. package/bin/utils/update-tsconfig-paths.js +286 -0
  180. package/package.json +154 -154
package/bin/new/form.js CHANGED
@@ -8,115 +8,259 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const cli_progress_1 = require("cli-progress");
9
9
  const degit_1 = __importDefault(require("degit"));
10
10
  const inquirer_1 = __importDefault(require("inquirer"));
11
- const node_child_process_1 = require("node:child_process");
12
11
  const node_fs_1 = __importDefault(require("node:fs"));
13
12
  const node_path_1 = __importDefault(require("node:path"));
14
- const cli_1 = require("../cli");
15
13
  const center_text_1 = require("../utils/center-text");
16
14
  const change_package_info_1 = require("../utils/change-package-info");
17
15
  const cli_ui_1 = require("../utils/cli-ui");
16
+ const input_validation_1 = require("../utils/input-validation");
17
+ const safe_spawn_1 = require("../utils/safe-spawn");
18
+ /**
19
+ * Install dependencies using the selected package manager
20
+ */
18
21
  async function packageManagerInstall({ packageManager, directory, progressBar, }) {
19
- const command = process.platform === "win32" ? `${packageManager}.cmd` : packageManager;
20
- const args = ["install", "--silent"];
22
+ if (!(0, input_validation_1.isValidPackageManager)(packageManager)) {
23
+ throw new Error(`Invalid package manager: ${packageManager}`);
24
+ }
25
+ // npm's `--silent` swallows errors too (loglevel=silent), which
26
+ // makes failures impossible to diagnose. `--loglevel=error` keeps
27
+ // the install quiet on the happy path but lets real failures
28
+ // stream to stderr so we can capture and surface them below.
29
+ const args = packageManager === "npm"
30
+ ? ["install", "--loglevel=error"]
31
+ : ["install", "--silent"];
21
32
  if (packageManager === "yarn") {
22
33
  args.push("--ignore-engines");
23
- args.splice(args.indexOf("--prefer-offline"), 1);
24
34
  }
25
35
  return new Promise((resolve, reject) => {
26
- const installProcess = (0, node_child_process_1.spawn)(command, args, {
36
+ // `safeSpawn` (cross-spawn) handles the Windows `.cmd` shim
37
+ // resolution and properly escapes argv even when the shell is
38
+ // involved on Windows. The `directory` value is only used as
39
+ // cwd; it is never interpolated into a command string.
40
+ const installProcess = (0, safe_spawn_1.safeSpawn)(packageManager, args, {
27
41
  cwd: directory,
28
- shell: true,
29
42
  timeout: 600000,
30
43
  });
31
- // Simulate incremental progress
32
- let progress = 0;
44
+ let progress = 50;
45
+ let lastProgressUpdate = Date.now();
33
46
  const interval = setInterval(() => {
34
- if (progress < 90) {
35
- progress += 5;
36
- progressBar.update(progress);
47
+ const now = Date.now();
48
+ if (progress < 88) {
49
+ const increment = progress < 70 ? 3 : 1;
50
+ progress = Math.min(progress + increment, 88);
51
+ progressBar.update(progress, {
52
+ doing: "Installing dependencies...",
53
+ });
54
+ }
55
+ else if (now - lastProgressUpdate > 3000) {
56
+ progressBar.update(progress, {
57
+ doing: "Installing dependencies...",
58
+ });
37
59
  }
38
60
  }, 1000);
39
- // Handle stdout for meaningful output or progress feedback
61
+ // Keep a rolling tail of stderr/stdout so we can surface a
62
+ // meaningful diagnostic when the install exits non-zero (npm's
63
+ // real error is otherwise hidden behind `--silent`).
64
+ const diagnosticBuffer = [];
65
+ const MAX_DIAGNOSTIC_LINES = 20;
66
+ const recordDiagnostic = (chunk) => {
67
+ for (const line of chunk.split(/\r?\n/)) {
68
+ const trimmed = line.trim();
69
+ if (!trimmed)
70
+ continue;
71
+ diagnosticBuffer.push(trimmed);
72
+ if (diagnosticBuffer.length > MAX_DIAGNOSTIC_LINES) {
73
+ diagnosticBuffer.shift();
74
+ }
75
+ }
76
+ };
40
77
  installProcess.stdout?.on("data", (data) => {
41
- const output = data.toString().trim();
42
- // Remove all data from || to the end of the line
43
- const cleanedOutput = output.replace(/\|\|.*$/g, "");
44
- // Match and handle npm-specific progress
78
+ const output = data.toString();
79
+ recordDiagnostic(output);
80
+ const cleanedOutput = output.trim().replace(/\|\|.*$/g, "");
45
81
  const npmProgressMatch = cleanedOutput.match(/\[(\d+)\/(\d+)\] (?:npm )?([\w\s]+)\.{3}/);
46
82
  if (npmProgressMatch) {
47
83
  const [, current, total, task] = npmProgressMatch;
48
- progress = Math.round((parseInt(current) / parseInt(total)) * 100);
84
+ const npmProgress = (parseInt(current) / parseInt(total)) * 100;
85
+ progress = Math.round(50 + npmProgress * 0.4);
86
+ lastProgressUpdate = Date.now();
49
87
  progressBar.update(progress, { doing: task });
50
88
  }
51
- else {
52
- // Update "task" without changing the progress
89
+ else if (cleanedOutput) {
90
+ lastProgressUpdate = Date.now();
53
91
  progressBar.update(progress, { doing: cleanedOutput });
54
92
  }
55
93
  });
56
- // Handle errors
94
+ installProcess.stderr?.on("data", (data) => {
95
+ const output = data.toString();
96
+ recordDiagnostic(output);
97
+ const cleanedOutput = output.trim().replace(/\|\|.*$/g, "");
98
+ const npmProgressMatch = cleanedOutput.match(/\[(\d+)\/(\d+)\] (?:npm )?([\w\s]+)\.{3}/);
99
+ if (npmProgressMatch) {
100
+ const [, current, total, task] = npmProgressMatch;
101
+ const npmProgress = (parseInt(current) / parseInt(total)) * 100;
102
+ progress = Math.round(50 + npmProgress * 0.4);
103
+ lastProgressUpdate = Date.now();
104
+ progressBar.update(progress, { doing: task });
105
+ }
106
+ });
57
107
  installProcess.on("error", (error) => {
58
- clearInterval(interval); // Stop interval on error
108
+ clearInterval(interval);
59
109
  progressBar.stop();
60
110
  reject(new Error(`Failed to start subprocess: ${error.message}`));
61
111
  });
62
- // Finalize progress on close
63
112
  installProcess.on("close", (code) => {
64
- clearInterval(interval); // Stop interval when the process ends
113
+ clearInterval(interval);
65
114
  if (code === 0) {
66
- progressBar.update(100, { doing: "Complete!" }); // Finalize progress
67
- progressBar.stop();
115
+ progressBar.update(90, { doing: "Dependencies installed" });
68
116
  resolve("Installation Done!");
69
117
  }
70
118
  else {
71
119
  progressBar.stop();
120
+ if (diagnosticBuffer.length > 0) {
121
+ console.log("\n");
122
+ console.log(chalk_1.default.bold.red(`${packageManager} install failed with exit code ${code}:`));
123
+ for (const line of diagnosticBuffer) {
124
+ console.log(chalk_1.default.gray(` ${line}`));
125
+ }
126
+ console.log("");
127
+ }
72
128
  reject(new Error(`${packageManager} install exited with code ${code}`));
73
129
  }
74
130
  });
75
131
  });
76
132
  }
133
+ /**
134
+ * Check if the package manager is installed
135
+ */
77
136
  async function checkIfPackageManagerExists(packageManager) {
78
- try {
79
- (0, node_child_process_1.execSync)(`${packageManager} --version`);
80
- return true;
137
+ if (!(0, input_validation_1.isValidPackageManager)(packageManager)) {
138
+ (0, cli_ui_1.printError)("Package manager not found!", packageManager);
139
+ process.exit(1);
81
140
  }
82
- catch (error) {
141
+ const result = (0, safe_spawn_1.safeSpawnSync)(packageManager, ["--version"], {
142
+ stdio: "ignore",
143
+ });
144
+ if (result.error || result.status !== 0) {
83
145
  (0, cli_ui_1.printError)("Package manager not found!", packageManager);
84
146
  process.exit(1);
85
147
  }
148
+ return true;
86
149
  }
87
- function renameEnvFile(directory) {
88
- try {
89
- const envExamplePath = node_path_1.default.join(directory, ".env.example");
90
- const envPath = node_path_1.default.join(directory, ".env");
91
- if (!node_fs_1.default.existsSync(envExamplePath)) {
92
- throw new Error(`File not found: ${envExamplePath}`);
93
- }
94
- node_fs_1.default.renameSync(envExamplePath, envPath);
150
+ /**
151
+ * Copy directory recursively (for local template testing)
152
+ */
153
+ function copyDirectorySync(src, dest) {
154
+ if (!node_fs_1.default.existsSync(dest)) {
155
+ node_fs_1.default.mkdirSync(dest, { recursive: true });
95
156
  }
96
- catch (error) {
97
- (0, cli_ui_1.printError)("Error renaming .env.example file", ".env.example to .env");
98
- process.exit(1);
157
+ const entries = node_fs_1.default.readdirSync(src, { withFileTypes: true });
158
+ for (const entry of entries) {
159
+ const srcPath = node_path_1.default.join(src, entry.name);
160
+ const destPath = node_path_1.default.join(dest, entry.name);
161
+ // Skip node_modules and dist directories
162
+ if (entry.name === "node_modules" || entry.name === "dist") {
163
+ continue;
164
+ }
165
+ if (entry.isDirectory()) {
166
+ copyDirectorySync(srcPath, destPath);
167
+ }
168
+ else {
169
+ node_fs_1.default.copyFileSync(srcPath, destPath);
170
+ }
99
171
  }
100
172
  }
173
+ /**
174
+ * Template definitions for v4.0
175
+ */
101
176
  var Template;
102
177
  (function (Template) {
103
- Template["nonopinionated"] = "Non-Opinionated :: Start with a clean slate and build your project from scratch.";
104
- Template["opinionated"] = "Opinionated :: Automatically scaffolds resources into a preset project structure. (Recommended)";
105
- Template["micro"] = "Micro :: A minimalistic template for building micro api's.";
178
+ Template["application"] = "Application :: Full-featured ExpressoTS application. (Recommended)";
179
+ Template["micro"] = "Micro :: A minimalistic template for building micro APIs and serverless functions.";
106
180
  })(Template || (Template = {}));
181
+ /**
182
+ * Middleware presets for Application template
183
+ */
184
+ var MiddlewarePreset;
185
+ (function (MiddlewarePreset) {
186
+ MiddlewarePreset["api"] = "API :: REST API with security, compression, and auto-logging. (Recommended)";
187
+ MiddlewarePreset["web"] = "Web :: Full web app with cookies and session support.";
188
+ MiddlewarePreset["graphql"] = "GraphQL :: Optimized for GraphQL APIs.";
189
+ MiddlewarePreset["microservice"] = "Microservice :: Minimal setup for microservices.";
190
+ MiddlewarePreset["minimal"] = "Minimal :: Just request parsing, customize everything yourself.";
191
+ })(MiddlewarePreset || (MiddlewarePreset = {}));
192
+ /**
193
+ * Template folder mapping
194
+ */
195
+ const TEMPLATE_FOLDERS = {
196
+ Application: "application",
197
+ Micro: "micro",
198
+ };
199
+ /**
200
+ * Middleware preset mapping to code
201
+ */
202
+ const PRESET_CODE = {
203
+ API: `this.Middleware.applyPreset("api");`,
204
+ Web: `this.Middleware.applyPreset("web");`,
205
+ GraphQL: `this.Middleware.applyPreset("graphql");`,
206
+ Microservice: `this.Middleware.applyPreset("microservice");`,
207
+ Minimal: `this.Middleware.parse();`,
208
+ };
209
+ /**
210
+ * Apply the selected middleware preset to the generated app.ts
211
+ */
212
+ function applyMiddlewarePreset(directory, preset) {
213
+ const appTsPath = node_path_1.default.join(directory, "src", "app.ts");
214
+ if (!node_fs_1.default.existsSync(appTsPath)) {
215
+ return;
216
+ }
217
+ // Extract preset name from selection (e.g., "API :: ..." -> "API")
218
+ const presetMatch = preset.match(/^(\w+) ::/);
219
+ const presetName = presetMatch ? presetMatch[1] : "API";
220
+ const presetCode = PRESET_CODE[presetName] || PRESET_CODE["API"];
221
+ let content = node_fs_1.default.readFileSync(appTsPath, "utf-8");
222
+ // Replace the placeholder with the preset code
223
+ content = content.replace(/\/\/ __MIDDLEWARE_PRESET_PLACEHOLDER__/, presetCode);
224
+ node_fs_1.default.writeFileSync(appTsPath, content, "utf-8");
225
+ }
226
+ /**
227
+ * Enable local template mode for testing.
228
+ * Opt-in via `EXPRESSOTS_DEV=1` and `EXPRESSOTS_USE_LOCAL_TEMPLATES=1`.
229
+ * Both must be set so a stray env var alone cannot redirect a user's
230
+ * `expressots new` to local files.
231
+ */
232
+ const USE_LOCAL_TEMPLATES = process.env.EXPRESSOTS_DEV === "1" &&
233
+ process.env.EXPRESSOTS_USE_LOCAL_TEMPLATES === "1";
234
+ /**
235
+ * Skip the package-manager install step. Useful when iterating on
236
+ * templates that depend on unpublished package versions. Same dual
237
+ * env-var guard as `USE_LOCAL_TEMPLATES`.
238
+ */
239
+ const SKIP_INSTALL_FOR_TESTING = process.env.EXPRESSOTS_DEV === "1" &&
240
+ process.env.EXPRESSOTS_SKIP_INSTALL === "1";
241
+ /**
242
+ * Local templates path (relative to CLI installation)
243
+ * For development: points to the templates folder in the monorepo
244
+ * For production: this will be replaced with the actual path
245
+ */
246
+ const LOCAL_TEMPLATES_PATH = node_path_1.default.resolve(__dirname, "../../../templates");
247
+ /**
248
+ * Main project creation form
249
+ */
107
250
  const projectForm = async (projectName, args) => {
108
251
  let answer;
109
- const [packageManager, template, directory] = args;
252
+ const [packageManager, template, directory, preset] = args;
110
253
  if (packageManager && template) {
111
254
  answer = {
112
255
  name: projectName,
113
256
  packageManager: packageManager,
114
257
  template: Template[template],
258
+ preset: preset ? MiddlewarePreset[preset] : undefined,
115
259
  confirm: true,
116
260
  };
117
261
  }
118
262
  else {
119
- answer = await inquirer_1.default.prompt([
263
+ const baseAnswers = await inquirer_1.default.prompt([
120
264
  {
121
265
  type: "input",
122
266
  name: "name",
@@ -130,18 +274,42 @@ const projectForm = async (projectName, args) => {
130
274
  type: "list",
131
275
  name: "packageManager",
132
276
  message: "Package manager",
133
- choices: ["npm", "yarn", "pnpm", "bun"],
277
+ choices: [
278
+ "npm",
279
+ "yarn",
280
+ "pnpm",
281
+ ...(process.platform !== "win32" ? ["bun"] : []),
282
+ ],
134
283
  },
135
284
  {
136
285
  type: "list",
137
286
  name: "template",
138
287
  message: "Select a template",
139
288
  choices: [
140
- `Opinionated :: Automatically scaffolds resources into a preset project structure. (${chalk_1.default.yellow("Recommended")})`,
141
- "NonOpinionated :: Allows users to choose where to scaffold resources, offering flexible project organization.",
142
- "Micro :: A minimalistic template for building micro api's.",
289
+ `Application :: Full-featured ExpressoTS application. (${chalk_1.default.yellow("Recommended")})`,
290
+ "Micro :: A minimalistic template for building micro APIs and serverless functions.",
143
291
  ],
144
292
  },
293
+ ]);
294
+ // Only show preset selection for Application template
295
+ let presetAnswer = {};
296
+ if (baseAnswers.template.startsWith("Application")) {
297
+ presetAnswer = await inquirer_1.default.prompt([
298
+ {
299
+ type: "list",
300
+ name: "preset",
301
+ message: "Select a middleware preset",
302
+ choices: [
303
+ `API :: REST API with security, compression, and auto-logging. (${chalk_1.default.yellow("Recommended")})`,
304
+ "Web :: Full web app with cookies and session support.",
305
+ "GraphQL :: Optimized for GraphQL APIs.",
306
+ "Microservice :: Minimal setup for microservices.",
307
+ "Minimal :: Just request parsing, customize everything yourself.",
308
+ ],
309
+ },
310
+ ]);
311
+ }
312
+ const confirmAnswer = await inquirer_1.default.prompt([
145
313
  {
146
314
  type: "confirm",
147
315
  name: "confirm",
@@ -149,6 +317,11 @@ const projectForm = async (projectName, args) => {
149
317
  default: true,
150
318
  },
151
319
  ]);
320
+ answer = {
321
+ ...baseAnswers,
322
+ ...presetAnswer,
323
+ ...confirmAnswer,
324
+ };
152
325
  }
153
326
  if (directory) {
154
327
  if (!node_fs_1.default.existsSync(node_path_1.default.join(directory, answer.name))) {
@@ -159,12 +332,6 @@ const projectForm = async (projectName, args) => {
159
332
  process.exit(1);
160
333
  }
161
334
  }
162
- // Hashmap of templates and their directories
163
- const templates = {
164
- NonOpinionated: "non_opinionated",
165
- Opinionated: "opinionated",
166
- Micro: "micro",
167
- };
168
335
  if (answer.confirm) {
169
336
  // Check if package manager is bun and OS is Windows
170
337
  if (answer.packageManager === "bun" && process.platform === "win32") {
@@ -180,28 +347,76 @@ const projectForm = async (projectName, args) => {
180
347
  hideCursor: true,
181
348
  }, cli_progress_1.Presets.rect);
182
349
  progressBar.start(100, 0, {
183
- doing: "Cloning project",
350
+ doing: "Creating project",
184
351
  });
185
- const [_, template] = answer.template.match(/(.*) ::/);
186
- const repo = `expressots/templates/${templates[template]}#${cli_1.BUNDLE_VERSION}`;
352
+ // Extract template name from selection
353
+ const templateMatch = answer.template.match(/(.*) ::/);
354
+ if (!templateMatch || !templateMatch[1]) {
355
+ progressBar.stop();
356
+ (0, cli_ui_1.printError)(`Could not parse selected template: ${answer.template}`, "new");
357
+ process.exit(1);
358
+ }
359
+ const templateName = templateMatch[1];
360
+ const templateFolder = TEMPLATE_FOLDERS[templateName];
361
+ if (!templateFolder) {
362
+ progressBar.stop();
363
+ (0, cli_ui_1.printError)(`Unknown template: ${templateName}`, "new");
364
+ process.exit(1);
365
+ }
187
366
  try {
188
- const emitter = (0, degit_1.default)(repo);
189
- await emitter.clone(answer.name);
367
+ if (USE_LOCAL_TEMPLATES) {
368
+ // LOCAL TEMPLATE MODE (for testing)
369
+ const localTemplatePath = node_path_1.default.join(LOCAL_TEMPLATES_PATH, templateFolder);
370
+ if (!node_fs_1.default.existsSync(localTemplatePath)) {
371
+ progressBar.stop();
372
+ (0, cli_ui_1.printError)(`Local template not found at: ${localTemplatePath}`, "Please check your templates folder");
373
+ process.exit(1);
374
+ }
375
+ // Create target directory
376
+ node_fs_1.default.mkdirSync(answer.name, { recursive: true });
377
+ // Copy template files
378
+ copyDirectorySync(localTemplatePath, answer.name);
379
+ progressBar.update(30, { doing: "Template copied" });
380
+ }
381
+ else {
382
+ // GITHUB MODE (production)
383
+ // Pinned to the v4.0.0 GA tag of the templates repo so a
384
+ // CLI shipped at v4.0.0 keeps working even if `main` moves.
385
+ const repo = `expressots/templates/${templateFolder}#v4.0.0-preview.1`;
386
+ const emitter = (0, degit_1.default)(repo);
387
+ await emitter.clone(answer.name);
388
+ progressBar.update(30, { doing: "Template cloned" });
389
+ }
190
390
  }
191
391
  catch (err) {
192
392
  console.log("\n");
193
393
  (0, cli_ui_1.printError)("Project already exists or Folder is not empty", answer.name);
194
394
  process.exit(1);
195
395
  }
196
- progressBar.update(50, {
197
- doing: "Installing dependencies",
198
- });
199
- await packageManagerInstall({
200
- packageManager: answer.packageManager,
201
- directory: answer.name,
202
- progressBar,
203
- });
204
- progressBar.update(90);
396
+ if (SKIP_INSTALL_FOR_TESTING) {
397
+ progressBar.update(90, {
398
+ doing: "Skipping install (testing mode)",
399
+ });
400
+ }
401
+ else {
402
+ progressBar.update(50, {
403
+ doing: "Installing dependencies",
404
+ });
405
+ await packageManagerInstall({
406
+ packageManager: answer.packageManager,
407
+ directory: answer.name,
408
+ progressBar,
409
+ });
410
+ }
411
+ // Progress should already be at 90% from packageManagerInstall
412
+ // Only update if we skipped installation
413
+ if (!SKIP_INSTALL_FOR_TESTING) {
414
+ progressBar.update(90, { doing: "Finalizing project" });
415
+ }
416
+ // Apply middleware preset for Application template
417
+ if (answer.preset && templateFolder === "application") {
418
+ applyMiddlewarePreset(answer.name, answer.preset);
419
+ }
205
420
  (0, change_package_info_1.changePackageName)({
206
421
  directory: answer.name,
207
422
  name: projectName,
@@ -0,0 +1,27 @@
1
+ export interface DockerfileAnalysis {
2
+ baseImage?: string;
3
+ nodeVersion?: string;
4
+ layers: number;
5
+ hasMultiStage: boolean;
6
+ hasHealthCheck: boolean;
7
+ hasNonRootUser: boolean;
8
+ hasDockerignore: boolean;
9
+ hasNpmInstallWithoutCi: boolean;
10
+ hasCurlOrWgetWithoutCleanup: boolean;
11
+ instructions: DockerInstruction[];
12
+ stages: string[];
13
+ }
14
+ export interface DockerInstruction {
15
+ line: number;
16
+ instruction: string;
17
+ arguments: string;
18
+ raw: string;
19
+ }
20
+ /**
21
+ * Analyze a Dockerfile for issues and metrics
22
+ */
23
+ export declare function analyzeDockerfile(dockerfilePath: string): Promise<DockerfileAnalysis>;
24
+ /**
25
+ * Parse a specific Dockerfile instruction
26
+ */
27
+ export declare function parseInstruction(line: string): DockerInstruction | null;
@@ -0,0 +1,122 @@
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.parseInstruction = exports.analyzeDockerfile = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ /**
10
+ * Analyze a Dockerfile for issues and metrics
11
+ */
12
+ async function analyzeDockerfile(dockerfilePath) {
13
+ const content = fs_1.default.readFileSync(dockerfilePath, "utf-8");
14
+ const lines = content.split("\n");
15
+ const instructions = [];
16
+ const stages = [];
17
+ let hasMultiStage = false;
18
+ let hasHealthCheck = false;
19
+ let hasNonRootUser = false;
20
+ let hasNpmInstallWithoutCi = false;
21
+ let hasCurlOrWgetWithoutCleanup = false;
22
+ let baseImage;
23
+ let nodeVersion;
24
+ let runLayerCount = 0;
25
+ // Parse Dockerfile
26
+ let currentLine = 0;
27
+ for (const line of lines) {
28
+ currentLine++;
29
+ const trimmed = line.trim();
30
+ // Skip comments and empty lines
31
+ if (!trimmed || trimmed.startsWith("#")) {
32
+ continue;
33
+ }
34
+ // Parse instruction
35
+ const match = trimmed.match(/^(\w+)\s*(.*)/);
36
+ if (!match)
37
+ continue;
38
+ const instruction = match[1].toUpperCase();
39
+ const args = match[2];
40
+ instructions.push({
41
+ line: currentLine,
42
+ instruction,
43
+ arguments: args,
44
+ raw: trimmed,
45
+ });
46
+ // Analyze instruction
47
+ switch (instruction) {
48
+ case "FROM":
49
+ if (stages.length > 0) {
50
+ hasMultiStage = true;
51
+ }
52
+ stages.push(args);
53
+ // Extract base image info
54
+ if (!baseImage) {
55
+ baseImage = args.split(/\s+/)[0];
56
+ // Try to extract Node version
57
+ const nodeMatch = baseImage.match(/node:(\d+)/);
58
+ if (nodeMatch) {
59
+ nodeVersion = nodeMatch[1];
60
+ }
61
+ }
62
+ break;
63
+ case "HEALTHCHECK":
64
+ hasHealthCheck = true;
65
+ break;
66
+ case "USER":
67
+ // Check if non-root user
68
+ if (args && args !== "root" && args !== "0") {
69
+ hasNonRootUser = true;
70
+ }
71
+ break;
72
+ case "RUN":
73
+ runLayerCount++;
74
+ // Check for npm install vs npm ci
75
+ if (args.includes("npm install") && !args.includes("npm ci")) {
76
+ hasNpmInstallWithoutCi = true;
77
+ }
78
+ // Check for curl/wget without cleanup
79
+ if ((args.includes("curl") || args.includes("wget")) &&
80
+ !args.includes("rm ")) {
81
+ hasCurlOrWgetWithoutCleanup = true;
82
+ }
83
+ break;
84
+ }
85
+ }
86
+ // Check for .dockerignore
87
+ const dockerignorePath = path_1.default.join(path_1.default.dirname(dockerfilePath), ".dockerignore");
88
+ const hasDockerignore = fs_1.default.existsSync(dockerignorePath);
89
+ return {
90
+ baseImage,
91
+ nodeVersion,
92
+ layers: runLayerCount,
93
+ hasMultiStage,
94
+ hasHealthCheck,
95
+ hasNonRootUser,
96
+ hasDockerignore,
97
+ hasNpmInstallWithoutCi,
98
+ hasCurlOrWgetWithoutCleanup,
99
+ instructions,
100
+ stages,
101
+ };
102
+ }
103
+ exports.analyzeDockerfile = analyzeDockerfile;
104
+ /**
105
+ * Parse a specific Dockerfile instruction
106
+ */
107
+ function parseInstruction(line) {
108
+ const trimmed = line.trim();
109
+ if (!trimmed || trimmed.startsWith("#")) {
110
+ return null;
111
+ }
112
+ const match = trimmed.match(/^(\w+)\s*(.*)/);
113
+ if (!match)
114
+ return null;
115
+ return {
116
+ line: 0,
117
+ instruction: match[1].toUpperCase(),
118
+ arguments: match[2],
119
+ raw: trimmed,
120
+ };
121
+ }
122
+ exports.parseInstruction = parseInstruction;
@@ -0,0 +1,19 @@
1
+ export interface ImageAnalysis {
2
+ size: string;
3
+ layers: number;
4
+ created: string;
5
+ os: string;
6
+ architecture: string;
7
+ vulnerabilities: Vulnerability[];
8
+ }
9
+ export interface Vulnerability {
10
+ id: string;
11
+ severity: "low" | "medium" | "high" | "critical";
12
+ description: string;
13
+ package?: string;
14
+ fixedVersion?: string;
15
+ }
16
+ /**
17
+ * Analyze a Docker image for size, layers, and vulnerabilities
18
+ */
19
+ export declare function analyzeImage(imageName: string): Promise<ImageAnalysis>;