|
| 1 | +# Run 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 run` to execute an existing file on the MicroPython device using a path relative to `device_upload_dir`. |
| 6 | + |
| 7 | +**Architecture:** Extend CLI routing with a dedicated `run` subcommand that reuses existing config loading, port resolution, confirmation flow, and remote-path joining logic. Add backend support in `MpremoteBackend` to execute a remote file via `mpremote ... exec` with a robust path-resolution script on the device side. |
| 8 | + |
| 9 | +**Tech Stack:** Python, argparse, pathlib, mpremote, pytest. |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +### Task 1: Add backend run command builder and executor |
| 14 | + |
| 15 | +**Files:** |
| 16 | +- Modify: `mpy_cli/backend/mpremote.py` |
| 17 | +- Test: `tests/test_mpremote_backend.py` |
| 18 | + |
| 19 | +**Step 1: Write the failing tests** |
| 20 | + |
| 21 | +```python |
| 22 | +def test_run_builds_expected_command() -> None: |
| 23 | + backend = MpremoteBackend(binary="mpremote") |
| 24 | + |
| 25 | + cmd = backend.build_run_command(port="/dev/ttyACM0", remote="apps/demo/main.py") |
| 26 | + |
| 27 | + assert cmd[0:4] == ["mpremote", "connect", "/dev/ttyACM0", "resume"] |
| 28 | + assert cmd[4] == "exec" |
| 29 | + assert "apps/demo/main.py" in cmd[5] |
| 30 | + |
| 31 | + |
| 32 | +def test_run_file_invokes_exec_command() -> None: |
| 33 | + called: list[list[str]] = [] |
| 34 | + |
| 35 | + def fake_runner(command, capture_output, text, check): # noqa: ANN001 |
| 36 | + called.append(command) |
| 37 | + return subprocess.CompletedProcess( |
| 38 | + args=command, |
| 39 | + returncode=0, |
| 40 | + stdout="ok\n", |
| 41 | + stderr="", |
| 42 | + ) |
| 43 | + |
| 44 | + backend = MpremoteBackend( |
| 45 | + binary="mpremote", |
| 46 | + runner=fake_runner, |
| 47 | + resolver=lambda _: "/usr/bin/mpremote", |
| 48 | + ) |
| 49 | + |
| 50 | + backend.run_file(port="/dev/ttyACM0", remote_path="apps/demo/main.py") |
| 51 | + |
| 52 | + assert called |
| 53 | + assert called[0][0:5] == ["mpremote", "connect", "/dev/ttyACM0", "resume", "exec"] |
| 54 | +``` |
| 55 | + |
| 56 | +**Step 2: Run tests to verify they fail** |
| 57 | + |
| 58 | +Run: `python3 -m pytest -q tests/test_mpremote_backend.py -k run` |
| 59 | +Expected: FAIL because `build_run_command/run_file` do not exist. |
| 60 | + |
| 61 | +**Step 3: Write minimal implementation** |
| 62 | + |
| 63 | +Implement `build_run_command()` and `run_file()` in `MpremoteBackend`. |
| 64 | + |
| 65 | +**Step 4: Run tests to verify they pass** |
| 66 | + |
| 67 | +Run: `python3 -m pytest -q tests/test_mpremote_backend.py -k run` |
| 68 | +Expected: PASS. |
| 69 | + |
| 70 | +### Task 2: Add CLI `run` subcommand routing and argument parsing |
| 71 | + |
| 72 | +**Files:** |
| 73 | +- Modify: `mpy_cli/cli.py` |
| 74 | +- Test: `tests/test_cli.py` |
| 75 | + |
| 76 | +**Step 1: Write the failing test** |
| 77 | + |
| 78 | +```python |
| 79 | +def test_run_non_interactive_requires_path(tmp_path: Path, monkeypatch) -> None: |
| 80 | + monkeypatch.chdir(tmp_path) |
| 81 | + main(["init", "--no-interactive"]) |
| 82 | + |
| 83 | + code = main(["run", "--no-interactive", "--port", "COM3", "--yes"]) |
| 84 | + |
| 85 | + assert code == 1 |
| 86 | +``` |
| 87 | + |
| 88 | +**Step 2: Run test to verify it fails** |
| 89 | + |
| 90 | +Run: `python3 -m pytest -q tests/test_cli.py -k run_non_interactive_requires_path` |
| 91 | +Expected: FAIL because parser does not include `run`. |
| 92 | + |
| 93 | +**Step 3: Write minimal implementation** |
| 94 | + |
| 95 | +Add parser entry and command dispatch for `run`. |
| 96 | + |
| 97 | +**Step 4: Run test to verify it passes** |
| 98 | + |
| 99 | +Run: `python3 -m pytest -q tests/test_cli.py -k run_non_interactive_requires_path` |
| 100 | +Expected: PASS. |
| 101 | + |
| 102 | +### Task 3: Implement `_cmd_run` flow with path join, confirmation, and execution |
| 103 | + |
| 104 | +**Files:** |
| 105 | +- Modify: `mpy_cli/cli.py` |
| 106 | +- Test: `tests/test_cli.py` |
| 107 | + |
| 108 | +**Step 1: Write the failing tests** |
| 109 | + |
| 110 | +```python |
| 111 | +def test_run_executes_remote_file_with_device_upload_prefix(tmp_path: Path, monkeypatch) -> None: |
| 112 | + ... |
| 113 | + |
| 114 | + |
| 115 | +def test_run_returns_failure_code_when_backend_run_fails(tmp_path: Path, monkeypatch) -> None: |
| 116 | + ... |
| 117 | +``` |
| 118 | + |
| 119 | +**Step 2: Run tests to verify they fail** |
| 120 | + |
| 121 | +Run: `python3 -m pytest -q tests/test_cli.py -k "run_executes_remote_file or run_returns_failure_code"` |
| 122 | +Expected: FAIL because `_cmd_run` is not implemented. |
| 123 | + |
| 124 | +**Step 3: Write minimal implementation** |
| 125 | + |
| 126 | +Implement `_cmd_run()` in `cli.py`: |
| 127 | +- load config |
| 128 | +- resolve port |
| 129 | +- resolve/validate path input |
| 130 | +- compute final remote path via `_join_upload_target` |
| 131 | +- preview + confirmation |
| 132 | +- `backend.ensure_available()` and `backend.run_file(...)` |
| 133 | +- return code `0/1/2` per design |
| 134 | + |
| 135 | +**Step 4: Run tests to verify they pass** |
| 136 | + |
| 137 | +Run: `python3 -m pytest -q tests/test_cli.py -k "run_executes_remote_file or run_returns_failure_code or run_non_interactive_requires_path"` |
| 138 | +Expected: PASS. |
| 139 | + |
| 140 | +### Task 4: Update README and docs consistency tests |
| 141 | + |
| 142 | +**Files:** |
| 143 | +- Modify: `README.md` |
| 144 | +- Modify: `tests/test_docs_and_ci.py` |
| 145 | + |
| 146 | +**Step 1: Write the failing test** |
| 147 | + |
| 148 | +```python |
| 149 | +def test_readme_lists_run_command_parameters() -> None: |
| 150 | + content = Path("README.md").read_text(encoding="utf-8") |
| 151 | + for token in ["mpy-cli run", "--path"]: |
| 152 | + assert token in content |
| 153 | +``` |
| 154 | + |
| 155 | +**Step 2: Run test to verify it fails** |
| 156 | + |
| 157 | +Run: `python3 -m pytest -q tests/test_docs_and_ci.py -k run` |
| 158 | +Expected: FAIL. |
| 159 | + |
| 160 | +**Step 3: Write minimal implementation** |
| 161 | + |
| 162 | +Add `run` command section to README and include `--path` semantics relative to `device_upload_dir`. |
| 163 | + |
| 164 | +**Step 4: Run test to verify it passes** |
| 165 | + |
| 166 | +Run: `python3 -m pytest -q tests/test_docs_and_ci.py -k run` |
| 167 | +Expected: PASS. |
| 168 | + |
| 169 | +### Task 5: Regression verification |
| 170 | + |
| 171 | +**Files:** |
| 172 | +- Test: `tests/test_cli.py` |
| 173 | +- Test: `tests/test_mpremote_backend.py` |
| 174 | +- Test: `tests/test_docs_and_ci.py` |
| 175 | +- Test: `tests/test_executor.py` |
| 176 | + |
| 177 | +**Step 1: Run focused suites** |
| 178 | + |
| 179 | +Run: `python3 -m pytest -q tests/test_cli.py tests/test_mpremote_backend.py tests/test_docs_and_ci.py` |
| 180 | +Expected: PASS. |
| 181 | + |
| 182 | +**Step 2: Run full regression** |
| 183 | + |
| 184 | +Run: `python3 -m pytest -q` |
| 185 | +Expected: PASS with no new failures. |
0 commit comments