# CLI

The CLI is a standalone command-line tool that provides scripting and automation access to all ScadaBridge administrative operations. It connects to the central cluster's HTTP Management API and dispatches commands to the `ManagementActor` via `POST /management`. Authentication and role resolution happen server-side; the CLI sends LDAP credentials as HTTP Basic Auth on every request.

## Overview

The CLI component lives in `src/ZB.MOM.WW.ScadaBridge.CLI/` and builds to a self-contained `scadabridge` executable (or `scadabridge.exe` on Windows). It is not part of the Host binary — it deploys on any machine with HTTP access to a central node.

The tool is built on `System.CommandLine` and exposes a hierarchical command tree organized by management domain (`template`, `instance`, `site`, `data-connection`, and so on). Every command follows the same pattern: resolve the management URL and credentials, construct a command object whose type maps to a `ManagementCommandRegistry` entry, serialize it into the `{ command, payload }` JSON envelope, and `POST` it to `<managementUrl>/management`. The server response — JSON success body or `{ error, code }` error envelope — is printed to stdout or stderr respectively.

The CLI is the preferred automation interface. When setting up system state (sites, templates, data connections, deployments, security) without the Central UI, use `scadabridge` commands rather than direct database manipulation.

## Key Concepts

### Connection to the active node

The CLI connects through Traefik, the reverse proxy fronting the central cluster. Traefik routes each request to the active central node based on the `/health/active` probe, so the CLI does not need to know which node (`central-a` or `central-b`) is currently primary. Pointing `--url` (or `managementUrl`) at the Traefik address (default `http://localhost:9000` in Docker) provides automatic HA failover without any CLI-side configuration change.

Direct-to-node access (`http://localhost:9001` for `central-a`, `http://localhost:9002` for `central-b`) bypasses Traefik and is useful for diagnostics but not for production automation.

### Command dispatch

Most commands serialize to a named management command and reach the `ManagementActor` through the `POST /management` endpoint. The `audit query` and `audit export` commands are the exception — they call plain REST endpoints (`GET /api/audit/query`, `GET /api/audit/export`) introduced by Audit Log (#23) and therefore use `SendGetAsync`/`SendGetStreamAsync` on `ManagementHttpClient` rather than `SendCommandAsync`. The streaming `audit export` path uses `HttpCompletionOption.ResponseHeadersRead` to avoid buffering large payloads in memory.

### Authentication and roles

The CLI encodes `--username` and `--password` as an HTTP Basic Auth header on every request. The server performs the LDAP bind, group lookup, and role resolution. The CLI never contacts LDAP directly and never caches credentials between invocations.

Role enforcement is applied by the `ManagementActor`. Operations require the appropriate role:

| Role | Covers |
|------|--------|
| `Admin` | Security settings, site management, SMTP config, audit-config queries |
| `Design` | Templates, shared scripts, external systems, DB connections, API methods, notification lists |
| `Deployment` | Instance lifecycle (deploy, enable, disable, delete), data connection bindings, bundle import/export |
| `OperationalAudit` | Reading the Audit Log (`audit query`) |
| `AuditExport` | Exporting the Audit Log (`audit export`) |

A request lacking the required role exits with code `2` (authorization failure). A bad-credential response (HTTP 401) exits with code `1`.

### Output formats

Every command accepts `--format json` (default) or `--format table`. For JSON output, successful server responses are written to stdout verbatim — the server controls the JSON shape and no re-serialization is applied. Locally-constructed output (errors, `debug stream` events) is serialized by `OutputFormatter` with indentation and camelCase. Table output renders a padded plain-text table derived from the response JSON — arrays become rows, single objects become a two-column `Property / Value` table. Errors are always written as `{ "error": "...", "code": "..." }` to stderr, regardless of format.

## Architecture

`Program.cs` builds the `RootCommand` tree, attaches the four global options (`--url`, `--username`, `--password`, `--format`), and registers each command group:

```csharp
rootCommand.Add(TemplateCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(InstanceCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(SiteCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DeployCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DataConnectionCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(ExternalSystemCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(NotificationCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(SecurityCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(AuditLogCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(AuditCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(HealthCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DebugCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(SharedScriptCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DbConnectionCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(ApiMethodCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(BundleCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
```

Every command's action delegates to `CommandHelpers.ExecuteCommandAsync`, which owns the URL/credential resolution, `ManagementHttpClient` lifetime, and exit-code mapping. This consolidation means the authorization-failure exit code (`2`) is enforced uniformly — including for the `bundle` group, which uses `ExecuteCommandAsync` with a longer 5-minute timeout and a per-command `onSuccess` handler rather than a separate HTTP path.

`CliConfig.Load()` is called at the start of every invocation. It merges `~/.scadabridge/config.json`, environment variables, and any defaults. A malformed or unreadable config file emits a warning to stderr and falls through to the environment variable / command-line precedence chain without crashing.

## Usage

### Running the CLI

```bash
# Build
dotnet build src/ZB.MOM.WW.ScadaBridge.CLI

# Minimal invocation (URL and credentials required)
scadabridge --url http://localhost:9000 --username multi-role --password password template list

# Using Traefik (HA; routes to the active node automatically)
scadabridge --url http://localhost:9000 --username multi-role --password password site list

# Table output
scadabridge --url http://localhost:9000 --username multi-role --password password \
  --format table instance list --site-id 1
```

Credentials are safer supplied via environment variables than on the command line, where they appear in process listings and shell history:

```bash
export SCADABRIDGE_MANAGEMENT_URL=http://localhost:9000
export SCADABRIDGE_USERNAME=multi-role
export SCADABRIDGE_PASSWORD=password

scadabridge template create --name "PumpStation" --description "Standard pump station"
scadabridge template attribute add --template-id 3 --name Speed --data-type Float --default-value 0
scadabridge instance create --name "PS-01" --template-id 3 --site-id 1
scadabridge instance deploy --id 7
```

### Command groups

| Group | Subcommands | Role required |
|-------|-------------|---------------|
| `template` | `list`, `get`, `create`, `update`, `delete`, `validate`; `attribute add/update/delete`; `alarm add/update/delete`; `script add/update/delete`; `composition add/delete`; `native-alarm-source add/list/remove` | `Design` |
| `instance` | `list`, `get`, `create`, `set-bindings`, `set-overrides`, `alarm-override set/delete/list`, `native-alarm-source set/clear`, `set-area`, `diff`, `deploy`, `enable`, `disable`, `delete` | `Deployment` |
| `site` | `list`, `get`, `create`, `delete`, `deploy-artifacts`; `area list/create/update/delete` | `Admin` |
| `deploy` | `instance`, `artifacts`, `status` | `Deployment` |
| `data-connection` | `list`, `get`, `create`, `update`, `delete` | `Design` / `Deployment` |
| `external-system` | `list`, `get`, `create`, `update`, `delete` | `Design` |
| `notification` | `list`, `get`, `create`, `update`, `delete`; `smtp list/update` | `Design` / `Admin` |
| `security` | `api-key list/create/update/delete/set-methods`; `role-mapping list/create/update/delete`; `scope-rule list/add/delete` | `Admin` |
| `shared-script` | `list`, `get`, `create`, `update`, `delete` | `Design` |
| `db-connection` | `list`, `get`, `create`, `update`, `delete` | `Design` |
| `api-method` | `list`, `get`, `create`, `update`, `delete` | `Design` |
| `bundle` | `export`, `preview`, `import` | `Deployment` |
| `audit` | `query`, `export`, `verify-chain` | `OperationalAudit` / `AuditExport` |
| `audit-config` | `query` (config-change audit trail; was `audit-log` pre-M8) | `Admin` |
| `health` | `summary`, `site`, `event-log`, `parked-messages` | `Deployment` |
| `debug` | `snapshot`, `stream` | `Deployment` |

### Selected examples

```bash
# Query the operational Audit Log for failed API outbound events in the last 24 hours
scadabridge audit query --since 24h --channel ApiOutbound --errors-only --format table

# Export a full audit window to CSV
scadabridge audit export --since 2026-05-01T00:00:00Z --until 2026-06-01T00:00:00Z \
  --format csv --output /tmp/audit-may-2026.csv

# Export a Transport bundle for selected templates (with transitive dependencies)
scadabridge bundle export --output /tmp/pump-station.scadabundle \
  --templates "PumpStation,BaseModule" --include-dependencies

# Preview a bundle diff before importing
scadabridge bundle preview --input /tmp/pump-station.scadabundle

# Import with overwrite conflict policy
scadabridge bundle import --input /tmp/pump-station.scadabundle --on-conflict overwrite

# Stream live attribute and alarm changes for a running instance
scadabridge debug stream --id 7

# Query deployment records for a specific instance
scadabridge deploy status --instance-id 7 --page-size 20
```

### Exit codes

| Code | Meaning |
|------|---------|
| `0` | Success |
| `1` | Command error (connection failure, validation error, server error) |
| `2` | Authorization failure (insufficient role; HTTP 403 or `FORBIDDEN`/`UNAUTHORIZED` error code) |

## Configuration

`~/.scadabridge/config.json` is loaded on every invocation. A malformed or unreadable file emits a warning to stderr; it does not abort the invocation.

```json
{
  "managementUrl": "http://localhost:9000",
  "defaultFormat": "json"
}
```

| Key | Default | Description |
|-----|---------|-------------|
| `managementUrl` | — | Base URL for the Management API. Overridden by `SCADABRIDGE_MANAGEMENT_URL` env var, then by `--url`. |
| `defaultFormat` | `json` | Default output format when `--format` is not supplied. Overridden by `SCADABRIDGE_FORMAT` env var, then by `--format`. |

Credentials are intentionally never stored in the config file — they are sourced from environment variables or supplied per-invocation on the command line. Storing them in the file would persist them to disk in plaintext.

### Environment variables

| Variable | Overrides | Description |
|----------|-----------|-------------|
| `SCADABRIDGE_MANAGEMENT_URL` | `managementUrl` in config file | Management API base URL |
| `SCADABRIDGE_FORMAT` | `defaultFormat` in config file | Default output format |
| `SCADABRIDGE_USERNAME` | — | LDAP username; overridden by `--username` |
| `SCADABRIDGE_PASSWORD` | — | LDAP password; overridden by `--password`. Preferred over `--password` to avoid leaking credentials into process listings and shell history. |

### URL precedence

The management URL is resolved in this order: `--url` flag → `SCADABRIDGE_MANAGEMENT_URL` env var → `managementUrl` in config file. If none is set, the command exits with code `1` and a `NO_URL` error.

## Dependencies & Interactions

- [Management Service (#18)](./ManagementService.md) — the server-side counterpart. Every CLI command (except `audit query`/`audit export`) translates to a named management command dispatched through `POST /management` to the `ManagementActor`. Role enforcement and LDAP authentication are applied there. The `ManagementCommandRegistry` in Commons maps command types to their names; both sides must stay in sync.
- [Traefik Proxy (#20)](./TraefikProxy.md) — the recommended connection target. Pointing `--url` at the Traefik address ensures requests reach the active central node without per-command failover logic in the CLI. The `debug stream` command's WebSocket connection (SignalR `/hubs/debug-stream`) also traverses Traefik, which proxies the WebSocket upgrade natively.
- [Audit Log (#23)](./AuditLog.md) — the `audit` command group targets the `GET /api/audit/query` and `GET /api/audit/export` REST endpoints exposed by the Audit Log component, bypassing the management command envelope. The `audit-config` group (formerly `audit-log`) targets the configuration-change audit trail (`IAuditService`) via the standard management envelope.
- [Security & Auth (#10)](./Security.md) — the server resolves LDAP credentials and maps group memberships to ScadaBridge roles (`Admin`, `Design`, `Deployment`, `OperationalAudit`, `AuditExport`). The CLI does not interact with LDAP directly.
- [Commons (#16)](./Commons.md) — owns the management command record types and the `ManagementCommandRegistry` that maps each type to its wire name. The CLI project references Commons for these contracts.
- [Transport (#24)](./Transport.md) — the `bundle` command group drives the Transport feature: `bundle export` requests a base64-encoded bundle from the server and streams it to a local `.scadabundle` file; `bundle preview` uploads a file and returns the diff manifest; `bundle import` uploads a file and applies it with a configurable conflict policy.
- Design spec: [Component-CLI.md](../requirements/Component-CLI.md).

## Troubleshooting

### No management URL

```json
{"error":"No management URL specified. Use --url, set SCADABRIDGE_MANAGEMENT_URL, or add 'managementUrl' to ~/.scadabridge/config.json.","code":"NO_URL"}
```

The URL is not set via `--url`, `SCADABRIDGE_MANAGEMENT_URL`, or the config file. Set one of these before running any command.

### Connection failed

```json
{"error":"Connection failed: Connection refused (localhost:9000)","code":"CONNECTION_FAILED"}
```

The central node or Traefik is not reachable at the configured URL. Verify the cluster is running and the URL matches the Traefik port (default `9000` in Docker) or the node's direct port (`9001`/`9002`).

### Authorization failure (exit 2)

The server returned HTTP 403 or an error code of `FORBIDDEN`/`UNAUTHORIZED`. The authenticated user's LDAP groups do not map to a role with permission for the requested operation. Use `security role-mapping list` (requires `Admin`) to inspect role mappings. The `multi-role` test user (`password`) holds `Admin`, `Design`, and `Deployment` roles.

### Malformed config file warning

```text
warning: ignoring malformed or unreadable /home/user/.scadabridge/config.json: ...
```

`CliConfig.Load()` caught a `JsonException`, `IOException`, or `UnauthorizedAccessException` reading the config file. The invocation continues using environment variables and command-line options. Fix or recreate the config file.

### `audit-log` deprecation warning

```text
Warning: 'audit-log' is deprecated and will be removed in a future release. Use 'audit-config' instead.
```

The `audit-log` command group was renamed to `audit-config` in M8 of Audit Log (#23). The old name still works but emits this warning to stderr. Migrate any scripts from `scadabridge audit-log ...` to `scadabridge audit-config ...`.

### Bundle timeout

`bundle export`, `bundle preview`, and `bundle import` all use a 5-minute per-command timeout (compared to the 30-second default). If a bundle operation times out, the server-side operation may still be running. Re-try with a smaller selection or check the central node logs.

## Related Documentation

- [CLI design specification](../requirements/Component-CLI.md)
- [Management Service](./ManagementService.md)
- [Traefik Proxy](./TraefikProxy.md)
- [Audit Log](./AuditLog.md)
- [Security](./Security.md)
- [Commons](./Commons.md)
