CLI Design¶
This document describes the design of the Donguru command-line interface: its goals, its command surface, how it is configured, and how it composes with other Unix tools. A companion page covers the implementation details (crate selection, module layout, testing, packaging).
Status
This is a design document. It captures intent and decisions. Where a detail is still open it is marked with a TBD note so it can be resolved during implementation.
Goals¶
The CLI is the primary way a user interacts with a Donguru dongle from a host computer. It must be:
- Unix-native — line-oriented, pipeable, and scriptable. It reads from arguments/stdin and writes results to stdout, diagnostics to stderr, and signals outcome through exit codes.
- Easy to type and memorable — short, consistent verbs. A short binary alias. Sensible defaults so the common case needs few flags.
- Layered and predictable configuration — defaults, config files, then environment, then command-line flags, each overriding the previous.
- Cross-platform — Linux is the priority target, followed by macOS, then Windows. Nothing in the design should hard-code Linux-only assumptions.
- Extensible — third parties can add subcommands without modifying the core, using a Git/Cargo-style discovery mechanism.
Non-goals¶
- The CLI is not a daemon or a long-running service. Each invocation does
one job and exits. (A
watch/monitormode may stream until interrupted, but it is still a single foreground process.) - The CLI is not a GUI and makes no assumptions about a terminal being interactive. All behaviour must degrade cleanly when stdout is a pipe.
What the CLI controls¶
The Donguru dongle exposes several subsystems. The CLI groups its commands to match them:
| Subsystem | Responsibility |
|---|---|
| Firmware | Report firmware version and update the dongle firmware. |
| GPIO | Read and drive general-purpose I/O pins. |
| USB Hub | Enable/disable and inspect downstream USB ports. |
| Power | Control the power switch and read the power meter (V/I). |
| Bus bridge | USART / I2C access for prototyping and debugging. |
| Device | Enumerate and select dongles, report identity and status. |
Transport model¶
The host talks to the dongle over a control channel exposed by the dongle's MCU. The channel is carried over USB, and the wire protocol is WireWeaver. The CLI treats this as a typed request/response transport: each subcommand maps to one or more RPC calls.
flowchart LR
User -->|argv / stdin| CLI
CLI -->|wireweaver RPC over USB| MCU[Dongle MCU]
MCU --> GPIO & Hub & Power & Bus[USART/I2C]
CLI -->|stdout / stderr / exit code| User
Design Notes:
- The transport layer should be abstracted behind a trait so the RPC surface can be exercised in tests with a mock, and so an alternative transport (e.g. a raw serial fallback) can be added later without touching command code.
- Firmware update is exposed through the same front end even though its underlying mechanism (bootloader/DFU flow) is TBD. The user-facing command is stable regardless of how flashing is implemented.
Many small tools behind a single front end¶
Like git, Donguru presents a single, user-facing front end while the
project itself is assembled from many small, targeted tools (binaries, scripts,
…) such as donguru-gpio, donguru-update, and so on.
The front-end binary that assembles the full subcommand tree is named
donguru, with a short alias dg. It is the single point of contact
for a regular user. Rationale:
- One thing to install, discover (
donguru help), and version. - Shared behaviour lives in one place. Configuration, device selection, and output formatting are implemented once in the front end instead of being duplicated in every tool.
- A subcommand tree is familiar. Tools like
git,cargo,docker, andipfollow the same pattern, and it is easy to explore via--helpat every level.
This approach keeps the benefits of separate, independently developed tools while giving the user a single entry point. Because configuration loading and layering is itself exposed as a command (see Configuration), both built-in commands and later-added plugin commands can reuse the same mechanism.
Naming¶
- Canonical binary:
donguru. - Short alias:
dg(installed alongside, the same executable under two names). - Subcommands are short, lowercase verbs and nouns. Prefer
noun verbgrouping where a subsystem has several actions (e.g.gpio read,power on), and a bare verb where there is only one obvious action. - Third-party extensions are encouraged to add a two-letter namespace after
the
donguru-prefix to avoid future collisions with built-ins that do not exist yet — e.g.donguru-vh-scanis invoked asdonguru vh-scan.
Command tree¶
The following is the intended top-level surface. Names are chosen to be short
and memorable, every node supports --help.
donguru
├── device # enumerate and inspect dongles
│ ├── list # (alias: ls) list connected dongles
│ ├── info # identity, serial, firmware version
│ └── select # persist a default device for this project
├── gpio
│ ├── read <pin> # read one or more pins
│ ├── write <pin> <level>
│ ├── mode <pin> <in|out|...>
│ └── watch <pin> # stream level changes until interrupted
├── usb # downstream hub ports
│ ├── list # port map and status
│ ├── on <port> # enable a downstream port
│ └── off <port> # disable a downstream port
├── power
│ ├── on|off # power switch to downstream devices
│ ├── status # switch state
│ ├── meter # instantaneous voltage/current
│ └── watch # stream meter readings until interrupted
├── i2c
│ ├── scan # probe the bus for devices
│ ├── read <addr> <reg> [len]
│ └── write <addr> <reg> <bytes...>
├── uart # USART bridge
│ └── ... # TBD: open/attach a serial bridge
├── fw # firmware
│ ├── version # report running firmware version
│ └── update <image> # update the dongle firmware
├── completions <shell> # generate shell completions
├── config # generate a default config, and inspect/evaluate the effective one
└── help
Global flags¶
These flags are accepted at every level of the tree and apply to all subcommands. They are collected here so the individual command sections can stay focused on their own arguments.
| Flag | Purpose |
|---|---|
-d, --device <sel> |
Select the target dongle (see Device selection). |
-f, --format <fmt> |
Output format: text (default), json, or jsonl. |
--color <when> |
Colour output: auto (default), always, or never. |
-v, --verbose |
Increase diagnostic detail on stderr (repeatable, e.g. -vv). |
-q, --quiet |
Suppress non-essential stderr output. |
-h, --help |
Show help for the current command. |
-V, --version |
Print the donguru version and exit. |
Unix-philosophy¶
The CLI is built to be a good citizen in pipelines.
- stdout is data, stderr is diagnostics. Only requested results go to
stdout. Progress, warnings, and errors go to stderr, so
donguru … | jqnever sees log noise. - Exit codes are meaningful.
0success; non-zero on failure with distinct codes for categories (usage error, no device, transport error, device error, timeout). These are documented in implementation. - Output formats are selectable. A global
--format/-fflag chooses the representation:text(default) — human-readable, aligned, possibly colourised when stdout is a TTY.json— one JSON value, for machine consumption.jsonl— newline-delimited JSON for streaming commands (watch).
- No colour / no interactivity when piped. Colour and progress spinners are
auto-disabled when stdout is not a TTY, and can be forced with
--color=always|never. HonourNO_COLOR. - Read from stdin where it composes. Byte payloads (e.g.
i2c write,fw update -) may be read from stdin so data can be piped in. - Quiet and verbose knobs.
-q/--quietsuppresses non-essential stderr;-v/--verbose(repeatable) increases diagnostic detail.
Example pipelines:
# Machine-readable power reading fed to jq
dg power meter -f json | jq .current_ma
# Stream GPIO changes into a log
dg gpio watch 3 -f jsonl >> gpio.log
Configuration¶
Configuration is layered. Later layers override earlier ones on a per-key basis: a lower-priority layer still provides any keys that a higher-priority layer omits.
Ideal three-state merge
Where the libraries make it practical, a richer merge that distinguishes three states per key — None, Default, and Some(value) — would be preferable. The rules would be:
- A previous layer's None or Default can always be overwritten by the next layer.
- If the current layer explicitly sets None, a Default or Some(value) from a previous layer takes precedence.
- An explicit Some(value) in a higher layer always wins.
Whether this is worth the added complexity depends on library support and is left open for implementation.
| Priority | Layer | Source |
|---|---|---|
| 0 (lowest) | Built-in defaults | Compiled-in fallback values. |
| 1 | Config file: global | System-wide (e.g. /etc/donguru/config.toml). |
| 2 | Config file: user | Per-user (e.g. ~/.config/donguru/config.toml). |
| 3 | Config file: project | donguru.toml/.donguru.toml found by walking up from the CWD. |
| 4 | Environment variables | DONGURU_-prefixed variables. |
| 5 (highest) | Command-line flags | Explicit flags. |
Higher priority numbers override lower ones on a per-key basis.
- Config files use a documented format (TOML) and are searched in three
scopes, merged in order:
- global — system-wide (e.g.
/etc/donguru/config.toml). - user — per-user, via the platform config dir
(e.g.
~/.config/donguru/config.toml). - project — a
donguru.toml(or.donguru.toml) discovered by walking up from the current directory, so a repository can pin settings such as a default device.
- global — system-wide (e.g.
- Environment variables use a
DONGURU_prefix and mirror the key path (e.g.DONGURU_DEVICE,DONGURU_OUTPUT). - Command-line flags always win.
Typical configurable keys: default device selector, default output format, default timeouts, log verbosity. The exact schema and precedence mechanics are specified in implementation.
Device selection¶
With multiple dongles connected, a single device is chosen by, in order:
--device/-d <selector>flag,DONGURU_DEVICEenvironment variable,- a
devicekey from the config layers (e.g. a project pin), - auto-selection when exactly one dongle is present.
A selector may be a serial number, an index, or a USB path. If none of the
above resolves to exactly one device, the CLI errors and asks the user to
disambiguate (and device list shows the choices).
Extensibility¶
The core is extended with a Git/Cargo-style external subcommand mechanism.
When donguru foo is run and foo is not a built-in subcommand, the CLI
searches PATH for an executable named donguru-foo and executes it,
forwarding the remaining arguments and the resolved context (selected device,
output format) via environment variables (e.g. DONGURU_DEVICE,
DONGURU_OUTPUT).
- Plugins are ordinary executables in any language.
donguru helplists discovered plugins alongside built-ins.- Naming convention:
donguru-<name>→donguru <name>(mirrorsgit-<x>/cargo-<x>).
This preserves the single-front-end user experience while letting features be developed, shipped, and versioned independently.
Open questions¶
- WireWeaver surface — the exact RPC schema and how firmware update is carried over it (TBD, tracked with firmware work).
- Windows specifics — driver/enumeration differences for USB access on Windows (lowest priority target).