Skip to content

Commit 94b898b

Browse files
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>
1 parent 624adeb commit 94b898b

65 files changed

Lines changed: 3138 additions & 8 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/release-please/config.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@
77
"include-v-in-tag": true,
88
"packages": {
99
".": {
10-
"exclude-paths": ["ui/web", "tests/ocpp-server"],
10+
"exclude-paths": ["ui/common", "ui/cli", "ui/web", "tests/ocpp-server"],
1111
"component": "simulator",
1212
"extra-files": ["sonar-project.properties"]
1313
},
14+
"ui/common": {
15+
"component": "ui-common",
16+
"extra-files": ["sonar-project.properties"]
17+
},
18+
"ui/cli": {
19+
"component": "cli",
20+
"extra-files": ["sonar-project.properties"]
21+
},
1422
"ui/web": {
1523
"component": "webui",
1624
"extra-files": ["sonar-project.properties"]
@@ -24,7 +32,7 @@
2432
{
2533
"type": "linked-versions",
2634
"groupName": "simulator-ui-ocpp-server",
27-
"components": ["simulator", "webui", "ocpp-server"]
35+
"components": ["simulator", "ui-common", "cli", "webui", "ocpp-server"]
2836
}
2937
],
3038
"changelog-sections": [
@@ -34,10 +42,8 @@
3442
{ "type": "refactor", "section": "✨ Polish", "hidden": false },
3543
{ "type": "test", "section": "🧪 Tests", "hidden": false },
3644
{ "type": "docs", "section": "📚 Documentation", "hidden": false },
37-
3845
{ "type": "build", "section": "🤖 Automation", "hidden": false },
3946
{ "type": "ci", "section": "🤖 Automation", "hidden": true },
40-
4147
{ "type": "chore", "section": "🧹 Chores", "hidden": true }
4248
]
4349
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
".": "4.4.0",
3+
"ui/common": "4.4.0",
4+
"ui/cli": "4.4.0",
35
"ui/web": "4.4.0",
46
"tests/ocpp-server": "4.4.0"
57
}

.github/workflows/autofix.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@ jobs:
3232

3333
- run: pnpm format
3434

35+
- working-directory: ui/common
36+
run: pnpm format
37+
38+
- working-directory: ui/cli
39+
run: pnpm format
40+
3541
- working-directory: ui/web
36-
run: |
37-
pnpm format
38-
pnpm lint:fix
42+
run: pnpm format
3943

4044
- uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8

.github/workflows/ci.yml

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
else
2222
echo "defined=false" >> $GITHUB_OUTPUT;
2323
fi
24+
2425
build-ocpp-server:
2526
strategy:
2627
matrix:
@@ -54,6 +55,7 @@ jobs:
5455
- name: Test with coverage
5556
if: ${{ github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.python == '3.13' }}
5657
run: poetry run task test_coverage
58+
5759
build-simulator:
5860
needs: [check-secrets]
5961
strategy:
@@ -106,6 +108,104 @@ jobs:
106108
env:
107109
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
108110
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
111+
112+
build-common:
113+
needs: [check-secrets]
114+
name: Build UI common library with Node ${{ matrix.node }} on ${{ matrix.os }}
115+
runs-on: ${{ matrix.os }}
116+
strategy:
117+
matrix:
118+
os: [ubuntu-latest]
119+
node: ['24.x']
120+
defaults:
121+
run:
122+
working-directory: ui/common
123+
steps:
124+
- uses: actions/checkout@v6
125+
with:
126+
fetch-depth: 0
127+
- uses: pnpm/action-setup@v6
128+
- name: Setup node ${{ matrix.node }}
129+
uses: actions/setup-node@v6
130+
with:
131+
node-version: ${{ matrix.node }}
132+
cache: 'pnpm'
133+
- name: pnpm install
134+
run: pnpm install --ignore-scripts --frozen-lockfile
135+
- name: pnpm typecheck
136+
run: pnpm typecheck
137+
- name: pnpm lint
138+
run: pnpm lint
139+
- name: pnpm test
140+
if: ${{ !(github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x') }}
141+
run: pnpm test
142+
- name: pnpm test:coverage
143+
if: ${{ github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }}
144+
run: pnpm test:coverage
145+
- name: SonarCloud Scan
146+
if: ${{ needs.check-secrets.outputs.sonar-token-exists == 'true' && github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }}
147+
uses: sonarsource/sonarqube-scan-action@v7.1.0
148+
with:
149+
projectBaseDir: ui/common
150+
env:
151+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
152+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
153+
154+
build-cli:
155+
needs: [check-secrets, build-common]
156+
strategy:
157+
matrix:
158+
os: [windows-latest, macos-latest, ubuntu-latest]
159+
node: ['22.x', '24.x', 'latest']
160+
name: Build CLI with Node ${{ matrix.node }} on ${{ matrix.os }}
161+
runs-on: ${{ matrix.os }}
162+
defaults:
163+
run:
164+
working-directory: ui/cli
165+
steps:
166+
- uses: actions/checkout@v6
167+
with:
168+
fetch-depth: 0
169+
- name: Dependency Review
170+
if: ${{ github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }}
171+
uses: actions/dependency-review-action@v4
172+
with:
173+
base-ref: ${{ github.ref_name }}
174+
head-ref: ${{ github.sha }}
175+
- name: Pull Request Dependency Review
176+
if: ${{ github.event_name == 'pull_request' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }}
177+
uses: actions/dependency-review-action@v4
178+
- uses: pnpm/action-setup@v6
179+
- name: Setup node ${{ matrix.node }}
180+
uses: actions/setup-node@v6
181+
with:
182+
node-version: ${{ matrix.node }}
183+
cache: 'pnpm'
184+
- name: pnpm install
185+
run: pnpm install --ignore-scripts --frozen-lockfile
186+
- name: pnpm typecheck
187+
if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }}
188+
run: pnpm typecheck
189+
- name: pnpm lint
190+
if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }}
191+
run: pnpm lint
192+
- name: pnpm build
193+
run: pnpm build
194+
- name: pnpm test
195+
if: ${{ !(github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x') }}
196+
run: pnpm test
197+
- name: pnpm test:coverage
198+
if: ${{ github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }}
199+
run: pnpm test:coverage
200+
- name: SonarCloud Scan
201+
if: ${{ needs.check-secrets.outputs.sonar-token-exists == 'true' && github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }}
202+
uses: sonarsource/sonarqube-scan-action@v7.1.0
203+
with:
204+
projectBaseDir: ui/cli
205+
env:
206+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
207+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
208+
109209
build-dashboard:
110210
needs: [check-secrets]
111211
strategy:
@@ -163,6 +263,7 @@ jobs:
163263
env:
164264
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
165265
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
266+
166267
build-simulator-docker-image:
167268
runs-on: ubuntu-latest
168269
name: Build simulator docker image
@@ -175,6 +276,7 @@ jobs:
175276
run: |
176277
cd docker
177278
make SUBMODULES_INIT=false
279+
178280
build-dashboard-docker-image:
179281
runs-on: ubuntu-latest
180282
defaults:

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ coverage
22
dist
33
outputs
44
.nyc_output
5+
ui/common
6+
ui/cli
57
ui/web
68
pnpm-lock.yaml
79
package-lock.json

0 commit comments

Comments
 (0)