Skip to content

Commit 1ba5450

Browse files
authored
Merge pull request #5 from LanternCX/dev
v1.1.0
2 parents a765274 + 1eedcf0 commit 1ba5450

18 files changed

Lines changed: 2590 additions & 96 deletions

.progress/PROGRESS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ abc123d (or TBD)
4040
| --- | --- | --- | --- | --- |
4141
| 2026-03-03-1 | 2026-03-03 | Stabilized full deploy flow and improved onboarding UX | `.progress/entries/2026/2026-03-03-1.md` | deploy, mpremote, config-wizard, docs |
4242
| 2026-03-04-1 | 2026-03-04 | Added configurable device upload directory with scoped full wipe | `.progress/entries/2026/2026-03-04-1.md` | sync, device-upload-dir, full-mode, planner, mpremote |
43+
| 2026-03-06-1 | 2026-03-06 | 统一 source_dir 为远端根映射语义 | `.progress/entries/2026/2026-03-06-1.md` | source-dir, scanner, incremental, upload, docs |
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# 2026-03-06-1
2+
3+
## Date
4+
2026-03-06
5+
6+
## Title
7+
统一 source_dir 为远端根映射语义
8+
9+
## Background / Issue
10+
用户反馈 `source_dir = "src"` 时上传仍保留 `src/` 前缀,不符合“source_dir 作为本地根并直接映射远端根”的期望。仓库内 full、incremental、upload 默认路径和 `.mpyignore` 的路径语言存在语义分裂。
11+
12+
## Actions / Outcome
13+
- Approach 1: 仅调整 incremental 前缀拼接逻辑 -> 可修复增量路径,但无法覆盖 full 扫描和 upload 默认值,语义仍不统一。
14+
- Approach 2: 增加新旧语义开关兼容 -> 兼容性更强,但引入长期维护和测试矩阵负担。
15+
- Final approach: 采用统一语义改造(scanner/full、cli/incremental、upload 默认路径、README 与测试同步) -> full/incremental 一致输出 source-relative 路径,`source_dir="src"` 不再保留 `src/` 远端前缀。
16+
17+
## Lessons / Refinements
18+
- 路径语义必须跨命令保持单一定义,否则用户会在不同命令间反复切换心智模型。
19+
- 配置语义变更需要同步更新文档断言测试,避免 README 与实际行为偏移。
20+
21+
## Related Commit Message
22+
fix(path): align source_dir to source-relative remote mapping
23+
24+
## Related Commit Hash
25+
TBD

README.md

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ mpy-cli config
147147
mpy-cli plan
148148
mpy-cli deploy
149149
mpy-cli upload
150+
mpy-cli run
151+
mpy-cli delete
152+
mpy-cli tree
150153
```
151154

152155
说明:
@@ -185,6 +188,10 @@ mpy-cli config
185188

186189
常用配置项说明:
187190

191+
- `source_dir`:本地源码根目录。`plan/deploy` 计算远端路径时以该目录为根,不保留 `source_dir` 前缀。
192+
- `.mpyignore`:规则匹配对象为“相对 `source_dir` 的路径”。
193+
-`source_dir = "src"` 时,本地 `src/main.py` 对应远端 `:main.py`
194+
- 若历史 `.mpyignore` 规则包含 `src/...` 前缀,需迁移为相对 `source_dir` 的写法。
188195
- `device_upload_dir`:设备端上传目录前缀,留空表示设备根目录。
189196
-`device_upload_dir = "apps/demo"` 时,本地 `main.py` 会上传到设备 `:apps/demo/main.py`
190197
- `full` 模式会清空该上传目录,而不是整机设备根目录。
@@ -226,7 +233,7 @@ mpy-cli upload [--local LOCAL] [--remote REMOTE] [--port PORT] [--no-interactive
226233
```
227234

228235
- `--local`:本地文件路径(如 `seekfree_demo/E01_demo.py`)。
229-
- `--remote`:设备目标路径;不传时交互模式默认与本地路径一致,可手动修改。
236+
- `--remote`:设备目标路径;不传时交互模式默认优先使用“相对 `source_dir` 路径”,若本地文件不在 `source_dir` 下则回退为本地输入路径,可手动修改。
230237
- `--port`:指定设备端口。
231238
- `--no-interactive`:禁用交互提问;此时需显式提供 `--local``--remote`
232239
- `--yes`:跳过执行前确认。
@@ -239,6 +246,63 @@ mpy-cli upload --local <LOCAL>
239246

240247
填写字段 `LOCAL` 指定本地文件路径之后交互式确认远程路径
241248

249+
### `mpy-cli run`
250+
251+
```bash
252+
mpy-cli run [--path PATH] [--port PORT] [--no-interactive] [--yes]
253+
```
254+
255+
- `--path`:设备目标文件路径,语义为相对 `device_upload_dir`
256+
- `--port`:指定设备端口。
257+
- `--no-interactive`:禁用交互提问;此时需显式提供 `--path`
258+
- `--yes`:跳过执行前确认。
259+
260+
推荐用法:
261+
262+
```bash
263+
mpy-cli run --path main.py
264+
```
265+
266+
若配置 `device_upload_dir = "apps/demo"`,则会执行 `:apps/demo/main.py`
267+
268+
### `mpy-cli delete`
269+
270+
```bash
271+
mpy-cli delete [--path PATH] [--port PORT] [--no-interactive] [--yes]
272+
```
273+
274+
- `--path`:设备目标路径,语义为相对 `device_upload_dir`,可为文件或目录。
275+
- `--port`:指定设备端口。
276+
- `--no-interactive`:禁用交互提问;此时需显式提供 `--path`
277+
- `--yes`:跳过执行前确认。
278+
279+
推荐用法:
280+
281+
```bash
282+
mpy-cli delete --path obsolete.py
283+
```
284+
285+
若配置 `device_upload_dir = "apps/demo"`,则会删除 `:apps/demo/obsolete.py`
286+
`--path` 指向目录时,默认递归删除整个目录。
287+
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+
242306
---
243307

244308
## 常见问题
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# run 命令功能设计文档
2+
3+
## 背景
4+
5+
当前 `mpy-cli` 已支持 `plan/deploy/upload`,但缺少“直接执行设备端已存在脚本”的入口。
6+
在调试场景中,用户经常希望指定设备目标目录下的某个文件直接运行,而不触发上传流程。
7+
8+
## 目标
9+
10+
- 新增 `mpy-cli run` 子命令。
11+
- 支持通过 `--path` 指定设备端目标文件。
12+
- `--path` 语义为相对 `device_upload_dir`,与现有 `upload --remote` 规则一致。
13+
- 复用现有端口解析与执行前确认体验。
14+
15+
## 非目标
16+
17+
- 不在本次支持“先上传再执行”。
18+
- 不新增批量执行、多文件通配等能力。
19+
- 不改动 `plan/deploy/upload` 现有语义。
20+
21+
## 命令设计
22+
23+
```bash
24+
mpy-cli run [--path PATH] [--port PORT] [--no-interactive] [--yes]
25+
```
26+
27+
- `--path`:设备目标文件路径(相对 `device_upload_dir`)。
28+
- `--port`:设备串口。
29+
- `--no-interactive`:禁用交互提问;缺少 `--path` 时直接报错。
30+
- `--yes`:跳过执行前确认。
31+
32+
## 交互流程
33+
34+
1. 加载配置并解析端口(沿用现有优先级:`--port` > 配置 > 扫描/手输)。
35+
2. 解析 `--path`
36+
- 有值:直接使用。
37+
- 无值且交互模式:提示输入。
38+
- 无值且非交互:报错退出。
39+
3. 计算最终设备路径:`device_upload_dir + path`
40+
4. 打印执行预览并确认(`--yes` 可跳过)。
41+
5. 执行设备端脚本并输出结果。
42+
43+
## 执行架构
44+
45+
- `mpy_cli/cli.py`
46+
- 新增 `run` 子命令解析与 `_cmd_run` 流程。
47+
- 复用 `_resolve_port()``_join_upload_target()`
48+
- `mpy_cli/backend/mpremote.py`
49+
- 新增 `build_run_command()``run_file()`
50+
- 通过 `mpremote connect <port> resume exec <script>` 在设备端执行目标文件。
51+
52+
## 设备端脚本语义
53+
54+
- 优先尝试用户给定路径。
55+
- 若设备存在 `/flash`,额外尝试 `/flash/<path>` 兜底。
56+
- 使用 `open + compile + exec` 执行脚本,并设置:
57+
- `__name__ = "__main__"`
58+
- `__file__ = <resolved path>`
59+
60+
## 错误处理与返回码
61+
62+
- 参数缺失、配置错误、端口缺失、用户取消:返回 `1`
63+
- 设备执行失败(文件不存在、运行时报错、命令失败):返回 `2`
64+
- 执行成功:返回 `0`
65+
66+
## 测试策略
67+
68+
- `tests/test_cli.py`
69+
- `run` 非交互缺少 `--path` 报错。
70+
- 交互模式支持输入 `--path`
71+
- `--path` 会按 `device_upload_dir` 拼接。
72+
- 执行失败返回 `2`,成功返回 `0`
73+
- `tests/test_mpremote_backend.py`
74+
- `build_run_command()` 命令拼装正确。
75+
- `run_file()` 调用正确命令。
76+
- `tests/test_docs_and_ci.py`
77+
- README 包含 `mpy-cli run``--path`
78+
79+
## 验收标准
80+
81+
- 用户可以执行 `mpy-cli run --path <target.py>`
82+
- 当配置 `device_upload_dir` 时,`--path` 作为相对路径拼接执行。
83+
- 有清晰预览与确认,`--yes` 可跳过。
84+
- 新增测试通过,且不影响现有功能。
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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

Comments
 (0)