@expressots/cli 3.0.0-beta.4 → 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
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ /**
3
+ * Input validation utilities used across the CLI to defend against
4
+ * command injection and path traversal when user-supplied values flow
5
+ * into child_process spawn/exec calls or filesystem writes.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.assertValidScriptName = exports.assertValidVersion = exports.assertValidPackageName = exports.safeResolveWithin = exports.isValidPackageManager = exports.isValidScriptName = exports.isValidVersion = exports.isValidPackageName = exports.containsShellMetachars = void 0;
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ /**
14
+ * Characters that have shell-special meaning across POSIX shells and
15
+ * Windows cmd. If any appear in a value that will be interpolated into
16
+ * a shell-evaluated command (`shell: true` or `execSync(string)`), the
17
+ * value must be rejected.
18
+ */
19
+ const SHELL_METACHARACTERS = /[;&|`$()<>\n\r\\"'*?{}[\]!#~]/;
20
+ /**
21
+ * Conservative regex for npm package names. Allows scoped packages,
22
+ * dotted segments and dashes, mirroring https://docs.npmjs.com/cli/v10/configuring-npm/package-json#name
23
+ * but stricter than npm itself to remove ambiguity (no leading dots,
24
+ * no uppercase to keep it portable across registries).
25
+ */
26
+ const PACKAGE_NAME_RE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
27
+ /**
28
+ * Semver-ish range validator. Accepts the common syntactic shapes
29
+ * (e.g. 1.2.3, ^1.2, ~1, 1.x, latest, next, >=1.2.3 <2.0.0) without
30
+ * pulling in the full semver parser for this guard.
31
+ */
32
+ const VERSION_RE = /^[A-Za-z0-9.\-+~^>=<* |]+$/;
33
+ /**
34
+ * Script name validator for npm/yarn/pnpm `run` targets. Matches what
35
+ * those package managers actually accept (alphanumerics plus
36
+ * `:_-./`), explicitly rejecting whitespace and shell metacharacters.
37
+ */
38
+ const SCRIPT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9:_./-]*$/;
39
+ /**
40
+ * Guard against shell metacharacters in any value that will be
41
+ * interpolated into a shell-evaluated command line.
42
+ */
43
+ function containsShellMetachars(value) {
44
+ return SHELL_METACHARACTERS.test(value);
45
+ }
46
+ exports.containsShellMetachars = containsShellMetachars;
47
+ /**
48
+ * Validate an npm package name (with optional scope).
49
+ */
50
+ function isValidPackageName(name) {
51
+ if (typeof name !== "string" || name.length === 0 || name.length > 214) {
52
+ return false;
53
+ }
54
+ if (containsShellMetachars(name))
55
+ return false;
56
+ return PACKAGE_NAME_RE.test(name);
57
+ }
58
+ exports.isValidPackageName = isValidPackageName;
59
+ /**
60
+ * Validate a version specifier passed to a package manager. Accepts
61
+ * `latest`, `next`, exact versions and common range syntaxes
62
+ * (`>=1.2.3 <2.0.0`, `*`, `1.x`). Returns false for the boolean
63
+ * fallback yargs sometimes assigns when the flag is absent.
64
+ *
65
+ * Versions are forwarded via argv (`shell: false`), so we whitelist
66
+ * the characters npm itself accepts in semver ranges and reject the
67
+ * rest. We do NOT layer the broader `containsShellMetachars` check
68
+ * here because legitimate ranges include `<`, `>`, `|`, and `*`.
69
+ */
70
+ function isValidVersion(version) {
71
+ if (typeof version !== "string" || version.length === 0)
72
+ return false;
73
+ if (version.length > 64)
74
+ return false;
75
+ return VERSION_RE.test(version);
76
+ }
77
+ exports.isValidVersion = isValidVersion;
78
+ /**
79
+ * Validate an npm/yarn/pnpm script name.
80
+ */
81
+ function isValidScriptName(name) {
82
+ if (typeof name !== "string" || name.length === 0 || name.length > 214) {
83
+ return false;
84
+ }
85
+ if (containsShellMetachars(name))
86
+ return false;
87
+ return SCRIPT_NAME_RE.test(name);
88
+ }
89
+ exports.isValidScriptName = isValidScriptName;
90
+ /**
91
+ * Validate a package manager identifier.
92
+ */
93
+ const ALLOWED_PACKAGE_MANAGERS = new Set(["npm", "yarn", "pnpm", "bun"]);
94
+ function isValidPackageManager(pm) {
95
+ return typeof pm === "string" && ALLOWED_PACKAGE_MANAGERS.has(pm);
96
+ }
97
+ exports.isValidPackageManager = isValidPackageManager;
98
+ /**
99
+ * Resolve `target` against `base` and verify the result is contained
100
+ * within `base`. Returns the resolved absolute path on success, or
101
+ * `null` when the resolved path escapes the base directory (path
102
+ * traversal attempt).
103
+ */
104
+ function safeResolveWithin(base, target) {
105
+ const absoluteBase = node_path_1.default.resolve(base);
106
+ const absoluteTarget = node_path_1.default.resolve(absoluteBase, target);
107
+ const baseWithSep = absoluteBase.endsWith(node_path_1.default.sep)
108
+ ? absoluteBase
109
+ : absoluteBase + node_path_1.default.sep;
110
+ if (absoluteTarget !== absoluteBase &&
111
+ !absoluteTarget.startsWith(baseWithSep)) {
112
+ return null;
113
+ }
114
+ return absoluteTarget;
115
+ }
116
+ exports.safeResolveWithin = safeResolveWithin;
117
+ /**
118
+ * Throws a generic `Error` if the value is not a safe package name.
119
+ */
120
+ function assertValidPackageName(name) {
121
+ if (!isValidPackageName(name)) {
122
+ throw new Error(`Invalid package name: ${JSON.stringify(name)}. Names must match npm package name rules and contain no shell metacharacters.`);
123
+ }
124
+ }
125
+ exports.assertValidPackageName = assertValidPackageName;
126
+ /**
127
+ * Throws a generic `Error` if the value is not a safe version range.
128
+ */
129
+ function assertValidVersion(version) {
130
+ if (!isValidVersion(version)) {
131
+ throw new Error(`Invalid version specifier: ${JSON.stringify(version)}.`);
132
+ }
133
+ }
134
+ exports.assertValidVersion = assertValidVersion;
135
+ /**
136
+ * Throws a generic `Error` if the value is not a safe script name.
137
+ */
138
+ function assertValidScriptName(name) {
139
+ if (!isValidScriptName(name)) {
140
+ throw new Error(`Invalid script name: ${JSON.stringify(name)}. Script names must match ^[a-zA-Z0-9][a-zA-Z0-9:_./-]*$.`);
141
+ }
142
+ }
143
+ exports.assertValidScriptName = assertValidScriptName;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared package-manager command helpers for code generators
3
+ * (Dockerfiles, CI/CD pipelines). These centralize the mapping
4
+ * between an analyzer-detected package manager and the literal
5
+ * shell strings emitted into generated files.
6
+ *
7
+ * Two flavors exist:
8
+ * - `RUN`/`CMD`-style strings used INSIDE Dockerfiles.
9
+ * - Plain shell invocations used in CI scripts and informational
10
+ * comments (no `RUN ` prefix).
11
+ */
12
+ export type SupportedPackageManager = "npm" | "yarn" | "pnpm" | "bun";
13
+ /**
14
+ * Shell invocation that installs project dependencies, suitable for
15
+ * a CI step (no `RUN ` prefix). Uses the strict, lockfile-respecting
16
+ * variant for each package manager because CI runs should be
17
+ * reproducible.
18
+ */
19
+ export declare function getCiInstallCommand(packageManager: string): string;
20
+ /**
21
+ * Shell invocation that runs an npm-style script (e.g. `lint`,
22
+ * `test`, `build`). Used in CI scripts and informational comments.
23
+ */
24
+ export declare function getRunScriptCommand(packageManager: string, scriptName: string): string;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * Shared package-manager command helpers for code generators
4
+ * (Dockerfiles, CI/CD pipelines). These centralize the mapping
5
+ * between an analyzer-detected package manager and the literal
6
+ * shell strings emitted into generated files.
7
+ *
8
+ * Two flavors exist:
9
+ * - `RUN`/`CMD`-style strings used INSIDE Dockerfiles.
10
+ * - Plain shell invocations used in CI scripts and informational
11
+ * comments (no `RUN ` prefix).
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.getRunScriptCommand = exports.getCiInstallCommand = void 0;
15
+ /**
16
+ * Shell invocation that installs project dependencies, suitable for
17
+ * a CI step (no `RUN ` prefix). Uses the strict, lockfile-respecting
18
+ * variant for each package manager because CI runs should be
19
+ * reproducible.
20
+ */
21
+ function getCiInstallCommand(packageManager) {
22
+ switch (packageManager) {
23
+ case "pnpm":
24
+ return "pnpm install --frozen-lockfile";
25
+ case "yarn":
26
+ return "yarn install --frozen-lockfile";
27
+ case "bun":
28
+ return "bun install --frozen-lockfile";
29
+ default:
30
+ return "npm ci";
31
+ }
32
+ }
33
+ exports.getCiInstallCommand = getCiInstallCommand;
34
+ /**
35
+ * Shell invocation that runs an npm-style script (e.g. `lint`,
36
+ * `test`, `build`). Used in CI scripts and informational comments.
37
+ */
38
+ function getRunScriptCommand(packageManager, scriptName) {
39
+ switch (packageManager) {
40
+ case "pnpm":
41
+ return `pnpm run ${scriptName}`;
42
+ case "yarn":
43
+ return `yarn ${scriptName}`;
44
+ case "bun":
45
+ return `bun run ${scriptName}`;
46
+ default:
47
+ return `npm run ${scriptName}`;
48
+ }
49
+ }
50
+ exports.getRunScriptCommand = getRunScriptCommand;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Cross-platform wrappers around `child_process.spawn` / `spawnSync` for
3
+ * launching package-manager binaries (npm, yarn, pnpm, npx, tsx, tsc,
4
+ * docker, etc.) reliably on every supported platform.
5
+ *
6
+ * Why this exists
7
+ * ---------------
8
+ * Starting with Node.js 18.20.2 / 20.12.2 / 21.7.3 (and every v22+), the
9
+ * runtime refuses to spawn `.bat` / `.cmd` files unless `shell: true` is
10
+ * passed (see CVE-2024-27980). On Windows, `npm`, `yarn`, `pnpm`, `npx`,
11
+ * `tsx`, `tsc`, and the `node_modules/.bin/*` shims are all `.cmd` files,
12
+ * so a direct `spawn("npm", [...], { shell: false })` call now fails with
13
+ * `EINVAL` ("Package manager not found"). At the same time, just flipping
14
+ * `shell: true` reintroduces the original CVE: arguments containing shell
15
+ * metacharacters (`|`, `>`, `<`, `^`, `&`, `(`, `)`, ...) get interpreted
16
+ * by `cmd.exe` instead of being passed verbatim, which is a real concern
17
+ * for inputs like a semver range (`>=1.0.0 <2.0.0`) or a user-supplied
18
+ * `--src` flag.
19
+ *
20
+ * `cross-spawn` solves both problems:
21
+ * - On Windows it parses the command, resolves `.cmd` shims via PATHEXT,
22
+ * and invokes `cmd.exe /d /s /c "command args"` with `shell: false` and
23
+ * `windowsVerbatimArguments: true`. Each argv entry is passed through a
24
+ * cmd.exe-aware escaper, so metacharacters stay literal.
25
+ * - On Unix it falls through to plain `spawn` with `shell: false`.
26
+ *
27
+ * The helpers here are thin wrappers that default `windowsHide: true` so
28
+ * the Windows console doesn't flash, and re-export the same options shape
29
+ * as `child_process` for drop-in usage.
30
+ */
31
+ /// <reference types="node" />
32
+ /// <reference types="node" />
33
+ import type { ChildProcess, SpawnOptions, SpawnSyncOptions, SpawnSyncReturns } from "node:child_process";
34
+ export declare function safeSpawn(command: string, args?: ReadonlyArray<string>, options?: SpawnOptions): ChildProcess;
35
+ export declare function safeSpawnSync(command: string, args?: ReadonlyArray<string>, options?: SpawnSyncOptions): SpawnSyncReturns<Buffer>;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * Cross-platform wrappers around `child_process.spawn` / `spawnSync` for
4
+ * launching package-manager binaries (npm, yarn, pnpm, npx, tsx, tsc,
5
+ * docker, etc.) reliably on every supported platform.
6
+ *
7
+ * Why this exists
8
+ * ---------------
9
+ * Starting with Node.js 18.20.2 / 20.12.2 / 21.7.3 (and every v22+), the
10
+ * runtime refuses to spawn `.bat` / `.cmd` files unless `shell: true` is
11
+ * passed (see CVE-2024-27980). On Windows, `npm`, `yarn`, `pnpm`, `npx`,
12
+ * `tsx`, `tsc`, and the `node_modules/.bin/*` shims are all `.cmd` files,
13
+ * so a direct `spawn("npm", [...], { shell: false })` call now fails with
14
+ * `EINVAL` ("Package manager not found"). At the same time, just flipping
15
+ * `shell: true` reintroduces the original CVE: arguments containing shell
16
+ * metacharacters (`|`, `>`, `<`, `^`, `&`, `(`, `)`, ...) get interpreted
17
+ * by `cmd.exe` instead of being passed verbatim, which is a real concern
18
+ * for inputs like a semver range (`>=1.0.0 <2.0.0`) or a user-supplied
19
+ * `--src` flag.
20
+ *
21
+ * `cross-spawn` solves both problems:
22
+ * - On Windows it parses the command, resolves `.cmd` shims via PATHEXT,
23
+ * and invokes `cmd.exe /d /s /c "command args"` with `shell: false` and
24
+ * `windowsVerbatimArguments: true`. Each argv entry is passed through a
25
+ * cmd.exe-aware escaper, so metacharacters stay literal.
26
+ * - On Unix it falls through to plain `spawn` with `shell: false`.
27
+ *
28
+ * The helpers here are thin wrappers that default `windowsHide: true` so
29
+ * the Windows console doesn't flash, and re-export the same options shape
30
+ * as `child_process` for drop-in usage.
31
+ */
32
+ var __importDefault = (this && this.__importDefault) || function (mod) {
33
+ return (mod && mod.__esModule) ? mod : { "default": mod };
34
+ };
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.safeSpawnSync = exports.safeSpawn = void 0;
37
+ const cross_spawn_1 = __importDefault(require("cross-spawn"));
38
+ function safeSpawn(command, args = [], options = {}) {
39
+ return (0, cross_spawn_1.default)(command, args, {
40
+ windowsHide: true,
41
+ ...options,
42
+ });
43
+ }
44
+ exports.safeSpawn = safeSpawn;
45
+ function safeSpawnSync(command, args = [], options = {}) {
46
+ return cross_spawn_1.default.sync(command, args, {
47
+ windowsHide: true,
48
+ ...options,
49
+ });
50
+ }
51
+ exports.safeSpawnSync = safeSpawnSync;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Generate path alias from folder name
3
+ * Handles both default mappings and custom folder names
4
+ *
5
+ * @param folderName - The folder name (e.g., "useCases", "my-custom-folder")
6
+ * @returns The path alias (e.g., "@useCases", "@myCustomFolder")
7
+ */
8
+ export declare function generatePathAlias(folderName: string): string;
9
+ /**
10
+ * Update tsconfig.json paths to include missing aliases for opinionated scaffolding
11
+ * Handles both default folders and custom scaffoldSchematics overrides
12
+ *
13
+ * @param folderName - The folder name where the schematic is being created
14
+ * @param sourceRoot - The source root directory (default: "src")
15
+ */
16
+ export declare function updateTsconfigPaths(folderName: string, sourceRoot?: string): Promise<void>;
17
+ /**
18
+ * Get the path alias for a given folder name
19
+ * Used by other utilities to determine the correct import path
20
+ *
21
+ * @param folderName - The folder name
22
+ * @returns The path alias (e.g., "@useCases")
23
+ */
24
+ export declare function getPathAliasForFolder(folderName: string): string;
25
+ /**
26
+ * Check if tsconfig already has all required path aliases for opinionated mode
27
+ * This can be used to validate project setup
28
+ *
29
+ * @param requiredFolders - List of folder names that need path aliases
30
+ * @returns Object with missing aliases and whether all are present
31
+ */
32
+ export declare function validateTsconfigPaths(requiredFolders: string[]): {
33
+ valid: boolean;
34
+ missingAliases: string[];
35
+ };
@@ -0,0 +1,286 @@
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.validateTsconfigPaths = exports.getPathAliasForFolder = exports.updateTsconfigPaths = exports.generatePathAlias = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const cli_ui_1 = require("./cli-ui");
10
+ /**
11
+ * Default path alias mappings for opinionated scaffolding
12
+ * Maps folder names to their corresponding path aliases
13
+ */
14
+ const DEFAULT_PATH_ALIASES = {
15
+ controllers: "@controllers",
16
+ useCases: "@useCases",
17
+ providers: "@providers",
18
+ entities: "@entities",
19
+ middleware: "@middleware",
20
+ interceptors: "@interceptors",
21
+ events: "@events",
22
+ guards: "@guards",
23
+ config: "@config",
24
+ };
25
+ /**
26
+ * Generate path alias from folder name
27
+ * Handles both default mappings and custom folder names
28
+ *
29
+ * @param folderName - The folder name (e.g., "useCases", "my-custom-folder")
30
+ * @returns The path alias (e.g., "@useCases", "@myCustomFolder")
31
+ */
32
+ function generatePathAlias(folderName) {
33
+ // Check if we have a default mapping
34
+ if (DEFAULT_PATH_ALIASES[folderName]) {
35
+ return DEFAULT_PATH_ALIASES[folderName];
36
+ }
37
+ // For custom folder names, convert to camelCase and add @ prefix
38
+ // Handles: kebab-case, snake_case, PascalCase, camelCase
39
+ const camelCase = folderName
40
+ .split(/[-_]/)
41
+ .map((word, index) => {
42
+ if (index === 0) {
43
+ // First word: keep original case for already camelCase names
44
+ return word.charAt(0).toLowerCase() + word.slice(1);
45
+ }
46
+ // Subsequent words: capitalize first letter
47
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
48
+ })
49
+ .join("");
50
+ return `@${camelCase}`;
51
+ }
52
+ exports.generatePathAlias = generatePathAlias;
53
+ /**
54
+ * Parse JSONC (JSON with Comments) by stripping comments and trailing commas
55
+ * Handles:
56
+ * - Single-line comments (//)
57
+ * - Multi-line comments
58
+ * - Trailing commas (common in tsconfig.json)
59
+ *
60
+ * @param content - The JSONC content
61
+ * @returns Cleaned JSON string that can be parsed by JSON.parse
62
+ */
63
+ function stripJsonComments(content) {
64
+ let result = content;
65
+ // Remove multi-line comments first (they can span multiple lines)
66
+ result = result.replace(/\/\*[\s\S]*?\*\//g, "");
67
+ // Remove single-line comments (but not inside strings)
68
+ // This regex looks for // that are not inside strings
69
+ result = result.replace(/^(\s*)\/\/.*$/gm, "$1");
70
+ // Also handle inline comments after values
71
+ // Match: value, // comment or value // comment
72
+ result = result.replace(/,\s*\/\/.*$/gm, ",");
73
+ result = result.replace(/(["\d\w\]}\s])\s*\/\/.*$/gm, "$1");
74
+ // Remove trailing commas before } or ]
75
+ // This handles cases like: { "key": "value", }
76
+ result = result.replace(/,(\s*[}\]])/g, "$1");
77
+ return result;
78
+ }
79
+ /**
80
+ * Try to parse JSONC content, first stripping comments then parsing
81
+ * @param content - Raw file content
82
+ * @returns Parsed config object or null if parsing fails
83
+ */
84
+ function parseJsonc(content) {
85
+ // Always try to strip comments first for consistency
86
+ const stripped = stripJsonComments(content);
87
+ try {
88
+ return JSON.parse(stripped);
89
+ }
90
+ catch {
91
+ // If stripping didn't help, try the original (unlikely but safe)
92
+ try {
93
+ return JSON.parse(content);
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ }
100
+ /**
101
+ * Text-based fallback to add path alias when JSON parsing fails
102
+ * This preserves the original file format (comments, etc.)
103
+ *
104
+ * @param content - Original file content
105
+ * @param aliasKey - The path alias key (e.g., "@config/*")
106
+ * @param aliasValue - The path alias value (e.g., ["./src/config/*"])
107
+ * @returns Modified content or null if update failed
108
+ */
109
+ function addPathAliasTextBased(content, aliasKey, aliasValue) {
110
+ // Check if the alias already exists
111
+ if (content.includes(`"${aliasKey}"`)) {
112
+ return null; // Already exists, no update needed
113
+ }
114
+ // Find the "paths" object
115
+ const pathsMatch = content.match(/"paths"\s*:\s*\{/);
116
+ if (pathsMatch && pathsMatch.index !== undefined) {
117
+ // Insert new path alias after "paths": {
118
+ const insertPos = pathsMatch.index + pathsMatch[0].length;
119
+ const aliasEntry = `\n\t\t\t"${aliasKey}": ${JSON.stringify(aliasValue)},`;
120
+ return (content.slice(0, insertPos) + aliasEntry + content.slice(insertPos));
121
+ }
122
+ // Find compilerOptions to add paths object
123
+ const compilerOptionsMatch = content.match(/"compilerOptions"\s*:\s*\{/);
124
+ if (compilerOptionsMatch && compilerOptionsMatch.index !== undefined) {
125
+ // Check if baseUrl exists
126
+ const hasBaseUrl = /"baseUrl"\s*:/.test(content);
127
+ // Find the end of compilerOptions opening brace
128
+ const insertPos = compilerOptionsMatch.index + compilerOptionsMatch[0].length;
129
+ let insertion = "\n";
130
+ if (!hasBaseUrl) {
131
+ insertion += '\t\t"baseUrl": ".",\n';
132
+ }
133
+ insertion += `\t\t"paths": {\n\t\t\t"${aliasKey}": ${JSON.stringify(aliasValue)}\n\t\t},`;
134
+ return (content.slice(0, insertPos) + insertion + content.slice(insertPos));
135
+ }
136
+ // Can't find compilerOptions, give up
137
+ return null;
138
+ }
139
+ /**
140
+ * Update tsconfig.json paths to include missing aliases for opinionated scaffolding
141
+ * Handles both default folders and custom scaffoldSchematics overrides
142
+ *
143
+ * @param folderName - The folder name where the schematic is being created
144
+ * @param sourceRoot - The source root directory (default: "src")
145
+ */
146
+ async function updateTsconfigPaths(folderName, sourceRoot = "src") {
147
+ if (!folderName) {
148
+ return; // No folder specified
149
+ }
150
+ const tsconfigPath = node_path_1.default.join(process.cwd(), "tsconfig.json");
151
+ const tsconfigBuildPath = node_path_1.default.join(process.cwd(), "tsconfig.build.json");
152
+ // Generate alias from folder name (handles custom names)
153
+ const alias = generatePathAlias(folderName);
154
+ const aliasKey = `${alias}/*`;
155
+ // Track if we updated any config
156
+ let updated = false;
157
+ // Update both tsconfig files if they exist
158
+ for (const configPath of [tsconfigPath, tsconfigBuildPath]) {
159
+ if (!node_fs_1.default.existsSync(configPath)) {
160
+ continue;
161
+ }
162
+ try {
163
+ const configContent = node_fs_1.default.readFileSync(configPath, "utf-8");
164
+ // Try JSON parsing first
165
+ const config = parseJsonc(configContent);
166
+ if (config) {
167
+ // JSON parsing succeeded - use structured approach
168
+ // Ensure compilerOptions exists
169
+ if (!config.compilerOptions) {
170
+ config.compilerOptions = {};
171
+ }
172
+ const compilerOptions = config.compilerOptions;
173
+ // Ensure baseUrl is set (required for paths to work)
174
+ if (!compilerOptions.baseUrl) {
175
+ compilerOptions.baseUrl = ".";
176
+ }
177
+ // Determine the correct path value based on baseUrl
178
+ // If baseUrl is "./src" or "src", paths should be relative to src
179
+ // If baseUrl is ".", paths should include the full path from root
180
+ const baseUrl = compilerOptions.baseUrl || ".";
181
+ const isBaseUrlSrc = baseUrl === `./${sourceRoot}` || baseUrl === sourceRoot;
182
+ const aliasValue = isBaseUrlSrc
183
+ ? [`./${folderName}/*`]
184
+ : [`./${sourceRoot}/${folderName}/*`];
185
+ // Ensure paths object exists
186
+ if (!compilerOptions.paths) {
187
+ compilerOptions.paths = {};
188
+ }
189
+ const paths = compilerOptions.paths;
190
+ // Only add if it doesn't exist
191
+ if (!paths[aliasKey]) {
192
+ paths[aliasKey] = aliasValue;
193
+ // Write back to file with proper formatting (tab indent)
194
+ node_fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, "\t") + "\n", "utf-8");
195
+ updated = true;
196
+ }
197
+ }
198
+ else {
199
+ // JSON parsing failed - use text-based fallback
200
+ // This preserves comments and formatting
201
+ // Try to detect baseUrl from content
202
+ const baseUrlMatch = configContent.match(/"baseUrl"\s*:\s*"([^"]+)"/);
203
+ const baseUrl = baseUrlMatch ? baseUrlMatch[1] : ".";
204
+ const isBaseUrlSrc = baseUrl === `./${sourceRoot}` || baseUrl === sourceRoot;
205
+ const aliasValue = isBaseUrlSrc
206
+ ? [`./${folderName}/*`]
207
+ : [`./${sourceRoot}/${folderName}/*`];
208
+ const modifiedContent = addPathAliasTextBased(configContent, aliasKey, aliasValue);
209
+ if (modifiedContent) {
210
+ node_fs_1.default.writeFileSync(configPath, modifiedContent, "utf-8");
211
+ updated = true;
212
+ }
213
+ else if (!configContent.includes(`"${aliasKey}"`)) {
214
+ // Couldn't update and alias doesn't exist
215
+ (0, cli_ui_1.printWarning)(`Could not update ${node_path_1.default.basename(configPath)}. Please add "${aliasKey}" path alias manually`, "tsconfig");
216
+ }
217
+ }
218
+ }
219
+ catch (error) {
220
+ // Log warning but don't fail the scaffolding process
221
+ (0, cli_ui_1.printWarning)(`Could not update ${node_path_1.default.basename(configPath)}: ${error instanceof Error ? error.message : "Unknown error"}`, "tsconfig");
222
+ }
223
+ }
224
+ // Print success message only if we updated something
225
+ if (updated) {
226
+ (0, cli_ui_1.printInfo)(`Path alias ${aliasKey} added`, "tsconfig");
227
+ }
228
+ }
229
+ exports.updateTsconfigPaths = updateTsconfigPaths;
230
+ /**
231
+ * Get the path alias for a given folder name
232
+ * Used by other utilities to determine the correct import path
233
+ *
234
+ * @param folderName - The folder name
235
+ * @returns The path alias (e.g., "@useCases")
236
+ */
237
+ function getPathAliasForFolder(folderName) {
238
+ return generatePathAlias(folderName);
239
+ }
240
+ exports.getPathAliasForFolder = getPathAliasForFolder;
241
+ /**
242
+ * Check if tsconfig already has all required path aliases for opinionated mode
243
+ * This can be used to validate project setup
244
+ *
245
+ * @param requiredFolders - List of folder names that need path aliases
246
+ * @returns Object with missing aliases and whether all are present
247
+ */
248
+ function validateTsconfigPaths(requiredFolders) {
249
+ const tsconfigPath = node_path_1.default.join(process.cwd(), "tsconfig.json");
250
+ if (!node_fs_1.default.existsSync(tsconfigPath)) {
251
+ return {
252
+ valid: false,
253
+ missingAliases: requiredFolders.map((f) => generatePathAlias(f)),
254
+ };
255
+ }
256
+ try {
257
+ const configContent = node_fs_1.default.readFileSync(tsconfigPath, "utf-8");
258
+ let config;
259
+ try {
260
+ config = JSON.parse(configContent);
261
+ }
262
+ catch {
263
+ config = JSON.parse(stripJsonComments(configContent));
264
+ }
265
+ const paths = config.compilerOptions
266
+ ?.paths || {};
267
+ const missingAliases = [];
268
+ for (const folder of requiredFolders) {
269
+ const aliasKey = `${generatePathAlias(folder)}/*`;
270
+ if (!paths[aliasKey]) {
271
+ missingAliases.push(aliasKey);
272
+ }
273
+ }
274
+ return {
275
+ valid: missingAliases.length === 0,
276
+ missingAliases,
277
+ };
278
+ }
279
+ catch {
280
+ return {
281
+ valid: false,
282
+ missingAliases: requiredFolders.map((f) => generatePathAlias(f)),
283
+ };
284
+ }
285
+ }
286
+ exports.validateTsconfigPaths = validateTsconfigPaths;