]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
feat(ui-cli): short hash prefix matching, human output formatters, embedded agent...
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 17 Apr 2026 21:35:15 +0000 (23:35 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 17 Apr 2026 21:35:15 +0000 (23:35 +0200)
Short hash resolution:
- Resolve hash ID prefixes via station list lookup before command execution
- Skip resolution when all IDs are full-length (>= 48 chars, SHA-384)
- Clear error messages for ambiguous or unknown prefixes
- Silent mode suppresses spinner during internal resolution call

Human output formatters:
- Borderless table renderers for station list, template list, simulator state, performance stats
- Shared format utils: truncateId, statusIcon, wsIcon, fuzzyTime, countConnectors
- Truncate hash IDs in all human output tables (station list + success/failure)
- Type guards with Array.isArray + null checks for safe payload dispatch
- Handle supervisionUrls as string | string[], optional configuration
- Dynamic status capitalization, shared captureStream test helper
- 70 new tests (format + renderers), using canonical ChargingStationData type

Embedded agent skill:
- `skill show` prints embedded SKILL.md to stdout
- `skill install [--global] [--force]` writes to .agents/skills/
- SKILL.md embedded at build time via esbuild define
- Follows AgentSkills.io open standard

CLI option rename:
- Global --url renamed to --server-url (explicit intent)
- supervision set-url --url renamed to --supervision-url (no collision)

README restructured with prefix matching docs, output modes table,
station delete options, grouped simulator vs local commands

ui/cli/README.md
ui/cli/scripts/bundle.js
ui/cli/skills/evse-simulator/SKILL.md [new file with mode: 0644]
ui/cli/src/cli.ts
ui/cli/src/client/lifecycle.ts
ui/cli/src/commands/action.ts
ui/cli/src/commands/skill.ts [new file with mode: 0644]
ui/cli/src/output/renderers.ts
ui/cli/src/output/table.ts

index 6b02186fa42a989fd26759cb4987060e1ea35617..2ff767b2eae4bcb4e21831fb2be0375d77c80e91 100644 (file)
@@ -98,7 +98,7 @@ Use `--config <path>` to load a specific config file instead of the XDG default.
 ## Usage
 
 ```shell
-node dist/cli.js [global-options] <command> [subcommand] [options]
+evse-cli [global-options] <command> [subcommand] [options]
 ```
 
 ### Global Options
@@ -111,24 +111,43 @@ node dist/cli.js [global-options] <command> [subcommand] [options]
 | `--server-url <url>`  | WebSocket URL (overrides config host/port/secure) |
 | `-h, --help`          | Show help                                         |
 
-### Commands
+### Using hashIds
+
+Most commands accept optional `[hashId...]` arguments to target specific stations. Omitting them applies the command to all stations.
+
+Hash IDs support **prefix matching** — you can use a short prefix instead of the full ID. The CLI resolves it automatically:
+
+```shell
+# Full hash:
+evse-cli station start e9041c294a82a2d6aa194a801c3ba39d6b24d1cb...
+
+# Short prefix (copied from `station list` output):
+evse-cli station start e9041c294a82
+
+# Even shorter (as long as it's unambiguous):
+evse-cli station start e904
+```
+
+If a prefix matches multiple stations, the CLI returns an error.
+
+### Simulator 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
+evse-cli simulator state   # Get simulator state and statistics
+evse-cli simulator start   # Start the simulator
+evse-cli 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 <template> -n <count>  # Add stations from template
-node dist/cli.js station delete [hashId...]            # Delete station(s)
+evse-cli station list                          # List all charging stations
+evse-cli station start [hashId...]             # Start station(s)
+evse-cli station stop [hashId...]              # Stop station(s)
+evse-cli station add -t <template> -n <count>  # Add stations from template
+evse-cli station delete [hashId...]            # Delete station(s)
 ```
 
 **`station add` options:**
@@ -140,38 +159,44 @@ node dist/cli.js station delete [hashId...]            # Delete station(s)
 | `--supervision-url <url>` | No       | Override supervision URL  |
 | `--auto-start`            | No       | Auto-start added stations |
 
+**`station delete` options:**
+
+| Option            | Required | Description                   |
+| ----------------- | -------- | ----------------------------- |
+| `--delete-config` | No       | Also delete persistent config |
+
 #### template
 
 ```shell
-node dist/cli.js template list     # List available station templates
+evse-cli template list     # List available station templates
 ```
 
 #### connection
 
 ```shell
-node dist/cli.js connection open [hashId...]   # Open WebSocket connection
-node dist/cli.js connection close [hashId...]  # Close WebSocket connection
+evse-cli connection open [hashId...]   # Open WebSocket connection to CSMS
+evse-cli connection close [hashId...]  # Close WebSocket connection to CSMS
 ```
 
 #### connector
 
 ```shell
-node dist/cli.js connector lock --connector-id <id> [hashId...]   # Lock connector
-node dist/cli.js connector unlock --connector-id <id> [hashId...]  # Unlock connector
+evse-cli connector lock --connector-id <id> [hashId...]    # Lock connector
+evse-cli connector unlock --connector-id <id> [hashId...]  # Unlock connector
 ```
 
 #### atg
 
 ```shell
-node dist/cli.js atg start [hashId...] [--connector-ids <ids...>]  # Start ATG
-node dist/cli.js atg stop [hashId...]  [--connector-ids <ids...>]  # Stop ATG
+evse-cli atg start [hashId...] [--connector-ids <ids...>]  # Start ATG
+evse-cli atg stop [hashId...]  [--connector-ids <ids...>]  # Stop ATG
 ```
 
 #### transaction
 
 ```shell
-node dist/cli.js transaction start --connector-id <id> --id-tag <tag> [hashId...]
-node dist/cli.js transaction stop --transaction-id <id> [hashId...]
+evse-cli transaction start --connector-id <id> --id-tag <tag> [hashId...]
+evse-cli transaction stop --transaction-id <id> [hashId...]
 ```
 
 #### ocpp
@@ -179,47 +204,53 @@ node dist/cli.js transaction stop --transaction-id <id> [hashId...]
 Send OCPP commands directly to charging stations:
 
 ```shell
-node dist/cli.js ocpp heartbeat [hashId...]
-node dist/cli.js ocpp authorize --id-tag <tag> [hashId...]
-node dist/cli.js ocpp boot-notification [hashId...]
+evse-cli ocpp heartbeat [hashId...]
+evse-cli ocpp authorize --id-tag <tag> [hashId...]
+evse-cli ocpp boot-notification [hashId...]
+evse-cli ocpp status-notification --connector-id <id> --error-code <code> --status <status> [hashId...]
+evse-cli ocpp meter-values --connector-id <id> [hashId...]
+evse-cli ocpp data-transfer --vendor-id <id> [--message-id <id>] [--data <json>] [hashId...]
 ```
 
-Available OCPP commands: `authorize`, `boot-notification`, `data-transfer`, `diagnostics-status-notification`, `firmware-status-notification`, `get-15118-ev-certificate`, `get-certificate-status`, `heartbeat`, `log-status-notification`, `meter-values`, `notify-customer-information`, `notify-report`, `security-event-notification`, `sign-certificate`, `status-notification`, `transaction-event`.
+Other OCPP commands (no extra options): `diagnostics-status-notification`, `firmware-status-notification`, `get-15118-ev-certificate`, `get-certificate-status`, `log-status-notification`, `notify-customer-information`, `notify-report`, `security-event-notification`, `sign-certificate`, `transaction-event`.
 
-#### performance
+#### supervision
 
 ```shell
-node dist/cli.js performance stats  # Get performance statistics
+evse-cli supervision set-url --supervision-url <url> [hashId...]  # Set supervision URL
 ```
 
-#### supervision
+#### performance
 
 ```shell
-node dist/cli.js supervision set-url --supervision-url <url> [hashId...]  # Set supervision URL
+evse-cli performance stats  # Get performance statistics
 ```
 
-### JSON Output Mode
+### Local Commands
+
+#### skill
 
-Use `--json` for machine-readable output on stdout:
+Install the embedded [Agent Skills](https://agentskills.io) skill for AI agent integration:
 
 ```shell
-node dist/cli.js --json simulator state
-# {"status":"success","state":{...}}
+evse-cli skill show                # Print the SKILL.md to stdout
+evse-cli skill install             # Install to .agents/skills/evse-simulator/
+evse-cli skill install --global    # Install to ~/.agents/skills/evse-simulator/
+evse-cli skill install --force     # Overwrite existing installation
 ```
 
-Errors are written to stdout as JSON in `--json` mode.
+Works with OpenCode, Claude Code, GitHub Copilot, Cursor, and other compatible agents.
 
-### Using hashIds
+### Output Modes
 
-Most station commands accept optional `[hashId...]` variadic arguments. Omitting them applies the command to all stations:
+| Mode            | Flag     | Description                            |
+| --------------- | -------- | -------------------------------------- |
+| Human (default) | —        | Colored tables, status icons, counters |
+| JSON            | `--json` | Structured JSON on stdout              |
 
-```shell
-# All stations:
-node dist/cli.js station start
+Hash IDs are truncated in human mode for readability. Use `--json` for full hash IDs.
 
-# Specific stations:
-node dist/cli.js station start abc123 def456
-```
+Errors in `--json` mode are written to stdout as structured JSON.
 
 ## Exit Codes
 
index dd81b5a0b1c2e7f7a938a16b622d2a3615fb22b6..45ca76031bd8f1e07919d58fe03c77dd786febec 100644 (file)
@@ -6,6 +6,7 @@ import { readFileSync } from 'node:fs'
 import { env } from 'node:process'
 
 const pkg = JSON.parse(readFileSync('./package.json', 'utf8'))
+const skill = readFileSync('./skills/evse-simulator/SKILL.md', 'utf8')
 
 const isDevelopmentBuild = env.BUILD === 'development'
 
@@ -16,6 +17,7 @@ await build({
   bundle: true,
   define: {
     __CLI_VERSION__: JSON.stringify(pkg.version),
+    __EMBEDDED_SKILL__: JSON.stringify(skill),
     'process.env.WS_NO_BUFFER_UTIL': JSON.stringify('1'),
     'process.env.WS_NO_UTF_8_VALIDATE': JSON.stringify('1'),
   },
diff --git a/ui/cli/skills/evse-simulator/SKILL.md b/ui/cli/skills/evse-simulator/SKILL.md
new file mode 100644 (file)
index 0000000..ec45704
--- /dev/null
@@ -0,0 +1,177 @@
+---
+name: evse-simulator
+description: Control and monitor an OCPP charging station simulator via CLI. Use when users ask to manage charging stations, send OCPP messages, start/stop the simulator or ATG, list stations or templates, or check simulator state.
+license: Apache-2.0
+compatibility: Requires the simulator UI server to be running with WebSocket enabled.
+metadata:
+  author: SAP
+---
+
+# e-Mobility Charging Station Simulator CLI
+
+## Status
+
+!`evse-cli --version 2>/dev/null || echo "Not installed — see ui/cli/README.md"`
+
+## Configuration
+
+The CLI connects to the simulator UI server via WebSocket (SRPC protocol).
+
+Config file location: `${XDG_CONFIG_HOME:-$HOME/.config}/evse-cli/config.json`
+
+```json
+{
+  "uiServer": {
+    "host": "localhost",
+    "port": 8080,
+    "protocol": "ui",
+    "version": "0.0.1",
+    "authentication": {
+      "enabled": true,
+      "type": "protocol-basic-auth",
+      "username": "admin",
+      "password": "admin"
+    }
+  }
+}
+```
+
+Precedence: defaults < config file < `--config <path>` < `--server-url <url>`.
+
+## Global Options
+
+| Option               | Description                  |
+| -------------------- | ---------------------------- |
+| `--json`             | Machine-readable JSON output |
+| `--config <path>`    | Path to config file          |
+| `--server-url <url>` | WebSocket URL override       |
+
+## Commands
+
+### Simulator
+
+```shell
+evse-cli simulator state   # Get state, version, template statistics
+evse-cli simulator start   # Start the simulator
+evse-cli simulator stop    # Stop the simulator
+```
+
+### Stations
+
+```shell
+evse-cli station list                                     # List all stations
+evse-cli station start [hashId...]                        # Start station(s)
+evse-cli station stop [hashId...]                         # Stop station(s)
+evse-cli station add -t <template> -n <count>             # Add stations
+evse-cli station add -t <template> -n 2 --auto-start      # Add and auto-start
+evse-cli station add -t <template> -n 1 --supervision-url ws://csms:8180/path
+evse-cli station delete [hashId...]                       # Delete station(s)
+```
+
+### Templates
+
+```shell
+evse-cli template list    # List available station templates
+```
+
+### Connections
+
+```shell
+evse-cli connection open [hashId...]    # Open WebSocket to CSMS
+evse-cli connection close [hashId...]   # Close WebSocket to CSMS
+```
+
+### Connectors
+
+```shell
+evse-cli connector lock --connector-id <id> [hashId...]    # Lock connector
+evse-cli connector unlock --connector-id <id> [hashId...]  # Unlock connector
+```
+
+### ATG (Automatic Transaction Generator)
+
+```shell
+evse-cli atg start [hashId...]                         # Start ATG on all connectors
+evse-cli atg start --connector-ids 1,2 [hashId...]     # Start on specific connectors
+evse-cli atg stop [hashId...]                          # Stop ATG
+```
+
+### Transactions
+
+```shell
+evse-cli transaction start --connector-id <id> --id-tag <tag> [hashId...]
+evse-cli transaction stop --transaction-id <id> [hashId...]
+```
+
+### OCPP Messages
+
+```shell
+evse-cli ocpp heartbeat [hashId...]
+evse-cli ocpp boot-notification [hashId...]
+evse-cli ocpp authorize --id-tag <tag> [hashId...]
+evse-cli ocpp status-notification --connector-id <id> --error-code <code> --status <status> [hashId...]
+evse-cli ocpp meter-values --connector-id <id> [hashId...]
+evse-cli ocpp data-transfer --vendor-id <id> [--message-id <id>] [--data <json>] [hashId...]
+```
+
+Other OCPP commands (no extra options): `diagnostics-status-notification`, `firmware-status-notification`, `get-15118-ev-certificate`, `get-certificate-status`, `log-status-notification`, `notify-customer-information`, `notify-report`, `security-event-notification`, `sign-certificate`, `transaction-event`.
+
+### Supervision
+
+```shell
+evse-cli supervision set-url --supervision-url <url> [hashId...]
+```
+
+### Performance
+
+```shell
+evse-cli performance stats   # Get performance statistics
+```
+
+## Output Modes
+
+- **Human** (default): borderless tables, status icons, colored output
+- **JSON** (`--json`): structured JSON on stdout, parseable by scripts
+
+## Exit Codes
+
+| Code  | Meaning                          |
+| ----- | -------------------------------- |
+| `0`   | Success                          |
+| `1`   | Error (connection, server, auth) |
+| `130` | Interrupted (Ctrl+C)             |
+
+## hashId Convention
+
+Omitting `[hashId...]` applies the command to ALL stations. Pass one or more hash IDs to target specific stations. Get hash IDs from `evse-cli station list` or `evse-cli --json station list`.
+
+## Common Workflows
+
+### Start simulator and check state
+
+```shell
+evse-cli simulator start
+evse-cli simulator state
+```
+
+### Add stations from template and start ATG
+
+```shell
+evse-cli template list
+evse-cli station add -t keba-ocpp2.station-template -n 3 --auto-start
+evse-cli atg start
+```
+
+### Send OCPP heartbeat to all stations
+
+```shell
+evse-cli ocpp heartbeat
+```
+
+### Change supervision URL and reconnect
+
+```shell
+evse-cli station stop <hashId>
+evse-cli supervision set-url --supervision-url ws://new-csms:8180/path <hashId>
+evse-cli station start <hashId>
+```
index c1697ae8bea545e4b84586ec8f6239a1333557cd..cd82d795341f066c4de55cd62f2b7b09f816af25 100644 (file)
@@ -8,6 +8,7 @@ import { createConnectorCommands } from './commands/connector.js'
 import { createOcppCommands } from './commands/ocpp.js'
 import { createPerformanceCommands } from './commands/performance.js'
 import { createSimulatorCommands } from './commands/simulator.js'
+import { createSkillCommands } from './commands/skill.js'
 import { createStationCommands } from './commands/station.js'
 import { createSupervisionCommands } from './commands/supervision.js'
 import { createTemplateCommands } from './commands/template.js'
@@ -35,6 +36,7 @@ program.addCommand(createTransactionCommands(program))
 program.addCommand(createOcppCommands(program))
 program.addCommand(createPerformanceCommands(program))
 program.addCommand(createSupervisionCommands(program))
+program.addCommand(createSkillCommands())
 
 registerSignalHandlers()
 await program.parseAsync(argv)
index 07546ae755b8242714159d8d6597285f6b2201b8..65667e63deef43f7e13a54248b57fce7bc9b8e60 100644 (file)
@@ -25,19 +25,20 @@ let cleanupInProgress = false
 
 export interface ExecuteOptions {
   config: UIServerConfigurationSection
-  formatter: Formatter
+  formatter?: Formatter
   payload: RequestPayload
   procedureName: ProcedureName
+  silent?: boolean
   timeoutMs?: number
 }
 
-export const executeCommand = async (options: ExecuteOptions): Promise<void> => {
-  const { config, formatter, payload, procedureName, timeoutMs } = options
+export const executeCommand = async (options: ExecuteOptions): Promise<ResponsePayload> => {
+  const { config, formatter, payload, procedureName, silent, timeoutMs } = options
 
   const client = new WebSocketClient(wsFactory, config, timeoutMs)
   const { url } = client
 
-  const isInteractive = process.stderr.isTTY
+  const isInteractive = !silent && process.stderr.isTTY
   const spinner = isInteractive
     ? ora({ stream: process.stderr }).start(`Connecting to ${url}`)
     : null
@@ -85,7 +86,8 @@ export const executeCommand = async (options: ExecuteOptions): Promise<void> =>
     }
     const response: ResponsePayload = await client.sendRequest(procedureName, payload, remaining)
     spinner?.stop()
-    formatter.output(response)
+    formatter?.output(response)
+    return response
   } catch (error: unknown) {
     spinner?.fail()
     throw error
index 9f91ffb426b66206f5574ef745b419dc1a975fcf..ff812703174c494dcb99987b165f085bfd1c8274 100644 (file)
@@ -1,14 +1,20 @@
 import type { Command } from 'commander'
 
 import process from 'node:process'
-import { type ProcedureName, type RequestPayload, ServerFailureError } from 'ui-common'
+import {
+  ProcedureName,
+  type RequestPayload,
+  ResponseStatus,
+  ServerFailureError,
+  type UIServerConfigurationSection,
+} from 'ui-common'
 
+import type { StationListPayload } from '../output/renderers.js'
 import type { GlobalOptions } from '../types.js'
 
 import { executeCommand } from '../client/lifecycle.js'
 import { loadConfig } from '../config/loader.js'
 import { createFormatter } from '../output/formatter.js'
-
 export const parseInteger = (value: string): number => {
   const n = Number.parseInt(value, 10)
   if (Number.isNaN(n)) {
@@ -17,6 +23,48 @@ export const parseInteger = (value: string): number => {
   return n
 }
 
+// SHA-384 hex hashes are 96 chars. Treat anything >= half-length as a full hash (skip resolution).
+const MIN_FULL_HASH_LENGTH = 48
+
+const resolveShortHashIds = async (
+  hashIds: string[],
+  config: UIServerConfigurationSection
+): Promise<string[]> => {
+  if (hashIds.length === 0) return []
+
+  const allFull = hashIds.every(id => id.length >= MIN_FULL_HASH_LENGTH)
+  if (allFull) return hashIds
+
+  const response = await executeCommand({
+    config,
+    payload: {},
+    procedureName: ProcedureName.LIST_CHARGING_STATIONS,
+    silent: true,
+  })
+
+  if (response.status !== ResponseStatus.SUCCESS || !Array.isArray(response.chargingStations)) {
+    throw new Error(
+      `Failed to list charging stations for hash ID resolution (status: ${response.status})`
+    )
+  }
+
+  const listResponse = response as StationListPayload
+  const allHashIds = listResponse.chargingStations.map(cs => cs.stationInfo.hashId)
+
+  return hashIds.map(input => {
+    if (input.length >= MIN_FULL_HASH_LENGTH) return input
+
+    const matches = allHashIds.filter(full => full.startsWith(input))
+    if (matches.length === 1) return matches[0]
+    if (matches.length === 0) {
+      throw new Error(`No station found matching hash prefix '${input}'`)
+    }
+    throw new Error(
+      `Ambiguous hash prefix '${input}' matches ${matches.length.toString()} stations`
+    )
+  })
+}
+
 export const runAction = async (
   program: Command,
   procedureName: ProcedureName,
@@ -26,7 +74,16 @@ export const runAction = async (
   const formatter = createFormatter(rootOpts.json)
   try {
     const config = await loadConfig({ configPath: rootOpts.config, url: rootOpts.serverUrl })
-    await executeCommand({ config, formatter, payload, procedureName })
+
+    let resolvedPayload = payload
+    if (Array.isArray(payload.hashIds) && payload.hashIds.length > 0) {
+      resolvedPayload = {
+        ...payload,
+        hashIds: await resolveShortHashIds(payload.hashIds, config),
+      }
+    }
+
+    await executeCommand({ config, formatter, payload: resolvedPayload, procedureName })
     process.exitCode = 0
   } catch (error: unknown) {
     if (error instanceof ServerFailureError) {
diff --git a/ui/cli/src/commands/skill.ts b/ui/cli/src/commands/skill.ts
new file mode 100644 (file)
index 0000000..47f2d20
--- /dev/null
@@ -0,0 +1,55 @@
+import { Command } from 'commander'
+import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
+import { homedir } from 'node:os'
+import { resolve } from 'node:path'
+import process from 'node:process'
+
+declare const __EMBEDDED_SKILL__: string
+
+const SKILL_DIR_NAME = 'evse-simulator'
+const SKILL_FILE_NAME = 'SKILL.md'
+
+const getInstallDir = (global: boolean): string =>
+  global
+    ? resolve(homedir(), '.agents', 'skills', SKILL_DIR_NAME)
+    : resolve(process.cwd(), '.agents', 'skills', SKILL_DIR_NAME)
+
+export const createSkillCommands = (): Command => {
+  const cmd = new Command('skill').description('Show or install the embedded agent skill')
+
+  cmd
+    .command('show')
+    .description('Print the embedded SKILL.md to stdout')
+    .action(() => {
+      process.stdout.write(__EMBEDDED_SKILL__)
+    })
+
+  cmd
+    .command('install')
+    .description('Install the skill into .agents/skills/evse-simulator/')
+    .option('--global', 'Install to ~/.agents/skills/ instead of project-local')
+    .option('-f, --force', 'Overwrite existing installation')
+    .action((options: { force?: boolean; global?: boolean }) => {
+      const dir = getInstallDir(options.global === true)
+      const filepath = resolve(dir, SKILL_FILE_NAME)
+
+      if (existsSync(filepath) && options.force !== true) {
+        process.stderr.write(`Skill already installed at ${filepath}\nUse --force to overwrite.\n`)
+        process.exitCode = 1
+        return
+      }
+
+      try {
+        mkdirSync(dir, { recursive: true })
+        writeFileSync(filepath, __EMBEDDED_SKILL__, 'utf8')
+      } catch (error: unknown) {
+        const msg = error instanceof Error ? error.message : String(error)
+        process.stderr.write(`Failed to install skill: ${msg}\n`)
+        process.exitCode = 1
+        return
+      }
+      process.stdout.write(`Installed skill to ${filepath}\n`)
+    })
+
+  return cmd
+}
index ca1e55bc579b9c304c7118d7130826f33e948fd0..c17447e6a2c03d2c23805062efd1676b0a9a8319 100644 (file)
@@ -1,4 +1,4 @@
-import type { ConnectorEntry, EvseEntry, ResponsePayload } from 'ui-common'
+import type { ChargingStationData, ResponsePayload } from 'ui-common'
 
 import chalk from 'chalk'
 import process from 'node:process'
@@ -12,6 +12,10 @@ import {
   wsIcon,
 } from './format.js'
 
+export type StationListPayload = ResponsePayload & {
+  chargingStations: ChargingStationData[]
+}
+
 type PerformancePayload = ResponsePayload & {
   performanceStatistics: unknown[]
 }
@@ -37,23 +41,6 @@ type SimulatorStatePayload = ResponsePayload & {
   }
 }
 
-type StationPayload = ResponsePayload & {
-  chargingStations: {
-    connectors?: ConnectorEntry[]
-    evses?: EvseEntry[]
-    started?: boolean
-    stationInfo: {
-      chargingStationId: string
-      hashId: string
-      ocppVersion?: string
-      templateName?: string
-    }
-    supervisionUrl?: string
-    timestamp?: number
-    wsState?: number
-  }[]
-}
-
 type TemplatePayload = ResponsePayload & {
   templates: string[]
 }
@@ -72,7 +59,7 @@ const isSimulatorState = (p: ResponsePayload): p is SimulatorStatePayload => {
   )
 }
 
-const isStationList = (p: ResponsePayload): p is StationPayload =>
+const isStationList = (p: ResponsePayload): p is StationListPayload =>
   'chargingStations' in p && Array.isArray(p.chargingStations)
 
 const isTemplateList = (p: ResponsePayload): p is TemplatePayload =>
@@ -135,7 +122,7 @@ const renderSimulatorState = (payload: SimulatorStatePayload): void => {
   )
 }
 
-const renderStationList = (payload: StationPayload): void => {
+const renderStationList = (payload: StationListPayload): void => {
   const stations = payload.chargingStations
   if (stations.length === 0) {
     process.stdout.write(chalk.dim('No charging stations\n'))
@@ -152,13 +139,13 @@ const renderStationList = (payload: StationPayload): void => {
       chalk.dim(truncateId(si.hashId)),
       `${wsIcon(cs.wsState)} ${chalk.dim(`${available.toString()}/${total.toString()}`)}`,
       chalk.dim(si.ocppVersion ?? '–'),
-      chalk.dim(si.templateName?.replace('.station-template', '') ?? '–'),
+      chalk.dim(si.templateName.replace('.station-template', '')),
       fuzzyTime(cs.timestamp),
     ])
   }
   process.stdout.write(`${table.toString()}\n`)
 
-  const started = stations.filter(s => s.started === true).length
+  const started = stations.filter(s => s.started).length
   const connected = stations.filter(s => s.wsState === 1).length
   process.stdout.write(
     chalk.dim(
index f0c7fd7f12b5f2b351aa9553dc2b7f3d4e53a6fc..e0d1de6952912ab904d1ebf7a90756d81f8826f1 100644 (file)
@@ -2,7 +2,7 @@ import chalk from 'chalk'
 import process from 'node:process'
 import { type ResponsePayload, ResponseStatus } from 'ui-common'
 
-import { borderlessTable } from './format.js'
+import { borderlessTable, truncateId } from './format.js'
 import { tryRenderPayload } from './renderers.js'
 
 export const outputTable = (payload: ResponsePayload): void => {
@@ -14,7 +14,7 @@ export const outputTable = (payload: ResponsePayload): void => {
     )
     const table = borderlessTable(['Hash ID'])
     for (const id of payload.hashIdsSucceeded) {
-      table.push([id])
+      table.push([truncateId(id)])
     }
     process.stdout.write(table.toString() + '\n')
   }
@@ -24,13 +24,13 @@ export const outputTable = (payload: ResponsePayload): void => {
     if (payload.responsesFailed != null && payload.responsesFailed.length > 0) {
       const table = borderlessTable(['Hash ID', 'Error'])
       for (const entry of payload.responsesFailed) {
-        table.push([entry.hashId ?? '(unknown)', entry.errorMessage ?? 'Unknown error'])
+        table.push([truncateId(entry.hashId ?? '(unknown)'), entry.errorMessage ?? 'Unknown error'])
       }
       process.stderr.write(table.toString() + '\n')
     } else {
       const table = borderlessTable(['Hash ID'])
       for (const id of payload.hashIdsFailed) {
-        table.push([id])
+        table.push([truncateId(id)])
       }
       process.stderr.write(table.toString() + '\n')
     }