docs: add code review process and baseline review of all 19 modules
Establishes a per-module code review workflow under code-reviews/ and
records the 2026-05-16 baseline review (commit 9c60592): 241 findings
across all src/ modules (6 Critical, 46 High, 100 Medium, 89 Low).
This is the clean starting point for remediation work.
This commit is contained in:
442
code-reviews/CLI/findings.md
Normal file
442
code-reviews/CLI/findings.md
Normal file
@@ -0,0 +1,442 @@
|
||||
# Code Review — CLI
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.CLI` |
|
||||
| Design doc | `docs/requirements/Component-CLI.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-16 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `9c60592` |
|
||||
| Open findings | 13 |
|
||||
|
||||
## Summary
|
||||
|
||||
The CLI is a small, well-structured HTTP client over the Management API. The command-tree
|
||||
construction is consistent and repetitive in a good way: every subcommand funnels through
|
||||
`CommandHelpers.ExecuteCommandAsync`, which centralizes URL/credential resolution, HTTP
|
||||
dispatch, and response handling. There are no Akka.NET concerns (the CLI is a pure HTTP
|
||||
client) and no concurrency-sensitive code apart from the `debug stream` SignalR handler.
|
||||
|
||||
The dominant theme is **graceful-degradation gaps**: several user-supplied inputs (malformed
|
||||
URLs, malformed `--bindings`/`--overrides` JSON, non-JSON success bodies) are deserialized
|
||||
or constructed without `try/catch`, so a normal user mistake surfaces as an unhandled
|
||||
exception with a stack trace instead of a clean error message and exit code 1. A second
|
||||
theme is **dead configuration**: the `SCADALINK_FORMAT` environment variable and the
|
||||
`defaultFormat` config-file field are loaded by `CliConfig` but never consulted by any
|
||||
command, so the documented format-precedence chain does not work. The third theme is
|
||||
**substantial design-document drift**: `Component-CLI.md` describes a name-keyed,
|
||||
`--file`-based command surface that bears little resemblance to the implemented
|
||||
ID-keyed, flag-based surface. Test coverage exercises `OutputFormatter`, `CliConfig`, and
|
||||
`CommandHelpers.HandleResponse`, but the HTTP client, the `debug stream` path, the JSON
|
||||
argument parsing, and the command-tree wiring are untested.
|
||||
|
||||
## Checklist coverage
|
||||
|
||||
| # | Category | Examined | Notes |
|
||||
|---|----------|----------|-------|
|
||||
| 1 | Correctness & logic bugs | ☑ | Format precedence is broken (CLI-001); empty/non-JSON success bodies crash table rendering (CLI-002, CLI-003). |
|
||||
| 2 | Akka.NET conventions | ☑ | Not applicable — CLI is a pure HTTP/SignalR client with no Akka.NET runtime (design doc confirms). No issues. |
|
||||
| 3 | Concurrency & thread safety | ☑ | Only `debug stream` is concurrent; `CancellationTokenSource` is never disposed (CLI-011). Exit-code resolution after Ctrl+C is loose (CLI-012). |
|
||||
| 4 | Error handling & resilience | ☑ | Unhandled exceptions on malformed URL (CLI-004) and malformed JSON arguments (CLI-005); `StartAsync` cancellation is misreported (CLI-010). |
|
||||
| 5 | Security | ☑ | `--password` on the command line leaks into process listings / shell history with no env-var or prompt alternative (CLI-006). |
|
||||
| 6 | Performance & resource management | ☑ | `HttpClient` per invocation is acceptable for a one-shot CLI. `CancellationTokenSource` leak noted in CLI-011. |
|
||||
| 7 | Design-document adherence | ☑ | `Component-CLI.md` is heavily stale relative to the implemented command surface (CLI-007). |
|
||||
| 8 | Code organization & conventions | ☑ | Consistent and clean; `CliConfig.DefaultFormat` is loaded but unused (covered by CLI-001). Minor: `--format` not validated (CLI-008). |
|
||||
| 9 | Testing coverage | ☑ | No tests for `ManagementHttpClient`, `DebugCommands`, command-tree wiring, or JSON argument parsing (CLI-013). |
|
||||
| 10 | Documentation & comments | ☑ | `Component-CLI.md` mismatch (CLI-007); the in-repo `README.md` is reasonably accurate. Minor exit-code doc mismatch (CLI-009). |
|
||||
|
||||
## Findings
|
||||
|
||||
### CLI-001 — `SCADALINK_FORMAT` env var and config-file format are dead; format precedence broken
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CLI/Commands/CommandHelpers.cs:18`, `src/ScadaLink.CLI/Commands/DebugCommands.cs:45`, `src/ScadaLink.CLI/CliConfig.cs:37-39` |
|
||||
|
||||
**Description**
|
||||
|
||||
`CliConfig.Load()` reads `SCADALINK_FORMAT` and the `defaultFormat` config-file field into
|
||||
`CliConfig.DefaultFormat`, and `Component-CLI.md` documents a format-precedence chain
|
||||
(command-line option → env var → config file). However, every command resolves the format
|
||||
with `var format = result.GetValue(formatOption) ?? "json";` and `formatOption` is created
|
||||
in `Program.cs:11` with `DefaultValueFactory = _ => "json"`. `GetValue` therefore always
|
||||
returns a non-null value ("json" when the flag is absent), so the `?? "json"` fallback never
|
||||
fires and `config.DefaultFormat` is never consulted. The env var and config-file format
|
||||
settings are dead code: `scadalink site list` always outputs JSON regardless of
|
||||
`SCADALINK_FORMAT=table` or a `defaultFormat` entry in `~/.scadalink/config.json`. The
|
||||
documented behaviour silently does not work.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Either remove the `--format` option's `DefaultValueFactory` and have `CommandHelpers`
|
||||
resolve precedence explicitly (`result.GetValue(formatOption)` → `config.DefaultFormat`),
|
||||
or detect whether the option was explicitly supplied (`result.GetResult(formatOption)`) and
|
||||
only then override the config value. Apply the same fix to `DebugCommands.BuildStream`.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-002 — Empty success body crashes table rendering with an unhandled exception
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CLI/Commands/CommandHelpers.cs:59-68`, `src/ScadaLink.CLI/Commands/CommandHelpers.cs:78-80` |
|
||||
|
||||
**Description**
|
||||
|
||||
`ManagementHttpClient.SendCommandAsync` returns `JsonData = responseBody` for any
|
||||
success status code, including a 200/204 with an empty body. `HandleResponse` then tests
|
||||
`response.JsonData != null` — an empty string is non-null — and for `--format table`
|
||||
calls `WriteAsTable(response.JsonData)`, which immediately does `JsonDocument.Parse(json)`.
|
||||
`JsonDocument.Parse("")` throws `JsonException`, which is not caught anywhere, so a
|
||||
command that legitimately returns no body (e.g. a delete that returns 204) terminates with
|
||||
a stack trace instead of a clean success message.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
In `HandleResponse`, treat a null-or-whitespace `JsonData` as a "command succeeded, no
|
||||
output" case (print nothing or `(ok)`), and return 0 before attempting to parse.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-003 — Non-JSON success body crashes table rendering
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CLI/Commands/CommandHelpers.cs:80` |
|
||||
|
||||
**Description**
|
||||
|
||||
`WriteAsTable` calls `JsonDocument.Parse(json)` with no `try/catch`. If the server returns
|
||||
a success status but a body that is not valid JSON (a proxy/HTML error page returned with
|
||||
a 200, a plain-text message, etc.), the CLI throws an unhandled `JsonException`. The
|
||||
error-path code in `ManagementHttpClient` (lines 52-61) already defensively wraps
|
||||
`JsonDocument.Parse` in a `try/catch`; the success path and `WriteAsTable` do not get the
|
||||
same treatment.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Wrap the `JsonDocument.Parse` in `WriteAsTable` in a `try/catch`; on failure, fall back to
|
||||
printing the raw body verbatim (as the JSON path already does at line 66).
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-004 — Malformed `--url` throws an unhandled `UriFormatException`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CLI/ManagementHttpClient.cs:13` |
|
||||
|
||||
**Description**
|
||||
|
||||
The `ManagementHttpClient` constructor does `new Uri(baseUrl.TrimEnd('/') + "/")` with no
|
||||
validation. If the user passes a malformed URL (e.g. `--url localhost:9001` without a
|
||||
scheme, or `--url ""`), `new Uri(...)` throws `UriFormatException`. This call is not
|
||||
guarded by the `try/catch` in `SendCommandAsync` (it happens in the constructor at
|
||||
`CommandHelpers.cs:50`), so a common typo terminates the CLI with a stack trace rather
|
||||
than the documented "connection failure → exit 1 with a descriptive message".
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Validate the URL before constructing the client — e.g. `Uri.TryCreate(url, UriKind.Absolute, out _)` in `CommandHelpers.ExecuteCommandAsync` and `DebugCommands.BuildStream` — and emit a
|
||||
clean `INVALID_URL` error with exit code 1 on failure.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-005 — Malformed `--bindings` / `--overrides` JSON throws unhandled exceptions
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CLI/Commands/InstanceCommands.cs:55-58`, `src/ScadaLink.CLI/Commands/InstanceCommands.cs:181-182` |
|
||||
|
||||
**Description**
|
||||
|
||||
`set-bindings` deserializes the `--bindings` argument with
|
||||
`JsonSerializer.Deserialize<List<List<JsonElement>>>(...)` and then indexes `p[0]`/`p[1]`
|
||||
and calls `p[0].GetString()!` / `p[1].GetInt32()`. `set-overrides` deserializes `--overrides`
|
||||
with `JsonSerializer.Deserialize<Dictionary<string, string?>>(...)`. None of this is wrapped
|
||||
in a `try/catch`. Invalid JSON throws `JsonException`; a pair with fewer than two elements
|
||||
throws `ArgumentOutOfRangeException`; a non-string/non-int element throws `InvalidOperationException`. All of these surface as raw stack traces, so a user typo in a JSON argument
|
||||
crashes the CLI instead of producing a clean validation error and exit code 1.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Wrap the parsing in `try/catch (JsonException ...)` (and guard the pair length / element
|
||||
kinds), and on failure call `OutputFormatter.WriteError(...)` with an `INVALID_ARGUMENT`
|
||||
code and return 1.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-006 — Password is passed as a command-line argument with no safer alternative
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CLI/Program.cs:9`, `src/ScadaLink.CLI/Commands/CommandHelpers.cs:36-44` |
|
||||
|
||||
**Description**
|
||||
|
||||
Credentials are supplied only via `--username` / `--password`. A password on the command
|
||||
line is visible to any local user via the process list (`ps`, `/proc/<pid>/cmdline`) and is
|
||||
typically persisted into shell history. Unlike the management URL — which can also come
|
||||
from `SCADALINK_MANAGEMENT_URL` or the config file — there is no environment-variable
|
||||
fallback, no `--password-stdin`, and no interactive prompt for the password. For a tool
|
||||
explicitly intended for CI/CD automation this materially increases the chance of credential
|
||||
leakage.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add a `SCADALINK_PASSWORD` environment variable fallback and/or a `--password-stdin`
|
||||
option (read the password from stdin), and document that `--password` on the command line
|
||||
is discouraged. Optionally prompt interactively when stdin is a TTY and no password was
|
||||
supplied.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-007 — `Component-CLI.md` command surface is substantially stale
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Open |
|
||||
| Location | `docs/requirements/Component-CLI.md:51-211` (vs. all files under `src/ScadaLink.CLI/Commands/`) |
|
||||
|
||||
**Description**
|
||||
|
||||
The "Command Structure" section of the design doc no longer matches the implemented CLI.
|
||||
Examples of the drift:
|
||||
|
||||
- The doc keys most operations by **name** (`template get <name>`, `instance get <code>`,
|
||||
`site get <site-id>`); the implementation keys everything by integer **ID** via `--id`
|
||||
(`TemplateCommands.cs:40`, `InstanceCommands.cs:31`, `SiteCommands.cs:26`).
|
||||
- The doc shows `template create ... --file <path>` and `site update <site-id> --file <path>`;
|
||||
the implementation has no `--file` option anywhere and instead takes individual flags
|
||||
(`TemplateCommands.cs:52-72`, `SiteCommands.cs:83-115`).
|
||||
- The doc lists commands that do not exist (`template diff`, `instance bind-connections`,
|
||||
`instance assign-area`, `template attribute add --tag-path`, `data-connection assign/unassign`,
|
||||
`security api-key enable/disable` as separate commands) and omits commands that do exist
|
||||
(`instance alarm-override set/delete/list`, `external-system method` subgroup).
|
||||
- The doc's `notification smtp update --file` differs from the implemented
|
||||
`--server/--port/--auth-mode/--from-address` flags (`NotificationCommands.cs:72-94`).
|
||||
- The doc uses `--site` for site identification in several places where the implementation
|
||||
uses `--site-id` or `--identifier`.
|
||||
|
||||
A reader following the design doc would be unable to drive the CLI.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Regenerate the "Command Structure" section of `Component-CLI.md` from the actual command
|
||||
tree (the in-repo `src/ScadaLink.CLI/README.md` is much closer to reality and could be the
|
||||
source), or mark the doc's command list as illustrative and point to the README as
|
||||
authoritative.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-008 — `--format` value is not validated
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CLI/Program.cs:10-11`, `src/ScadaLink.CLI/Commands/CommandHelpers.cs:60` |
|
||||
|
||||
**Description**
|
||||
|
||||
The `--format` option accepts any string. `HandleResponse` only checks
|
||||
`string.Equals(format, "table", ...)`; any other value — including a typo like
|
||||
`--format tabel` or `--format xml` — silently falls through to JSON output. The user gets
|
||||
no feedback that their requested format was not honoured.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Restrict the option to the accepted values, e.g. `formatOption.AcceptOnlyFromAmong("json", "table")`, so `System.CommandLine` rejects invalid input with a clear parse error.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-009 — Exit-code documentation does not match `HandleResponse` behaviour
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Status | Open |
|
||||
| Location | `docs/requirements/Component-CLI.md:238-249`, `src/ScadaLink.CLI/Commands/CommandHelpers.cs:75` |
|
||||
|
||||
**Description**
|
||||
|
||||
The design doc's Exit Codes table defines code 2 as "Authorization failure (insufficient
|
||||
role)" and the Error Handling section says "If the server returns HTTP 403, the CLI exits
|
||||
with code 2." `HandleResponse` implements `return response.StatusCode == 403 ? 2 : 1;`,
|
||||
which is correct for the HTTP error path. However, the `NO_URL`, `NO_CREDENTIALS`,
|
||||
`INVALID_OPERATION` (from `set-bindings`/`set-overrides`) and any other client-side failure
|
||||
all return 1, and a connection failure carries `StatusCode == 0` — none of which the doc
|
||||
enumerates. More importantly, an authorization failure that the server signals with a body
|
||||
`code` of `UNAUTHORIZED` but an HTTP status other than 403 would be classified as a generic
|
||||
error (exit 1). The mapping is purely status-driven and the doc does not state that.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Either document precisely that exit code 2 is determined solely by HTTP 403, or key the
|
||||
"authorization failure" exit code off the response `code` field as well. Align the doc
|
||||
with whichever is chosen.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-010 — `debug stream` reports Ctrl+C during connect as a connection failure
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CLI/Commands/DebugCommands.cs:181-189` |
|
||||
|
||||
**Description**
|
||||
|
||||
`StreamDebugAsync` calls `await connection.StartAsync(cts.Token)` inside a
|
||||
`try { } catch (Exception ex)` that unconditionally reports
|
||||
`"Connection failed: {ex.Message}"` with code `CONNECTION_FAILED` and returns 1. If the
|
||||
user presses Ctrl+C while the connection is still being established, `cts` is cancelled and
|
||||
`StartAsync` throws `OperationCanceledException`; this is caught by the generic handler and
|
||||
misreported as a connection failure (with exit code 1) rather than a clean user-initiated
|
||||
cancellation (exit code 0).
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Catch `OperationCanceledException` separately (return 0 quietly) before the generic
|
||||
`catch (Exception)` handler, mirroring how the `exitTcs.Task.WaitAsync(cts.Token)` path at
|
||||
lines 209-215 already treats cancellation as graceful.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-011 — `CancellationTokenSource` in `debug stream` is never disposed
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CLI/Commands/DebugCommands.cs:89` |
|
||||
|
||||
**Description**
|
||||
|
||||
`var cts = new CancellationTokenSource();` is created in `StreamDebugAsync` but never
|
||||
disposed; there is no `using` declaration and no explicit `Dispose()` call on any exit
|
||||
path. `CancellationTokenSource` owns a `WaitHandle` and should be disposed. The impact is
|
||||
small because the process exits shortly after, but it is an `IDisposable` left undisposed,
|
||||
contrary to the review checklist's resource-management expectation.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Declare it as `using var cts = new CancellationTokenSource();` (or wrap the method body in
|
||||
a `try/finally`).
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-012 — `debug stream` exit code is unreliable after stream termination
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.CLI/Commands/DebugCommands.cs:208-227` |
|
||||
|
||||
**Description**
|
||||
|
||||
After `await exitTcs.Task.WaitAsync(cts.Token)`, the method returns
|
||||
`exitTcs.Task.IsCompletedSuccessfully ? exitTcs.Task.Result : 0`. When the user cancels
|
||||
with Ctrl+C, `WaitAsync` throws `OperationCanceledException` and `exitTcs` is typically
|
||||
still incomplete, so the method returns 0 — correct. However, the `OnStreamTerminated`
|
||||
handler and the `Closed` handler both call `exitTcs.TrySetResult`, and these run on
|
||||
SignalR callback threads concurrently with the Ctrl+C path. If a stream termination and a
|
||||
Ctrl+C race, the final exit code depends on which `TrySetResult` won and whether
|
||||
`WaitAsync` observed completion before cancellation — the result is not deterministic. A
|
||||
stream the server terminated abnormally can end up returning 0.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Resolve the exit code from a single authoritative source: after the `try/catch` around
|
||||
`WaitAsync`, check `exitTcs.Task` completion explicitly and treat a Ctrl+C with no prior
|
||||
result as 0, but always prefer a result that was set by `OnStreamTerminated`/`Closed`.
|
||||
Consider awaiting `exitTcs.Task` without the cancellation token after a brief grace period.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### CLI-013 — HTTP client, `debug stream`, and JSON-argument parsing are untested
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Open |
|
||||
| Location | `tests/ScadaLink.CLI.Tests/` (vs. `src/ScadaLink.CLI/ManagementHttpClient.cs`, `src/ScadaLink.CLI/Commands/DebugCommands.cs`, `src/ScadaLink.CLI/Commands/InstanceCommands.cs:55-58`) |
|
||||
|
||||
**Description**
|
||||
|
||||
The test project covers `OutputFormatter`, `CliConfig.Load`, and
|
||||
`CommandHelpers.HandleResponse`. It does not cover:
|
||||
|
||||
- `ManagementHttpClient.SendCommandAsync` — the timeout (504), connection-failure (code 0),
|
||||
and error-body-parsing paths are untested.
|
||||
- The `debug stream` SignalR command — no tests at all.
|
||||
- The JSON-argument parsing in `InstanceCommands` (`set-bindings`, `set-overrides`) — the
|
||||
paths most likely to crash on bad input (CLI-005) have no coverage.
|
||||
- Command-tree wiring — there is no test asserting that each `Build` produces the expected
|
||||
subcommands/options or that the command-name derivation
|
||||
(`ManagementCommandRegistry.GetCommandName`) resolves for every command type the CLI
|
||||
constructs.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add tests for `ManagementHttpClient` (using a stub `HttpMessageHandler`), for the
|
||||
JSON-argument parsing helpers (extracting the parsing into testable methods), and a
|
||||
smoke test that walks the root command tree and asserts every leaf command's payload type
|
||||
resolves via `ManagementCommandRegistry`.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
Reference in New Issue
Block a user