zudojs-cli 1.2.0 → 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 +15 -6
  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 +6 -6
package/README.md CHANGED
@@ -93,6 +93,16 @@ allows esbuild's build script, which pnpm 10+ would otherwise refuse to run.
93
93
  - **Fullstack + microservice:** the gateway and services are written at
94
94
  `apps/gateway` and `apps/services/<name>`, next to `apps/web`, and are part
95
95
  of the root workspace.
96
+ - **Capabilities:** `--capabilities cqrs,events,messaging,queue,observability,openapi,database,security`
97
+ picks what the project is wired with. The interactive prompt starts from
98
+ the same list, so both ways of running `create` can produce the same
99
+ project. Without the flag, a non-interactive run enables `cqrs`,
100
+ `messaging`, `observability`, `openapi` and `database`.
101
+ - **Failed installs fail the command.** If dependency installation fails,
102
+ `create` keeps the generated project, prints the command to retry with and
103
+ exits non-zero, so CI cannot go green with no `node_modules`.
104
+ - **Ctrl-C** during `create` removes the half-written project and exits with
105
+ status 130.
96
106
 
97
107
  ### Generating code
98
108
 
@@ -102,12 +112,36 @@ subclass), exports it from the modules barrel and registers it in `app.ts`.
102
112
  change, and lists those files; pass `--force` to overwrite them. Barrels are
103
113
  only appended to. `--dry-run` lists the files without writing anything.
104
114
 
115
+ It can be run from any directory inside the project: the project root is
116
+ found by walking up, and everything is generated relative to that root.
117
+ Outside a Zudojs project the command fails instead of writing files into the
118
+ current directory.
119
+
120
+ **Schematics:** `service`, `module`, `command`, `query`, `controller`,
121
+ `repository`, `middleware`, `event`, `job`, `route`, `model`, `dto`,
122
+ `validator`.
123
+
124
+ **Options:**
125
+
126
+ | Option | Applies to | Description |
127
+ | ----------------- | ----------------------------- | ------------------------------------------------------------------ |
128
+ | `--module <name>` | every schematic but `module` | Generates inside `<root>/modules/<name>` instead of the schematic's default directory |
129
+ | `--service <name>`| microservice projects, CQRS | Selects the app a schematic belongs to; also names the CQRS service for `command`/`query` |
130
+ | `--dry-run` | all | Lists the files without writing anything |
131
+ | `--force` | all | Overwrites files that already exist |
132
+
133
+ Where a schematic lands depends on the architecture: a `monolith` puts
134
+ services in `src/services` and modules in `src/modules`; a
135
+ `modular-monolith` has modules only, so `generate service` there generates a
136
+ module; in a `microservice` project a schematic goes to the gateway app, or
137
+ to `apps/services/<name>` with `--service <name>`.
138
+
105
139
  ## Commands
106
140
 
107
141
  | Command | Description |
108
142
  | ---------- | ------------------------------------------------------------------------ |
109
143
  | `create` | Scaffold a new project (backend, frontend, or fullstack) |
110
- | `generate` | Generate files (service, module, command, query, controller, repository) |
144
+ | `generate` | Generate files — see the schematics below |
111
145
  | `add` | Add feature packages (database, queue, messaging, etc.); `--service` targets one microservice app |
112
146
  | `dev` | Start development servers through the project's package manager (`pnpm run dev`, …) |
113
147
  | `build` | Build the project with its detected package manager |
@@ -12,6 +12,15 @@ export declare class FlutterAdapter implements FrontendAdapter {
12
12
  readonly framework = "flutter";
13
13
  isAvailable(): Promise<boolean>;
14
14
  getLatestVersion(): Promise<string>;
15
+ /**
16
+ * Converts a project name to a Flutter package name.
17
+ *
18
+ * `flutter create --project-name` only accepts lower_snake_case (a valid
19
+ * Dart package identifier), while `zudojs create` accepts uppercase and
20
+ * hyphens. Passing the raw name made `flutter create` fail outright, and
21
+ * this adapter has no fallback template, so the whole run was rolled back.
22
+ */
23
+ static toFlutterProjectName(name: string): string;
15
24
  scaffold(context: FrontendGenerationContext): Promise<void>;
16
25
  getDependencies(context: FrontendGenerationContext): readonly DependencyRequirement[];
17
26
  applyZudojsStructure(context: FrontendGenerationContext): Promise<void>;
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { execCommand } from "../../utils/utils.exec.js";
7
7
  import { writeFileTree } from "../../utils/utils.fileSystem.js";
8
+ import { normalizeName } from "../../utils/utils.name.js";
8
9
  /**
9
10
  * Flutter adapter for mobile applications.
10
11
  */
@@ -23,6 +24,20 @@ export class FlutterAdapter {
23
24
  async getLatestVersion() {
24
25
  return "3";
25
26
  }
27
+ /**
28
+ * Converts a project name to a Flutter package name.
29
+ *
30
+ * `flutter create --project-name` only accepts lower_snake_case (a valid
31
+ * Dart package identifier), while `zudojs create` accepts uppercase and
32
+ * hyphens. Passing the raw name made `flutter create` fail outright, and
33
+ * this adapter has no fallback template, so the whole run was rolled back.
34
+ */
35
+ static toFlutterProjectName(name) {
36
+ const snake = normalizeName(name).replace(/-/g, "_");
37
+ if (snake === "")
38
+ return "zudojs_app";
39
+ return /^[0-9]/.test(snake) ? `app_${snake}` : snake;
40
+ }
26
41
  async scaffold(context) {
27
42
  const { projectPath } = context;
28
43
  const org = "com.zudojs";
@@ -32,7 +47,7 @@ export class FlutterAdapter {
32
47
  "--org",
33
48
  org,
34
49
  "--project-name",
35
- context.project.name,
50
+ FlutterAdapter.toFlutterProjectName(context.project.name),
36
51
  "--platforms",
37
52
  "android,ios,web",
38
53
  ], projectPath);
@@ -6,7 +6,8 @@
6
6
  */
7
7
  import { createCLI } from "../cliApplication/index.js";
8
8
  import { createCommand } from "../cliCommand/index.js";
9
- import { CLI_VERSION, FEATURE_PACKAGES } from "../constants/index.js";
9
+ import { CLI_VERSION, FEATURE_NAMES, SCHEMATIC_NAMES, } from "../constants/index.js";
10
+ import { CLI_NAME } from "../cliConstant/cliConstant.value.js";
10
11
  import { runCreateCommand } from "../commands/create.command.js";
11
12
  import { runDevCommand } from "../commands/dev.command.js";
12
13
  import { runGenerateCommand } from "../commands/generate.command.js";
@@ -15,7 +16,8 @@ import { runBuildCommand } from "../commands/build.command.js";
15
16
  import { runDoctorCommand } from "../commands/doctor.command.js";
16
17
  import { runInfoCommand } from "../commands/info.command.js";
17
18
  const app = createCLI({
18
- name: "Zudojs",
19
+ // The name the help output tells people to type, so it must be the binary.
20
+ name: CLI_NAME,
19
21
  // Read from the CLI's own package.json, so `zudojs --version` cannot
20
22
  // drift from the published version.
21
23
  version: CLI_VERSION,
@@ -108,6 +110,15 @@ app.register(createCommand({
108
110
  description: "Comma-separated service names (microservice architecture only)",
109
111
  type: "string",
110
112
  },
113
+ {
114
+ // No default. An absent flag keeps the non-interactive branch's
115
+ // historical set (cqrs, messaging, observability, openapi, database)
116
+ // and seeds the interactive prompt with nothing, so adding the flag
117
+ // does not change what an existing command line produces.
118
+ name: "capabilities",
119
+ description: "Comma-separated capabilities (cqrs, events, messaging, queue, observability, openapi, database, security)",
120
+ type: "string",
121
+ },
111
122
  ],
112
123
  execute: async (context) => {
113
124
  await runCreateCommand(context);
@@ -148,7 +159,7 @@ app.register(createCommand({
148
159
  arguments: [
149
160
  {
150
161
  name: "schematic",
151
- description: "The schematic to generate (service, module, command, query, controller, repository)",
162
+ description: `The schematic to generate (${SCHEMATIC_NAMES.join(", ")})`,
152
163
  required: true,
153
164
  },
154
165
  {
@@ -167,7 +178,7 @@ app.register(createCommand({
167
178
  {
168
179
  name: "module",
169
180
  short: "m",
170
- description: "The module name",
181
+ description: "The module a generated file belongs to (modular monolith), e.g. --module billing",
171
182
  type: "string",
172
183
  },
173
184
  {
@@ -193,7 +204,7 @@ app.register(createCommand({
193
204
  arguments: [
194
205
  {
195
206
  name: "feature",
196
- description: `The feature to add (${Object.keys(FEATURE_PACKAGES).join(", ")})`,
207
+ description: `The feature to add (${FEATURE_NAMES.join(", ")})`,
197
208
  required: true,
198
209
  },
199
210
  ],
@@ -4,6 +4,10 @@
4
4
  * Help and version request detection and output.
5
5
  */
6
6
  import type { CLIArguments, CLIWriter } from "../cliType/cliType.type.js";
7
+ /** Returns whether a single token is a help flag (`-h` or `--help`). */
8
+ export declare function isHelpFlag(token: string): boolean;
9
+ /** Returns whether a single token is a version flag (`-v` or `--version`). */
10
+ export declare function isVersionFlag(token: string): boolean;
7
11
  /**
8
12
  * Returns whether the args are a help request.
9
13
  *
@@ -3,7 +3,15 @@
3
3
  *
4
4
  * Help and version request detection and output.
5
5
  */
6
- import { CLI_COMMANDS, CLI_DEFAULTS, } from "../cliConstant/cliConstant.value.js";
6
+ import { CLI_ALIASES, CLI_COMMANDS, CLI_DEFAULTS, CLI_FORMAT, CLI_HELP, CLI_SYMBOLS, } from "../cliConstant/cliConstant.value.js";
7
+ /** Returns whether a single token is a help flag (`-h` or `--help`). */
8
+ export function isHelpFlag(token) {
9
+ return CLI_ALIASES.HELP.includes(token);
10
+ }
11
+ /** Returns whether a single token is a version flag (`-v` or `--version`). */
12
+ export function isVersionFlag(token) {
13
+ return CLI_ALIASES.VERSION.includes(token);
14
+ }
7
15
  /**
8
16
  * Returns whether the args are a help request.
9
17
  *
@@ -12,7 +20,9 @@ import { CLI_COMMANDS, CLI_DEFAULTS, } from "../cliConstant/cliConstant.value.js
12
20
  */
13
21
  export function isHelpRequest(args) {
14
22
  const first = args[0];
15
- return first === CLI_COMMANDS.HELP || first === "-h" || first === "--help";
23
+ if (first === undefined)
24
+ return false;
25
+ return first === CLI_COMMANDS.HELP || isHelpFlag(first);
16
26
  }
17
27
  /**
18
28
  * Returns whether the args are a version request.
@@ -21,7 +31,9 @@ export function isHelpRequest(args) {
21
31
  */
22
32
  export function isVersionRequest(args) {
23
33
  const first = args[0];
24
- return (first === CLI_COMMANDS.VERSION || first === "-v" || first === "--version");
34
+ if (first === undefined)
35
+ return false;
36
+ return first === CLI_COMMANDS.VERSION || isVersionFlag(first);
25
37
  }
26
38
  /** Prints the version string. */
27
39
  export function printVersion(writer, version) {
@@ -33,18 +45,25 @@ export function printHelp(writer, name, version, description, commands = []) {
33
45
  lines.push(`${name} ${version ? `v${version}` : ""}`.trim());
34
46
  if (description)
35
47
  lines.push(description);
36
- lines.push("", "Usage:", ` ${name} <command> [options]`, "", "Commands:");
48
+ lines.push(CLI_FORMAT.EMPTY, CLI_HELP.USAGE, `${CLI_FORMAT.INDENT}${name} <${CLI_SYMBOLS.COMMAND}> [${CLI_SYMBOLS.OPTION}s]`, CLI_FORMAT.EMPTY, CLI_HELP.COMMANDS);
37
49
  const sorted = commands.slice().sort((a, b) => a.name.localeCompare(b.name));
38
50
  if (sorted.length === 0) {
39
- lines.push(" No commands registered.");
51
+ lines.push(`${CLI_FORMAT.INDENT}No commands registered.`);
40
52
  }
41
53
  else {
54
+ const width = sorted.reduce((max, cmd) => Math.max(max, formatCommandTerm(cmd).length), 0);
42
55
  for (const cmd of sorted) {
43
- const aliases = cmd.aliases?.length ? ` (${cmd.aliases.join(", ")})` : "";
44
- lines.push(` ${cmd.name}${aliases}${cmd.description ? ` ${cmd.description}` : ""}`);
56
+ const term = formatCommandTerm(cmd).padEnd(width);
57
+ lines.push(`${CLI_FORMAT.INDENT}${term}${cmd.description ? `${CLI_FORMAT.INDENT}${cmd.description}` : ""}`.trimEnd());
45
58
  }
46
59
  }
47
- lines.push("", "Options:", " -h, --help Show help.", " -v, --version Show version.");
48
- writer.writeLine(lines.join("\n"));
60
+ lines.push(CLI_FORMAT.EMPTY, CLI_HELP.OPTIONS, `${CLI_FORMAT.INDENT}-h, --help Show help.`, `${CLI_FORMAT.INDENT}-v, --version Show version.`, CLI_FORMAT.EMPTY, `Run "${name} ${CLI_COMMANDS.HELP} <${CLI_SYMBOLS.COMMAND}>" for help on one ${CLI_SYMBOLS.COMMAND}.`);
61
+ writer.writeLine(lines.join(CLI_FORMAT.NEWLINE));
62
+ }
63
+ /** Renders a command's name and aliases for the command list. */
64
+ function formatCommandTerm(cmd) {
65
+ return cmd.aliases?.length
66
+ ? `${cmd.name} (${cmd.aliases.join(", ")})`
67
+ : cmd.name;
49
68
  }
50
69
  //# sourceMappingURL=cliApplication.builtins.js.map
@@ -32,6 +32,14 @@ export declare class ZudojsCLI implements CLIApplication {
32
32
  use(hooks: CLIHooks): this;
33
33
  /** Runs the CLI application. */
34
34
  run(args?: CLIArguments): Promise<number>;
35
+ private printApplicationHelp;
36
+ /**
37
+ * Resolves the command named after a help request, if any.
38
+ *
39
+ * Throws when a name is given that is not registered, so `zudojs help
40
+ * bogus` says so instead of silently printing the global help.
41
+ */
42
+ private resolveHelpTarget;
35
43
  private findCommand;
36
44
  private getCommandArguments;
37
45
  private createContext;
@@ -4,15 +4,17 @@
4
4
  * Main CLI application class that orchestrates command registration,
5
5
  * parsing, execution, and built-in help/version handling.
6
6
  */
7
- import { CLI_DEFAULTS, CLI_EXIT_CODES, } from "../cliConstant/cliConstant.value.js";
7
+ import { CLI_DEFAULTS, CLI_EXIT_CODES, CLI_MESSAGES, CLI_OPTION_PREFIXES, } from "../cliConstant/cliConstant.value.js";
8
8
  import { CLIExecutionError, normalizeCLIError } from "../cliError/index.js";
9
9
  import { CommandNotFoundError } from "../cliError/cliError.command.js";
10
+ import { InvalidArgumentsError } from "../cliError/cliError.argument.js";
10
11
  import { CLICommandRegistry } from "../cliCommand/cliCommand.registry.js";
11
12
  import { executeCommand } from "../cliCommand/cliCommand.factory.js";
12
13
  import { CLIParser, resolveCommand } from "../cliParser/index.js";
13
14
  import { createCLIWriter } from "./cliApplication.writer.js";
14
15
  import { createCLILogger } from "./cliApplication.logger.js";
15
- import { isHelpRequest, isVersionRequest, printVersion, printHelp, } from "./cliApplication.builtins.js";
16
+ import { isHelpFlag, isHelpRequest, isVersionRequest, printVersion, printHelp, } from "./cliApplication.builtins.js";
17
+ import { printCommandHelp } from "./cliApplication.help.js";
16
18
  /* -------------------------------------------------------------------------- */
17
19
  /* Application */
18
20
  /* -------------------------------------------------------------------------- */
@@ -72,7 +74,13 @@ export class ZudojsCLI {
72
74
  this.running = true;
73
75
  try {
74
76
  if (isHelpRequest(args)) {
75
- printHelp(this.writer, this.name, this.version, this.description, this.commands.list());
77
+ // `zudojs help create` and `zudojs --help create` name a command.
78
+ const target = this.resolveHelpTarget(args);
79
+ if (target) {
80
+ printCommandHelp(this.writer, this.name, target);
81
+ return CLI_EXIT_CODES.SUCCESS;
82
+ }
83
+ this.printApplicationHelp();
76
84
  return CLI_EXIT_CODES.SUCCESS;
77
85
  }
78
86
  if (isVersionRequest(args)) {
@@ -82,12 +90,24 @@ export class ZudojsCLI {
82
90
  const command = this.findCommand(args);
83
91
  if (!command) {
84
92
  if (args.length === 0) {
85
- printHelp(this.writer, this.name, this.version, this.description, this.commands.list());
93
+ this.printApplicationHelp();
86
94
  return CLI_EXIT_CODES.SUCCESS;
87
95
  }
88
- throw new CommandNotFoundError(String(args[0]));
96
+ const first = String(args[0]);
97
+ // `findCommand` skips flag-shaped tokens, so a lone `-vh` would
98
+ // otherwise be reported as a command named "-vh".
99
+ if (first.startsWith(CLI_OPTION_PREFIXES.SHORT)) {
100
+ throw new InvalidArgumentsError(CLI_MESSAGES.MISSING_COMMAND);
101
+ }
102
+ throw new CommandNotFoundError(first);
89
103
  }
90
104
  const commandArgs = this.getCommandArguments(args, command);
105
+ // Checked before parsing: `--help` is not a declared option, so the
106
+ // parser would reject it as invalid and exit 2.
107
+ if (commandArgs.some(isHelpFlag)) {
108
+ printCommandHelp(this.writer, this.name, command);
109
+ return CLI_EXIT_CODES.SUCCESS;
110
+ }
91
111
  const commandContext = this.createContext(commandArgs, command);
92
112
  if (this.hooks.beforeRun) {
93
113
  await this.hooks.beforeRun(commandContext);
@@ -131,6 +151,26 @@ export class ZudojsCLI {
131
151
  }
132
152
  }
133
153
  /* ---- Internal ---- */
154
+ printApplicationHelp() {
155
+ printHelp(this.writer, this.name, this.version, this.description, this.commands.list());
156
+ }
157
+ /**
158
+ * Resolves the command named after a help request, if any.
159
+ *
160
+ * Throws when a name is given that is not registered, so `zudojs help
161
+ * bogus` says so instead of silently printing the global help.
162
+ */
163
+ resolveHelpTarget(args) {
164
+ const target = args
165
+ .slice(1)
166
+ .find((arg) => !arg.startsWith(CLI_OPTION_PREFIXES.SHORT));
167
+ if (target === undefined)
168
+ return undefined;
169
+ const command = resolveCommand(this.commands.list(), target);
170
+ if (!command)
171
+ throw new CommandNotFoundError(target);
172
+ return command;
173
+ }
134
174
  findCommand(args) {
135
175
  for (const arg of args) {
136
176
  if (arg.startsWith("-"))
@@ -0,0 +1,17 @@
1
+ /**
2
+ * zudojs-cli — Command Help
3
+ *
4
+ * Renders the help block for one command: its usage line, aliases,
5
+ * positional arguments and options, with shorts and defaults.
6
+ */
7
+ import type { CLICommand, CLIWriter } from "../cliType/cliType.type.js";
8
+ /**
9
+ * Prints the help block for a single command.
10
+ *
11
+ * Reached by `zudojs <command> --help`, `zudojs <command> -h` and
12
+ * `zudojs help <command>`. The first two used to be handed to the parser,
13
+ * which rejected `--help` as an undeclared option and exited 2, so no
14
+ * command's flags could be discovered from the CLI.
15
+ */
16
+ export declare function printCommandHelp(writer: CLIWriter, applicationName: string, command: CLICommand): void;
17
+ //# sourceMappingURL=cliApplication.help.d.ts.map
@@ -0,0 +1,94 @@
1
+ /**
2
+ * zudojs-cli — Command Help
3
+ *
4
+ * Renders the help block for one command: its usage line, aliases,
5
+ * positional arguments and options, with shorts and defaults.
6
+ */
7
+ import { CLI_FORMAT, CLI_HELP, CLI_OPTION_PREFIXES, CLI_SYMBOLS, } from "../cliConstant/cliConstant.value.js";
8
+ /* -------------------------------------------------------------------------- */
9
+ /* Formatting */
10
+ /* -------------------------------------------------------------------------- */
11
+ /** Renders one positional as it appears in the usage line. */
12
+ function formatArgumentToken(argument) {
13
+ const name = argument.variadic ? `${argument.name}...` : argument.name;
14
+ return argument.required ? `<${name}>` : `[<${name}>]`;
15
+ }
16
+ /** Renders the flag column of one option, e.g. `-t, --type <string>`. */
17
+ function formatOptionFlags(option) {
18
+ const short = option.short
19
+ ? `${CLI_OPTION_PREFIXES.SHORT}${option.short}, `
20
+ : "";
21
+ const long = `${CLI_OPTION_PREFIXES.LONG}${option.name}`;
22
+ const placeholder = option.type === "boolean" ? "" : ` <${option.type ?? "string"}>`;
23
+ return `${short}${long}${placeholder}`;
24
+ }
25
+ /** Renders the description column of one option. */
26
+ function formatOptionDescription(option) {
27
+ const parts = [];
28
+ if (option.description)
29
+ parts.push(option.description);
30
+ if (option.defaultValue !== undefined) {
31
+ parts.push(`(default: ${String(option.defaultValue)})`);
32
+ }
33
+ return parts.join(" ");
34
+ }
35
+ /** Renders `<term> <description>` rows on a common column. */
36
+ function formatRows(rows) {
37
+ const width = rows.reduce((max, [term]) => Math.max(max, term.length), 0);
38
+ return rows.map(([term, description]) => description
39
+ ? `${CLI_FORMAT.INDENT}${term.padEnd(width)}${CLI_FORMAT.INDENT}${description}`
40
+ : `${CLI_FORMAT.INDENT}${term}`);
41
+ }
42
+ /* -------------------------------------------------------------------------- */
43
+ /* Command Help */
44
+ /* -------------------------------------------------------------------------- */
45
+ /**
46
+ * Prints the help block for a single command.
47
+ *
48
+ * Reached by `zudojs <command> --help`, `zudojs <command> -h` and
49
+ * `zudojs help <command>`. The first two used to be handed to the parser,
50
+ * which rejected `--help` as an undeclared option and exited 2, so no
51
+ * command's flags could be discovered from the CLI.
52
+ */
53
+ export function printCommandHelp(writer, applicationName, command) {
54
+ const args = command.arguments ?? [];
55
+ const options = command.options ?? [];
56
+ const lines = [];
57
+ const usage = [
58
+ applicationName,
59
+ command.name,
60
+ ...args.map(formatArgumentToken),
61
+ `[${CLI_SYMBOLS.OPTION}s]`,
62
+ ].join(" ");
63
+ lines.push(CLI_HELP.USAGE, `${CLI_FORMAT.INDENT}${usage}`);
64
+ if (command.description) {
65
+ lines.push(CLI_FORMAT.EMPTY, CLI_HELP.DESCRIPTION, `${CLI_FORMAT.INDENT}${command.description}`);
66
+ }
67
+ if (command.aliases?.length) {
68
+ lines.push(CLI_FORMAT.EMPTY, CLI_HELP.ALIASES, `${CLI_FORMAT.INDENT}${command.aliases.join(", ")}`);
69
+ }
70
+ lines.push(CLI_FORMAT.EMPTY, CLI_HELP.ARGUMENTS);
71
+ if (args.length === 0) {
72
+ lines.push(`${CLI_FORMAT.INDENT}None — this ${CLI_SYMBOLS.COMMAND} takes no positional ${CLI_SYMBOLS.ARGUMENT}s.`);
73
+ }
74
+ else {
75
+ lines.push(...formatRows(args.map((argument) => [
76
+ formatArgumentToken(argument),
77
+ [
78
+ argument.description ?? "",
79
+ argument.defaultValue !== undefined
80
+ ? `(default: ${String(argument.defaultValue)})`
81
+ : "",
82
+ ]
83
+ .filter(Boolean)
84
+ .join(" "),
85
+ ])));
86
+ }
87
+ lines.push(CLI_FORMAT.EMPTY, CLI_HELP.OPTIONS);
88
+ lines.push(...formatRows([
89
+ ...options.map((option) => [formatOptionFlags(option), formatOptionDescription(option)]),
90
+ ["-h, --help", `Show help for this ${CLI_SYMBOLS.COMMAND}.`],
91
+ ]));
92
+ writer.writeLine(lines.join(CLI_FORMAT.NEWLINE));
93
+ }
94
+ //# sourceMappingURL=cliApplication.help.js.map
@@ -14,6 +14,7 @@
14
14
  * @module cliApplication/logger
15
15
  */
16
16
  import { createLogger, LoggerLevel, } from "@zudojs/logger";
17
+ import { CLI_ENVIRONMENT, CLI_NAME } from "../cliConstant/cliConstant.value.js";
17
18
  /** Renders one entry as the single line the user sees. */
18
19
  export function formatCLILogLine(entry) {
19
20
  switch (entry.levelName) {
@@ -31,14 +32,13 @@ export function formatCLILogLine(entry) {
31
32
  }
32
33
  /** Creates the logger handed to every command as `context.logger`. */
33
34
  export function createCLILogger(options = {}) {
35
+ const debug = process.env[CLI_ENVIRONMENT.DEBUG];
34
36
  const verbose = options.verbose ??
35
- (process.env["ZUDOJS_DEBUG"] !== undefined &&
36
- process.env["ZUDOJS_DEBUG"] !== "" &&
37
- process.env["ZUDOJS_DEBUG"] !== "0");
37
+ (debug !== undefined && debug !== "" && debug !== "0");
38
38
  const stdout = options.stdout ?? ((line) => process.stdout.write(`${line}\n`));
39
39
  const stderr = options.stderr ?? ((line) => process.stderr.write(`${line}\n`));
40
40
  return createLogger({
41
- name: "zudojs",
41
+ name: CLI_NAME,
42
42
  level: verbose ? LoggerLevel.TRACE : LoggerLevel.INFO,
43
43
  // Writes must complete before a command returns; `process.exit` follows
44
44
  // an error immediately.
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * Validation logic and utility functions for CLI commands.
5
5
  */
6
+ import { CLI_LIMITS } from "../cliConstant/cliConstant.value.js";
6
7
  import { InvalidCommandNameError, DuplicateCommandError, } from "../cliError/cliError.command.js";
8
+ import { InvalidOptionNameError } from "../cliError/cliError.option.js";
7
9
  /* -------------------------------------------------------------------------- */
8
10
  /* Validation */
9
11
  /* -------------------------------------------------------------------------- */
@@ -18,10 +20,27 @@ export function validateCommand(command) {
18
20
  if (!/^[a-zA-Z0-9][a-zA-Z0-9:_-]*$/.test(command.name.trim())) {
19
21
  throw new InvalidCommandNameError(command.name);
20
22
  }
23
+ if (command.name.trim().length > CLI_LIMITS.MAX_COMMAND_NAME_LENGTH) {
24
+ throw new InvalidCommandNameError(command.name);
25
+ }
21
26
  if (command.description !== undefined &&
22
27
  typeof command.description !== "string") {
23
28
  throw new TypeError(`Description for command "${command.name}" must be a string.`);
24
29
  }
30
+ if (command.description !== undefined &&
31
+ command.description.length > CLI_LIMITS.MAX_DESCRIPTION_LENGTH) {
32
+ throw new TypeError(`Description for command "${command.name}" exceeds ${CLI_LIMITS.MAX_DESCRIPTION_LENGTH} characters.`);
33
+ }
34
+ for (const option of command.options ?? []) {
35
+ if (option.name.length > CLI_LIMITS.MAX_OPTION_NAME_LENGTH) {
36
+ throw new InvalidOptionNameError(option.name);
37
+ }
38
+ }
39
+ for (const argument of command.arguments ?? []) {
40
+ if (argument.name.length > CLI_LIMITS.MAX_ARGUMENT_NAME_LENGTH) {
41
+ throw new InvalidCommandNameError(argument.name);
42
+ }
43
+ }
25
44
  if (command.aliases) {
26
45
  const seen = new Set();
27
46
  for (const alias of command.aliases) {
@@ -29,6 +48,9 @@ export function validateCommand(command) {
29
48
  throw new InvalidCommandNameError(String(alias));
30
49
  }
31
50
  const normalized = alias.trim();
51
+ if (normalized.length > CLI_LIMITS.MAX_ALIAS_LENGTH) {
52
+ throw new InvalidCommandNameError(normalized);
53
+ }
32
54
  if (normalized === command.name.trim()) {
33
55
  throw new DuplicateCommandError(normalized);
34
56
  }
@@ -14,6 +14,14 @@ export declare const CLI_DEFAULTS: {
14
14
  readonly DEFAULT_CWD: string;
15
15
  readonly EXIT_CODE: 0;
16
16
  };
17
+ /** Internal symbolic names for CLI elements. */
18
+ export declare const CLI_SYMBOLS: {
19
+ readonly COMMAND: "command";
20
+ readonly OPTION: "option";
21
+ readonly ARGUMENT: "argument";
22
+ readonly HELP: "help";
23
+ readonly VERSION: "version";
24
+ };
17
25
  /** Built-in command names. */
18
26
  export declare const CLI_COMMANDS: {
19
27
  readonly HELP: "help";
@@ -30,14 +38,6 @@ export declare const CLI_OPTION_PREFIXES: {
30
38
  readonly LONG: "--";
31
39
  readonly VALUE_SEPARATOR: "=";
32
40
  };
33
- /** Internal symbolic names for CLI elements. */
34
- export declare const CLI_SYMBOLS: {
35
- readonly COMMAND: "command";
36
- readonly OPTION: "option";
37
- readonly ARGUMENT: "argument";
38
- readonly HELP: "help";
39
- readonly VERSION: "version";
40
- };
41
41
  /** Standard CLI messages. */
42
42
  export declare const CLI_MESSAGES: {
43
43
  readonly COMMAND_NOT_FOUND: "Command not found.";
@@ -45,7 +45,7 @@ export declare const CLI_MESSAGES: {
45
45
  readonly PERMISSION_DENIED: "Permission denied.";
46
46
  readonly INTERRUPTED: "Process interrupted.";
47
47
  readonly UNKNOWN_ERROR: "An unexpected error occurred.";
48
- readonly MISSING_COMMAND: "A command is required.";
48
+ readonly MISSING_COMMAND: "No command given.";
49
49
  };
50
50
  /** Labels used in help output. */
51
51
  export declare const CLI_HELP: {
@@ -57,11 +57,17 @@ export declare const CLI_HELP: {
57
57
  readonly DESCRIPTION: "Description:";
58
58
  readonly EXAMPLES: "Examples:";
59
59
  };
60
- /** Environment variable names. */
60
+ /**
61
+ * Environment variable names the CLI reads.
62
+ *
63
+ * Every entry has a single reader: `DEBUG` turns on verbose logging,
64
+ * `CI` and `NO_UPDATE_CHECK`/`NPM_OFFLINE` suppress the update check.
65
+ */
61
66
  export declare const CLI_ENVIRONMENT: {
62
- readonly NODE_ENV: "NODE_ENV";
63
- readonly DEBUG: "DEBUG";
67
+ readonly DEBUG: "ZUDOJS_DEBUG";
64
68
  readonly CI: "CI";
69
+ readonly NO_UPDATE_CHECK: "ZUDOJS_NO_UPDATE_CHECK";
70
+ readonly NPM_OFFLINE: "npm_config_offline";
65
71
  };
66
72
  /** Formatting constants. */
67
73
  export declare const CLI_FORMAT: {