]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
feat(ui): add CLI client and shared UI common library (#1789)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 15 Apr 2026 16:59:22 +0000 (18:59 +0200)
committerGitHub <noreply@github.com>
Wed, 15 Apr 2026 16:59:22 +0000 (18:59 +0200)
commit94b898b618410975a64b9e39b076a3909391dbf6
tree3200f3a252660e05d4aa50cbd9fef5b6037243ff
parent624adeb8ca2a1b726969d6f18b9a09202556a1a5
feat(ui): add CLI client and shared UI common library (#1789)

* feat(ui): scaffold ui/common and ui/cli workspaces

- Add ui/common workspace with shared protocol types, SRPC WebSocket client,
  UUID utilities, config types, and Zod validation schemas (21 tests, 0 errors)
- Add ui/cli workspace scaffolding with all configuration files
- Register both workspaces in pnpm-workspace.yaml
- Update .prettierignore, release-please config and manifest
- Add build-common (library, single env) and build-cli (3x3 matrix) CI jobs
- Add formatting steps for new workspaces in autofix.yml
- Add sonar-project.properties for both workspaces

* feat(cli): implement CLI core infrastructure

- Commander.js entry point with 10 subcommand groups (simulator, station,
  template, connection, connector, atg, transaction, ocpp, performance,
  supervision)
- Config loading with lilconfig + Zod validation + merge precedence
  (defaults < config file < CLI flags)
- Output formatters: JSON (--json flag) and table (human-readable)
- WS client lifecycle: executeCommand() connects, sends, receives,
  disconnects; registerSignalHandlers() for SIGINT/SIGTERM
- Typed error classes: ConnectionError, AuthenticationError,
  TimeoutError, ServerError
- esbuild bundle with version injection and shebang
- 23 unit tests passing, zero lint warnings

* feat(cli): implement all 35 UI protocol command groups

All 35 ProcedureName procedures exposed as CLI subcommands:
- simulator: state, start, stop (3)
- station: list, start, stop, add, delete (5)
- template: list (1)
- connection: open, close (2)
- connector: lock, unlock (2)
- atg: start, stop (2)
- transaction: start, stop (2)
- ocpp: authorize, boot-notification, data-transfer, heartbeat,
  meter-values, status-notification, firmware-status-notification,
  diagnostics-status-notification, security-event-notification,
  sign-certificate, notify-report, notify-customer-information,
  log-status-notification, get-15118-ev-certificate,
  get-certificate-status, transaction-event (16)
- performance: stats (1)
- supervision: set-url (1)

Shared runAction() helper DRYs up all command action handlers.

* test(cli): add integration tests

- 8 integration tests covering --help, --version, subcommand help,
  connection error handling, JSON mode, and missing required options
- Separate test:integration script targeting tests/integration/
- Unit tests narrowed to tests/*.test.ts (no integration overlap)

* docs(cli): add README for ui/cli and ui/common

- ui/cli/README.md: installation, configuration, all command groups
  with examples, global options, exit codes, environment variables,
  available scripts
- ui/common/README.md: exported API reference (types, WebSocketClient,
  config validation, UUID utilities)

* [autofix.ci] apply automated fixes

* fix(cli): improve error handling for config and connection failures

- Catch config file ENOENT in loader.ts and throw clean error message
- Move loadConfig inside try/catch in runAction to prevent stack traces
- Use event.error in WebSocketClient onerror for better error propagation
- Separate connect errors from request errors in lifecycle.ts
- Include cause message in ConnectionError for descriptive output

* fix(cli): address PR review feedback

- Fix onerror stale after connect: replace with persistent error handler
  that fails all pending sendRequest promises immediately on WS error
- Fix dead code: call registerSignalHandlers() in cli.ts for SIGINT/SIGTERM
- Fix JSON error output: write to stdout (not stderr) in --json mode to
  match documented contract and enable scripting with 2>/dev/null
- Fix process.exit() in action.ts: use process.exitCode for proper async
  cleanup and testability
- Fix Map iteration: use snapshot+clear pattern in clearHandlers/failAllPending
- Fix empty array edge case in table formatter: check .length === 0
- Fix README: merge exit codes 1+2 into single row (Commander uses 1)
- Fix CI: add needs: build-common to build-cli job

* fix(cli): address second round of PR review feedback

- Fix ServerFailureError: WebSocketClient.handleMessage now rejects
  with a typed Error (carrying the ResponsePayload) instead of a raw
  object, preventing [object Object] in CLI output
- Fix table formatter: FAILURE responses now display hashIdsFailed/
  hashIdsSucceeded tables instead of early-returning with minimal info
- Fix auth schema: Zod refinement requires username+password when
  authentication enabled with protocol-basic-auth
- Fix AuthenticationConfig.type: use AuthenticationType enum instead
  of plain string for compile-time safety
- Fix signal handlers: use process.exitCode instead of process.exit()
  so finally blocks in executeCommand can run cleanup

* fix(cli): restore process.exit in signal handlers, fix type mismatch, remove dead code

- Signal handlers: restore process.exit() — setting only process.exitCode
  keeps the process alive since registering SIGINT listener removes Node's
  default termination behavior
- BroadcastChannelResponsePayload: align field name with server wire format
  (hashId: string | undefined, not id: string)
- Remove unused DEFAULT_TIMEOUT_MS export from defaults.ts

* refactor(ui-common): extract WS timeout to shared constant

Move UI_WEBSOCKET_REQUEST_TIMEOUT_MS from a private constant in
WebSocketClient.ts to ui/common/src/constants.ts as a single source
of truth, exported via barrel for consumers.

* refactor(cli): merge duplicate ui-common and commander imports

Consolidate split type/value imports from the same module into single
import statements using inline type syntax (`import { type X, Y }`).
Resolves SonarCloud 'imported multiple times' warning.

* refactor(cli): use Number.parseInt instead of parseInt

Resolves SonarCloud 'Prefer Number.parseInt over parseInt' warning.

* refactor(cli): remove unnecessary Command alias from commander import

Use import { Command } from 'commander' directly instead of aliasing
to Cmd. Single import serves both type and value usage.

* chore: reorder ui workspaces consistently (common, cli, web)

Apply dependency-first ordering across all config files:
pnpm-workspace.yaml, .prettierignore, release-please config/manifest,
ci.yml job order, and autofix.yml step order.

* refactor(ui-common): widen validateUUID to accept unknown

Align with ui/web pattern — move typeof string check into validateUUID
itself, removing redundant guard at call sites. Add tests for
non-string inputs (number, null, undefined, object, boolean).

* fix(cli): use Vercel CLI pattern for graceful signal shutdown

Replace AbortController with module-level activeClient/activeSpinner
refs + cleanupInProgress guard (Vercel CLI pattern). Signal handler
stops spinner, disconnects WS, then process.exit(130/143). Simpler,
battle-tested, correct for batch request-response CLI.

* chore: reorder linked-versions components (common, cli, web)

* style(ci): add blank line between all job definitions in ci.yml

* refactor(cli): rename parseIntList to parseCommaSeparatedInts

* fix(cli): validate connector IDs input and wrap config search errors

- parseCommaSeparatedInts now rejects NaN values with a clear error
  message instead of silently sending garbage to the server
- lilconfig search() path now wrapped in try/catch like the explicit
  config path, giving consistent error messages for malformed configs

* feat(cli): standalone build + XDG-only config + install script

- Bundle all dependencies into single 504KB dist/cli.js (no node_modules
  needed at runtime). Only ws native addons (bufferutil, utf-8-validate)
  are external — ws falls back to pure JS automatically.
- Replace lilconfig with direct XDG config file reading:
  ${XDG_CONFIG_HOME:-~/.config}/evse-cli/config.json
- Remove lilconfig dependency
- Add install.sh: builds CLI, copies to ~/.local/bin/evse-cli, creates
  default XDG config, warns if ~/.local/bin not in PATH
- Isolate ALL config tests from host env via XDG_CONFIG_HOME in
  beforeEach/afterEach + add XDG auto-discovery happy path test
- Guard against JSON array in config file
- Update README: standalone install instructions + XDG config location

* [autofix.ci] apply automated fixes

* fix(cli): address review feedback round 3

WebSocketClient:
- Validate responsePayload shape before casting (guard against [uuid, null])
- Reject connect() Promise if socket closes before onopen fires
- Add tests for both edge cases

CLI:
- Validate ws:/wss: URL scheme in parseServerUrl
- Output ServerFailureError.payload via formatter (show hashIdsFailed details)
- Extract shared parseInteger() validator — reject NaN with clear error
- Remove dead error types (AuthenticationError, ServerError, TimeoutError)
- Chain build in test:integration script
- Remove unreachable FAILURE branch in outputTable

Schema:
- Require password.length > 0 in auth refinement (reject empty string)

* ci: remove redundant lint:fix from autofix workflow

pnpm format already runs eslint --cache --fix, making the separate
pnpm lint:fix step redundant in all three ui workspaces.

* refactor(cli): remove unnecessary comment in outputTable

* fix(cli): don't override template defaults in station add, reject array config

- station add: only include autoStart, persistentConfiguration,
  ocppStrictCompliance, deleteConfiguration in payload when explicitly
  passed — lets server use template defaults otherwise
- config loader: reject uiServer array with clear error instead of
  silently spreading array keys into object

* refactor(cli): remove unused terminal-link dep and dead exports

- Remove terminal-link from dependencies (never imported)
- Remove unused exports: printSuccess, printWarning, printInfo (human.ts),
  outputTableList (table.ts)
- Remove corresponding test for printSuccess

* [autofix.ci] apply automated fixes

* fix(ui-common): reject malformed payloads, replace ReadyState with enum

- handleMessage: reject pending handler immediately when server sends
  response with matching UUID but missing status field, instead of
  silently dropping and waiting for 60s timeout
- Replace ReadyState type alias with WebSocketReadyState const enum
- Remove redundant ReadyState type (duplicated enum semantics)

* fix(cli): add connection timeout, remove dead defaultUIServerConfig

- Wrap client.connect() with Promise.race timeout to prevent infinite
  hang when server accepts TCP but never completes WS handshake
- Remove unused defaultUIServerConfig export from defaults.ts

* fix(cli): clear connection timeout timer on success

* refactor(ui-common): expose url getter, remove duplicate ConfigurationType

- WebSocketClient: make buildUrl() a public url getter so consumers
  use the canonical URL instead of reconstructing it
- lifecycle.ts: use client.url instead of building URL independently
- Remove ConfigurationType.ts: UIServerConfigurationSection is now
  a type alias for the Zod-inferred UIServerConfig (single source)

* fix(cli): display failure status instead of misleading Success

displayGenericPayload now checks payload.status before printing — a
failure response without hashIds shows the red status line instead of
a green checkmark.

* fix: align ui/web ResponsePayload with server, reject non-object config

- ui/web ResponsePayload: replace incorrect hashIds with
  hashIdsFailed/hashIdsSucceeded/responsesFailed matching server schema
- cli config loader: reject non-object uiServer values with clear error
  instead of silently falling back to defaults

* fix: reject non-object config files, merge duplicate import

- Config loader: throw on primitive JSON config values (42, "hello")
  instead of silently falling back to defaults
- Merge duplicate UIProtocol.ts import in ui/common/src/client/types.ts

* fix: suppress dangling connect rejection, remove dead Zod defaults

- Attach .catch() to connect() promise to prevent unhandled rejection
  when the timeout wins Promise.race and disconnect triggers onclose
- Remove .default() calls from Zod schema — the CLI loader always
  provides all fields via its canonical defaults map, making Zod
  defaults dead code paths

* fix(cli): preserve error cause in config loader for debuggability

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
65 files changed:
.github/release-please/config.json
.github/release-please/manifest.json
.github/workflows/autofix.yml
.github/workflows/ci.yml
.prettierignore
pnpm-lock.yaml
pnpm-workspace.yaml
ui/cli/.editorconfig [new file with mode: 0644]
ui/cli/.lintstagedrc.js [new file with mode: 0644]
ui/cli/.npmrc [new file with mode: 0644]
ui/cli/.prettierignore [new file with mode: 0644]
ui/cli/.prettierrc.json [new file with mode: 0644]
ui/cli/README.md [new file with mode: 0644]
ui/cli/install.sh [new file with mode: 0755]
ui/cli/package.json [new file with mode: 0644]
ui/cli/scripts/bundle.js [new file with mode: 0644]
ui/cli/sonar-project.properties [new file with mode: 0644]
ui/cli/src/cli.ts [new file with mode: 0644]
ui/cli/src/client/errors.ts [new file with mode: 0644]
ui/cli/src/client/lifecycle.ts [new file with mode: 0644]
ui/cli/src/commands/action.ts [new file with mode: 0644]
ui/cli/src/commands/atg.ts [new file with mode: 0644]
ui/cli/src/commands/connection.ts [new file with mode: 0644]
ui/cli/src/commands/connector.ts [new file with mode: 0644]
ui/cli/src/commands/ocpp.ts [new file with mode: 0644]
ui/cli/src/commands/performance.ts [new file with mode: 0644]
ui/cli/src/commands/simulator.ts [new file with mode: 0644]
ui/cli/src/commands/station.ts [new file with mode: 0644]
ui/cli/src/commands/supervision.ts [new file with mode: 0644]
ui/cli/src/commands/template.ts [new file with mode: 0644]
ui/cli/src/commands/transaction.ts [new file with mode: 0644]
ui/cli/src/config/defaults.ts [new file with mode: 0644]
ui/cli/src/config/loader.ts [new file with mode: 0644]
ui/cli/src/output/formatter.ts [new file with mode: 0644]
ui/cli/src/output/human.ts [new file with mode: 0644]
ui/cli/src/output/json.ts [new file with mode: 0644]
ui/cli/src/output/table.ts [new file with mode: 0644]
ui/cli/src/types.ts [new file with mode: 0644]
ui/cli/tests/config.test.ts [new file with mode: 0644]
ui/cli/tests/integration/cli.test.ts [new file with mode: 0644]
ui/cli/tests/lifecycle.test.ts [new file with mode: 0644]
ui/cli/tests/output.test.ts [new file with mode: 0644]
ui/cli/tsconfig.json [new file with mode: 0644]
ui/common/.editorconfig [new file with mode: 0644]
ui/common/.lintstagedrc.js [new file with mode: 0644]
ui/common/.npmrc [new file with mode: 0644]
ui/common/.prettierignore [new file with mode: 0644]
ui/common/.prettierrc.json [new file with mode: 0644]
ui/common/README.md [new file with mode: 0644]
ui/common/package.json [new file with mode: 0644]
ui/common/sonar-project.properties [new file with mode: 0644]
ui/common/src/client/WebSocketClient.ts [new file with mode: 0644]
ui/common/src/client/types.ts [new file with mode: 0644]
ui/common/src/config/schema.ts [new file with mode: 0644]
ui/common/src/constants.ts [new file with mode: 0644]
ui/common/src/index.ts [new file with mode: 0644]
ui/common/src/types/JsonType.ts [new file with mode: 0644]
ui/common/src/types/UIProtocol.ts [new file with mode: 0644]
ui/common/src/types/UUID.ts [new file with mode: 0644]
ui/common/src/utils/UUID.ts [new file with mode: 0644]
ui/common/tests/UUID.test.ts [new file with mode: 0644]
ui/common/tests/WebSocketClient.test.ts [new file with mode: 0644]
ui/common/tests/config.test.ts [new file with mode: 0644]
ui/common/tsconfig.json [new file with mode: 0644]
ui/web/src/types/UIProtocol.ts