Skip to content

Commit 1eedcf0

Browse files
feat(cli): add remote tree command for device directories
Add mpy-cli tree with --path support, backend typed directory listing, README updates, and tests. Validate behavior with full test suite and on a connected MicroPython device. Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
1 parent 4609637 commit 1eedcf0

8 files changed

Lines changed: 722 additions & 1 deletion

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ mpy-cli deploy
149149
mpy-cli upload
150150
mpy-cli run
151151
mpy-cli delete
152+
mpy-cli tree
152153
```
153154

154155
说明:
@@ -284,6 +285,24 @@ mpy-cli delete --path obsolete.py
284285
若配置 `device_upload_dir = "apps/demo"`,则会删除 `:apps/demo/obsolete.py`
285286
`--path` 指向目录时,默认递归删除整个目录。
286287

288+
### `mpy-cli tree`
289+
290+
```bash
291+
mpy-cli tree [--path PATH] [--port PORT] [--no-interactive]
292+
```
293+
294+
- `--path`:设备目标目录路径,语义为相对 `device_upload_dir`;不传时默认读取 `device_upload_dir` 根目录。
295+
- `--port`:指定设备端口。
296+
- `--no-interactive`:禁用交互提问;此时需通过 `--port` 或配置文件提供端口。
297+
298+
推荐用法:
299+
300+
```bash
301+
mpy-cli tree --path .
302+
```
303+
304+
若配置 `device_upload_dir = "apps/demo"`,则默认读取 `:apps/demo`;例如 `--path services` 会读取 `:apps/demo/services`
305+
287306
---
288307

289308
## 常见问题
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# tree 命令功能设计文档
2+
3+
## 背景
4+
5+
当前 `mpy-cli` 已支持 `upload/run/delete`,但缺少“查看设备端目录结构”的能力。用户在部署前后无法快速确认设备上的目录层级,尤其在设置了 `device_upload_dir` 后,不容易判断目标目录是否符合预期。
6+
7+
## 目标
8+
9+
- 新增 `mpy-cli tree` 子命令。
10+
- 默认读取设备端 `device_upload_dir` 对应目录树。
11+
- 支持通过 `--path` 指定相对 `device_upload_dir` 的子目录。
12+
- 输出采用类似 `tree` 的文本结构,便于快速阅读。
13+
14+
## 命令设计
15+
16+
```bash
17+
mpy-cli tree [--path PATH] [--port PORT] [--no-interactive]
18+
```
19+
20+
- `--path`:设备目标目录,语义为相对 `device_upload_dir`;为空时读取 `device_upload_dir` 根。
21+
- `--port`:指定设备串口。
22+
- `--no-interactive`:禁用交互提问;无 `--port` 时按既有规则处理。
23+
24+
## 方案对比
25+
26+
### 方案 A(推荐)主机递归 + 设备单层目录读取
27+
28+
- 每次请求设备一个目录的直接子项(文件/目录),主机侧递归构建树并打印。
29+
- 优点:结构清晰,错误定位直接,便于测试。
30+
- 缺点:深层目录会产生多次设备往返。
31+
32+
### 方案 B 设备端一次性递归回传
33+
34+
- 在设备端执行完整递归脚本,直接输出最终树文本。
35+
- 优点:设备往返少。
36+
- 缺点:设备脚本复杂,兼容性与可测试性较差。
37+
38+
最终采用方案 A。
39+
40+
## 架构与数据流
41+
42+
1. CLI 解析 `tree` 子命令参数。
43+
2. 复用 `_resolve_port()` 解析端口。
44+
3.`device_upload_dir` + `--path` 计算目标设备目录。
45+
4. `MpremoteBackend` 提供“单层目录读取”能力,返回结构化条目(名称 + 是否目录)。
46+
5. CLI 递归请求并生成树形文本输出。
47+
48+
## 错误处理
49+
50+
- 配置错误、端口缺失、`mpremote` 不可用:返回 `1`
51+
- 目录读取失败(目录不存在、权限问题、设备异常):返回 `2`
52+
53+
## 测试策略
54+
55+
- `tests/test_mpremote_backend.py`
56+
- 校验目录读取命令构建。
57+
- 校验目录读取输出解析(`D\tname` / `F\tname`)。
58+
- `tests/test_cli.py`
59+
- 校验 `tree` 命令在 `device_upload_dir` 语义下拼接目标路径。
60+
- 校验 `tree` 读取失败时返回退出码 `2`
61+
- `tests/test_docs_and_ci.py` + `README.md`
62+
- 保持命令列表和参数说明同步。
63+
64+
## 验收标准
65+
66+
- 可执行 `mpy-cli tree --path <dir>` 查看设备目录树。
67+
- 未传 `--path` 时默认查看 `device_upload_dir` 根目录。
68+
- 输出有稳定树结构,目录优先排序。
69+
- 新增测试通过,不影响现有命令行为。
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
# Tree Command Implementation Plan
2+
3+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4+
5+
**Goal:** Add `mpy-cli tree` to print a remote directory tree on the MicroPython device, scoped by `device_upload_dir` and optional `--path`.
6+
7+
**Architecture:** Extend CLI command routing with a new read-only `tree` subcommand. Add a backend API that reads exactly one remote directory level and returns typed entries, then build tree output recursively in CLI for deterministic sorting and formatting. Keep existing port/config/error flow unchanged to minimize behavioral risk.
8+
9+
**Tech Stack:** Python 3.10+, argparse, mpremote backend adapter, pytest.
10+
11+
---
12+
13+
### Task 1: Backend typed directory listing API
14+
15+
**Files:**
16+
- Modify: `mpy_cli/backend/mpremote.py`
17+
- Test: `tests/test_mpremote_backend.py`
18+
19+
**Step 1: Write the failing test**
20+
21+
```python
22+
def test_list_dir_parses_typed_entries() -> None:
23+
called: list[list[str]] = []
24+
25+
def fake_runner(command, capture_output, text, check): # noqa: ANN001
26+
called.append(command)
27+
return subprocess.CompletedProcess(
28+
args=command,
29+
returncode=0,
30+
stdout="D\tapps\nF\tmain.py\n",
31+
stderr="",
32+
)
33+
34+
backend = MpremoteBackend(binary="mpremote", runner=fake_runner, resolver=lambda _: "/usr/bin/mpremote")
35+
entries = backend.list_dir(port="/dev/ttyACM0", remote_path="apps/demo")
36+
37+
assert called[0][0:5] == ["mpremote", "connect", "/dev/ttyACM0", "resume", "exec"]
38+
assert [entry.name for entry in entries] == ["apps", "main.py"]
39+
assert [entry.is_dir for entry in entries] == [True, False]
40+
```
41+
42+
**Step 2: Run test to verify it fails**
43+
44+
Run: `python3 -m pytest -q tests/test_mpremote_backend.py::test_list_dir_parses_typed_entries`
45+
Expected: FAIL with missing `list_dir` or related symbol.
46+
47+
**Step 3: Write minimal implementation**
48+
49+
```python
50+
@dataclass(frozen=True)
51+
class RemoteDirEntry:
52+
name: str
53+
is_dir: bool
54+
55+
def list_dir(self, port: str, remote_path: str) -> list[RemoteDirEntry]:
56+
cmd = self.build_list_dir_command(port=port, remote=remote_path)
57+
result = self._run(cmd)
58+
return _parse_remote_dir_entries(result.stdout)
59+
```
60+
61+
Also add `_build_remote_list_dir_script()` and parser for `D\tname` / `F\tname` lines.
62+
63+
**Step 4: Run test to verify it passes**
64+
65+
Run: `python3 -m pytest -q tests/test_mpremote_backend.py::test_list_dir_parses_typed_entries`
66+
Expected: PASS.
67+
68+
**Step 5: Commit**
69+
70+
```bash
71+
git add mpy_cli/backend/mpremote.py tests/test_mpremote_backend.py
72+
git commit -m "feat: add typed remote directory listing API"
73+
```
74+
75+
### Task 2: CLI `tree` command routing and execution
76+
77+
**Files:**
78+
- Modify: `mpy_cli/cli.py`
79+
- Test: `tests/test_cli.py`
80+
81+
**Step 1: Write the failing test**
82+
83+
```python
84+
def test_tree_executes_remote_list_with_device_upload_prefix(...):
85+
# setup config with device_upload_dir="apps/demo"
86+
# run main(["tree", "--no-interactive", "--port", "COM3", "--path", "services"])
87+
# assert backend.list_dir called with "apps/demo/services"
88+
```
89+
90+
Add another test for backend failure returning `2`.
91+
92+
**Step 2: Run test to verify it fails**
93+
94+
Run: `python3 -m pytest -q tests/test_cli.py::test_tree_executes_remote_list_with_device_upload_prefix`
95+
Expected: FAIL because `tree` command does not exist yet.
96+
97+
**Step 3: Write minimal implementation**
98+
99+
```python
100+
tree_parser = subparsers.add_parser("tree", help="读取设备端目录树")
101+
tree_parser.add_argument("--path", help="设备目标目录路径")
102+
tree_parser.add_argument("--port", help="设备串口")
103+
tree_parser.add_argument("--no-interactive", action="store_true", help="禁用 questionary 交互")
104+
```
105+
106+
Implement `_cmd_tree(args)`:
107+
- load config and setup logging
108+
- resolve port (reuse existing function)
109+
- resolve target directory with `_join_upload_target`
110+
- call backend recursively and print tree lines
111+
- map failure to exit code `2`
112+
113+
**Step 4: Run test to verify it passes**
114+
115+
Run: `python3 -m pytest -q tests/test_cli.py::test_tree_executes_remote_list_with_device_upload_prefix tests/test_cli.py::test_tree_returns_failure_code_when_backend_list_fails`
116+
Expected: PASS.
117+
118+
**Step 5: Commit**
119+
120+
```bash
121+
git add mpy_cli/cli.py tests/test_cli.py
122+
git commit -m "feat: add tree command for remote directory view"
123+
```
124+
125+
### Task 3: Tree output formatting and deterministic order
126+
127+
**Files:**
128+
- Modify: `mpy_cli/cli.py`
129+
- Test: `tests/test_cli.py`
130+
131+
**Step 1: Write the failing test**
132+
133+
```python
134+
def test_tree_prints_nested_structure_in_tree_style(...):
135+
# fake backend returns nested entries
136+
# assert stdout contains ├── / └── / │ formatting and stable order
137+
```
138+
139+
**Step 2: Run test to verify it fails**
140+
141+
Run: `python3 -m pytest -q tests/test_cli.py::test_tree_prints_nested_structure_in_tree_style`
142+
Expected: FAIL because output is not formatted yet.
143+
144+
**Step 3: Write minimal implementation**
145+
146+
Implement helper recursion in `cli.py`:
147+
148+
```python
149+
def _render_remote_tree(...):
150+
# sort: directories first, then by name
151+
# render with connectors
152+
```
153+
154+
**Step 4: Run test to verify it passes**
155+
156+
Run: `python3 -m pytest -q tests/test_cli.py::test_tree_prints_nested_structure_in_tree_style`
157+
Expected: PASS.
158+
159+
**Step 5: Commit**
160+
161+
```bash
162+
git add mpy_cli/cli.py tests/test_cli.py
163+
git commit -m "feat: format tree command output as hierarchical view"
164+
```
165+
166+
### Task 4: README and docs consistency
167+
168+
**Files:**
169+
- Modify: `README.md`
170+
- Modify: `tests/test_docs_and_ci.py`
171+
172+
**Step 1: Write the failing test**
173+
174+
```python
175+
def test_readme_lists_tree_command_parameters() -> None:
176+
content = Path("README.md").read_text(encoding="utf-8")
177+
for token in ["mpy-cli tree", "--path"]:
178+
assert token in content
179+
```
180+
181+
**Step 2: Run test to verify it fails**
182+
183+
Run: `python3 -m pytest -q tests/test_docs_and_ci.py::test_readme_lists_tree_command_parameters`
184+
Expected: FAIL because README has no tree section.
185+
186+
**Step 3: Write minimal implementation**
187+
188+
Update README command list and CLI 参数总览 with:
189+
190+
```bash
191+
mpy-cli tree [--path PATH] [--port PORT] [--no-interactive]
192+
```
193+
194+
Include semantics: path is relative to `device_upload_dir`.
195+
196+
**Step 4: Run test to verify it passes**
197+
198+
Run: `python3 -m pytest -q tests/test_docs_and_ci.py::test_readme_lists_tree_command_parameters`
199+
Expected: PASS.
200+
201+
**Step 5: Commit**
202+
203+
```bash
204+
git add README.md tests/test_docs_and_ci.py
205+
git commit -m "docs: document tree command parameters"
206+
```
207+
208+
### Task 5: Verification sweep
209+
210+
**Files:**
211+
- No code changes required unless failures appear.
212+
213+
**Step 1: Run focused suites**
214+
215+
Run: `python3 -m pytest -q tests/test_mpremote_backend.py tests/test_cli.py tests/test_docs_and_ci.py`
216+
Expected: PASS.
217+
218+
**Step 2: Run full suite**
219+
220+
Run: `python3 -m pytest -q`
221+
Expected: PASS.
222+
223+
**Step 3: Optional syntax check**
224+
225+
Run: `python3 -m compileall mpy_cli`
226+
Expected: No syntax errors.
227+
228+
**Step 4: Final diff review**
229+
230+
Run:
231+
232+
```bash
233+
git status
234+
git diff -- mpy_cli/cli.py mpy_cli/backend/mpremote.py tests/test_cli.py tests/test_mpremote_backend.py tests/test_docs_and_ci.py README.md
235+
```
236+
237+
Expected: Only intended files changed.

0 commit comments

Comments
 (0)