@expressots/cli 4.0.0-preview.2 → 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 (63) hide show
  1. package/bin/cicd/cli.d.ts +1 -1
  2. package/bin/cicd/cli.js +3 -1
  3. package/bin/cicd/form.js +5 -4
  4. package/bin/cli.d.ts +1 -5
  5. package/bin/cli.js +56 -6
  6. package/bin/commands/project.commands.js +233 -26
  7. package/bin/containerize/cli.d.ts +1 -1
  8. package/bin/containerize/cli.js +1 -1
  9. package/bin/containerize/form.js +49 -51
  10. package/bin/containerize/generators/ci-generator.js +16 -12
  11. package/bin/containerize/generators/docker-compose-generator.js +3 -2
  12. package/bin/containerize/generators/dockerfile-generator.js +50 -28
  13. package/bin/containerize/generators/kubernetes-generator.js +5 -4
  14. package/bin/costs/cli.d.ts +1 -1
  15. package/bin/costs/cli.js +4 -2
  16. package/bin/dev/cli.d.ts +1 -1
  17. package/bin/dev/cli.js +3 -1
  18. package/bin/generate/cli.d.ts +1 -1
  19. package/bin/generate/templates/nonopinionated/config.tpl +12 -12
  20. package/bin/generate/templates/nonopinionated/event.tpl +10 -10
  21. package/bin/generate/templates/nonopinionated/guard.tpl +18 -18
  22. package/bin/generate/templates/nonopinionated/handler.tpl +12 -12
  23. package/bin/generate/templates/nonopinionated/interceptor.tpl +27 -27
  24. package/bin/generate/templates/opinionated/config.tpl +47 -47
  25. package/bin/generate/templates/opinionated/event.tpl +15 -15
  26. package/bin/generate/templates/opinionated/guard.tpl +41 -41
  27. package/bin/generate/templates/opinionated/handler.tpl +23 -23
  28. package/bin/generate/templates/opinionated/interceptor.tpl +50 -50
  29. package/bin/generate/utils/command-utils.d.ts +13 -2
  30. package/bin/generate/utils/command-utils.js +50 -17
  31. package/bin/generate/utils/opinionated-cmd.js +19 -12
  32. package/bin/help/cli.d.ts +1 -1
  33. package/bin/help/command-help-registry.d.ts +23 -0
  34. package/bin/help/command-help-registry.js +303 -0
  35. package/bin/help/command-help.d.ts +36 -0
  36. package/bin/help/command-help.js +56 -0
  37. package/bin/help/form.js +127 -30
  38. package/bin/help/main-help.d.ts +8 -0
  39. package/bin/help/main-help.js +126 -0
  40. package/bin/help/render.d.ts +32 -0
  41. package/bin/help/render.js +46 -0
  42. package/bin/info/cli.d.ts +1 -1
  43. package/bin/info/form.d.ts +1 -1
  44. package/bin/info/form.js +11 -11
  45. package/bin/migrate/cli.d.ts +1 -1
  46. package/bin/migrate/cli.js +3 -1
  47. package/bin/migrate/form.js +4 -3
  48. package/bin/new/cli.d.ts +5 -1
  49. package/bin/new/cli.js +62 -14
  50. package/bin/new/form.d.ts +3 -1
  51. package/bin/new/form.js +338 -23
  52. package/bin/profile/cli.d.ts +1 -1
  53. package/bin/profile/cli.js +3 -1
  54. package/bin/profile/form.js +5 -4
  55. package/bin/providers/create/form.js +53 -4
  56. package/bin/studio/cli.js +9 -3
  57. package/bin/templates/cli.js +7 -5
  58. package/bin/utils/add-module-to-container.d.ts +14 -3
  59. package/bin/utils/add-module-to-container.js +330 -111
  60. package/bin/utils/cli-ui.d.ts +20 -1
  61. package/bin/utils/cli-ui.js +41 -3
  62. package/bin/utils/update-tsconfig-paths.js +73 -33
  63. package/package.json +22 -13
@@ -11,6 +11,7 @@ exports.templatesCommand = void 0;
11
11
  const chalk_1 = __importDefault(require("chalk"));
12
12
  const manager_1 = require("./manager");
13
13
  const config_1 = require("../config");
14
+ const cli_ui_1 = require("../utils/cli-ui");
14
15
  const templatesCommand = () => {
15
16
  return {
16
17
  command: "templates <action> [args...]",
@@ -77,8 +78,9 @@ const templatesCommand = () => {
77
78
  await showStatus();
78
79
  break;
79
80
  default:
80
- console.log(chalk_1.default.red(`Unknown action: ${action}`));
81
+ (0, cli_ui_1.printError)(`Unknown action: ${action}`, "templates");
81
82
  console.log(chalk_1.default.gray("Run 'expressots templates --help' for usage."));
83
+ process.exit(1);
82
84
  }
83
85
  },
84
86
  };
@@ -88,7 +90,7 @@ exports.templatesCommand = templatesCommand;
88
90
  * List available templates
89
91
  */
90
92
  async function listTemplates(category, platform) {
91
- console.log(chalk_1.default.cyan("\n📋 Available Templates\n"));
93
+ (0, cli_ui_1.printSection)("📋 Available Templates");
92
94
  const manager = (0, manager_1.getTemplateManager)();
93
95
  const templates = await manager.listTemplates();
94
96
  // Show template source
@@ -161,7 +163,7 @@ async function listTemplates(category, platform) {
161
163
  * Update template cache
162
164
  */
163
165
  async function updateTemplates() {
164
- console.log(chalk_1.default.cyan("\n🔄 Updating Templates...\n"));
166
+ (0, cli_ui_1.printSection)("🔄 Updating Templates...");
165
167
  const manager = (0, manager_1.getTemplateManager)();
166
168
  const result = await manager.updateCache();
167
169
  if (result.updated > 0) {
@@ -182,7 +184,7 @@ async function updateTemplates() {
182
184
  * Clear template cache
183
185
  */
184
186
  async function clearCache() {
185
- console.log(chalk_1.default.cyan("\n🗑️ Clearing Template Cache...\n"));
187
+ (0, cli_ui_1.printSection)("🗑️ Clearing Template Cache...");
186
188
  const manager = (0, manager_1.getTemplateManager)();
187
189
  const stats = manager.getCacheStats();
188
190
  manager.clearCache();
@@ -240,7 +242,7 @@ async function manageRepository(args) {
240
242
  const subAction = args[0];
241
243
  if (!subAction) {
242
244
  const config = configManager.getTemplateConfig();
243
- console.log(chalk_1.default.cyan("\n📦 Template Repository Configuration\n"));
245
+ (0, cli_ui_1.printSection)("📦 Template Repository Configuration");
244
246
  console.log(` Repository: ${chalk_1.default.yellow(config.repository)}`);
245
247
  console.log(` Branch: ${chalk_1.default.cyan(config.branch)}`);
246
248
  console.log(` Cache TTL: ${config.cacheTTL} seconds`);
@@ -1,3 +1,14 @@
1
- declare function addModuleToContainer(name: string, modulePath?: string, path?: string, folderName?: string): Promise<void>;
2
- declare function addModuleToContainerNestedPath(name: string, path?: string, folderName?: string): Promise<void>;
3
- export { addModuleToContainer, addModuleToContainerNestedPath };
1
+ /**
2
+ * Register a scaffolded module in `app.ts`.
3
+ *
4
+ * The import specifier is derived from the module file's real location
5
+ * (`moduleOutputPath`) so it always resolves to the file on disk, regardless of
6
+ * the path style used to scaffold it.
7
+ *
8
+ * @param moduleClassName - Exported module symbol, e.g. `UserModule`.
9
+ * @param moduleOutputPath - Path to the generated `*.module.ts` file.
10
+ * @param folderToScaffold - Destination root, e.g. `src/useCases`.
11
+ * @param folderName - Folder basename used to resolve the path alias, e.g. `useCases`.
12
+ */
13
+ declare function addModuleToContainerByPath(moduleClassName: string, moduleOutputPath: string, folderToScaffold: string, folderName: string): Promise<void>;
14
+ export { addModuleToContainerByPath };
@@ -1,21 +1,46 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
27
  };
5
28
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.addModuleToContainerNestedPath = exports.addModuleToContainer = void 0;
7
- const chalk_1 = __importDefault(require("chalk"));
29
+ exports.addModuleToContainerByPath = void 0;
8
30
  const glob_1 = require("glob");
9
31
  const node_fs_1 = __importDefault(require("node:fs"));
32
+ const nodePath = __importStar(require("node:path"));
10
33
  const cli_ui_1 = require("./cli-ui");
11
34
  const compiler_1 = __importDefault(require("./compiler"));
12
35
  const update_tsconfig_paths_1 = require("./update-tsconfig-paths");
13
36
  const APP_CONTAINER = "app.ts";
14
- async function validateAppContainer() {
37
+ /**
38
+ * Locate `app.ts` and read its raw content. We deliberately avoid any
39
+ * line-based parsing here so multi-line imports and nested array literals
40
+ * survive untouched.
41
+ */
42
+ async function readAppContainer() {
15
43
  const { sourceRoot } = await compiler_1.default.loadConfig();
16
- const imports = [];
17
- const notImports = [];
18
- // Locate the container file
19
44
  const path = (0, glob_1.globSync)(`./${sourceRoot}/${APP_CONTAINER}`, {
20
45
  absolute: true,
21
46
  ignore: "**/node_modules/**",
@@ -24,121 +49,315 @@ async function validateAppContainer() {
24
49
  (0, cli_ui_1.printError)("Module not added to Container. Container file not found!", APP_CONTAINER);
25
50
  process.exit(1);
26
51
  }
27
- // Read the container file
28
- const fileContent = await node_fs_1.default.promises.readFile(path[0], "utf8");
29
- // Collect imports and other lines
30
- fileContent.split("\n").forEach((line) => {
31
- if (line.startsWith("import")) {
32
- imports.push(line);
52
+ const content = await node_fs_1.default.promises.readFile(path[0], "utf8");
53
+ return { path: path[0], content };
54
+ }
55
+ /**
56
+ * Detect the indentation (leading whitespace) of the line containing `index`.
57
+ */
58
+ function getLineIndent(source, index) {
59
+ const lineStart = source.lastIndexOf("\n", index - 1) + 1;
60
+ const match = /^[\t ]*/.exec(source.slice(lineStart));
61
+ return match ? match[0] : "";
62
+ }
63
+ /**
64
+ * Skip over a string literal starting at `i` (the index of the opening quote).
65
+ * Handles `"..."`, `'...'`, and template strings ``` `...` ``` including escapes.
66
+ * Returns the index just past the closing quote.
67
+ */
68
+ function skipString(source, i) {
69
+ const quote = source[i];
70
+ i++;
71
+ while (i < source.length) {
72
+ const ch = source[i];
73
+ if (ch === "\\") {
74
+ i += 2;
75
+ continue;
76
+ }
77
+ if (ch === quote)
78
+ return i + 1;
79
+ i++;
80
+ }
81
+ return source.length;
82
+ }
83
+ /**
84
+ * Skip over a `// line` or `/* block *\/` comment starting at `i`.
85
+ * Returns the index just past the comment, or `i` unchanged if not a comment.
86
+ */
87
+ function skipComment(source, i) {
88
+ if (source[i] === "/" && source[i + 1] === "/") {
89
+ const nl = source.indexOf("\n", i);
90
+ return nl === -1 ? source.length : nl;
91
+ }
92
+ if (source[i] === "/" && source[i + 1] === "*") {
93
+ const close = source.indexOf("*/", i + 2);
94
+ return close === -1 ? source.length : close + 2;
95
+ }
96
+ return i;
97
+ }
98
+ /**
99
+ * Find the index immediately after the last top-level `import ... ;`
100
+ * statement. Returns 0 if no imports are present. The scan is string- and
101
+ * comment-aware so multi-line imports and quoted semicolons don't confuse it.
102
+ */
103
+ function findLastImportEnd(source) {
104
+ let i = 0;
105
+ let lastEnd = 0;
106
+ while (i < source.length) {
107
+ while (i < source.length && /\s/.test(source[i]))
108
+ i++;
109
+ const afterComment = skipComment(source, i);
110
+ if (afterComment !== i) {
111
+ i = afterComment;
112
+ continue;
33
113
  }
34
- else {
35
- notImports.push(line);
114
+ if (!source.startsWith("import", i))
115
+ break;
116
+ // Walk forward until the terminating semicolon, ignoring strings/comments.
117
+ let j = i + "import".length;
118
+ while (j < source.length) {
119
+ const ch = source[j];
120
+ if (ch === '"' || ch === "'" || ch === "`") {
121
+ j = skipString(source, j);
122
+ continue;
123
+ }
124
+ const afterCmt = skipComment(source, j);
125
+ if (afterCmt !== j) {
126
+ j = afterCmt;
127
+ continue;
128
+ }
129
+ if (ch === ";") {
130
+ j++;
131
+ break;
132
+ }
133
+ j++;
36
134
  }
37
- });
38
- // Regex to detect and extract modules from configContainer
39
- const moduleRegex = /this\.configContainer\(\s*\[\s*([\s\S]*?)\s*]\s*\)/;
40
- const moduleMatch = fileContent.match(moduleRegex);
41
- if (!moduleMatch) {
42
- (0, cli_ui_1.printError)("The App class does not contain a valid configContainer([]) declaration!", APP_CONTAINER);
43
- process.exit(1);
135
+ lastEnd = j;
136
+ i = j;
44
137
  }
45
- // Extract modules if present
46
- const modules = moduleMatch[1]
47
- .trim()
48
- .split(",")
49
- .filter((m) => m.trim() !== "")
50
- .map((m) => m.trim());
51
- return {
52
- regex: moduleRegex,
53
- path: path[0],
54
- content: fileContent,
55
- modules,
56
- imports,
57
- notImports,
58
- };
138
+ return lastEnd;
59
139
  }
60
- async function addModuleToContainer(name, modulePath, path, folderName) {
61
- const containerData = await validateAppContainer();
62
- const moduleName = (name[0].toUpperCase() + name.slice(1)).trimStart();
63
- const { opinionated } = await compiler_1.default.loadConfig();
64
- let usecaseDir;
65
- if (opinionated) {
66
- // Use dynamic path alias based on the actual folder
67
- // Default to @useCases for backward compatibility
68
- const pathAlias = folderName
69
- ? (0, update_tsconfig_paths_1.getPathAliasForFolder)(folderName)
70
- : "@useCases";
71
- usecaseDir = `${pathAlias}/`;
72
- }
73
- else {
74
- usecaseDir = `./`;
75
- }
76
- let newImport = "";
77
- const modulePathRegex = /^[^/]=$/;
78
- if (!modulePathRegex.test(modulePath)) {
79
- if (path.split("/").length > 1) {
80
- newImport = `import { ${moduleName}Module } from "${usecaseDir}${name.toLowerCase()}/${name.toLowerCase()}.module";`;
140
+ /**
141
+ * Locate the call expression `callName(` starting at or after `from`, then
142
+ * return the `[start, end]` indices (inclusive of the opening `(` and matching
143
+ * `)`) of its argument list. Returns null if not found.
144
+ *
145
+ * The matcher respects strings, comments, and balanced brackets so the inner
146
+ * `CreateModule([...])` does not prematurely close the outer call.
147
+ */
148
+ function findCallRange(source, callName, from = 0) {
149
+ const needle = `${callName}(`;
150
+ const start = source.indexOf(needle, from);
151
+ if (start === -1)
152
+ return null;
153
+ const open = start + needle.length - 1; // index of '('
154
+ let depthParen = 1;
155
+ let depthBracket = 0;
156
+ let depthBrace = 0;
157
+ let i = open + 1;
158
+ while (i < source.length && depthParen > 0) {
159
+ const ch = source[i];
160
+ if (ch === '"' || ch === "'" || ch === "`") {
161
+ i = skipString(source, i);
162
+ continue;
81
163
  }
82
- else {
83
- newImport = `import { ${moduleName}Module } from "${usecaseDir}${name.toLowerCase()}.module";`;
164
+ const afterCmt = skipComment(source, i);
165
+ if (afterCmt !== i) {
166
+ i = afterCmt;
167
+ continue;
84
168
  }
169
+ if (ch === "(")
170
+ depthParen++;
171
+ else if (ch === ")")
172
+ depthParen--;
173
+ else if (ch === "[")
174
+ depthBracket++;
175
+ else if (ch === "]")
176
+ depthBracket--;
177
+ else if (ch === "{")
178
+ depthBrace++;
179
+ else if (ch === "}")
180
+ depthBrace--;
181
+ i++;
85
182
  }
86
- else {
87
- newImport = `import { ${moduleName}Module } from "${usecaseDir}${name}/${name.toLowerCase()}.module";`;
183
+ if (depthParen !== 0 || depthBracket !== 0 || depthBrace !== 0)
184
+ return null;
185
+ return { open, close: i - 1 }; // close = index of ')'
186
+ }
187
+ /**
188
+ * Locate the first array literal `[...]` at or after `from`, returning the
189
+ * indices of the opening `[` and matching `]`. Bracket-balanced and
190
+ * string/comment safe.
191
+ */
192
+ function findArrayRange(source, from) {
193
+ let i = from;
194
+ while (i < source.length) {
195
+ const ch = source[i];
196
+ if (ch === "[")
197
+ break;
198
+ if (ch === '"' || ch === "'" || ch === "`") {
199
+ i = skipString(source, i);
200
+ continue;
201
+ }
202
+ const afterCmt = skipComment(source, i);
203
+ if (afterCmt !== i) {
204
+ i = afterCmt;
205
+ continue;
206
+ }
207
+ i++;
88
208
  }
89
- if (containerData.imports.includes(newImport) &&
90
- containerData.modules.includes(`${moduleName}Module`)) {
91
- return;
209
+ if (i >= source.length || source[i] !== "[")
210
+ return null;
211
+ const open = i;
212
+ let depth = 1;
213
+ let j = open + 1;
214
+ while (j < source.length && depth > 0) {
215
+ const ch = source[j];
216
+ if (ch === '"' || ch === "'" || ch === "`") {
217
+ j = skipString(source, j);
218
+ continue;
219
+ }
220
+ const afterCmt = skipComment(source, j);
221
+ if (afterCmt !== j) {
222
+ j = afterCmt;
223
+ continue;
224
+ }
225
+ if (ch === "[")
226
+ depth++;
227
+ else if (ch === "]")
228
+ depth--;
229
+ j++;
230
+ }
231
+ if (depth !== 0)
232
+ return null;
233
+ return { open, close: j - 1 };
234
+ }
235
+ /**
236
+ * Insert `name` as a new entry in the array literal that starts at `arrayOpen`
237
+ * (the index of `[`) and ends at `arrayClose` (the index of the matching `]`).
238
+ *
239
+ * Preserves the original formatting: trailing-comma style is honoured, inline
240
+ * arrays stay inline, and multi-line arrays receive the new entry on its own
241
+ * line with matching indentation. If `name` is already present in the array
242
+ * the source is returned unchanged.
243
+ */
244
+ function insertIntoArray(source, arrayOpen, arrayClose, name) {
245
+ const inner = source.slice(arrayOpen + 1, arrayClose);
246
+ // Skip insertion if the identifier is already a top-level token in the array.
247
+ const tokenRegex = /\b[A-Za-z_$][\w$]*\b/g;
248
+ let match;
249
+ while ((match = tokenRegex.exec(inner)) !== null) {
250
+ if (match[0] === name)
251
+ return source;
252
+ }
253
+ if (inner.trim().length === 0) {
254
+ return source.slice(0, arrayOpen + 1) + name + source.slice(arrayClose);
255
+ }
256
+ // Find the last meaningful character inside the array (skip trailing
257
+ // whitespace before `]`).
258
+ let lastChar = arrayClose - 1;
259
+ while (lastChar > arrayOpen && /\s/.test(source[lastChar]))
260
+ lastChar--;
261
+ const hasTrailingComma = source[lastChar] === ",";
262
+ const isMultiLine = inner.includes("\n");
263
+ if (!isMultiLine) {
264
+ const insertion = hasTrailingComma ? ` ${name}` : `, ${name}`;
265
+ return (source.slice(0, lastChar + 1) +
266
+ insertion +
267
+ source.slice(lastChar + 1));
268
+ }
269
+ // Multi-line: match the indentation of the last item and place the new
270
+ // entry on its own line just before the closing bracket.
271
+ const itemIndent = getLineIndent(source, lastChar);
272
+ const insertAt = lastChar + 1;
273
+ const insertion = (hasTrailingComma ? "" : ",") + `\n${itemIndent}${name},`;
274
+ return source.slice(0, insertAt) + insertion + source.slice(insertAt);
275
+ }
276
+ /**
277
+ * Insert a new import statement after the last existing one, preserving any
278
+ * blank line that already separates imports from code. If the import is
279
+ * already present (textually) the file is returned unchanged.
280
+ */
281
+ function insertImport(source, importLine) {
282
+ if (source.includes(importLine))
283
+ return source;
284
+ const insertAt = findLastImportEnd(source);
285
+ if (insertAt === 0) {
286
+ // No existing imports: prepend at top.
287
+ const sep = source.startsWith("\n") ? "" : "\n";
288
+ return importLine + "\n" + sep + source;
289
+ }
290
+ const before = source.slice(0, insertAt);
291
+ const after = source.slice(insertAt);
292
+ return before + "\n" + importLine + after;
293
+ }
294
+ /**
295
+ * Add `${moduleName}Module` to the container declaration in `app.ts`.
296
+ *
297
+ * Handles both layouts:
298
+ * - v4 wrapper: this.configContainer([CreateModule([...]), ModuleA, ModuleB])
299
+ * - legacy flat: this.configContainer([ModuleA, ModuleB])
300
+ *
301
+ * Scaffolded modules are always inserted into the outer `configContainer([...])`
302
+ * array as peer entries alongside `CreateModule([...])`. The inner
303
+ * `CreateModule([...])` is reserved for orphan controllers that don't have
304
+ * their own module file.
305
+ */
306
+ function addModuleToContainerSource(source, className) {
307
+ const configCall = findCallRange(source, "this.configContainer");
308
+ if (!configCall) {
309
+ (0, cli_ui_1.printError)("The App class does not contain a valid configContainer([]) declaration!", APP_CONTAINER);
310
+ process.exit(1);
311
+ }
312
+ const outerArray = findArrayRange(source, configCall.open + 1);
313
+ if (!outerArray || outerArray.close > configCall.close) {
314
+ (0, cli_ui_1.printError)("configContainer must be called with an array literal argument.", APP_CONTAINER);
315
+ process.exit(1);
92
316
  }
93
- containerData.imports.push(newImport);
94
- containerData.modules.push(`${moduleName}Module`);
95
- const newModule = containerData.modules.join(", ");
96
- const newModuleDeclaration = `this.configContainer([${newModule}])`;
97
- const newFileContent = [
98
- ...containerData.imports,
99
- ...containerData.notImports,
100
- ]
101
- .join("\n")
102
- .replace(containerData.regex, newModuleDeclaration);
103
- console.log(" ", chalk_1.default.greenBright(`[container]`.padEnd(14)), chalk_1.default.bold.white(`${moduleName}Module added to ${APP_CONTAINER}! ✔️`));
104
- await node_fs_1.default.promises.writeFile(containerData.path, newFileContent, "utf8");
317
+ return insertIntoArray(source, outerArray.open, outerArray.close, className);
105
318
  }
106
- exports.addModuleToContainer = addModuleToContainer;
107
- async function addModuleToContainerNestedPath(name, path, folderName) {
108
- const containerData = await validateAppContainer();
109
- const moduleName = (name[0].toUpperCase() + name.slice(1)).trimStart();
110
- const { opinionated } = await compiler_1.default.loadConfig();
111
- let usecaseDir;
319
+ /**
320
+ * Build the import specifier for a scaffolded module from its real on-disk
321
+ * location. In opinionated mode the path alias for the destination folder is
322
+ * used (e.g. `@useCases/user-create/user.module`); otherwise a relative import
323
+ * from the source root is produced. Deriving the specifier from the actual file
324
+ * path guarantees the generated import always matches where the module was
325
+ * written, independent of the path style (sugar/single/nested).
326
+ */
327
+ async function buildModuleImportSpec(moduleOutputPath, folderToScaffold, folderName) {
328
+ const { opinionated, sourceRoot } = await compiler_1.default.loadConfig();
329
+ const normalize = (p) => p.replace(/\\/g, "/").replace(/\.ts$/, "");
112
330
  if (opinionated) {
113
- // Use dynamic path alias based on the actual folder
114
- // Default to @useCases for backward compatibility
115
- const pathAlias = folderName
116
- ? (0, update_tsconfig_paths_1.getPathAliasForFolder)(folderName)
117
- : "@useCases";
118
- usecaseDir = `${pathAlias}/`;
119
- }
120
- else {
121
- usecaseDir = `./`;
122
- }
123
- if (path.endsWith("/")) {
124
- path = path.slice(0, -1);
125
- }
126
- const newImport = `import { ${moduleName}Module } from "${usecaseDir}${path}.module";`;
127
- if (containerData.imports.includes(newImport) &&
128
- containerData.modules.includes(`${moduleName}Module`)) {
129
- return;
331
+ const rel = normalize(nodePath.relative(folderToScaffold, moduleOutputPath));
332
+ return `${(0, update_tsconfig_paths_1.getPathAliasForFolder)(folderName)}/${rel}`;
130
333
  }
131
- containerData.imports.push(newImport);
132
- containerData.modules.push(`${moduleName}Module`);
133
- const newModule = containerData.modules.join(", ");
134
- const newModuleDeclaration = `this.configContainer([${newModule}])`;
135
- const newFileContent = [
136
- ...containerData.imports,
137
- ...containerData.notImports,
138
- ]
139
- .join("\n")
140
- .replace(containerData.regex, newModuleDeclaration);
141
- console.log(" ", chalk_1.default.greenBright(`[container]`.padEnd(14)), chalk_1.default.bold.white(`${moduleName}Module added to ${APP_CONTAINER}! ✔️`));
142
- await node_fs_1.default.promises.writeFile(containerData.path, newFileContent, "utf8");
334
+ const rel = normalize(nodePath.relative(sourceRoot, moduleOutputPath));
335
+ return `./${rel}`;
336
+ }
337
+ async function applyContainerEdit(className, importLine) {
338
+ const info = await readAppContainer();
339
+ let next = insertImport(info.content, importLine);
340
+ next = addModuleToContainerSource(next, className);
341
+ if (next === info.content)
342
+ return;
343
+ await node_fs_1.default.promises.writeFile(info.path, next, "utf8");
344
+ (0, cli_ui_1.printSuccess)(`${className} registered in ${APP_CONTAINER}`, "container");
345
+ }
346
+ /**
347
+ * Register a scaffolded module in `app.ts`.
348
+ *
349
+ * The import specifier is derived from the module file's real location
350
+ * (`moduleOutputPath`) so it always resolves to the file on disk, regardless of
351
+ * the path style used to scaffold it.
352
+ *
353
+ * @param moduleClassName - Exported module symbol, e.g. `UserModule`.
354
+ * @param moduleOutputPath - Path to the generated `*.module.ts` file.
355
+ * @param folderToScaffold - Destination root, e.g. `src/useCases`.
356
+ * @param folderName - Folder basename used to resolve the path alias, e.g. `useCases`.
357
+ */
358
+ async function addModuleToContainerByPath(moduleClassName, moduleOutputPath, folderToScaffold, folderName) {
359
+ const importSpec = await buildModuleImportSpec(moduleOutputPath, folderToScaffold, folderName);
360
+ const importLine = `import { ${moduleClassName} } from "${importSpec}";`;
361
+ await applyContainerEdit(moduleClassName, importLine);
143
362
  }
144
- exports.addModuleToContainerNestedPath = addModuleToContainerNestedPath;
363
+ exports.addModuleToContainerByPath = addModuleToContainerByPath;
@@ -26,7 +26,26 @@ export declare function printGenerateError(schematic: string, file: string): Pro
26
26
  * Print generate success (simplified format for scaffolding)
27
27
  */
28
28
  export declare function printGenerateSuccess(schematic: string, file: string): Promise<void>;
29
+ /**
30
+ * Print a section title. Intended for interactive/listing output (e.g.
31
+ * `templates list`, `costs compare`) where the structured logger format
32
+ * (`[ExpressoTS] timestamp LEVEL`) would be noisy. Leading newline keeps
33
+ * sections visually separated.
34
+ */
35
+ export declare function printSection(title: string): void;
36
+ /**
37
+ * Print an indented bullet point under a section.
38
+ */
39
+ export declare function printBullet(text: string): void;
40
+ /**
41
+ * Print a dim horizontal divider sized to the terminal width (capped).
42
+ */
43
+ export declare function printDivider(): void;
44
+ /**
45
+ * Print an aligned key/value pair (e.g. `Source: remote`).
46
+ */
47
+ export declare function printKeyValue(key: string, value: string, padding?: number): void;
29
48
  /**
30
49
  * Print the ExpressoTS CLI header
31
50
  */
32
- export declare function printHeader(): void;
51
+ export declare function printHeader(version?: string): void;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.printHeader = exports.printGenerateSuccess = exports.printGenerateError = exports.printDebug = exports.printInfo = exports.printWarning = exports.printSuccess = exports.printError = void 0;
6
+ exports.printHeader = exports.printKeyValue = exports.printDivider = exports.printBullet = exports.printSection = exports.printGenerateSuccess = exports.printGenerateError = exports.printDebug = exports.printInfo = exports.printWarning = exports.printSuccess = exports.printError = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const process_1 = require("process");
9
9
  /**
@@ -105,10 +105,48 @@ async function printGenerateSuccess(schematic, file) {
105
105
  log("INFO", schematic, `${file.split(".")[0]} created!`, "✔️");
106
106
  }
107
107
  exports.printGenerateSuccess = printGenerateSuccess;
108
+ /**
109
+ * Print a section title. Intended for interactive/listing output (e.g.
110
+ * `templates list`, `costs compare`) where the structured logger format
111
+ * (`[ExpressoTS] timestamp LEVEL`) would be noisy. Leading newline keeps
112
+ * sections visually separated.
113
+ */
114
+ function printSection(title) {
115
+ process_1.stdout.write(`\n${chalk_1.default.bold.cyan(title)}\n`);
116
+ }
117
+ exports.printSection = printSection;
118
+ /**
119
+ * Print an indented bullet point under a section.
120
+ */
121
+ function printBullet(text) {
122
+ process_1.stdout.write(` ${chalk_1.default.gray("-")} ${text}\n`);
123
+ }
124
+ exports.printBullet = printBullet;
125
+ /**
126
+ * Print a dim horizontal divider sized to the terminal width (capped).
127
+ */
128
+ function printDivider() {
129
+ const cols = typeof process.stdout.columns === "number" && process.stdout.columns > 0
130
+ ? process.stdout.columns
131
+ : 80;
132
+ const width = Math.min(Math.max(cols, 20), 80);
133
+ process_1.stdout.write(`${chalk_1.default.dim("\u2500".repeat(width))}\n`);
134
+ }
135
+ exports.printDivider = printDivider;
136
+ /**
137
+ * Print an aligned key/value pair (e.g. `Source: remote`).
138
+ */
139
+ function printKeyValue(key, value, padding = 12) {
140
+ process_1.stdout.write(` ${chalk_1.default.bold(`${key}:`.padEnd(padding))} ${value}\n`);
141
+ }
142
+ exports.printKeyValue = printKeyValue;
108
143
  /**
109
144
  * Print the ExpressoTS CLI header
110
145
  */
111
- function printHeader() {
112
- process_1.stdout.write(`\n${chalk_1.default.bold.green("🐎 ExpressoTS CLI")}\n\n`);
146
+ function printHeader(version) {
147
+ const title = version
148
+ ? `🐎 ExpressoTS CLI v${version}`
149
+ : "🐎 ExpressoTS CLI";
150
+ process_1.stdout.write(`\n${chalk_1.default.bold.green(title)}\n\n`);
113
151
  }
114
152
  exports.printHeader = printHeader;