zudojs-cli 1.2.1 → 2.0.1

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 (109) hide show
  1. package/README.md +43 -1
  2. package/dist/src/adapters/frontend/flutter.adapter.d.ts +9 -0
  3. package/dist/src/adapters/frontend/flutter.adapter.js +16 -1
  4. package/dist/src/bin/zudojs.js +16 -5
  5. package/dist/src/cliApplication/cliApplication.builtins.d.ts +4 -0
  6. package/dist/src/cliApplication/cliApplication.builtins.js +28 -9
  7. package/dist/src/cliApplication/cliApplication.core.d.ts +8 -0
  8. package/dist/src/cliApplication/cliApplication.core.js +45 -5
  9. package/dist/src/cliApplication/cliApplication.help.d.ts +17 -0
  10. package/dist/src/cliApplication/cliApplication.help.js +94 -0
  11. package/dist/src/cliApplication/cliApplication.logger.js +4 -4
  12. package/dist/src/cliCommand/cliCommand.validator.js +22 -0
  13. package/dist/src/cliConstant/cliConstant.value.d.ts +18 -12
  14. package/dist/src/cliConstant/cliConstant.value.js +25 -19
  15. package/dist/src/cliError/cliError.argument.js +2 -2
  16. package/dist/src/cliError/cliError.base.js +28 -5
  17. package/dist/src/cliError/cliError.command.js +5 -2
  18. package/dist/src/cliError/cliError.execution.js +3 -3
  19. package/dist/src/cliParser/cliParser.core.js +11 -5
  20. package/dist/src/cliParser/cliParser.helper.d.ts +11 -0
  21. package/dist/src/cliParser/cliParser.helper.js +19 -0
  22. package/dist/src/cliParser/cliParser.longOption.js +4 -4
  23. package/dist/src/cliParser/cliParser.shortOption.js +7 -4
  24. package/dist/src/cliVersion/cliVersion.update.js +5 -3
  25. package/dist/src/commands/add.command.js +17 -2
  26. package/dist/src/commands/commandError.helper.d.ts +15 -0
  27. package/dist/src/commands/commandError.helper.js +28 -0
  28. package/dist/src/commands/create.command.js +284 -46
  29. package/dist/src/commands/doctor.command.d.ts +8 -0
  30. package/dist/src/commands/doctor.command.js +38 -5
  31. package/dist/src/commands/generate.command.js +71 -32
  32. package/dist/src/constants/index.d.ts +40 -68
  33. package/dist/src/constants/index.js +73 -23
  34. package/dist/src/generators/frontend/frontendPipeline.js +16 -4
  35. package/dist/src/generators/integration/integrationGenerator.core.js +10 -6
  36. package/dist/src/generators/project/project.generator.js +5 -1
  37. package/dist/src/index.d.ts +4 -4
  38. package/dist/src/index.js +3 -3
  39. package/dist/src/manifest/index.d.ts +1 -0
  40. package/dist/src/manifest/index.js +1 -0
  41. package/dist/src/manifest/manifestFile.helper.d.ts +42 -0
  42. package/dist/src/manifest/manifestFile.helper.js +141 -0
  43. package/dist/src/manifest/manifestManager.core.d.ts +16 -0
  44. package/dist/src/manifest/manifestManager.core.js +65 -35
  45. package/dist/src/prompts/backend/api-style.prompt.js +2 -5
  46. package/dist/src/prompts/backend/backend-architecture.prompt.js +5 -22
  47. package/dist/src/prompts/backend/database.prompt.js +5 -23
  48. package/dist/src/prompts/backend/services.prompt.js +12 -10
  49. package/dist/src/prompts/cancel.prompt.d.ts +18 -0
  50. package/dist/src/prompts/cancel.prompt.js +26 -0
  51. package/dist/src/prompts/capabilities/capabilities.prompt.js +2 -5
  52. package/dist/src/prompts/frontend/framework.prompt.js +2 -5
  53. package/dist/src/prompts/frontend/frontend-architecture.prompt.js +2 -5
  54. package/dist/src/prompts/index.d.ts +1 -0
  55. package/dist/src/prompts/index.js +1 -0
  56. package/dist/src/prompts/project/confirmation.prompt.js +2 -5
  57. package/dist/src/prompts/project/project-name.prompt.js +6 -7
  58. package/dist/src/prompts/project/project-type.prompt.js +2 -5
  59. package/dist/src/prompts/workspace/package-manager.prompt.js +5 -12
  60. package/dist/src/registries/adapter/frontendAdapterRegistry.core.d.ts +6 -0
  61. package/dist/src/registries/adapter/frontendAdapterRegistry.core.js +12 -0
  62. package/dist/src/registries/adapter/packageManagerRegistry.core.d.ts +5 -0
  63. package/dist/src/registries/adapter/packageManagerRegistry.core.js +11 -0
  64. package/dist/src/registries/dependency/dependencyRegistry.core.d.ts +9 -0
  65. package/dist/src/registries/dependency/dependencyRegistry.core.js +16 -0
  66. package/dist/src/registries/generator/generatorRegistry.core.d.ts +8 -0
  67. package/dist/src/registries/generator/generatorRegistry.core.js +14 -0
  68. package/dist/src/resolvers/capability/capabilityResolver.core.d.ts +1 -1
  69. package/dist/src/resolvers/capability/capabilityResolver.core.js +0 -2
  70. package/dist/src/resolvers/layout/projectLayout.core.js +1 -6
  71. package/dist/src/resolvers/project.resolver.d.ts +9 -3
  72. package/dist/src/resolvers/project.resolver.js +11 -7
  73. package/dist/src/rollback/index.d.ts +1 -1
  74. package/dist/src/rollback/index.js +1 -1
  75. package/dist/src/rollback/rollbackManager.core.d.ts +19 -1
  76. package/dist/src/rollback/rollbackManager.core.js +21 -2
  77. package/dist/src/runners/package-manager/index.d.ts +1 -1
  78. package/dist/src/runners/package-manager/index.js +1 -1
  79. package/dist/src/runners/package-manager/packageManagerRunner.core.d.ts +13 -1
  80. package/dist/src/runners/package-manager/packageManagerRunner.core.js +19 -1
  81. package/dist/src/runners/process/processRunner.core.d.ts +11 -0
  82. package/dist/src/runners/process/processRunner.core.js +41 -6
  83. package/dist/src/runners/task/index.d.ts +1 -1
  84. package/dist/src/runners/task/taskRunner.core.d.ts +22 -1
  85. package/dist/src/runners/task/taskRunner.core.js +35 -6
  86. package/dist/src/scaffolders/framework.scaffolder.js +3 -0
  87. package/dist/src/scaffolders/scaffolder.helper.d.ts +10 -0
  88. package/dist/src/scaffolders/scaffolder.helper.js +25 -2
  89. package/dist/src/templates/microservice/index.d.ts +1 -1
  90. package/dist/src/templates/microservice/index.js +1 -1
  91. package/dist/src/templates/microservice/microservice.template.d.ts +11 -4
  92. package/dist/src/templates/microservice/microservice.template.js +40 -19
  93. package/dist/src/templates/modular-monolith/modularMonolith.template.js +27 -8
  94. package/dist/src/templates/monolith/monolith.template.js +11 -17
  95. package/dist/src/templates/shared/capability.template.d.ts +25 -0
  96. package/dist/src/templates/shared/capability.template.js +46 -0
  97. package/dist/src/templates/shared/index.d.ts +1 -0
  98. package/dist/src/templates/shared/index.js +1 -0
  99. package/dist/src/utils/utils.exec.d.ts +22 -0
  100. package/dist/src/utils/utils.exec.js +51 -4
  101. package/dist/src/utils/utils.fileSystem.js +46 -2
  102. package/dist/src/utils/utils.name.d.ts +9 -0
  103. package/dist/src/utils/utils.name.js +29 -3
  104. package/dist/src/validators/environment/environmentValidator.core.d.ts +11 -1
  105. package/dist/src/validators/environment/environmentValidator.core.js +40 -73
  106. package/dist/src/validators/project/index.d.ts +1 -1
  107. package/dist/src/validators/project/projectValidator.core.d.ts +11 -1
  108. package/dist/src/validators/project/projectValidator.core.js +20 -4
  109. package/package.json +1 -1
@@ -9,5 +9,15 @@ export interface ScaffolderOptions {
9
9
  readonly targetPath: string;
10
10
  readonly fallbackFiles: Record<string, string>;
11
11
  }
12
+ /**
13
+ * Timeout for an official framework scaffolder, in milliseconds.
14
+ *
15
+ * `npm create vite@latest` and `npx create-next-app@latest` download a
16
+ * package tree from the registry, which routinely takes several minutes on
17
+ * a cold cache or a slow link. The 2-minute default of `execCommand` killed
18
+ * them mid-download and the user silently received the built-in fallback
19
+ * template instead of the real framework scaffold.
20
+ */
21
+ export declare const SCAFFOLD_TIMEOUT_MS = 900000;
12
22
  export declare function scaffoldWithFallback(options: ScaffolderOptions): Promise<boolean>;
13
23
  //# sourceMappingURL=scaffolder.helper.d.ts.map
@@ -5,17 +5,40 @@
5
5
  */
6
6
  import { execCommand } from "../utils/utils.exec.js";
7
7
  import { writeFileTree } from "../utils/utils.fileSystem.js";
8
+ /**
9
+ * Timeout for an official framework scaffolder, in milliseconds.
10
+ *
11
+ * `npm create vite@latest` and `npx create-next-app@latest` download a
12
+ * package tree from the registry, which routinely takes several minutes on
13
+ * a cold cache or a slow link. The 2-minute default of `execCommand` killed
14
+ * them mid-download and the user silently received the built-in fallback
15
+ * template instead of the real framework scaffold.
16
+ */
17
+ export const SCAFFOLD_TIMEOUT_MS = 900000;
18
+ /** Describes why a scaffolder failed, including the child's own stderr. */
19
+ function describeFailure(error) {
20
+ if (!(error instanceof Error))
21
+ return String(error);
22
+ const killed = error.killed === true;
23
+ const signal = error.signal;
24
+ const stderr = error.stderr;
25
+ const detail = typeof stderr === "string" ? stderr.trim() : "";
26
+ const headline = killed && (signal === "SIGTERM" || signal === null || signal === undefined)
27
+ ? `timed out after ${Math.round(SCAFFOLD_TIMEOUT_MS / 1000)}s`
28
+ : `failed: ${error.message.split("\n")[0]}`;
29
+ return detail ? `${headline}; ${detail}` : headline;
30
+ }
8
31
  export async function scaffoldWithFallback(options) {
9
32
  try {
10
33
  await execCommand(options.command, Array.from(options.args), options.targetPath, {
11
34
  // Suppress interactive prompts from create-* tools.
12
35
  env: { ...process.env, CI: "1" },
36
+ timeout: SCAFFOLD_TIMEOUT_MS,
13
37
  });
14
38
  return true;
15
39
  }
16
40
  catch (error) {
17
- const message = error instanceof Error ? error.message : String(error);
18
- console.warn(`Warning: official scaffolder "${options.command} ${options.args.join(" ")}" failed (${message.split("\n")[0]}). Using built-in fallback template.`);
41
+ console.warn(`Warning: official scaffolder "${options.command} ${options.args.join(" ")}" ${describeFailure(error)}. Using built-in fallback template.`);
19
42
  await writeFileTree(options.targetPath, options.fallbackFiles);
20
43
  return false;
21
44
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * zudojs-cli — Microservice Template Index
3
3
  */
4
- export { generateMicroserviceFiles, resolveMicroserviceServices, DEFAULT_MICROSERVICE_SERVICES, RESERVED_MICROSERVICE_APP_NAMES, } from "./microservice.template.js";
4
+ export { generateMicroserviceFiles, resolveMicroserviceServices, RESERVED_MICROSERVICE_APP_NAMES, } from "./microservice.template.js";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * zudojs-cli — Microservice Template Index
3
3
  */
4
- export { generateMicroserviceFiles, resolveMicroserviceServices, DEFAULT_MICROSERVICE_SERVICES, RESERVED_MICROSERVICE_APP_NAMES, } from "./microservice.template.js";
4
+ export { generateMicroserviceFiles, resolveMicroserviceServices, RESERVED_MICROSERVICE_APP_NAMES, } from "./microservice.template.js";
5
5
  //# sourceMappingURL=index.js.map
@@ -22,8 +22,6 @@
22
22
  * ```
23
23
  */
24
24
  import type { ScaffoldOptions } from "../../types/index.js";
25
- /** Default service names used when none are provided. */
26
- export declare const DEFAULT_MICROSERVICE_SERVICES: readonly ["identity", "enrollment", "assessment", "notification"];
27
25
  /**
28
26
  * The gateway app is always generated at `apps/gateway`, so "gateway" is a
29
27
  * reserved name: a service called `gateway` would produce a second, competing
@@ -32,8 +30,17 @@ export declare const DEFAULT_MICROSERVICE_SERVICES: readonly ["identity", "enrol
32
30
  export declare const RESERVED_MICROSERVICE_APP_NAMES: readonly ["gateway"];
33
31
  /**
34
32
  * Normalizes a requested service list into the service apps generated under
35
- * `apps/services/`. Reserved names and duplicates are dropped; an empty list
36
- * falls back to DEFAULT_MICROSERVICE_SERVICES.
33
+ * `apps/services/`. Reserved names and duplicates are dropped.
34
+ *
35
+ * An empty request yields an empty list: a new project gets the gateway and
36
+ * nothing else. This used to substitute four example names
37
+ * (`identity`, `enrollment`, `assessment`, `notification`), which put four
38
+ * domains nobody asked for into every microservice project and left the
39
+ * author deleting them before they could start. Services are created when
40
+ * they are named — here, or in another `create` run. `generate service` is
41
+ * refused in a microservice project, because a service is a whole workspace
42
+ * app (package.json, tsconfig, Dockerfile, port) that the schematic does not
43
+ * produce; `generate module --service <name>` adds to an existing app.
37
44
  */
38
45
  export declare function resolveMicroserviceServices(requested: readonly string[]): readonly string[];
39
46
  export declare function generateMicroserviceFiles(options: ScaffoldOptions): Record<string, string>;
@@ -23,14 +23,7 @@
23
23
  */
24
24
  import { ZUDOJS_PACKAGES_VERSION } from "../../constants/index.js";
25
25
  import { normalizeName } from "../../utils/utils.name.js";
26
- import { RUNTIME_APP_DEPENDENCIES, moduleSpec, renderAppPackageDockerfile, renderAppFile, renderModuleFile, renderServerFile, renderPnpmWorkspaceFile, } from "../shared/index.js";
27
- /** Default service names used when none are provided. */
28
- export const DEFAULT_MICROSERVICE_SERVICES = [
29
- "identity",
30
- "enrollment",
31
- "assessment",
32
- "notification",
33
- ];
26
+ import { RUNTIME_APP_DEPENDENCIES, capabilityPackages, moduleSpec, renderAppPackageDockerfile, renderAppFile, renderModuleFile, renderServerFile, renderPnpmWorkspaceFile, resolveProjectCapabilities, } from "../shared/index.js";
34
27
  /**
35
28
  * The gateway app is always generated at `apps/gateway`, so "gateway" is a
36
29
  * reserved name: a service called `gateway` would produce a second, competing
@@ -39,22 +32,35 @@ export const DEFAULT_MICROSERVICE_SERVICES = [
39
32
  export const RESERVED_MICROSERVICE_APP_NAMES = ["gateway"];
40
33
  /**
41
34
  * Normalizes a requested service list into the service apps generated under
42
- * `apps/services/`. Reserved names and duplicates are dropped; an empty list
43
- * falls back to DEFAULT_MICROSERVICE_SERVICES.
35
+ * `apps/services/`. Reserved names and duplicates are dropped.
36
+ *
37
+ * An empty request yields an empty list: a new project gets the gateway and
38
+ * nothing else. This used to substitute four example names
39
+ * (`identity`, `enrollment`, `assessment`, `notification`), which put four
40
+ * domains nobody asked for into every microservice project and left the
41
+ * author deleting them before they could start. Services are created when
42
+ * they are named — here, or in another `create` run. `generate service` is
43
+ * refused in a microservice project, because a service is a whole workspace
44
+ * app (package.json, tsconfig, Dockerfile, port) that the schematic does not
45
+ * produce; `generate module --service <name>` adds to an existing app.
44
46
  */
45
47
  export function resolveMicroserviceServices(requested) {
46
48
  const normalized = requested
47
49
  .map((service) => normalizeName(service))
48
50
  .filter((service) => service.length > 0 &&
49
51
  !RESERVED_MICROSERVICE_APP_NAMES.some((reserved) => reserved === service));
50
- const unique = [...new Set(normalized)];
51
- return unique.length > 0 ? unique : [...DEFAULT_MICROSERVICE_SERVICES];
52
+ return [...new Set(normalized)];
52
53
  }
53
54
  export function generateMicroserviceFiles(options) {
54
55
  const nameSlug = options.projectName
55
56
  .replace(/[^a-z0-9-]+/gi, "-")
56
57
  .toLowerCase();
57
58
  const services = resolveMicroserviceServices(options.services);
59
+ // The workspace root records the capabilities for the project as a whole
60
+ // (`zudojs info` reads it when there is no manifest); the service apps
61
+ // record them next to the dependencies that back them, which is where
62
+ // `zudojs add` writes and `zudojs doctor` reads.
63
+ const capabilities = resolveProjectCapabilities(options);
58
64
  const files = {};
59
65
  // Root package.json (workspace root)
60
66
  const rootScripts = {
@@ -91,7 +97,7 @@ export function generateMicroserviceFiles(options) {
91
97
  zudojs: {
92
98
  projectType: "backend",
93
99
  architecture: "microservice",
94
- features: [],
100
+ features: capabilities,
95
101
  },
96
102
  ...(options.packageManager === "pnpm"
97
103
  ? {}
@@ -152,7 +158,21 @@ A microservice architecture built with the Zudojs framework.
152
158
  ## Services
153
159
 
154
160
  - **gateway** - Port 3000
155
- ${services.map((s, i) => `- **${s}** - Port ${3001 + i}`).join("\n")}
161
+ ${services.length > 0
162
+ ? services.map((s, i) => `- **${s}** - Port ${3001 + i}`).join("\n")
163
+ : `
164
+ No services yet. A service is a whole workspace app, so it is created with
165
+ \`create\` rather than by a schematic:
166
+
167
+ \`\`\`bash
168
+ npx zudojs create <project> --architecture microservice --services <name>
169
+ \`\`\`
170
+
171
+ To add domain logic to an app that already exists, generate a module into it:
172
+
173
+ \`\`\`bash
174
+ npx zudojs generate module <name> --service <existing-service>
175
+ \`\`\``}
156
176
 
157
177
  ## Getting Started
158
178
 
@@ -171,8 +191,10 @@ docker-compose up
171
191
 
172
192
  MIT
173
193
  `;
174
- // Shared types
175
- files["src/types/index.ts"] = ``;
194
+ // No `src/` at the workspace root: there is no package.json, tsconfig or
195
+ // app there, so nothing would compile a file placed in it. Every app owns
196
+ // its own `src/types/`, and code genuinely shared between apps belongs in a
197
+ // workspace package the author creates deliberately.
176
198
  const appTsconfig = `{
177
199
  "compilerOptions": {
178
200
  "target": "ES2024",
@@ -242,10 +264,8 @@ MIT
242
264
  "@zudojs/config",
243
265
  "@zudojs/errors",
244
266
  "@zudojs/http",
267
+ ...capabilityPackages(capabilities),
245
268
  ];
246
- if (options.enableCQRS) {
247
- serviceDeps.push("@zudojs/cqrs");
248
- }
249
269
  for (const svc of services) {
250
270
  const svcName = svc;
251
271
  const svcIndex = services.indexOf(svcName);
@@ -256,6 +276,7 @@ MIT
256
276
  version: "0.1.0",
257
277
  private: true,
258
278
  type: "module",
279
+ zudojs: { features: capabilities },
259
280
  scripts: {
260
281
  dev: "tsx watch src/server.ts",
261
282
  start: "node dist/server.js",
@@ -34,17 +34,25 @@
34
34
  import { renderDatabaseEnv } from "../../adapters/databases/databaseAdapter.resolver.js";
35
35
  import { ZUDOJS_PACKAGES_VERSION } from "../../constants/index.js";
36
36
  import { normalizeName } from "../../utils/utils.name.js";
37
- import { RUNTIME_APP_DEPENDENCIES, moduleSpec, renderAppFile, renderModuleFile, renderServerFile, renderPnpmWorkspaceFile, } from "../shared/index.js";
37
+ import { RUNTIME_APP_DEPENDENCIES, capabilityPackages, moduleSpec, renderAppFile, renderModuleFile, renderServerFile, renderPnpmWorkspaceFile, resolveProjectCapabilities, } from "../shared/index.js";
38
38
  export function generateModularMonolithFiles(options) {
39
39
  const nameSlug = options.projectName
40
40
  .replace(/[^a-z0-9-]+/gi, "-")
41
41
  .toLowerCase();
42
42
  // Normalized: each name becomes a directory segment and a class name.
43
- const modules = (options.services.length > 0
44
- ? options.services
45
- : ["identity", "enrollment", "assessment"])
43
+ //
44
+ // An empty list generates no modules. This used to substitute three example
45
+ // names (`identity`, `enrollment`, `assessment`), so every modular monolith
46
+ // arrived with three domains nobody asked for. Modules are created when they
47
+ // are named — with `--services`, or later with `zudojs generate module <name>`.
48
+ const modules = options.services
46
49
  .map((m) => normalizeName(m))
47
50
  .filter((m) => m.length > 0);
51
+ // Recorded in package.json and backed by the packages below, so the
52
+ // `zudojs doctor` feature check has something real to verify. The
53
+ // architecture's own CQRS structure means cqrs/messaging are always
54
+ // installed, whether or not they were requested as capabilities.
55
+ const capabilities = resolveProjectCapabilities(options);
48
56
  const deps = [
49
57
  ...RUNTIME_APP_DEPENDENCIES,
50
58
  "@zudojs/config",
@@ -54,6 +62,7 @@ export function generateModularMonolithFiles(options) {
54
62
  "@zudojs/cqrs",
55
63
  "@zudojs/messaging",
56
64
  "@zudojs/http",
65
+ ...capabilityPackages(capabilities),
57
66
  ];
58
67
  const devDeps = {
59
68
  tsx: "^4.7.0",
@@ -73,7 +82,7 @@ export function generateModularMonolithFiles(options) {
73
82
  zudojs: {
74
83
  projectType: "backend",
75
84
  architecture: "modular-monolith",
76
- features: [],
85
+ features: capabilities,
77
86
  },
78
87
  scripts: {
79
88
  dev: "tsx watch src/server.ts",
@@ -121,9 +130,17 @@ A modular monolith built with the Zudojs framework.
121
130
 
122
131
  ## Architecture
123
132
 
124
- This project uses a **modular monolith** architecture with the following modules:
133
+ This project uses a **modular monolith** architecture.
125
134
 
126
- ${modules.map((m) => `- **${m}**`).join("\n")}
135
+ ${modules.length > 0
136
+ ? `Modules:
137
+
138
+ ${modules.map((m) => `- **${m}**`).join("\n")}`
139
+ : `No modules yet. Add one with:
140
+
141
+ \`\`\`bash
142
+ npx zudojs generate module <name>
143
+ \`\`\``}
127
144
 
128
145
  ## Getting Started
129
146
 
@@ -182,7 +199,9 @@ MIT
182
199
  files[`src/modules/${spec.name}/commands/index.ts`] = ``;
183
200
  files[`src/modules/${spec.name}/queries/index.ts`] = ``;
184
201
  }
185
- files["tests/index.ts"] = `import { describe, it, expect } from "vitest";
202
+ // The name must match vitest's default `include`
203
+ // (`**/*.{test,spec}.?(c|m)[jt]s?(x)`); `tests/index.ts` did not.
204
+ files["tests/app.test.ts"] = `import { describe, it, expect } from "vitest";
186
205
 
187
206
  describe("Application", () => {
188
207
  it("should bootstrap correctly", () => {
@@ -40,10 +40,13 @@
40
40
  import { renderDatabaseEnv } from "../../adapters/databases/databaseAdapter.resolver.js";
41
41
  import { ZUDOJS_PACKAGES_VERSION } from "../../constants/index.js";
42
42
  import { normalizeName, toPascalCase } from "../../utils/utils.name.js";
43
- import { RUNTIME_APP_DEPENDENCIES, moduleSpec, renderAppFile, renderModuleFile, renderServerFile, renderServiceFile, renderPnpmWorkspaceFile, } from "../shared/index.js";
43
+ import { RUNTIME_APP_DEPENDENCIES, capabilityPackages, moduleSpec, renderAppFile, renderModuleFile, renderServerFile, renderServiceFile, renderPnpmWorkspaceFile, resolveProjectCapabilities, } from "../shared/index.js";
44
44
  export function generateMonolithFiles(options) {
45
45
  const name = options.projectName;
46
46
  const nameSlug = name.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
47
+ // Recorded in package.json and backed by the packages below, so the
48
+ // `zudojs doctor` feature check has something real to verify.
49
+ const capabilities = resolveProjectCapabilities(options);
47
50
  const deps = [
48
51
  ...RUNTIME_APP_DEPENDENCIES,
49
52
  "@zudojs/config",
@@ -52,21 +55,10 @@ export function generateMonolithFiles(options) {
52
55
  "@zudojs/validation",
53
56
  "@zudojs/schema",
54
57
  "@zudojs/http",
58
+ ...capabilityPackages(capabilities),
55
59
  ];
56
60
  if (options.enableCQRS) {
57
- deps.push("@zudojs/cqrs", "@zudojs/events", "@zudojs/messaging");
58
- }
59
- if (options.enableDatabase) {
60
- deps.push("@zudojs/database");
61
- }
62
- if (options.enableQueue) {
63
- deps.push("@zudojs/queue");
64
- }
65
- if (options.enableObservability) {
66
- deps.push("@zudojs/observability");
67
- }
68
- if (options.enableOpenAPI) {
69
- deps.push("@zudojs/openapi");
61
+ deps.push("@zudojs/events");
70
62
  }
71
63
  const devDeps = {
72
64
  tsx: "^4.7.0",
@@ -86,7 +78,7 @@ export function generateMonolithFiles(options) {
86
78
  zudojs: {
87
79
  projectType: "backend",
88
80
  architecture: "monolith",
89
- features: [],
81
+ features: capabilities,
90
82
  },
91
83
  scripts: {
92
84
  dev: "tsx watch src/server.ts",
@@ -240,8 +232,10 @@ MIT
240
232
  files["src/controllers/index.ts"] =
241
233
  `export { HealthController } from "./health.controller.js";
242
234
  `;
243
- // Tests
244
- files["tests/index.ts"] = `import { describe, it, expect } from "vitest";
235
+ // Tests. The file name must match vitest's default `include`
236
+ // (`**/*.{test,spec}.?(c|m)[jt]s?(x)`); `tests/index.ts` did not, so the
237
+ // generated `test` script exited 1 with "No test files found".
238
+ files["tests/app.test.ts"] = `import { describe, it, expect } from "vitest";
245
239
 
246
240
  describe("Application", () => {
247
241
  it("should be configured correctly", () => {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * zudojs-cli — Capabilities a generated project declares.
3
+ *
4
+ * Every template used to stamp `zudojs.features: []` into the package.json it
5
+ * wrote, although it had already used the `enable*` flags to choose the
6
+ * project's dependencies. `zudojs doctor` reads only `pkg.zudojs.features`,
7
+ * so its check ("every declared feature has its package") passed vacuously in
8
+ * every project the CLI created, and `zudojs add` was the only thing that
9
+ * could ever put a value there.
10
+ *
11
+ * The list produced here is the same one `zudojs create` records as
12
+ * `capabilities` in `.zudojs/manifest.json`, and {@link capabilityPackages}
13
+ * gives the packages that back it, so a declared feature is always installed.
14
+ */
15
+ import type { ScaffoldOptions } from "../../types/index.js";
16
+ /**
17
+ * The capability ids a scaffolded project was created with.
18
+ */
19
+ export declare function resolveProjectCapabilities(options: ScaffoldOptions): readonly string[];
20
+ /**
21
+ * The `@zudojs/*` packages that back `capabilities`, using the same mapping
22
+ * `zudojs add` and `zudojs doctor` use.
23
+ */
24
+ export declare function capabilityPackages(capabilities: readonly string[]): readonly string[];
25
+ //# sourceMappingURL=capability.template.d.ts.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * zudojs-cli — Capabilities a generated project declares.
3
+ *
4
+ * Every template used to stamp `zudojs.features: []` into the package.json it
5
+ * wrote, although it had already used the `enable*` flags to choose the
6
+ * project's dependencies. `zudojs doctor` reads only `pkg.zudojs.features`,
7
+ * so its check ("every declared feature has its package") passed vacuously in
8
+ * every project the CLI created, and `zudojs add` was the only thing that
9
+ * could ever put a value there.
10
+ *
11
+ * The list produced here is the same one `zudojs create` records as
12
+ * `capabilities` in `.zudojs/manifest.json`, and {@link capabilityPackages}
13
+ * gives the packages that back it, so a declared feature is always installed.
14
+ */
15
+ import { FEATURE_PACKAGES } from "../../constants/index.js";
16
+ /** Capability flags, in the order the manifest records them. */
17
+ const CAPABILITY_FLAGS = [
18
+ ["cqrs", "enableCQRS"],
19
+ ["messaging", "enableMessaging"],
20
+ ["observability", "enableObservability"],
21
+ ["openapi", "enableOpenAPI"],
22
+ ["database", "enableDatabase"],
23
+ ["queue", "enableQueue"],
24
+ ];
25
+ /**
26
+ * The capability ids a scaffolded project was created with.
27
+ */
28
+ export function resolveProjectCapabilities(options) {
29
+ return CAPABILITY_FLAGS.filter(([, flag]) => options[flag] === true).map(([capability]) => capability);
30
+ }
31
+ /**
32
+ * The `@zudojs/*` packages that back `capabilities`, using the same mapping
33
+ * `zudojs add` and `zudojs doctor` use.
34
+ */
35
+ export function capabilityPackages(capabilities) {
36
+ const packages = new Set();
37
+ for (const capability of capabilities) {
38
+ for (const name of FEATURE_PACKAGES[capability] ?? [
39
+ `@zudojs/${capability}`,
40
+ ]) {
41
+ packages.add(name);
42
+ }
43
+ }
44
+ return [...packages];
45
+ }
46
+ //# sourceMappingURL=capability.template.js.map
@@ -2,6 +2,7 @@
2
2
  * zudojs-cli — Shared Template Fragments
3
3
  */
4
4
  export * from "./appRuntime.template.js";
5
+ export * from "./capability.template.js";
5
6
  export * from "./dockerfile.template.js";
6
7
  export * from "./pnpm.template.js";
7
8
  export * from "./server.template.js";
@@ -2,6 +2,7 @@
2
2
  * zudojs-cli — Shared Template Fragments
3
3
  */
4
4
  export * from "./appRuntime.template.js";
5
+ export * from "./capability.template.js";
5
6
  export * from "./dockerfile.template.js";
6
7
  export * from "./pnpm.template.js";
7
8
  export * from "./server.template.js";
@@ -12,6 +12,9 @@ export interface ExecOptions {
12
12
  * Quotes one argument for cmd.exe. Arguments are validated before they reach
13
13
  * here (project and service names are `[A-Za-z0-9_-]`), so this is defence
14
14
  * in depth rather than the only barrier.
15
+ *
16
+ * @throws {CLIValidationError} If the argument contains `"`, `%` or `!`,
17
+ * none of which cmd.exe can be made to treat as literal text.
15
18
  */
16
19
  export declare function quoteForWindowsShell(argument: string): string;
17
20
  /** The spawn arguments and shell flag a command needs on this platform. */
@@ -20,6 +23,25 @@ export declare function resolveSpawnTarget(file: string, args: readonly string[]
20
23
  readonly args: string[];
21
24
  readonly shell: boolean;
22
25
  };
26
+ /**
27
+ * The environment a child gets, hardened for the Windows shell path.
28
+ *
29
+ * `cmd.exe` searches the current directory before `PATH`, so a `pnpm.cmd`
30
+ * or `npm.bat` committed into a cloned repository would win over the real
31
+ * package manager. `NoDefaultCurrentDirectoryInExePath` removes the current
32
+ * directory from that search. It is only meaningful on win32 and only when
33
+ * a shell is involved; everywhere else the caller's env is passed through
34
+ * unchanged (POSIX `PATH` lookup never implies the cwd, and `shell` is
35
+ * false there).
36
+ */
37
+ export declare function resolveChildEnv(shell: boolean, env: NodeJS.ProcessEnv | undefined, platform?: NodeJS.Platform): NodeJS.ProcessEnv | undefined;
38
+ /**
39
+ * Default timeout for {@link execCommand}, in milliseconds.
40
+ *
41
+ * Sized for quick probes. Anything that downloads from a registry needs an
42
+ * explicit, much larger timeout — see `SCAFFOLD_TIMEOUT_MS`.
43
+ */
44
+ export declare const DEFAULT_EXEC_TIMEOUT_MS = 120000;
23
45
  /**
24
46
  * Runs a short-lived command and buffers its output.
25
47
  *
@@ -1,5 +1,6 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import { CLIValidationError } from "../errors/index.js";
3
4
  const execFileAsync = promisify(execFile);
4
5
  /**
5
6
  * Commands that are `.cmd` shims rather than executables on Windows.
@@ -23,17 +24,35 @@ const WINDOWS_SHIM_COMMANDS = new Set([
23
24
  ]);
24
25
  /** Characters that need quoting when a command line goes through cmd.exe. */
25
26
  const CMD_UNSAFE = /[\s"&|<>^()%!]/;
27
+ /**
28
+ * Characters that cannot be made safe by quoting on the cmd.exe path.
29
+ *
30
+ * `cmd.exe` has no escape character inside a quoted string: it toggles quote
31
+ * state on every `"`, so an embedded quote ends the string and everything
32
+ * after it — `&`, `|`, `>` — is parsed as shell syntax. The C-runtime `\"`
33
+ * escape means nothing to cmd. `%VAR%` and delayed-expansion `!VAR!` are
34
+ * likewise expanded inside quotes. There is no correct escaping, so an
35
+ * argument carrying one of these is rejected instead.
36
+ */
37
+ const CMD_UNQUOTABLE = /["%!]/;
26
38
  /**
27
39
  * Quotes one argument for cmd.exe. Arguments are validated before they reach
28
40
  * here (project and service names are `[A-Za-z0-9_-]`), so this is defence
29
41
  * in depth rather than the only barrier.
42
+ *
43
+ * @throws {CLIValidationError} If the argument contains `"`, `%` or `!`,
44
+ * none of which cmd.exe can be made to treat as literal text.
30
45
  */
31
46
  export function quoteForWindowsShell(argument) {
47
+ if (CMD_UNQUOTABLE.test(argument)) {
48
+ throw new CLIValidationError(`Cannot pass the argument ${JSON.stringify(argument)} to a Windows shell command: ` +
49
+ `the characters ", % and ! cannot be escaped for cmd.exe. Remove them and try again.`);
50
+ }
32
51
  if (argument.length === 0)
33
52
  return '""';
34
53
  if (!CMD_UNSAFE.test(argument))
35
54
  return argument;
36
- return `"${argument.replace(/"/g, '\\"')}"`;
55
+ return `"${argument}"`;
37
56
  }
38
57
  /** The spawn arguments and shell flag a command needs on this platform. */
39
58
  export function resolveSpawnTarget(file, args, platform = process.platform) {
@@ -46,6 +65,32 @@ export function resolveSpawnTarget(file, args, platform = process.platform) {
46
65
  }
47
66
  return { file, args: Array.from(args), shell: false };
48
67
  }
68
+ /**
69
+ * The environment a child gets, hardened for the Windows shell path.
70
+ *
71
+ * `cmd.exe` searches the current directory before `PATH`, so a `pnpm.cmd`
72
+ * or `npm.bat` committed into a cloned repository would win over the real
73
+ * package manager. `NoDefaultCurrentDirectoryInExePath` removes the current
74
+ * directory from that search. It is only meaningful on win32 and only when
75
+ * a shell is involved; everywhere else the caller's env is passed through
76
+ * unchanged (POSIX `PATH` lookup never implies the cwd, and `shell` is
77
+ * false there).
78
+ */
79
+ export function resolveChildEnv(shell, env, platform = process.platform) {
80
+ if (!shell || platform !== "win32")
81
+ return env;
82
+ return {
83
+ ...(env ?? process.env),
84
+ NoDefaultCurrentDirectoryInExePath: "1",
85
+ };
86
+ }
87
+ /**
88
+ * Default timeout for {@link execCommand}, in milliseconds.
89
+ *
90
+ * Sized for quick probes. Anything that downloads from a registry needs an
91
+ * explicit, much larger timeout — see `SCAFFOLD_TIMEOUT_MS`.
92
+ */
93
+ export const DEFAULT_EXEC_TIMEOUT_MS = 120000;
49
94
  /**
50
95
  * Runs a short-lived command and buffers its output.
51
96
  *
@@ -55,11 +100,12 @@ export function resolveSpawnTarget(file, args, platform = process.platform) {
55
100
  */
56
101
  export async function execCommand(file, args, cwd, options = {}) {
57
102
  const target = resolveSpawnTarget(file, args);
103
+ const env = resolveChildEnv(target.shell, options.env);
58
104
  const { stdout, stderr } = await execFileAsync(target.file, target.args, {
59
105
  cwd,
60
- timeout: options.timeout ?? 120000,
106
+ timeout: options.timeout ?? DEFAULT_EXEC_TIMEOUT_MS,
61
107
  shell: target.shell,
62
- ...(options.env ? { env: options.env } : {}),
108
+ ...(env ? { env } : {}),
63
109
  ...(options.signal ? { signal: options.signal } : {}),
64
110
  });
65
111
  return { stdout, stderr };
@@ -74,12 +120,13 @@ export async function execCommand(file, args, cwd, options = {}) {
74
120
  */
75
121
  export function runStreaming(file, args, cwd, options = {}) {
76
122
  const target = resolveSpawnTarget(file, args);
123
+ const env = resolveChildEnv(target.shell, options.env);
77
124
  return new Promise((resolve, reject) => {
78
125
  const child = spawn(target.file, target.args, {
79
126
  cwd,
80
127
  stdio: "inherit",
81
128
  shell: target.shell,
82
- ...(options.env ? { env: options.env } : {}),
129
+ ...(env ? { env } : {}),
83
130
  ...(options.signal ? { signal: options.signal } : {}),
84
131
  });
85
132
  child.on("error", (error) => {
@@ -1,13 +1,57 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { mkdir, writeFile as writeFileAsync } from "node:fs/promises";
3
- import { dirname, join, relative } from "node:path";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
4
4
  import { activeWriteCapture } from "./utils.writeGuard.js";
5
+ /**
6
+ * Resolves `path` through symlinks as far as it exists on disk, keeping the
7
+ * components that do not exist yet verbatim.
8
+ *
9
+ * Containment cannot be decided on the literal string alone: `mkdir` with
10
+ * `recursive: true` is a no-op on an existing directory, so if a component of
11
+ * the path is a symlink the write follows it to wherever it points. Only the
12
+ * real path of the deepest existing ancestor answers where the bytes land.
13
+ */
14
+ function realizePath(path) {
15
+ const absolute = resolve(path);
16
+ const tail = [];
17
+ let current = absolute;
18
+ for (;;) {
19
+ try {
20
+ const real = realpathSync(current);
21
+ return tail.length === 0 ? real : join(real, ...tail);
22
+ }
23
+ catch {
24
+ const parent = dirname(current);
25
+ // Reached the filesystem root without finding anything that exists.
26
+ if (parent === current)
27
+ return absolute;
28
+ tail.unshift(basename(current));
29
+ current = parent;
30
+ }
31
+ }
32
+ }
33
+ /** Whether `target` is `base` itself or lives underneath it. */
34
+ function isContained(base, target) {
35
+ if (base === target)
36
+ return true;
37
+ const relativePath = relative(base, target);
38
+ return (relativePath !== "" &&
39
+ !isAbsolute(relativePath) &&
40
+ !relativePath.startsWith(`..${sep}`) &&
41
+ relativePath !== "..");
42
+ }
5
43
  function assertSafePath(basePath, filePath) {
6
44
  const resolved = join(basePath, filePath);
7
45
  const relativePath = relative(basePath, resolved);
8
46
  if (relativePath.startsWith("..") || relativePath.includes("..")) {
9
47
  throw new Error(`Path traversal detected: ${filePath}`);
10
48
  }
49
+ // Re-assert containment against what is actually on disk, so a symlinked
50
+ // directory inside the project cannot carry the write outside it.
51
+ if (!isContained(realizePath(basePath), realizePath(resolved))) {
52
+ throw new Error(`Path escapes the project through a symlink: ${filePath}. ` +
53
+ `Replace the symlinked directory with a real one, or generate into a different path.`);
54
+ }
11
55
  }
12
56
  export async function writeFile(basePath, filePath, content) {
13
57
  assertSafePath(basePath, filePath);