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.
- package/README.md +43 -1
- package/dist/src/adapters/frontend/flutter.adapter.d.ts +9 -0
- package/dist/src/adapters/frontend/flutter.adapter.js +16 -1
- package/dist/src/bin/zudojs.js +16 -5
- package/dist/src/cliApplication/cliApplication.builtins.d.ts +4 -0
- package/dist/src/cliApplication/cliApplication.builtins.js +28 -9
- package/dist/src/cliApplication/cliApplication.core.d.ts +8 -0
- package/dist/src/cliApplication/cliApplication.core.js +45 -5
- package/dist/src/cliApplication/cliApplication.help.d.ts +17 -0
- package/dist/src/cliApplication/cliApplication.help.js +94 -0
- package/dist/src/cliApplication/cliApplication.logger.js +4 -4
- package/dist/src/cliCommand/cliCommand.validator.js +22 -0
- package/dist/src/cliConstant/cliConstant.value.d.ts +18 -12
- package/dist/src/cliConstant/cliConstant.value.js +25 -19
- package/dist/src/cliError/cliError.argument.js +2 -2
- package/dist/src/cliError/cliError.base.js +28 -5
- package/dist/src/cliError/cliError.command.js +5 -2
- package/dist/src/cliError/cliError.execution.js +3 -3
- package/dist/src/cliParser/cliParser.core.js +11 -5
- package/dist/src/cliParser/cliParser.helper.d.ts +11 -0
- package/dist/src/cliParser/cliParser.helper.js +19 -0
- package/dist/src/cliParser/cliParser.longOption.js +4 -4
- package/dist/src/cliParser/cliParser.shortOption.js +7 -4
- package/dist/src/cliVersion/cliVersion.update.js +5 -3
- package/dist/src/commands/add.command.js +17 -2
- package/dist/src/commands/commandError.helper.d.ts +15 -0
- package/dist/src/commands/commandError.helper.js +28 -0
- package/dist/src/commands/create.command.js +284 -46
- package/dist/src/commands/doctor.command.d.ts +8 -0
- package/dist/src/commands/doctor.command.js +38 -5
- package/dist/src/commands/generate.command.js +71 -32
- package/dist/src/constants/index.d.ts +40 -68
- package/dist/src/constants/index.js +73 -23
- package/dist/src/generators/frontend/frontendPipeline.js +16 -4
- package/dist/src/generators/integration/integrationGenerator.core.js +10 -6
- package/dist/src/generators/project/project.generator.js +5 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +3 -3
- package/dist/src/manifest/index.d.ts +1 -0
- package/dist/src/manifest/index.js +1 -0
- package/dist/src/manifest/manifestFile.helper.d.ts +42 -0
- package/dist/src/manifest/manifestFile.helper.js +141 -0
- package/dist/src/manifest/manifestManager.core.d.ts +16 -0
- package/dist/src/manifest/manifestManager.core.js +65 -35
- package/dist/src/prompts/backend/api-style.prompt.js +2 -5
- package/dist/src/prompts/backend/backend-architecture.prompt.js +5 -22
- package/dist/src/prompts/backend/database.prompt.js +5 -23
- package/dist/src/prompts/backend/services.prompt.js +12 -10
- package/dist/src/prompts/cancel.prompt.d.ts +18 -0
- package/dist/src/prompts/cancel.prompt.js +26 -0
- package/dist/src/prompts/capabilities/capabilities.prompt.js +2 -5
- package/dist/src/prompts/frontend/framework.prompt.js +2 -5
- package/dist/src/prompts/frontend/frontend-architecture.prompt.js +2 -5
- package/dist/src/prompts/index.d.ts +1 -0
- package/dist/src/prompts/index.js +1 -0
- package/dist/src/prompts/project/confirmation.prompt.js +2 -5
- package/dist/src/prompts/project/project-name.prompt.js +6 -7
- package/dist/src/prompts/project/project-type.prompt.js +2 -5
- package/dist/src/prompts/workspace/package-manager.prompt.js +5 -12
- package/dist/src/registries/adapter/frontendAdapterRegistry.core.d.ts +6 -0
- package/dist/src/registries/adapter/frontendAdapterRegistry.core.js +12 -0
- package/dist/src/registries/adapter/packageManagerRegistry.core.d.ts +5 -0
- package/dist/src/registries/adapter/packageManagerRegistry.core.js +11 -0
- package/dist/src/registries/dependency/dependencyRegistry.core.d.ts +9 -0
- package/dist/src/registries/dependency/dependencyRegistry.core.js +16 -0
- package/dist/src/registries/generator/generatorRegistry.core.d.ts +8 -0
- package/dist/src/registries/generator/generatorRegistry.core.js +14 -0
- package/dist/src/resolvers/capability/capabilityResolver.core.d.ts +1 -1
- package/dist/src/resolvers/capability/capabilityResolver.core.js +0 -2
- package/dist/src/resolvers/layout/projectLayout.core.js +1 -6
- package/dist/src/resolvers/project.resolver.d.ts +9 -3
- package/dist/src/resolvers/project.resolver.js +11 -7
- package/dist/src/rollback/index.d.ts +1 -1
- package/dist/src/rollback/index.js +1 -1
- package/dist/src/rollback/rollbackManager.core.d.ts +19 -1
- package/dist/src/rollback/rollbackManager.core.js +21 -2
- package/dist/src/runners/package-manager/index.d.ts +1 -1
- package/dist/src/runners/package-manager/index.js +1 -1
- package/dist/src/runners/package-manager/packageManagerRunner.core.d.ts +13 -1
- package/dist/src/runners/package-manager/packageManagerRunner.core.js +19 -1
- package/dist/src/runners/process/processRunner.core.d.ts +11 -0
- package/dist/src/runners/process/processRunner.core.js +41 -6
- package/dist/src/runners/task/index.d.ts +1 -1
- package/dist/src/runners/task/taskRunner.core.d.ts +22 -1
- package/dist/src/runners/task/taskRunner.core.js +35 -6
- package/dist/src/scaffolders/framework.scaffolder.js +3 -0
- package/dist/src/scaffolders/scaffolder.helper.d.ts +10 -0
- package/dist/src/scaffolders/scaffolder.helper.js +25 -2
- package/dist/src/templates/microservice/index.d.ts +1 -1
- package/dist/src/templates/microservice/index.js +1 -1
- package/dist/src/templates/microservice/microservice.template.d.ts +11 -4
- package/dist/src/templates/microservice/microservice.template.js +40 -19
- package/dist/src/templates/modular-monolith/modularMonolith.template.js +27 -8
- package/dist/src/templates/monolith/monolith.template.js +11 -17
- package/dist/src/templates/shared/capability.template.d.ts +25 -0
- package/dist/src/templates/shared/capability.template.js +46 -0
- package/dist/src/templates/shared/index.d.ts +1 -0
- package/dist/src/templates/shared/index.js +1 -0
- package/dist/src/utils/utils.exec.d.ts +22 -0
- package/dist/src/utils/utils.exec.js +51 -4
- package/dist/src/utils/utils.fileSystem.js +46 -2
- package/dist/src/utils/utils.name.d.ts +9 -0
- package/dist/src/utils/utils.name.js +29 -3
- package/dist/src/validators/environment/environmentValidator.core.d.ts +11 -1
- package/dist/src/validators/environment/environmentValidator.core.js +40 -73
- package/dist/src/validators/project/index.d.ts +1 -1
- package/dist/src/validators/project/projectValidator.core.d.ts +11 -1
- package/dist/src/validators/project/projectValidator.core.js +20 -4
- 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
|
-
|
|
70
|
-
|
|
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,
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
407
|
-
//
|
|
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
|
-
:
|
|
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 = [
|
|
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 =
|
|
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
|
-
|
|
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",
|