meshcorectl 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meshcorectl-0.1.0/.github/workflows/ci.yml +66 -0
- meshcorectl-0.1.0/.gitignore +12 -0
- meshcorectl-0.1.0/LICENSE +21 -0
- meshcorectl-0.1.0/PKG-INFO +104 -0
- meshcorectl-0.1.0/PLAN.md +165 -0
- meshcorectl-0.1.0/README.md +74 -0
- meshcorectl-0.1.0/docs/command-reference.md +740 -0
- meshcorectl-0.1.0/pyproject.toml +90 -0
- meshcorectl-0.1.0/scripts/examples/README.md +9 -0
- meshcorectl-0.1.0/scripts/examples/ask_mepo_coords +25 -0
- meshcorectl-0.1.0/scripts/examples/contact_markers.sh +18 -0
- meshcorectl-0.1.0/scripts/examples/getpos.py +153 -0
- meshcorectl-0.1.0/scripts/generate_command_reference.py +80 -0
- meshcorectl-0.1.0/src/meshcorectl/__init__.py +3 -0
- meshcorectl-0.1.0/src/meshcorectl/__main__.py +6 -0
- meshcorectl-0.1.0/src/meshcorectl/cli.py +243 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/__init__.py +1 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/advert.py +33 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/completion.py +40 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/config_cmd.py +186 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/create.py +65 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/delete.py +100 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/describe.py +86 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/exec_.py +107 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/get.py +183 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/login.py +115 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/logs.py +130 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/reboot.py +40 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/scan.py +76 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/send.py +124 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/set_.py +73 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/top.py +77 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/trace.py +39 -0
- meshcorectl-0.1.0/src/meshcorectl/commands/version.py +58 -0
- meshcorectl-0.1.0/src/meshcorectl/connect.py +74 -0
- meshcorectl-0.1.0/src/meshcorectl/context_store.py +256 -0
- meshcorectl-0.1.0/src/meshcorectl/durations.py +32 -0
- meshcorectl-0.1.0/src/meshcorectl/logging_utils.py +44 -0
- meshcorectl-0.1.0/src/meshcorectl/mesh_data.py +460 -0
- meshcorectl-0.1.0/src/meshcorectl/output/__init__.py +109 -0
- meshcorectl-0.1.0/src/meshcorectl/output/resources.py +73 -0
- meshcorectl-0.1.0/src/meshcorectl/output/table.py +69 -0
- meshcorectl-0.1.0/src/meshcorectl/py.typed +0 -0
- meshcorectl-0.1.0/src/meshcorectl/resource_specs.py +89 -0
- meshcorectl-0.1.0/src/meshcorectl/selectors.py +185 -0
- meshcorectl-0.1.0/tests/__init__.py +0 -0
- meshcorectl-0.1.0/tests/commands/__init__.py +0 -0
- meshcorectl-0.1.0/tests/commands/test_advert_cmd.py +59 -0
- meshcorectl-0.1.0/tests/commands/test_cli_root.py +96 -0
- meshcorectl-0.1.0/tests/commands/test_completion_cmd.py +36 -0
- meshcorectl-0.1.0/tests/commands/test_config_cmd.py +211 -0
- meshcorectl-0.1.0/tests/commands/test_create_cmd.py +101 -0
- meshcorectl-0.1.0/tests/commands/test_delete_cmd.py +134 -0
- meshcorectl-0.1.0/tests/commands/test_describe_cmd.py +82 -0
- meshcorectl-0.1.0/tests/commands/test_exec_cmd.py +102 -0
- meshcorectl-0.1.0/tests/commands/test_get_cmd.py +332 -0
- meshcorectl-0.1.0/tests/commands/test_login_cmd.py +183 -0
- meshcorectl-0.1.0/tests/commands/test_logs_cmd.py +195 -0
- meshcorectl-0.1.0/tests/commands/test_reboot_cmd.py +27 -0
- meshcorectl-0.1.0/tests/commands/test_scan_cmd.py +104 -0
- meshcorectl-0.1.0/tests/commands/test_send_cmd.py +141 -0
- meshcorectl-0.1.0/tests/commands/test_set_cmd.py +61 -0
- meshcorectl-0.1.0/tests/commands/test_top_cmd.py +85 -0
- meshcorectl-0.1.0/tests/commands/test_trace_cmd.py +50 -0
- meshcorectl-0.1.0/tests/commands/test_version_cmd.py +58 -0
- meshcorectl-0.1.0/tests/conftest.py +70 -0
- meshcorectl-0.1.0/tests/fakes/__init__.py +0 -0
- meshcorectl-0.1.0/tests/fakes/meshcore_double.py +112 -0
- meshcorectl-0.1.0/tests/unit/__init__.py +0 -0
- meshcorectl-0.1.0/tests/unit/test_cli_state.py +110 -0
- meshcorectl-0.1.0/tests/unit/test_command_reference.py +45 -0
- meshcorectl-0.1.0/tests/unit/test_connect.py +78 -0
- meshcorectl-0.1.0/tests/unit/test_context_store.py +275 -0
- meshcorectl-0.1.0/tests/unit/test_dunder_main.py +15 -0
- meshcorectl-0.1.0/tests/unit/test_durations.py +29 -0
- meshcorectl-0.1.0/tests/unit/test_logging_utils.py +43 -0
- meshcorectl-0.1.0/tests/unit/test_mesh_data.py +910 -0
- meshcorectl-0.1.0/tests/unit/test_meshcore_double.py +125 -0
- meshcorectl-0.1.0/tests/unit/test_output_dispatch.py +86 -0
- meshcorectl-0.1.0/tests/unit/test_output_table.py +99 -0
- meshcorectl-0.1.0/tests/unit/test_selectors.py +193 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
|
|
21
|
+
- name: Install package + dev dependencies
|
|
22
|
+
run: pip install -e ".[dev]"
|
|
23
|
+
|
|
24
|
+
- name: Lint (ruff)
|
|
25
|
+
run: ruff check .
|
|
26
|
+
|
|
27
|
+
- name: Type check (mypy)
|
|
28
|
+
run: mypy
|
|
29
|
+
|
|
30
|
+
- name: Test with coverage gate
|
|
31
|
+
# Also enforces docs/command-reference.md staying in sync with the
|
|
32
|
+
# real CLI (tests/unit/test_command_reference.py regenerates it in
|
|
33
|
+
# memory and diffs against what's committed).
|
|
34
|
+
run: pytest
|
|
35
|
+
|
|
36
|
+
- name: Build wheel + sdist
|
|
37
|
+
run: python -m build
|
|
38
|
+
|
|
39
|
+
publish:
|
|
40
|
+
name: Publish to PyPI
|
|
41
|
+
needs: test
|
|
42
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
environment:
|
|
45
|
+
name: pypi
|
|
46
|
+
url: https://pypi.org/p/meshcorectl
|
|
47
|
+
permissions:
|
|
48
|
+
id-token: write # required for PyPI trusted publishing (OIDC)
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v4
|
|
51
|
+
|
|
52
|
+
- uses: actions/setup-python@v5
|
|
53
|
+
with:
|
|
54
|
+
python-version: "3.12"
|
|
55
|
+
|
|
56
|
+
- name: Build wheel + sdist
|
|
57
|
+
run: |
|
|
58
|
+
pip install build
|
|
59
|
+
python -m build
|
|
60
|
+
|
|
61
|
+
- name: Publish to PyPI
|
|
62
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
63
|
+
with:
|
|
64
|
+
# A push to main whose version wasn't bumped would otherwise fail
|
|
65
|
+
# the workflow trying to re-upload an existing release.
|
|
66
|
+
skip-existing: true
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Faradome
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: meshcorectl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A kubectl-style command line interface for MeshCore companion radios
|
|
5
|
+
Project-URL: Homepage, https://github.com/Faradome/meshcorectl
|
|
6
|
+
Project-URL: Issues, https://github.com/Faradome/meshcorectl/issues
|
|
7
|
+
Author-email: William Canterbury <william.canterbury@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Communications :: Ham Radio
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: bleak>=0.22
|
|
16
|
+
Requires-Dist: click>=8.1
|
|
17
|
+
Requires-Dist: meshcore>=2.3.9
|
|
18
|
+
Requires-Dist: pyserial>=3.5
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
27
|
+
Requires-Dist: types-pyserial; extra == 'dev'
|
|
28
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# meshcorectl
|
|
32
|
+
|
|
33
|
+
> **This is an AI-generated application.** The design, code, tests, and documentation in this
|
|
34
|
+
> repository were produced by an AI coding agent (Claude), directed and reviewed by a human
|
|
35
|
+
> maintainer.
|
|
36
|
+
|
|
37
|
+
A non-interactive, kubectl-style CLI for MeshCore companion radios:
|
|
38
|
+
`meshcorectl VERB [TYPE] [NAME] [flags]`, named connection contexts (BLE/serial/TCP), and
|
|
39
|
+
`-o table|json|yaml|wide|name` output on every read command.
|
|
40
|
+
|
|
41
|
+
For design rationale and architecture, see [PLAN.md](PLAN.md).
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install -e ".[dev]" # from a checkout, until this is published
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Point a named context at your device (BLE, serial, or TCP)
|
|
53
|
+
meshcorectl config set-context home --ble-address AA:BB:CC:DD:EE:FF
|
|
54
|
+
meshcorectl config use-context home
|
|
55
|
+
|
|
56
|
+
# Read
|
|
57
|
+
meshcorectl get contacts
|
|
58
|
+
meshcorectl get contact alice -o yaml
|
|
59
|
+
meshcorectl describe device
|
|
60
|
+
meshcorectl logs -f
|
|
61
|
+
|
|
62
|
+
# Write
|
|
63
|
+
meshcorectl send message alice "hello"
|
|
64
|
+
meshcorectl create channel 1 "#general"
|
|
65
|
+
meshcorectl delete contact -l 't=client,u>30d' # -l selectors batch across matches
|
|
66
|
+
|
|
67
|
+
# Shell completion
|
|
68
|
+
source <(meshcorectl completion bash) # or zsh / fish
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Every command has real `--help` text; [docs/command-reference.md](docs/command-reference.md) is
|
|
72
|
+
the same text generated into one file. Nothing here is interactive — no REPL, no prompts on the
|
|
73
|
+
happy path (an exception: `login` without `--password`/`--password-stdin` prompts securely for
|
|
74
|
+
one, the same as `ssh` or `git`) — every command is a single invocation that connects, does one
|
|
75
|
+
thing, and exits, which is what makes it scriptable.
|
|
76
|
+
|
|
77
|
+
## Status
|
|
78
|
+
|
|
79
|
+
Every command in this README is implemented and tested. A background-agent daemon for
|
|
80
|
+
low-latency repeated invocations (`meshcored`) is planned but not yet built.
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python3 -m venv .venv
|
|
86
|
+
source .venv/bin/activate
|
|
87
|
+
pip install -e ".[dev]"
|
|
88
|
+
pytest # 100% line+branch coverage enforced
|
|
89
|
+
ruff check .
|
|
90
|
+
mypy
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
After adding or changing a command, regenerate the reference doc:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python3 scripts/generate_command_reference.py
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A test (`tests/unit/test_command_reference.py`) fails if it's out of date.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
[MIT](LICENSE) — same license as the original
|
|
104
|
+
[meshcore-cli](https://github.com/meshcore-dev/meshcore-cli).
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# meshcorectl — design & architecture
|
|
2
|
+
|
|
3
|
+
A non-interactive, kubectl-style CLI for MeshCore companion radios. The `meshcore` PyPI package
|
|
4
|
+
provides the BLE/serial/TCP transports, frame parsing, an async client with a `commands.*` API,
|
|
5
|
+
and an `EventType` pub/sub model; this project implements only the CLI layer on top of it.
|
|
6
|
+
|
|
7
|
+
## Command tree
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
meshcorectl
|
|
11
|
+
├── get device | contacts [-l] [-A] | contact NAME | channels [-A] | channel N |
|
|
12
|
+
│ pending-contacts | path CONTACT | time
|
|
13
|
+
├── describe device | contact NAME
|
|
14
|
+
├── create contact --uri URI (import)
|
|
15
|
+
│ channel N NAME [KEY]
|
|
16
|
+
├── delete contact NAME | -l selector
|
|
17
|
+
│ channel N
|
|
18
|
+
├── send message [CONTACT | -l selector] TEXT [--wait-ack]
|
|
19
|
+
│ channel N TEXT
|
|
20
|
+
├── exec [REPEATER | -l selector] -- CLI_CMD (raw repeater console command)
|
|
21
|
+
├── login [REPEATER | -l selector] [--password | --password-stdin]
|
|
22
|
+
├── logout REPEATER
|
|
23
|
+
├── top contact NAME [--history] (telemetry / min-max-avg)
|
|
24
|
+
├── logs [-f] [--since DURATION] [--rx] (message stream)
|
|
25
|
+
├── trace PATH
|
|
26
|
+
├── advert [--flood]
|
|
27
|
+
├── reboot --yes
|
|
28
|
+
├── set device PARAM VALUE (radio, tx-power, telemetry-mode, …)
|
|
29
|
+
├── scan [--ble] [--serial] [--timeout] (non-interactive discovery table)
|
|
30
|
+
├── config get-contexts | use-context NAME | set-context NAME [flags] |
|
|
31
|
+
│ current-context | view | delete-context NAME
|
|
32
|
+
├── version
|
|
33
|
+
└── completion bash | zsh | fish
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Every noun gets singular + plural + short form (`contact`/`contacts`/`ct`, `channel`/`channels`/`ch`).
|
|
37
|
+
Every command has real `--help` text, generated into [docs/command-reference.md](docs/command-reference.md).
|
|
38
|
+
|
|
39
|
+
No `delete pending-contacts`: pending contacts are watched for over one `--timeout` window (see
|
|
40
|
+
`get pending-contacts`), never persisted, so there is nothing for a delete to ever clear.
|
|
41
|
+
|
|
42
|
+
## kubectl idioms this CLI borrows
|
|
43
|
+
|
|
44
|
+
| kubectl idiom | meshcorectl equivalent |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `kubectl get pods`, `kubectl get pod foo -o yaml` | `meshcorectl get contacts`, `meshcorectl get contact foo -o yaml` |
|
|
47
|
+
| `kubectl describe pod foo` | `meshcorectl describe contact foo` (telemetry, path, flags, last-seen, one human view) |
|
|
48
|
+
| Resource short names (`po`, `svc`, `deploy`) | `ct` (contact), `ch` (channel), `dev` (device) |
|
|
49
|
+
| `-o json\|yaml\|wide\|name` | Same flag, same values, on every read command |
|
|
50
|
+
| `-l/--selector key=value` batch targeting | Same flag on `get`, `delete`, `send`, `exec`, `login` (`-l t=2,u<24h`) |
|
|
51
|
+
| `kubectl exec pod -- cmd` | `meshcorectl exec repeater-name -- <raw console command>` |
|
|
52
|
+
| `kubectl logs [-f] [--since]` | `meshcorectl logs [-f] [--since]` |
|
|
53
|
+
| `kubectl top node/pod` | `meshcorectl top contact NAME [--history]` |
|
|
54
|
+
| `kubectl config get-contexts/use-context/current-context/view` | `meshcorectl config …` over named connection profiles |
|
|
55
|
+
| `kubectl version` | `meshcorectl version` (CLI version + connected device firmware) |
|
|
56
|
+
| `kubectl completion bash/zsh/fish` | `meshcorectl completion bash/zsh/fish` |
|
|
57
|
+
| `--dry-run` | Same, on every mutating command |
|
|
58
|
+
| Non-CRUD standalone verbs (`cordon`, `drain`) | `advert`, `reboot`, `trace`, `login`, `logout` stay top-level, not shoehorned into `get`/`create`/`delete` |
|
|
59
|
+
|
|
60
|
+
## Design decisions
|
|
61
|
+
|
|
62
|
+
### One-shot connections; a background daemon is future work
|
|
63
|
+
|
|
64
|
+
Each invocation opens a connection, acts, and disconnects. Simplest model; fine for interactive
|
|
65
|
+
use and for scripts that don't call `meshcorectl` in a tight loop (BLE connect is the slow case,
|
|
66
|
+
~1-3s; TCP/serial are fast).
|
|
67
|
+
|
|
68
|
+
A future `meshcored` background agent would own a persistent BLE/serial/TCP connection and
|
|
69
|
+
expose it over a local socket, making `meshcorectl` a thin client — unlocking `get contacts -w`
|
|
70
|
+
(watch), instant repeated invocations, and event subscriptions that outlive one command.
|
|
71
|
+
`connect.py` is the one seam such a client would replace: every command talks to a small
|
|
72
|
+
`MeshCoreConnection` protocol, never to `meshcore.MeshCore` directly, so swapping the connection
|
|
73
|
+
method touches no command module. This is also what makes command modules testable against a
|
|
74
|
+
fake connection (see Testing, below).
|
|
75
|
+
|
|
76
|
+
### Click as the CLI framework
|
|
77
|
+
|
|
78
|
+
Nested `Group`s map directly onto the command tree; a `click.Context` carries the resolved
|
|
79
|
+
connection and output formatter down through subcommands; shell completion is close to free.
|
|
80
|
+
`click.testing.CliRunner` runs any command in-process with captured stdout/stderr/exit code,
|
|
81
|
+
which is what makes full test coverage practical.
|
|
82
|
+
|
|
83
|
+
### Plain aligned text, no color
|
|
84
|
+
|
|
85
|
+
Fixed-width columns computed per invocation, consistent left-justification (text and numeric
|
|
86
|
+
columns alike, matching kubectl's own tabwriter), `-` placeholders for empty fields. No ANSI
|
|
87
|
+
color, no `rich`/`colorama` dependency — a ~50-line internal table writer covers it. JSON via
|
|
88
|
+
stdlib `json`; YAML via `pyyaml`.
|
|
89
|
+
|
|
90
|
+
### `-l/--selector` filtering
|
|
91
|
+
|
|
92
|
+
A `t=`/`h=`/`u=`/`d`/`f` contact-filter grammar as a `-l/--selector` flag reused across
|
|
93
|
+
`get`/`delete`/`send`/`exec`/`login`, rather than a separate batch-apply command:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
meshcorectl delete contact -l 't=1,u>2d'
|
|
97
|
+
meshcorectl login -l 't=2,d' --password-stdin < password.txt
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Exactly `kubectl delete pods -l app=foo`'s shape: one flag, reused everywhere.
|
|
101
|
+
|
|
102
|
+
### Binary name: `meshcorectl`
|
|
103
|
+
|
|
104
|
+
In the `kubectl`/`systemctl`/`<subject>ctl` family.
|
|
105
|
+
|
|
106
|
+
## Package layout
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
meshcorectl/
|
|
110
|
+
├── pyproject.toml
|
|
111
|
+
├── src/meshcorectl/
|
|
112
|
+
│ ├── cli.py # root Click group: global --context/-o/--timeout/-v
|
|
113
|
+
│ ├── context_store.py # ~/.config/meshcorectl/config.yaml — named connection contexts
|
|
114
|
+
│ ├── connect.py # resolve context -> meshcore.MeshCore via create_ble/serial/tcp
|
|
115
|
+
│ ├── mesh_data.py # Event/payload -> plain dict translation, no Click
|
|
116
|
+
│ ├── selectors.py # -l filter grammar (parse + match against a contact dict)
|
|
117
|
+
│ ├── output/
|
|
118
|
+
│ │ ├── __init__.py # dispatch on -o table|wide|json|yaml|name
|
|
119
|
+
│ │ ├── table.py # dependency-free column writer
|
|
120
|
+
│ │ └── resources.py # per-resource column definitions
|
|
121
|
+
│ └── commands/ # one module per verb: get.py, describe.py, create.py, ...
|
|
122
|
+
├── tests/
|
|
123
|
+
│ ├── fakes/meshcore_double.py # fake MeshCore/CommandHandler: scripted Event responses
|
|
124
|
+
│ ├── unit/ # selectors, table/output formatting, context_store, mesh_data
|
|
125
|
+
│ └── commands/ # one test module per commands/*.py, via Click CliRunner + the fake
|
|
126
|
+
└── docs/command-reference.md # generated from --help
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`connect.py` is the only module that imports `meshcore`'s transport classes; every command
|
|
130
|
+
module receives an already-connected client through the Click context, which is what keeps
|
|
131
|
+
`tests/commands/` fast and hardware-free.
|
|
132
|
+
|
|
133
|
+
## Non-interactive by design
|
|
134
|
+
|
|
135
|
+
No REPL, chat navigation, line-redirection DSL, alias engine, or interactive device picker. A
|
|
136
|
+
shell already covers these better: pipe to `jq`, redirect with `>`, use shell aliases/functions.
|
|
137
|
+
`scan` prints a plain table of discoverable devices to pick an address from, instead of an
|
|
138
|
+
interactive picker.
|
|
139
|
+
|
|
140
|
+
## Testing strategy
|
|
141
|
+
|
|
142
|
+
- **A fake connection is the foundation.** `tests/fakes/meshcore_double.py` implements the same
|
|
143
|
+
surface every command module calls (`commands.get_contacts`, `commands.send_msg`, `subscribe`,
|
|
144
|
+
…) and returns scripted `Event`/`EventType` values, including error payloads, timeouts, and
|
|
145
|
+
disconnects. No command module ever touches real BLE/serial/TCP in a test.
|
|
146
|
+
- **Every command gets a table-driven CliRunner test**: happy path, the device returning
|
|
147
|
+
`EventType.ERROR`, a not-found argument, and — for mutating commands — `--dry-run` performing
|
|
148
|
+
no `commands.*` call. Selector-aware commands get matrix cases (matches none/one/many).
|
|
149
|
+
Commands that only need one connection assert `connect_call_count == 1`.
|
|
150
|
+
- **Pure-logic modules get unit tests with no Click/CLI involvement**: `selectors.py` (grammar
|
|
151
|
+
parsing, comparison operators, relative-time suffixes), `output/table.py` (column widths, empty
|
|
152
|
+
placeholders), `context_store.py` (YAML round-trip, missing/corrupt file, unknown context),
|
|
153
|
+
`mesh_data.py` (Event-to-dict translation for every resource kind).
|
|
154
|
+
- **Coverage gate**: `pytest --cov=meshcorectl --cov-report=term-missing --cov-fail-under=90`.
|
|
155
|
+
`connect.py`'s transport-selection calls are the one piece that would need real hardware to
|
|
156
|
+
exercise directly; everything above that seam is covered by the fake.
|
|
157
|
+
- **Out of scope for now**: hardware-in-the-loop tests against a real device in CI. Worth
|
|
158
|
+
revisiting once a `meshcored` daemon exists, since a long-lived process is a better fit for an
|
|
159
|
+
opt-in nightly hardware job than a one-shot CLI is.
|
|
160
|
+
|
|
161
|
+
## Future work
|
|
162
|
+
|
|
163
|
+
A `meshcored` background agent (see Design decisions, above): a small daemon owns the persistent
|
|
164
|
+
connection and exposes it over a local socket, so `meshcorectl` becomes a thin, instant client.
|
|
165
|
+
Not built yet.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# meshcorectl
|
|
2
|
+
|
|
3
|
+
> **This is an AI-generated application.** The design, code, tests, and documentation in this
|
|
4
|
+
> repository were produced by an AI coding agent (Claude), directed and reviewed by a human
|
|
5
|
+
> maintainer.
|
|
6
|
+
|
|
7
|
+
A non-interactive, kubectl-style CLI for MeshCore companion radios:
|
|
8
|
+
`meshcorectl VERB [TYPE] [NAME] [flags]`, named connection contexts (BLE/serial/TCP), and
|
|
9
|
+
`-o table|json|yaml|wide|name` output on every read command.
|
|
10
|
+
|
|
11
|
+
For design rationale and architecture, see [PLAN.md](PLAN.md).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e ".[dev]" # from a checkout, until this is published
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Point a named context at your device (BLE, serial, or TCP)
|
|
23
|
+
meshcorectl config set-context home --ble-address AA:BB:CC:DD:EE:FF
|
|
24
|
+
meshcorectl config use-context home
|
|
25
|
+
|
|
26
|
+
# Read
|
|
27
|
+
meshcorectl get contacts
|
|
28
|
+
meshcorectl get contact alice -o yaml
|
|
29
|
+
meshcorectl describe device
|
|
30
|
+
meshcorectl logs -f
|
|
31
|
+
|
|
32
|
+
# Write
|
|
33
|
+
meshcorectl send message alice "hello"
|
|
34
|
+
meshcorectl create channel 1 "#general"
|
|
35
|
+
meshcorectl delete contact -l 't=client,u>30d' # -l selectors batch across matches
|
|
36
|
+
|
|
37
|
+
# Shell completion
|
|
38
|
+
source <(meshcorectl completion bash) # or zsh / fish
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Every command has real `--help` text; [docs/command-reference.md](docs/command-reference.md) is
|
|
42
|
+
the same text generated into one file. Nothing here is interactive — no REPL, no prompts on the
|
|
43
|
+
happy path (an exception: `login` without `--password`/`--password-stdin` prompts securely for
|
|
44
|
+
one, the same as `ssh` or `git`) — every command is a single invocation that connects, does one
|
|
45
|
+
thing, and exits, which is what makes it scriptable.
|
|
46
|
+
|
|
47
|
+
## Status
|
|
48
|
+
|
|
49
|
+
Every command in this README is implemented and tested. A background-agent daemon for
|
|
50
|
+
low-latency repeated invocations (`meshcored`) is planned but not yet built.
|
|
51
|
+
|
|
52
|
+
## Development
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python3 -m venv .venv
|
|
56
|
+
source .venv/bin/activate
|
|
57
|
+
pip install -e ".[dev]"
|
|
58
|
+
pytest # 100% line+branch coverage enforced
|
|
59
|
+
ruff check .
|
|
60
|
+
mypy
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
After adding or changing a command, regenerate the reference doc:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python3 scripts/generate_command_reference.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A test (`tests/unit/test_command_reference.py`) fails if it's out of date.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
[MIT](LICENSE) — same license as the original
|
|
74
|
+
[meshcore-cli](https://github.com/meshcore-dev/meshcore-cli).
|