From 94b898b618410975a64b9e39b076a3909391dbf6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 15 Apr 2026 18:59:22 +0200 Subject: [PATCH] feat(ui): add CLI client and shared UI common library (#1789) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * 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> --- .github/release-please/config.json | 14 +- .github/release-please/manifest.json | 2 + .github/workflows/autofix.yml | 10 +- .github/workflows/ci.yml | 102 +++++++ .prettierignore | 2 + pnpm-lock.yaml | 123 ++++++++ pnpm-workspace.yaml | 2 + ui/cli/.editorconfig | 21 ++ ui/cli/.lintstagedrc.js | 4 + ui/cli/.npmrc | 2 + ui/cli/.prettierignore | 3 + ui/cli/.prettierrc.json | 7 + ui/cli/README.md | 251 +++++++++++++++++ ui/cli/install.sh | 78 ++++++ ui/cli/package.json | 50 ++++ ui/cli/scripts/bundle.js | 32 +++ ui/cli/sonar-project.properties | 17 ++ ui/cli/src/cli.ts | 40 +++ ui/cli/src/client/errors.ts | 13 + ui/cli/src/client/lifecycle.ts | 106 +++++++ ui/cli/src/commands/action.ts | 39 +++ ui/cli/src/commands/atg.ts | 42 +++ ui/cli/src/commands/connection.ts | 26 ++ ui/cli/src/commands/connector.ts | 34 +++ ui/cli/src/commands/ocpp.ts | 172 ++++++++++++ ui/cli/src/commands/performance.ts | 17 ++ ui/cli/src/commands/simulator.ts | 31 ++ ui/cli/src/commands/station.ts | 83 ++++++ ui/cli/src/commands/supervision.ts | 22 ++ ui/cli/src/commands/template.ts | 17 ++ ui/cli/src/commands/transaction.ts | 36 +++ ui/cli/src/config/defaults.ts | 5 + ui/cli/src/config/loader.ts | 105 +++++++ ui/cli/src/output/formatter.ts | 26 ++ ui/cli/src/output/human.ts | 6 + ui/cli/src/output/json.ts | 13 + ui/cli/src/output/table.ts | 44 +++ ui/cli/src/types.ts | 5 + ui/cli/tests/config.test.ts | 132 +++++++++ ui/cli/tests/integration/cli.test.ts | 85 ++++++ ui/cli/tests/lifecycle.test.ts | 19 ++ ui/cli/tests/output.test.ts | 168 +++++++++++ ui/cli/tsconfig.json | 19 ++ ui/common/.editorconfig | 21 ++ ui/common/.lintstagedrc.js | 4 + ui/common/.npmrc | 2 + ui/common/.prettierignore | 3 + ui/common/.prettierrc.json | 7 + ui/common/README.md | 84 ++++++ ui/common/package.json | 38 +++ ui/common/sonar-project.properties | 17 ++ ui/common/src/client/WebSocketClient.ts | 183 ++++++++++++ ui/common/src/client/types.ts | 42 +++ ui/common/src/config/schema.ts | 43 +++ ui/common/src/constants.ts | 1 + ui/common/src/index.ts | 8 + ui/common/src/types/JsonType.ts | 5 + ui/common/src/types/UIProtocol.ts | 86 ++++++ ui/common/src/types/UUID.ts | 1 + ui/common/src/utils/UUID.ts | 13 + ui/common/tests/UUID.test.ts | 36 +++ ui/common/tests/WebSocketClient.test.ts | 357 ++++++++++++++++++++++++ ui/common/tests/config.test.ts | 148 ++++++++++ ui/common/tsconfig.json | 18 ++ ui/web/src/types/UIProtocol.ts | 4 +- 65 files changed, 3138 insertions(+), 8 deletions(-) create mode 100644 ui/cli/.editorconfig create mode 100644 ui/cli/.lintstagedrc.js create mode 100644 ui/cli/.npmrc create mode 100644 ui/cli/.prettierignore create mode 100644 ui/cli/.prettierrc.json create mode 100644 ui/cli/README.md create mode 100755 ui/cli/install.sh create mode 100644 ui/cli/package.json create mode 100644 ui/cli/scripts/bundle.js create mode 100644 ui/cli/sonar-project.properties create mode 100644 ui/cli/src/cli.ts create mode 100644 ui/cli/src/client/errors.ts create mode 100644 ui/cli/src/client/lifecycle.ts create mode 100644 ui/cli/src/commands/action.ts create mode 100644 ui/cli/src/commands/atg.ts create mode 100644 ui/cli/src/commands/connection.ts create mode 100644 ui/cli/src/commands/connector.ts create mode 100644 ui/cli/src/commands/ocpp.ts create mode 100644 ui/cli/src/commands/performance.ts create mode 100644 ui/cli/src/commands/simulator.ts create mode 100644 ui/cli/src/commands/station.ts create mode 100644 ui/cli/src/commands/supervision.ts create mode 100644 ui/cli/src/commands/template.ts create mode 100644 ui/cli/src/commands/transaction.ts create mode 100644 ui/cli/src/config/defaults.ts create mode 100644 ui/cli/src/config/loader.ts create mode 100644 ui/cli/src/output/formatter.ts create mode 100644 ui/cli/src/output/human.ts create mode 100644 ui/cli/src/output/json.ts create mode 100644 ui/cli/src/output/table.ts create mode 100644 ui/cli/src/types.ts create mode 100644 ui/cli/tests/config.test.ts create mode 100644 ui/cli/tests/integration/cli.test.ts create mode 100644 ui/cli/tests/lifecycle.test.ts create mode 100644 ui/cli/tests/output.test.ts create mode 100644 ui/cli/tsconfig.json create mode 100644 ui/common/.editorconfig create mode 100644 ui/common/.lintstagedrc.js create mode 100644 ui/common/.npmrc create mode 100644 ui/common/.prettierignore create mode 100644 ui/common/.prettierrc.json create mode 100644 ui/common/README.md create mode 100644 ui/common/package.json create mode 100644 ui/common/sonar-project.properties create mode 100644 ui/common/src/client/WebSocketClient.ts create mode 100644 ui/common/src/client/types.ts create mode 100644 ui/common/src/config/schema.ts create mode 100644 ui/common/src/constants.ts create mode 100644 ui/common/src/index.ts create mode 100644 ui/common/src/types/JsonType.ts create mode 100644 ui/common/src/types/UIProtocol.ts create mode 100644 ui/common/src/types/UUID.ts create mode 100644 ui/common/src/utils/UUID.ts create mode 100644 ui/common/tests/UUID.test.ts create mode 100644 ui/common/tests/WebSocketClient.test.ts create mode 100644 ui/common/tests/config.test.ts create mode 100644 ui/common/tsconfig.json diff --git a/.github/release-please/config.json b/.github/release-please/config.json index 73df0e29..fccd447d 100644 --- a/.github/release-please/config.json +++ b/.github/release-please/config.json @@ -7,10 +7,18 @@ "include-v-in-tag": true, "packages": { ".": { - "exclude-paths": ["ui/web", "tests/ocpp-server"], + "exclude-paths": ["ui/common", "ui/cli", "ui/web", "tests/ocpp-server"], "component": "simulator", "extra-files": ["sonar-project.properties"] }, + "ui/common": { + "component": "ui-common", + "extra-files": ["sonar-project.properties"] + }, + "ui/cli": { + "component": "cli", + "extra-files": ["sonar-project.properties"] + }, "ui/web": { "component": "webui", "extra-files": ["sonar-project.properties"] @@ -24,7 +32,7 @@ { "type": "linked-versions", "groupName": "simulator-ui-ocpp-server", - "components": ["simulator", "webui", "ocpp-server"] + "components": ["simulator", "ui-common", "cli", "webui", "ocpp-server"] } ], "changelog-sections": [ @@ -34,10 +42,8 @@ { "type": "refactor", "section": "✨ Polish", "hidden": false }, { "type": "test", "section": "🧪 Tests", "hidden": false }, { "type": "docs", "section": "📚 Documentation", "hidden": false }, - { "type": "build", "section": "🤖 Automation", "hidden": false }, { "type": "ci", "section": "🤖 Automation", "hidden": true }, - { "type": "chore", "section": "🧹 Chores", "hidden": true } ] } diff --git a/.github/release-please/manifest.json b/.github/release-please/manifest.json index 9e2531a9..0454f48f 100644 --- a/.github/release-please/manifest.json +++ b/.github/release-please/manifest.json @@ -1,5 +1,7 @@ { ".": "4.4.0", + "ui/common": "4.4.0", + "ui/cli": "4.4.0", "ui/web": "4.4.0", "tests/ocpp-server": "4.4.0" } diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 9981671f..37d183ac 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -32,9 +32,13 @@ jobs: - run: pnpm format + - working-directory: ui/common + run: pnpm format + + - working-directory: ui/cli + run: pnpm format + - working-directory: ui/web - run: | - pnpm format - pnpm lint:fix + run: pnpm format - uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 396fb09c..a5048d30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: else echo "defined=false" >> $GITHUB_OUTPUT; fi + build-ocpp-server: strategy: matrix: @@ -54,6 +55,7 @@ jobs: - name: Test with coverage if: ${{ github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.python == '3.13' }} run: poetry run task test_coverage + build-simulator: needs: [check-secrets] strategy: @@ -106,6 +108,104 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + build-common: + needs: [check-secrets] + name: Build UI common library with Node ${{ matrix.node }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + node: ['24.x'] + defaults: + run: + working-directory: ui/common + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@v6 + - name: Setup node ${{ matrix.node }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: 'pnpm' + - name: pnpm install + run: pnpm install --ignore-scripts --frozen-lockfile + - name: pnpm typecheck + run: pnpm typecheck + - name: pnpm lint + run: pnpm lint + - name: pnpm test + if: ${{ !(github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x') }} + run: pnpm test + - name: pnpm test:coverage + if: ${{ github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }} + run: pnpm test:coverage + - name: SonarCloud Scan + 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' }} + uses: sonarsource/sonarqube-scan-action@v7.1.0 + with: + projectBaseDir: ui/common + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + build-cli: + needs: [check-secrets, build-common] + strategy: + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] + node: ['22.x', '24.x', 'latest'] + name: Build CLI with Node ${{ matrix.node }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ui/cli + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Dependency Review + if: ${{ github.event_name == 'push' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }} + uses: actions/dependency-review-action@v4 + with: + base-ref: ${{ github.ref_name }} + head-ref: ${{ github.sha }} + - name: Pull Request Dependency Review + if: ${{ github.event_name == 'pull_request' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }} + uses: actions/dependency-review-action@v4 + - uses: pnpm/action-setup@v6 + - name: Setup node ${{ matrix.node }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: 'pnpm' + - name: pnpm install + run: pnpm install --ignore-scripts --frozen-lockfile + - name: pnpm typecheck + if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }} + run: pnpm typecheck + - name: pnpm lint + if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }} + run: pnpm lint + - name: pnpm build + run: pnpm build + - name: pnpm test + if: ${{ !(github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x') }} + run: pnpm test + - name: pnpm test:coverage + if: ${{ github.repository == 'sap/e-mobility-charging-stations-simulator' && matrix.os == 'ubuntu-latest' && matrix.node == '24.x' }} + run: pnpm test:coverage + - name: SonarCloud Scan + 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' }} + uses: sonarsource/sonarqube-scan-action@v7.1.0 + with: + projectBaseDir: ui/cli + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + build-dashboard: needs: [check-secrets] strategy: @@ -163,6 +263,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + build-simulator-docker-image: runs-on: ubuntu-latest name: Build simulator docker image @@ -175,6 +276,7 @@ jobs: run: | cd docker make SUBMODULES_INIT=false + build-dashboard-docker-image: runs-on: ubuntu-latest defaults: diff --git a/.prettierignore b/.prettierignore index 75989c0a..aefb1a93 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,8 @@ coverage dist outputs .nyc_output +ui/common +ui/cli ui/web pnpm-lock.yaml package-lock.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a421ba1d..06cf8156 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,6 +275,80 @@ importers: specifier: ^6.0.6 version: 6.0.6 + ui/cli: + dependencies: + chalk: + specifier: ^5.6.2 + version: 5.6.2 + cli-table3: + specifier: ^0.6.5 + version: 0.6.5 + commander: + specifier: ^14.0.0 + version: 14.0.3 + ora: + specifier: ^8.2.0 + version: 8.2.0 + ui-common: + specifier: workspace:* + version: link:../common + ws: + specifier: ^8.20.0 + version: 8.20.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + devDependencies: + '@types/node': + specifier: ^24.12.2 + version: 24.12.2 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + cross-env: + specifier: ^10.1.0 + version: 10.1.0 + esbuild: + specifier: ^0.28.0 + version: 0.28.0 + esbuild-plugin-clean: + specifier: ^1.0.1 + version: 1.0.1(esbuild@0.28.0) + prettier: + specifier: ^3.8.2 + version: 3.8.2 + rimraf: + specifier: ^6.1.3 + version: 6.1.3 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ~6.0.2 + version: 6.0.2 + + ui/common: + dependencies: + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@types/node': + specifier: ^24.12.2 + version: 24.12.2 + cross-env: + specifier: ^10.1.0 + version: 10.1.0 + prettier: + specifier: ^3.8.2 + version: 3.8.2 + rimraf: + specifier: ^6.1.3 + version: 6.1.3 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ~6.0.2 + version: 6.0.2 + ui/web: dependencies: finalhandler: @@ -4134,6 +4208,10 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -4222,6 +4300,14 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -4601,6 +4687,10 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -5139,6 +5229,10 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + os-browserify@0.3.0: resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} @@ -5889,6 +5983,10 @@ packages: std-env@4.0.0: resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -11202,6 +11300,8 @@ snapshots: is-interactive@1.0.0: {} + is-interactive@2.0.0: {} + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -11267,6 +11367,10 @@ snapshots: is-unicode-supported@0.1.0: {} + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -11612,6 +11716,11 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + log-symbols@6.0.0: + dependencies: + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + log-update@6.1.0: dependencies: ansi-escapes: 7.3.0 @@ -12166,6 +12275,18 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + ora@8.2.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.2.0 + os-browserify@0.3.0: {} os-name@4.0.1: @@ -13036,6 +13157,8 @@ snapshots: std-env@4.0.0: {} + stdin-discarder@0.2.2: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e39d0ca6..3513759d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,7 @@ packages: - ./ + - ./ui/common + - ./ui/cli - ./ui/web overrides: diff --git a/ui/cli/.editorconfig b/ui/cli/.editorconfig new file mode 100644 index 00000000..be65bb51 --- /dev/null +++ b/ui/cli/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf +max_line_length = 100 + +[*.ts{,x}] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false + +[{Makefile,**.mk}] +# Use tabs for indentation (Makefiles require tabs) +indent_style = tab diff --git a/ui/cli/.lintstagedrc.js b/ui/cli/.lintstagedrc.js new file mode 100644 index 00000000..a13b0e20 --- /dev/null +++ b/ui/cli/.lintstagedrc.js @@ -0,0 +1,4 @@ +export default { + '*.{css,json,md,yml,yaml,html,js,jsx,cjs,mjs,ts,tsx,cts,mts}': 'prettier --cache --write', + '*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}': 'eslint --cache --fix', +} diff --git a/ui/cli/.npmrc b/ui/cli/.npmrc new file mode 100644 index 00000000..22144e56 --- /dev/null +++ b/ui/cli/.npmrc @@ -0,0 +1,2 @@ +auto-install-peers=true +legacy-peer-deps=true diff --git a/ui/cli/.prettierignore b/ui/cli/.prettierignore new file mode 100644 index 00000000..9f7f384f --- /dev/null +++ b/ui/cli/.prettierignore @@ -0,0 +1,3 @@ +coverage +dist +pnpm-lock.yaml diff --git a/ui/cli/.prettierrc.json b/ui/cli/.prettierrc.json new file mode 100644 index 00000000..cafa9239 --- /dev/null +++ b/ui/cli/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "printWidth": 100, + "arrowParens": "avoid", + "singleQuote": true, + "semi": false, + "trailingComma": "es5" +} diff --git a/ui/cli/README.md b/ui/cli/README.md new file mode 100644 index 00000000..5b9321d5 --- /dev/null +++ b/ui/cli/README.md @@ -0,0 +1,251 @@ +# CLI + +Command-line tool to manage the e-mobility charging stations simulator via its WebSocket UI service (SRPC protocol). + +## Prerequisites + +The simulator must have its UI server enabled. Add a `uiServer` section to the simulator configuration: + +```json +{ + "uiServer": { + "enabled": true, + "type": "ws", + "authentication": { + "enabled": true, + "type": "protocol-basic-auth", + "username": "admin", + "password": "admin" + } + } +} +``` + +See the [simulator configuration](../../README.md#charging-stations-simulator-configuration). + +## Installation + +### Quick install + +```shell +cd ui/cli +./install.sh +``` + +This builds the CLI and installs it to `~/.local/bin/evse-cli`. Options: + +| Flag | Description | +| ----------------- | ------------------------------------------------------- | +| `--bin-dir ` | Install to a custom directory (default: `~/.local/bin`) | +| `--no-build` | Skip the build step (use existing `dist/cli.js`) | + +Ensure `~/.local/bin` is in your `$PATH`: + +```shell +export PATH="$HOME/.local/bin:$PATH" +``` + +### Manual build + +```shell +pnpm install +pnpm --filter cli build +node ui/cli/dist/cli.js --help +``` + +## Configuration + +The CLI reads its configuration from the XDG config directory: + +``` +${XDG_CONFIG_HOME:-$HOME/.config}/evse-cli/config.json +``` + +The install script creates a default config file. To override, edit `~/.config/evse-cli/config.json`: + +```json +{ + "uiServer": { + "host": "localhost", + "port": 8080, + "protocol": "ui", + "version": "0.0.1", + "secure": false, + "authentication": { + "enabled": true, + "type": "protocol-basic-auth", + "username": "admin", + "password": "admin" + } + } +} +``` + +### Configuration precedence + +Defaults < config file < `--config ` < `--url ` (highest priority). + +Use `--config ` to load a specific config file instead of the XDG default. + +| Option | Default | +| ---------- | ----------- | +| `host` | `localhost` | +| `port` | `8080` | +| `protocol` | `ui` | +| `version` | `0.0.1` | +| `secure` | `false` | + +## Usage + +```shell +node dist/cli.js [global-options] [subcommand] [options] +``` + +### Global Options + +| Option | Description | +| --------------------- | ------------------------------------------------- | +| `-V, --version` | Print version | +| `-C, --config ` | Path to configuration file | +| `--json` | Machine-readable JSON output on stdout | +| `--url ` | WebSocket URL (overrides config host/port/secure) | +| `-h, --help` | Show help | + +### Commands + +#### simulator + +```shell +node dist/cli.js simulator state # Get simulator state and statistics +node dist/cli.js simulator start # Start the simulator +node dist/cli.js simulator stop # Stop the simulator +``` + +#### station + +```shell +node dist/cli.js station list # List all charging stations +node dist/cli.js station start [hashId...] # Start station(s) +node dist/cli.js station stop [hashId...] # Stop station(s) +node dist/cli.js station add -t