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.
Files changed (81) hide show
  1. meshcorectl-0.1.0/.github/workflows/ci.yml +66 -0
  2. meshcorectl-0.1.0/.gitignore +12 -0
  3. meshcorectl-0.1.0/LICENSE +21 -0
  4. meshcorectl-0.1.0/PKG-INFO +104 -0
  5. meshcorectl-0.1.0/PLAN.md +165 -0
  6. meshcorectl-0.1.0/README.md +74 -0
  7. meshcorectl-0.1.0/docs/command-reference.md +740 -0
  8. meshcorectl-0.1.0/pyproject.toml +90 -0
  9. meshcorectl-0.1.0/scripts/examples/README.md +9 -0
  10. meshcorectl-0.1.0/scripts/examples/ask_mepo_coords +25 -0
  11. meshcorectl-0.1.0/scripts/examples/contact_markers.sh +18 -0
  12. meshcorectl-0.1.0/scripts/examples/getpos.py +153 -0
  13. meshcorectl-0.1.0/scripts/generate_command_reference.py +80 -0
  14. meshcorectl-0.1.0/src/meshcorectl/__init__.py +3 -0
  15. meshcorectl-0.1.0/src/meshcorectl/__main__.py +6 -0
  16. meshcorectl-0.1.0/src/meshcorectl/cli.py +243 -0
  17. meshcorectl-0.1.0/src/meshcorectl/commands/__init__.py +1 -0
  18. meshcorectl-0.1.0/src/meshcorectl/commands/advert.py +33 -0
  19. meshcorectl-0.1.0/src/meshcorectl/commands/completion.py +40 -0
  20. meshcorectl-0.1.0/src/meshcorectl/commands/config_cmd.py +186 -0
  21. meshcorectl-0.1.0/src/meshcorectl/commands/create.py +65 -0
  22. meshcorectl-0.1.0/src/meshcorectl/commands/delete.py +100 -0
  23. meshcorectl-0.1.0/src/meshcorectl/commands/describe.py +86 -0
  24. meshcorectl-0.1.0/src/meshcorectl/commands/exec_.py +107 -0
  25. meshcorectl-0.1.0/src/meshcorectl/commands/get.py +183 -0
  26. meshcorectl-0.1.0/src/meshcorectl/commands/login.py +115 -0
  27. meshcorectl-0.1.0/src/meshcorectl/commands/logs.py +130 -0
  28. meshcorectl-0.1.0/src/meshcorectl/commands/reboot.py +40 -0
  29. meshcorectl-0.1.0/src/meshcorectl/commands/scan.py +76 -0
  30. meshcorectl-0.1.0/src/meshcorectl/commands/send.py +124 -0
  31. meshcorectl-0.1.0/src/meshcorectl/commands/set_.py +73 -0
  32. meshcorectl-0.1.0/src/meshcorectl/commands/top.py +77 -0
  33. meshcorectl-0.1.0/src/meshcorectl/commands/trace.py +39 -0
  34. meshcorectl-0.1.0/src/meshcorectl/commands/version.py +58 -0
  35. meshcorectl-0.1.0/src/meshcorectl/connect.py +74 -0
  36. meshcorectl-0.1.0/src/meshcorectl/context_store.py +256 -0
  37. meshcorectl-0.1.0/src/meshcorectl/durations.py +32 -0
  38. meshcorectl-0.1.0/src/meshcorectl/logging_utils.py +44 -0
  39. meshcorectl-0.1.0/src/meshcorectl/mesh_data.py +460 -0
  40. meshcorectl-0.1.0/src/meshcorectl/output/__init__.py +109 -0
  41. meshcorectl-0.1.0/src/meshcorectl/output/resources.py +73 -0
  42. meshcorectl-0.1.0/src/meshcorectl/output/table.py +69 -0
  43. meshcorectl-0.1.0/src/meshcorectl/py.typed +0 -0
  44. meshcorectl-0.1.0/src/meshcorectl/resource_specs.py +89 -0
  45. meshcorectl-0.1.0/src/meshcorectl/selectors.py +185 -0
  46. meshcorectl-0.1.0/tests/__init__.py +0 -0
  47. meshcorectl-0.1.0/tests/commands/__init__.py +0 -0
  48. meshcorectl-0.1.0/tests/commands/test_advert_cmd.py +59 -0
  49. meshcorectl-0.1.0/tests/commands/test_cli_root.py +96 -0
  50. meshcorectl-0.1.0/tests/commands/test_completion_cmd.py +36 -0
  51. meshcorectl-0.1.0/tests/commands/test_config_cmd.py +211 -0
  52. meshcorectl-0.1.0/tests/commands/test_create_cmd.py +101 -0
  53. meshcorectl-0.1.0/tests/commands/test_delete_cmd.py +134 -0
  54. meshcorectl-0.1.0/tests/commands/test_describe_cmd.py +82 -0
  55. meshcorectl-0.1.0/tests/commands/test_exec_cmd.py +102 -0
  56. meshcorectl-0.1.0/tests/commands/test_get_cmd.py +332 -0
  57. meshcorectl-0.1.0/tests/commands/test_login_cmd.py +183 -0
  58. meshcorectl-0.1.0/tests/commands/test_logs_cmd.py +195 -0
  59. meshcorectl-0.1.0/tests/commands/test_reboot_cmd.py +27 -0
  60. meshcorectl-0.1.0/tests/commands/test_scan_cmd.py +104 -0
  61. meshcorectl-0.1.0/tests/commands/test_send_cmd.py +141 -0
  62. meshcorectl-0.1.0/tests/commands/test_set_cmd.py +61 -0
  63. meshcorectl-0.1.0/tests/commands/test_top_cmd.py +85 -0
  64. meshcorectl-0.1.0/tests/commands/test_trace_cmd.py +50 -0
  65. meshcorectl-0.1.0/tests/commands/test_version_cmd.py +58 -0
  66. meshcorectl-0.1.0/tests/conftest.py +70 -0
  67. meshcorectl-0.1.0/tests/fakes/__init__.py +0 -0
  68. meshcorectl-0.1.0/tests/fakes/meshcore_double.py +112 -0
  69. meshcorectl-0.1.0/tests/unit/__init__.py +0 -0
  70. meshcorectl-0.1.0/tests/unit/test_cli_state.py +110 -0
  71. meshcorectl-0.1.0/tests/unit/test_command_reference.py +45 -0
  72. meshcorectl-0.1.0/tests/unit/test_connect.py +78 -0
  73. meshcorectl-0.1.0/tests/unit/test_context_store.py +275 -0
  74. meshcorectl-0.1.0/tests/unit/test_dunder_main.py +15 -0
  75. meshcorectl-0.1.0/tests/unit/test_durations.py +29 -0
  76. meshcorectl-0.1.0/tests/unit/test_logging_utils.py +43 -0
  77. meshcorectl-0.1.0/tests/unit/test_mesh_data.py +910 -0
  78. meshcorectl-0.1.0/tests/unit/test_meshcore_double.py +125 -0
  79. meshcorectl-0.1.0/tests/unit/test_output_dispatch.py +86 -0
  80. meshcorectl-0.1.0/tests/unit/test_output_table.py +99 -0
  81. 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,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ build/
6
+ dist/
7
+ .coverage
8
+ .coverage.*
9
+ htmlcov/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
@@ -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).