zudojs-cli 1.2.1 → 2.0.0

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 +35 -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 +8 -4
  92. package/dist/src/templates/microservice/microservice.template.js +30 -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
@@ -4,7 +4,7 @@
4
4
  * The `zudojs create` command.
5
5
  */
6
6
  import { join } from "node:path";
7
- import { existsSync } from "node:fs";
7
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
8
8
  import { mkdir } from "node:fs/promises";
9
9
  import * as p from "@clack/prompts";
10
10
  import { generateProject } from "../generators/project/project.generator.js";
@@ -23,9 +23,17 @@ import { promptCapabilities } from "../prompts/capabilities/index.js";
23
23
  import { CLIValidationError, CLIGenerationError } from "../errors/index.js";
24
24
  import { execCommand, runStreaming } from "../utils/utils.exec.js";
25
25
  import { writeFileTree } from "../utils/utils.fileSystem.js";
26
+ import { normalizeName } from "../utils/utils.name.js";
26
27
  import { resolveMicroserviceServices } from "../templates/microservice/index.js";
27
28
  import { ManifestManager } from "../manifest/manifestManager.core.js";
28
- import { CLI_VERSION } from "../constants/index.js";
29
+ import { CLI_VERSION, FEATURE_PACKAGES, PACKAGE_MANAGERS, ZUDOJS_PACKAGES_VERSION, } from "../constants/index.js";
30
+ import { registerCLIInterruptHandler } from "../cliApplication/cliApplication.writer.js";
31
+ import { resolveProjectLayout } from "../resolvers/layout/projectLayout.core.js";
32
+ import { CapabilityResolver } from "../resolvers/capability/capabilityResolver.core.js";
33
+ import { CompatibilityValidator } from "../validators/compatibility/compatibilityValidator.core.js";
34
+ import { ProjectValidator } from "../validators/project/projectValidator.core.js";
35
+ import { selectAddTargets } from "./add.command.js";
36
+ import { describeError } from "./commandError.helper.js";
29
37
  const VALID_PROJECT_TYPES = ["backend", "frontend", "fullstack"];
30
38
  const VALID_ARCHITECTURES = [
31
39
  "monolith",
@@ -35,7 +43,6 @@ const VALID_ARCHITECTURES = [
35
43
  // No mongodb: there is no adapter for it, and it used to be accepted and
36
44
  // scaffolded as PostgreSQL (tooling/CLI-04).
37
45
  const VALID_DATABASES = ["postgresql", "mysql", "sqlite"];
38
- const VALID_PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
39
46
  const SERVICE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
40
47
  const VALID_FRONTENDS = [
41
48
  "none",
@@ -59,6 +66,45 @@ const VALID_FRONTEND_ARCHITECTURES = [
59
66
  ];
60
67
  const VALID_LANGUAGES = ["typescript", "javascript"];
61
68
  const VALID_APIS = ["rest", "graphql", "rpc"];
69
+ /**
70
+ * Capability ids `--capabilities` accepts.
71
+ *
72
+ * Mirrors the options offered by `promptCapabilities`, so the interactive
73
+ * and non-interactive branches can produce the same project.
74
+ */
75
+ const VALID_CAPABILITIES = [
76
+ "cqrs",
77
+ "events",
78
+ "messaging",
79
+ "queue",
80
+ "observability",
81
+ "openapi",
82
+ "database",
83
+ "security",
84
+ ];
85
+ /**
86
+ * What the non-interactive branch enables when `--capabilities` is absent.
87
+ * This is what it has always hard-coded; the flag is what makes it
88
+ * reproducible.
89
+ */
90
+ const DEFAULT_CAPABILITIES = [
91
+ "cqrs",
92
+ "messaging",
93
+ "observability",
94
+ "openapi",
95
+ "database",
96
+ ];
97
+ /** Maps a capability selection onto the ScaffoldOptions enable* flags. */
98
+ function capabilityFlags(capabilities) {
99
+ return {
100
+ enableCQRS: capabilities.includes("cqrs"),
101
+ enableMessaging: capabilities.includes("messaging"),
102
+ enableObservability: capabilities.includes("observability"),
103
+ enableOpenAPI: capabilities.includes("openapi"),
104
+ enableDatabase: capabilities.includes("database"),
105
+ enableQueue: capabilities.includes("queue"),
106
+ };
107
+ }
62
108
  function validateProjectName(name) {
63
109
  if (!name || name.trim().length === 0) {
64
110
  throw new CLIValidationError("Project name is required.");
@@ -66,8 +112,11 @@ function validateProjectName(name) {
66
112
  if (name.includes("..") || name.includes("/") || name.includes("\\")) {
67
113
  throw new CLIValidationError("Project name must not contain path separators or '..'.");
68
114
  }
69
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
70
- throw new CLIValidationError("Project name must contain only alphanumeric characters, hyphens, and underscores.");
115
+ // A leading "-" used to be accepted, so `zudojs create -- --weird` made a
116
+ // directory that `cd` cannot enter and `rm -rf` cannot remove without a
117
+ // `--` of its own. The first character must be alphanumeric.
118
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(name)) {
119
+ throw new CLIValidationError("Project name must start with a letter or digit and contain only alphanumeric characters, hyphens, and underscores.");
71
120
  }
72
121
  }
73
122
  /**
@@ -143,13 +192,24 @@ export async function runCreateCommand(context) {
143
192
  // The --services option only applies to microservice architecture; ignore
144
193
  // its parser-applied default for other architectures.
145
194
  const services = servicesExplicit || architecture === "microservice" ? parsedServices : [];
195
+ const capabilitiesRaw = context.values.capabilities;
196
+ const capabilitiesExplicit = hasExplicitFlag(context.args, "--capabilities");
197
+ const requestedCapabilities = capabilitiesRaw
198
+ ? capabilitiesRaw
199
+ .split(",")
200
+ .map((capability) => capability.trim())
201
+ .filter(Boolean)
202
+ : [];
203
+ for (const capability of requestedCapabilities) {
204
+ validateChoice(capability, VALID_CAPABILITIES, "capability");
205
+ }
146
206
  if (projectName) {
147
207
  validateProjectName(projectName);
148
208
  }
149
209
  const projectTypeValue = validateChoice(projectType, VALID_PROJECT_TYPES, "project type");
150
210
  const architectureValue = validateChoice(architecture, VALID_ARCHITECTURES, "architecture");
151
211
  const databaseValue = validateChoice(database, VALID_DATABASES, "database");
152
- const packageManagerValue = validateChoice(packageManager, VALID_PACKAGE_MANAGERS, "package manager");
212
+ const packageManagerValue = validateChoice(packageManager, PACKAGE_MANAGERS, "package manager");
153
213
  const frontendValue = validateChoice(frontend, VALID_FRONTENDS, "frontend");
154
214
  const frontendArchitectureValue = validateChoice(frontendArchitecture, VALID_FRONTEND_ARCHITECTURES, "frontend architecture");
155
215
  const languageValue = validateChoice(language, VALID_LANGUAGES, "language");
@@ -193,6 +253,7 @@ export async function runCreateCommand(context) {
193
253
  }
194
254
  const isInteractive = process.stdin.isTTY;
195
255
  let answers;
256
+ let selectedCapabilities;
196
257
  if (isInteractive) {
197
258
  p.intro("Zudojs");
198
259
  const name = (await promptProjectName(projectName ?? undefined));
@@ -221,13 +282,11 @@ export async function runCreateCommand(context) {
221
282
  ? await promptFrontendArchitecture(explicitOverrides.frontendArchitecture)
222
283
  : explicitOverrides.frontendArchitecture;
223
284
  const pkgManager = await promptPackageManager(explicitOverrides.packageManager);
224
- const capabilities = await promptCapabilities([]);
225
- const enableCQRS = capabilities.includes("cqrs");
226
- const enableMessaging = capabilities.includes("messaging");
227
- const enableObservability = capabilities.includes("observability");
228
- const enableOpenAPI = capabilities.includes("openapi");
229
- const enableDatabase = capabilities.includes("database");
230
- const enableQueue = capabilities.includes("queue");
285
+ // `--capabilities` seeds the prompt so both branches start from the
286
+ // same selection.
287
+ const capabilities = await promptCapabilities(capabilitiesExplicit ? requestedCapabilities : []);
288
+ selectedCapabilities = capabilities;
289
+ const flags = capabilityFlags(capabilities);
231
290
  const enableDocker = arch === "microservice";
232
291
  const confirmed = await promptConfirmation(`Create project "${name}"?`, true);
233
292
  if (!confirmed) {
@@ -247,18 +306,18 @@ export async function runCreateCommand(context) {
247
306
  language: (explicitOverrides.language ??
248
307
  "typescript"),
249
308
  services: interactiveServices,
250
- enableCQRS,
251
- enableMessaging,
252
- enableObservability,
253
- enableOpenAPI,
254
- enableDatabase,
255
- enableQueue,
309
+ ...flags,
256
310
  enableDocker,
257
311
  installDeps: !noInstall,
258
312
  initGit: !noGit,
259
313
  };
260
314
  }
261
315
  else {
316
+ // Without --capabilities this is the set the branch has always
317
+ // hard-coded; with it, the same project the prompts would produce.
318
+ selectedCapabilities = capabilitiesExplicit
319
+ ? requestedCapabilities
320
+ : DEFAULT_CAPABILITIES;
262
321
  answers = {
263
322
  projectName: projectName ?? "",
264
323
  projectType: projectTypeValue,
@@ -273,12 +332,7 @@ export async function runCreateCommand(context) {
273
332
  frontendPath: "apps/web",
274
333
  language: languageValue,
275
334
  services,
276
- enableCQRS: true,
277
- enableMessaging: true,
278
- enableObservability: true,
279
- enableOpenAPI: true,
280
- enableDatabase: true,
281
- enableQueue: false,
335
+ ...capabilityFlags(selectedCapabilities),
282
336
  // Match the interactive branch, which enables Docker for microservices.
283
337
  enableDocker: architectureValue === "microservice",
284
338
  installDeps: !noInstall,
@@ -299,9 +353,47 @@ export async function runCreateCommand(context) {
299
353
  context.logger.warn("--language javascript applies to the frontend (apps/web) only; the backend is generated in TypeScript.");
300
354
  }
301
355
  }
302
- await createProject(answers, context);
356
+ // `--frontend react --type backend` used to pass validation and then be
357
+ // discarded without a word, handing the user a backend-only project and
358
+ // exit 0. Handled like the --language mismatch above.
359
+ if (hasExplicitFlag(context.args, "--frontend", "-f") &&
360
+ frontendValue !== "none" &&
361
+ answers.projectType === "backend") {
362
+ throw new CLIValidationError(`--frontend ${frontendValue} applies to --type frontend or --type fullstack; a backend project has no frontend. Use --type fullstack to get one.`);
363
+ }
364
+ assertCompatibleOptions(answers, context);
365
+ await createProject(answers, context, selectedCapabilities);
303
366
  }
304
- async function createProject(options, context) {
367
+ /**
368
+ * Refuses option combinations the generators cannot honour, and reports the
369
+ * ones that merely need saying.
370
+ *
371
+ * The CompatibilityValidator existed, was exported and was tested, but no
372
+ * command ran it.
373
+ */
374
+ function assertCompatibleOptions(options, context) {
375
+ const result = new CompatibilityValidator().validate({
376
+ projectType: options.projectType ?? "backend",
377
+ architecture: options.architecture,
378
+ frontend: options.frontend ?? "none",
379
+ frontendArchitecture: options.frontendArchitecture ?? "zudojs-standard",
380
+ packageManager: options.packageManager,
381
+ database: options.database ?? "postgresql",
382
+ language: options.language ?? "typescript",
383
+ });
384
+ for (const check of result.checks) {
385
+ if (check.severity === "warning") {
386
+ context.logger.warn(`${check.option1} + ${check.option2}: ${check.reason}`);
387
+ }
388
+ }
389
+ const errors = result.checks.filter((check) => check.severity === "error");
390
+ if (errors.length > 0) {
391
+ throw new CLIValidationError(`Incompatible options:\n${errors
392
+ .map((check) => ` - ${check.option1} + ${check.option2}: ${check.reason}`)
393
+ .join("\n")}`);
394
+ }
395
+ }
396
+ async function createProject(options, context, capabilities) {
305
397
  const { projectName, packageManager } = options;
306
398
  const targetPath = join(context.cwd, projectName);
307
399
  if (existsSync(targetPath)) {
@@ -310,6 +402,29 @@ async function createProject(options, context) {
310
402
  const rollback = new RollbackManager();
311
403
  rollback.trackDirectory(targetPath);
312
404
  const spinner = p.spinner();
405
+ const installFile = installCommandFor(packageManager);
406
+ let installFailure = null;
407
+ // Ctrl-C used to be swallowed: @clack/prompts registers a SIGINT listener
408
+ // per spinner that only prints "Canceled", and registering any listener
409
+ // suppresses Node's default termination — so the scaffold ran to
410
+ // completion and exited 0. This removes the half-written project and
411
+ // exits with the interrupted status.
412
+ const unregisterInterrupt = registerCLIInterruptHandler(async () => {
413
+ try {
414
+ spinner.stop("Interrupted");
415
+ }
416
+ catch {
417
+ // The spinner may not be running; the rollback is what matters.
418
+ }
419
+ const result = await rollback.rollback();
420
+ if (result.failures.length > 0) {
421
+ p.cancel(`Cancelled, but ${result.failures
422
+ .map((failure) => `${failure.path} (${failure.reason})`)
423
+ .join(", ")} could not be removed — delete it by hand.`);
424
+ return;
425
+ }
426
+ p.cancel(`Cancelled. Removed ${targetPath}.`);
427
+ });
313
428
  try {
314
429
  spinner.start("Creating project structure");
315
430
  await mkdir(targetPath, { recursive: true });
@@ -333,16 +448,9 @@ async function createProject(options, context) {
333
448
  const result = await generateProject(options, targetPath);
334
449
  spinner.stop(`Backend project generated (${result.filesCreated.length} files)`);
335
450
  }
336
- await writeProjectManifest(options, targetPath);
451
+ await writeProjectManifest(options, targetPath, capabilities);
452
+ await applyCapabilityPackages(targetPath, capabilities, context);
337
453
  if (options.installDeps) {
338
- spinner.start("Installing dependencies");
339
- const installFile = packageManager === "pnpm"
340
- ? "pnpm"
341
- : packageManager === "yarn"
342
- ? "yarn"
343
- : packageManager === "bun"
344
- ? "bun"
345
- : "npm";
346
454
  try {
347
455
  // Streamed with no timeout: installs can be slow and their output
348
456
  // should reach the user.
@@ -352,11 +460,22 @@ async function createProject(options, context) {
352
460
  }
353
461
  catch (error) {
354
462
  // The project is complete without its node_modules; say what
355
- // failed and how to retry instead of a bare "skipped".
463
+ // failed and how to retry instead of a bare "skipped". The command
464
+ // still exits non-zero — `zudojs add` fails on the same error, and
465
+ // a CI job must not go green with no node_modules.
356
466
  const reason = error instanceof Error ? error.message : String(error);
467
+ installFailure = reason;
357
468
  p.log.warn(`Dependency installation failed: ${reason}\nRun "${installFile} install" inside ${projectName} to retry.`);
358
469
  }
359
470
  }
471
+ spinner.start("Validating the generated project");
472
+ const problems = await collectProjectProblems(targetPath, options, installFailure === null);
473
+ spinner.stop(problems.length === 0
474
+ ? "Project validated"
475
+ : `Project validated with ${problems.length} warning(s)`);
476
+ for (const problem of problems) {
477
+ p.log.warn(`Project check: ${problem}`);
478
+ }
360
479
  if (options.initGit) {
361
480
  spinner.start("Initializing git repository");
362
481
  try {
@@ -375,20 +494,127 @@ async function createProject(options, context) {
375
494
  ? "bun run dev"
376
495
  : "pnpm run dev";
377
496
  p.note(`cd ${projectName}\n${devCmd}`, "Next steps");
378
- p.outro("Project created successfully.");
497
+ p.outro(installFailure === null
498
+ ? "Project created successfully."
499
+ : "Project created, but dependencies were NOT installed.");
379
500
  }
380
501
  catch (error) {
381
- await rollback.rollback();
382
- const message = error instanceof Error ? error.message : String(error);
383
- p.cancel(`Failed to create project: ${message}`);
384
- throw new CLIGenerationError(`Failed to create project "${projectName}"`, error);
502
+ const result = await rollback.rollback();
503
+ // The cause chain is rendered here: generators throw
504
+ // CLIGenerationError("Failed to write project files:", cause) and the
505
+ // cause was the only thing that said what went wrong.
506
+ const message = describeError(error);
507
+ const leftBehind = result.failures.length > 0
508
+ ? `\nCould not remove ${result.failures
509
+ .map((failure) => `${failure.path} (${failure.reason})`)
510
+ .join(", ")} — delete it by hand before retrying.`
511
+ : "";
512
+ p.cancel(`Failed to create project: ${message}${leftBehind}`);
513
+ throw new CLIGenerationError(`Failed to create project "${projectName}": ${message}${leftBehind}`, error);
514
+ }
515
+ finally {
516
+ unregisterInterrupt();
517
+ }
518
+ if (installFailure !== null) {
519
+ // Thrown outside the try so the finished project is not rolled back.
520
+ throw new CLIGenerationError(`Project "${projectName}" was created at ${targetPath}, but dependency installation failed: ${installFailure}. Run "${installFile} install" inside ${projectName} to retry.`);
521
+ }
522
+ }
523
+ /** The binary that installs dependencies for a package manager. */
524
+ function installCommandFor(packageManager) {
525
+ switch (packageManager) {
526
+ case "pnpm":
527
+ return "pnpm";
528
+ case "yarn":
529
+ return "yarn";
530
+ case "bun":
531
+ return "bun";
532
+ default:
533
+ return "npm";
385
534
  }
386
535
  }
536
+ /**
537
+ * Adds the packages behind capabilities the templates do not wire
538
+ * themselves.
539
+ *
540
+ * The capability prompt offers `events` and `security`, but only six of its
541
+ * eight options reached ScaffoldOptions, so ticking **Security** produced
542
+ * no dependency, no manifest capability and no message.
543
+ */
544
+ async function applyCapabilityPackages(targetPath, capabilities, context) {
545
+ if (capabilities.length === 0)
546
+ return;
547
+ const layout = resolveProjectLayout(targetPath);
548
+ if (!layout || layout.backendDirs.length === 0)
549
+ return;
550
+ const required = new Set();
551
+ for (const capability of capabilities) {
552
+ for (const name of FEATURE_PACKAGES[capability] ?? []) {
553
+ required.add(name);
554
+ }
555
+ }
556
+ if (required.size === 0)
557
+ return;
558
+ const added = new Set();
559
+ for (const pkgPath of selectAddTargets(layout, undefined)) {
560
+ if (!existsSync(pkgPath))
561
+ continue;
562
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
563
+ pkg.dependencies ??= {};
564
+ let changed = false;
565
+ for (const name of required) {
566
+ if (!(name in pkg.dependencies)) {
567
+ pkg.dependencies[name] = ZUDOJS_PACKAGES_VERSION;
568
+ added.add(name);
569
+ changed = true;
570
+ }
571
+ }
572
+ const block = (pkg.zudojs ??= {});
573
+ const features = new Set(Array.isArray(block.features)
574
+ ? block.features.filter((f) => typeof f === "string")
575
+ : []);
576
+ for (const capability of capabilities) {
577
+ if (!features.has(capability)) {
578
+ features.add(capability);
579
+ changed = true;
580
+ }
581
+ }
582
+ block.features = [...features];
583
+ if (changed) {
584
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
585
+ }
586
+ }
587
+ if (added.size > 0) {
588
+ context.logger.info(`Capability packages added: ${[...added].join(", ")}`);
589
+ }
590
+ const resolution = new CapabilityResolver().resolve(capabilities);
591
+ if (resolution.dependencies.length > 0) {
592
+ context.logger.info(`Capabilities build on: ${resolution.dependencies.join(", ")}`);
593
+ }
594
+ }
595
+ /**
596
+ * Checks the project that was just written and returns anything wrong with
597
+ * it. Never fatal: the files exist and the user can look at them.
598
+ */
599
+ async function collectProjectProblems(targetPath, options, installed) {
600
+ const layout = resolveProjectLayout(targetPath);
601
+ const appDir = layout?.backendDirs[0];
602
+ // Backend apps only: a Flutter or React Native app has no tsconfig.json
603
+ // and no src/, and would fail every check for no reason.
604
+ if (appDir === undefined)
605
+ return [];
606
+ const dependenciesInstalled = installed && options.installDeps === true;
607
+ const result = await new ProjectValidator().validate(appDir, {
608
+ expectInstalled: dependenciesInstalled,
609
+ typecheck: dependenciesInstalled,
610
+ });
611
+ return result.errors;
612
+ }
387
613
  /**
388
614
  * Writes the machine-managed .zudojs/manifest.json so follow-up commands
389
615
  * (`zudojs dev`, `generate`, `add`) recognize the project immediately.
390
616
  */
391
- async function writeProjectManifest(options, targetPath) {
617
+ async function writeProjectManifest(options, targetPath, selected = []) {
392
618
  const capabilities = [];
393
619
  if (options.enableCQRS)
394
620
  capabilities.push("cqrs");
@@ -402,12 +628,24 @@ async function writeProjectManifest(options, targetPath) {
402
628
  capabilities.push("database");
403
629
  if (options.enableQueue)
404
630
  capabilities.push("queue");
631
+ // Capabilities with no ScaffoldOptions flag of their own (events,
632
+ // security) were selected and then dropped on the floor.
633
+ for (const capability of selected) {
634
+ if (!capabilities.includes(capability))
635
+ capabilities.push(capability);
636
+ }
405
637
  // Record the services actually generated, not the raw request: the
406
- // template drops reserved names (gateway) and duplicates, and substitutes
407
- // its own defaults for an empty list.
638
+ // microservice template drops reserved names (gateway) and duplicates, and
639
+ // the modular-monolith template normalizes each name into a directory
640
+ // segment. A modular monolith's modules were previously not recorded at
641
+ // all, so later commands had no way to know which modules existed.
408
642
  const services = options.architecture === "microservice"
409
643
  ? resolveMicroserviceServices(options.services)
410
- : undefined;
644
+ : options.architecture === "modular-monolith"
645
+ ? options.services
646
+ .map((module) => normalizeName(module))
647
+ .filter((module) => module.length > 0)
648
+ : undefined;
411
649
  const hasFrontend = options.frontend !== undefined && options.frontend !== "none";
412
650
  const hasBackend = options.projectType !== "frontend";
413
651
  await new ManifestManager(targetPath).create({
@@ -22,5 +22,13 @@ export interface DoctorCheck {
22
22
  * without going through the logger.
23
23
  */
24
24
  export declare function runDoctorChecks(cwd: string): DoctorCheck[];
25
+ /**
26
+ * Environment checks: Node.js, Git and the project's own package manager.
27
+ *
28
+ * Delegated to the EnvironmentValidator, which existed and was exported
29
+ * but was called from nowhere while `doctor` reimplemented a subset of it
30
+ * inline.
31
+ */
32
+ export declare function runEnvironmentChecks(cwd: string): Promise<DoctorCheck[]>;
25
33
  export declare function runDoctorCommand(context: CLIContext): Promise<void>;
26
34
  //# sourceMappingURL=doctor.command.d.ts.map
@@ -8,6 +8,7 @@ import { join, relative } from "node:path";
8
8
  import { CLIValidationError } from "../errors/index.js";
9
9
  import { FEATURE_PACKAGES } from "../constants/index.js";
10
10
  import { resolveProjectLayout, } from "../resolvers/layout/projectLayout.core.js";
11
+ import { EnvironmentValidator, } from "../validators/environment/environmentValidator.core.js";
11
12
  /**
12
13
  * Runs every diagnostic for the project at `cwd`.
13
14
  *
@@ -16,15 +17,39 @@ import { resolveProjectLayout, } from "../resolvers/layout/projectLayout.core.js
16
17
  */
17
18
  export function runDoctorChecks(cwd) {
18
19
  const layout = resolveProjectLayout(cwd);
19
- const checks = [checkNodeVersion(), checkProject(layout)];
20
+ const checks = [checkProject(layout)];
20
21
  if (!layout) {
21
22
  return checks;
22
23
  }
23
24
  checks.push(checkPackageManager(layout), checkInstalled(layout), checkTypeScriptConfig(layout), checkDependencies(layout), checkFeatures(layout));
24
25
  return checks;
25
26
  }
27
+ /**
28
+ * Environment checks: Node.js, Git and the project's own package manager.
29
+ *
30
+ * Delegated to the EnvironmentValidator, which existed and was exported
31
+ * but was called from nowhere while `doctor` reimplemented a subset of it
32
+ * inline.
33
+ */
34
+ export async function runEnvironmentChecks(cwd) {
35
+ const layout = resolveProjectLayout(cwd);
36
+ const result = await new EnvironmentValidator().validate(layout?.projectType ?? "backend", layout?.packageManager);
37
+ return result.checks.map((check) => check.name === "Node.js"
38
+ ? checkNodeVersion(check)
39
+ : {
40
+ name: check.name === "Git" ? "Git" : `Package manager (${check.name})`,
41
+ severity: "warning",
42
+ passed: check.installed,
43
+ message: check.installed
44
+ ? `${check.version ?? "installed"}`
45
+ : `${check.name} is not installed or not on PATH`,
46
+ });
47
+ }
26
48
  export async function runDoctorCommand(context) {
27
- const checks = runDoctorChecks(context.cwd);
49
+ const checks = [
50
+ ...(await runEnvironmentChecks(context.cwd)),
51
+ ...runDoctorChecks(context.cwd),
52
+ ];
28
53
  const warnings = [];
29
54
  const errors = [];
30
55
  context.logger.info("Zudojs Doctor - Project Diagnostics");
@@ -63,10 +88,18 @@ export async function runDoctorCommand(context) {
63
88
  context.logger.info("");
64
89
  context.logger.info("All checks passed!");
65
90
  }
66
- function checkNodeVersion() {
67
- const version = process.version;
91
+ function checkNodeVersion(check) {
92
+ if (!check.installed) {
93
+ return {
94
+ name: "Node.js version",
95
+ severity: "error",
96
+ passed: false,
97
+ message: "Node.js is not installed or not on PATH",
98
+ };
99
+ }
100
+ const version = check.version ?? process.version;
68
101
  const major = Number.parseInt(version.replace("v", "").split(".")[0], 10);
69
- const passed = major >= 24;
102
+ const passed = Number.isFinite(major) && major >= 24;
70
103
  return {
71
104
  name: "Node.js version",
72
105
  severity: "error",