|
| 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