mtr-cli 0.1.0__tar.gz → 0.3.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 (31) hide show
  1. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/PKG-INFO +29 -12
  2. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/README.md +28 -11
  3. mtr_cli-0.3.0/docs/intro-to-mtr.md +180 -0
  4. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/cli.py +63 -9
  5. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/config.py +14 -0
  6. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/sync.py +157 -25
  7. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/pyproject.toml +1 -1
  8. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_config.py +56 -0
  9. mtr_cli-0.3.0/tests/unit/test_sync_rsync.py +368 -0
  10. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_sync_sftp.py +28 -0
  11. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/uv.lock +2 -2
  12. mtr_cli-0.1.0/tests/unit/test_sync_rsync.py +0 -142
  13. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/.gitignore +0 -0
  14. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/.pre-commit-config.yaml +0 -0
  15. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/.python-version +0 -0
  16. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/AGENTS.md +0 -0
  17. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/LICENSE +0 -0
  18. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/examples/config.yaml +0 -0
  19. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/__init__.py +0 -0
  20. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/logger.py +0 -0
  21. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/ssh.py +0 -0
  22. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/__init__.py +0 -0
  23. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/conftest.py +0 -0
  24. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/integration/__init__.py +0 -0
  25. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/integration/test_cli_flow.py +0 -0
  26. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/integration/test_cli_phase1.py +0 -0
  27. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/__init__.py +0 -0
  28. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_logger.py +0 -0
  29. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_ssh.py +0 -0
  30. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_ssh_interactive.py +0 -0
  31. {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_ssh_pre_cmd.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtr-cli
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: A CLI tool for seamless local development and remote execution on GPU servers.
5
5
  Project-URL: Homepage, https://github.com/lecoan/mtremote
6
6
  Project-URL: Repository, https://github.com/lecoan/mtremote
@@ -25,7 +25,7 @@ Requires-Dist: pyyaml>=6.0
25
25
  Requires-Dist: rich>=12.0.0
26
26
  Description-Content-Type: text/markdown
27
27
 
28
- # MTRemote (mtr)
28
+ # MTRemote (mtr-cli)
29
29
 
30
30
  MTRemote 是一个专为 AI Infra 和 Python/C++ 混合开发设计的命令行工具。它允许你在本地修改代码,通过简单的 `mtr` 前缀,自动将代码同步到远端 GPU 服务器并执行命令,同时保留本地的交互体验(实时日志、颜色高亮、Ctrl+C 支持)。
31
31
 
@@ -48,20 +48,30 @@ MTRemote 是一个专为 AI Infra 和 Python/C++ 混合开发设计的命令行
48
48
  推荐使用 `uv` 或 `pipx` 安装:
49
49
 
50
50
  ```bash
51
- uv tool install mtremote
51
+ uv tool install mtr-cli
52
52
  # 或者
53
- pip install mtremote
53
+ pip install mtr-cli
54
54
  ```
55
55
 
56
56
  ### 系统依赖
57
57
 
58
58
  MTRemote 需要以下系统命令:
59
59
 
60
- | 命令 | 用途 | 安装方式 |
61
- |------|------|----------|
62
- | `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` |
63
- | `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 |
64
- | `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) |
60
+ | 命令 | 用途 | 安装方式 | 版本要求 |
61
+ |------|------|----------|----------|
62
+ | `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` | - |
63
+ | `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 | **≥ 3.1.0** (TTY 进度显示需要) |
64
+ | `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) | - |
65
+
66
+ **注意**:macOS 自带的 rsync 版本较旧(2.6.9),不支持 TTY 模式下的进度显示。建议通过 Homebrew 安装新版:
67
+
68
+ ```bash
69
+ # macOS 用户建议升级 rsync
70
+ brew install rsync
71
+
72
+ # 验证版本
73
+ rsync --version # 应显示 3.1.0 或更高版本
74
+ ```
65
75
 
66
76
  **注意**:交互式 Shell 功能(如 `mtr bash`, `mtr ipython`)**必须**安装 `ssh`。密码认证**必须**安装 `sshpass`。
67
77
 
@@ -129,7 +139,7 @@ Options:
129
139
  --to TEXT Local destination path for download (optional)
130
140
  --enable-log Enable logging to file
131
141
  --log-level TEXT Log level: DEBUG/INFO/WARNING/ERROR [default: INFO]
132
- --log-file PATH Custom log file path (default: ~/.mtr/logs/mtr_YYYYMMDD_HHMMSS.log)
142
+ --log-file PATH Custom log file path (default: ./.mtr/logs/mtr_YYYYMMDD_HHMMSS.log)
133
143
  --init Initialize configuration file
134
144
  --help Show this message and exit
135
145
  ```
@@ -192,9 +202,12 @@ sudo yum install sshpass
192
202
  使用 `--get` 参数可以从远端服务器下载文件或文件夹到本地:
193
203
 
194
204
  ```bash
195
- # 下载文件到当前目录
205
+ # 下载文件(绝对路径)
196
206
  mtr --get /remote/path/to/file.txt
197
207
 
208
+ # 下载文件(相对路径,基于 remote_dir)
209
+ mtr --get checkpoints/model.pt
210
+
198
211
  # 下载文件到指定位置
199
212
  mtr --get /remote/path/to/file.txt --to ./local/path/
200
213
 
@@ -205,6 +218,10 @@ mtr --get /remote/path/to/checkpoints/ --to ./backups/
205
218
  mtr --no-sync --get /remote/path/to/file.txt
206
219
  ```
207
220
 
221
+ **路径解析规则**:
222
+ - **绝对路径**(以 `/` 开头):直接使用指定的完整路径
223
+ - **相对路径**:自动拼接 `remote_dir`,例如配置 `remote_dir: "/workdir/project"`,执行 `--get checkpoints/model.pt` 将下载 `/workdir/project/checkpoints/model.pt`
224
+
208
225
  **配置下载目录**:
209
226
  可以在配置文件中设置默认下载位置:
210
227
 
@@ -235,7 +252,7 @@ mtr --enable-log python train.py
235
252
  mtr --enable-log --log-level DEBUG python train.py
236
253
 
237
254
  # 查看日志
238
- cat ~/.mtr/logs/mtr_20260128_171216.log
255
+ cat ./.mtr/logs/mtr_20260128_171216.log
239
256
  ```
240
257
 
241
258
  日志文件按会话独立生成,格式为 `mtr_YYYYMMDD_HHMMSS.log`,包含:
@@ -1,4 +1,4 @@
1
- # MTRemote (mtr)
1
+ # MTRemote (mtr-cli)
2
2
 
3
3
  MTRemote 是一个专为 AI Infra 和 Python/C++ 混合开发设计的命令行工具。它允许你在本地修改代码,通过简单的 `mtr` 前缀,自动将代码同步到远端 GPU 服务器并执行命令,同时保留本地的交互体验(实时日志、颜色高亮、Ctrl+C 支持)。
4
4
 
@@ -21,20 +21,30 @@ MTRemote 是一个专为 AI Infra 和 Python/C++ 混合开发设计的命令行
21
21
  推荐使用 `uv` 或 `pipx` 安装:
22
22
 
23
23
  ```bash
24
- uv tool install mtremote
24
+ uv tool install mtr-cli
25
25
  # 或者
26
- pip install mtremote
26
+ pip install mtr-cli
27
27
  ```
28
28
 
29
29
  ### 系统依赖
30
30
 
31
31
  MTRemote 需要以下系统命令:
32
32
 
33
- | 命令 | 用途 | 安装方式 |
34
- |------|------|----------|
35
- | `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` |
36
- | `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 |
37
- | `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) |
33
+ | 命令 | 用途 | 安装方式 | 版本要求 |
34
+ |------|------|----------|----------|
35
+ | `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` | - |
36
+ | `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 | **≥ 3.1.0** (TTY 进度显示需要) |
37
+ | `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) | - |
38
+
39
+ **注意**:macOS 自带的 rsync 版本较旧(2.6.9),不支持 TTY 模式下的进度显示。建议通过 Homebrew 安装新版:
40
+
41
+ ```bash
42
+ # macOS 用户建议升级 rsync
43
+ brew install rsync
44
+
45
+ # 验证版本
46
+ rsync --version # 应显示 3.1.0 或更高版本
47
+ ```
38
48
 
39
49
  **注意**:交互式 Shell 功能(如 `mtr bash`, `mtr ipython`)**必须**安装 `ssh`。密码认证**必须**安装 `sshpass`。
40
50
 
@@ -102,7 +112,7 @@ Options:
102
112
  --to TEXT Local destination path for download (optional)
103
113
  --enable-log Enable logging to file
104
114
  --log-level TEXT Log level: DEBUG/INFO/WARNING/ERROR [default: INFO]
105
- --log-file PATH Custom log file path (default: ~/.mtr/logs/mtr_YYYYMMDD_HHMMSS.log)
115
+ --log-file PATH Custom log file path (default: ./.mtr/logs/mtr_YYYYMMDD_HHMMSS.log)
106
116
  --init Initialize configuration file
107
117
  --help Show this message and exit
108
118
  ```
@@ -165,9 +175,12 @@ sudo yum install sshpass
165
175
  使用 `--get` 参数可以从远端服务器下载文件或文件夹到本地:
166
176
 
167
177
  ```bash
168
- # 下载文件到当前目录
178
+ # 下载文件(绝对路径)
169
179
  mtr --get /remote/path/to/file.txt
170
180
 
181
+ # 下载文件(相对路径,基于 remote_dir)
182
+ mtr --get checkpoints/model.pt
183
+
171
184
  # 下载文件到指定位置
172
185
  mtr --get /remote/path/to/file.txt --to ./local/path/
173
186
 
@@ -178,6 +191,10 @@ mtr --get /remote/path/to/checkpoints/ --to ./backups/
178
191
  mtr --no-sync --get /remote/path/to/file.txt
179
192
  ```
180
193
 
194
+ **路径解析规则**:
195
+ - **绝对路径**(以 `/` 开头):直接使用指定的完整路径
196
+ - **相对路径**:自动拼接 `remote_dir`,例如配置 `remote_dir: "/workdir/project"`,执行 `--get checkpoints/model.pt` 将下载 `/workdir/project/checkpoints/model.pt`
197
+
181
198
  **配置下载目录**:
182
199
  可以在配置文件中设置默认下载位置:
183
200
 
@@ -208,7 +225,7 @@ mtr --enable-log python train.py
208
225
  mtr --enable-log --log-level DEBUG python train.py
209
226
 
210
227
  # 查看日志
211
- cat ~/.mtr/logs/mtr_20260128_171216.log
228
+ cat ./.mtr/logs/mtr_20260128_171216.log
212
229
  ```
213
230
 
214
231
  日志文件按会话独立生成,格式为 `mtr_YYYYMMDD_HHMMSS.log`,包含:
@@ -0,0 +1,180 @@
1
+ # mtr-cli:远程训练框架开发的工作流优化
2
+
3
+ ## 背景与问题
4
+
5
+ 在训练框架开发过程中,我们面临一个结构性矛盾:代码开发在本地进行,但功能验证必须在远程 GPU/NPU 集群上完成。这种"本地-远程"的割裂带来了三个核心问题。
6
+
7
+ ### 网络与环境隔离
8
+
9
+ 训练集群通常部署在内网或隔离环境中,无法直接访问外部网络。这导致依赖安装困难,而训练框架的依赖链本身就很复杂(PyTorch、DeepSpeed、Megatron 等)。环境配置耗时较长,一旦集群环境需要重建,成本很高。
10
+
11
+ ### 多集群切换成本高
12
+
13
+ 框架开发需要在 GPU 集群和 NPU 集群之间频繁切换验证。每次切换涉及代码同步、路径检查、环境确认等重复性操作。虽然启动方式统一使用 torchrun,但不同集群的节点地址、Python 环境路径等配置存在差异,需要反复适配。
14
+
15
+ ### AI Agent 集成受限
16
+
17
+ 训练任务在远程集群执行时,代码报错和异常堆栈输出在远程终端。AI Agent 无法直接访问这些信息,开发者需要手动复制粘贴错误内容,打断开发流程。Agent 无法实时感知执行状态,失去了即时辅助调试的能力。
18
+
19
+ ## 解决方案:mtr-cli
20
+
21
+ mtr-cli 采用"本地开发,远程执行"的架构,在保持本地开发体验的同时,实现与远程集群的无缝集成。
22
+
23
+ ### 核心功能
24
+
25
+ **智能代码同步**
26
+
27
+ 支持 rsync(增量同步,速度快)和 SFTP(兼容性好)两种引擎。自动过滤 .git、__pycache__ 等无需同步的文件,避免大数据集误传。
28
+
29
+ **多集群配置管理**
30
+
31
+ 通过配置文件集中管理多个训练集群:
32
+
33
+ ```yaml
34
+ servers:
35
+ gpu-cluster:
36
+ host: "gpu-node-01"
37
+ user: "dev"
38
+ remote_dir: "/data/train-project"
39
+
40
+ npu-cluster:
41
+ host: "npu-node-01"
42
+ user: "dev"
43
+ remote_dir: "/home/dev/project"
44
+ ```
45
+
46
+ 集群切换通过命令行参数完成:
47
+
48
+ ```bash
49
+ # GPU 集群执行
50
+ mtr -s gpu-cluster torchrun --nproc_per_node=8 train.py
51
+
52
+ # NPU 集群执行
53
+ mtr -s npu-cluster torchrun --nproc_per_node=8 train.py
54
+ ```
55
+
56
+ **实时流式输出**
57
+
58
+ 远程训练日志实时回显至本地终端,包括 loss 曲线、吞吐量、显存占用等关键指标。代码异常时,堆栈信息直接显示在本地,AI Agent 可即时获取并分析。支持交互式命令(vim、ipython 等)。
59
+
60
+ ## 应用场景
61
+
62
+ **分布式训练框架开发**
63
+
64
+ 在本地完成数据并行或模型并行逻辑的实现后,需要上多卡环境验证。传统流程涉及打包代码、scp 传输、ssh 登录、路径定位、执行运行等多个步骤。使用 mtr-cli 可简化为:
65
+
66
+ ```bash
67
+ mtr torchrun --nproc_per_node=8 train.py
68
+ ```
69
+
70
+ 代码自动同步,远程执行,日志实时回传。
71
+
72
+ **跨硬件平台验证**
73
+
74
+ 框架在 GPU 环境验证通过后,需要在 NPU 环境测试兼容性。传统方式需要重复环境配置和代码同步流程。使用 mtr-cli 只需切换服务器参数:
75
+
76
+ ```bash
77
+ mtr -s npu-cluster torchrun --nproc_per_node=8 train.py
78
+ ```
79
+
80
+ **即时调试**
81
+
82
+ 训练脚本执行过程中发生异常,报错信息和堆栈直接输出在本地终端。AI Agent 实时获取错误内容,可立即进行分析和建议,无需手动复制粘贴。
83
+
84
+ ## 收益分析
85
+
86
+ - **时间效率**:消除手动 scp/rsync 和反复 ssh 登录的操作 overhead
87
+ - **认知负担**:本地维护单一的代码库和配置,远程仅作为计算资源
88
+ - **AI 辅助能力**:Agent 可获取完整执行日志,恢复实时调试辅助
89
+ - **流程一致性**:屏蔽 GPU/NPU 后端差异,提供统一的开发体验
90
+
91
+ ## 快速开始
92
+
93
+ ### 安装
94
+
95
+ 推荐使用 uv 安装(更快、更可靠):
96
+
97
+ ```bash
98
+ uv pip install mtr-cli
99
+ ```
100
+
101
+ 或使用 pip:
102
+
103
+ ```bash
104
+ pip install mtr-cli
105
+ ```
106
+
107
+ ### 初始化配置
108
+
109
+ 在项目目录下运行初始化命令,生成默认配置文件:
110
+
111
+ ```bash
112
+ mtr --init
113
+ ```
114
+
115
+ 这会创建 `.mtr/config.yaml` 文件,包含默认配置模板。根据你的集群信息编辑该文件:
116
+
117
+ ```yaml
118
+ servers:
119
+ dev-gpu:
120
+ host: "192.168.1.10"
121
+ user: "dev"
122
+ key_filename: "~/.ssh/id_rsa"
123
+ remote_dir: "/data/project"
124
+ sync: "rsync"
125
+ ```
126
+
127
+ ### 执行命令
128
+
129
+ 配置完成后,在项目根目录执行:
130
+
131
+ ```bash
132
+ # 使用默认服务器
133
+ mtr python train.py
134
+
135
+ # 指定服务器
136
+ mtr -s dev-gpu torchrun --nproc_per_node=8 train.py
137
+ ```
138
+
139
+ 完整 workflow:
140
+
141
+ ```bash
142
+ # 1. 安装
143
+ uv pip install mtr-cli
144
+
145
+ # 2. 进入项目目录
146
+ cd my-project
147
+
148
+ # 3. 初始化配置
149
+ mtr --init
150
+ # 编辑 .mtr/config.yaml 添加服务器信息
151
+
152
+ # 4. 运行
153
+ mtr python train.py
154
+ ```
155
+
156
+ ### 常用参数
157
+
158
+ ```bash
159
+ # 指定服务器
160
+ mtr -s gpu-cluster python train.py
161
+
162
+ # 跳过代码同步(仅执行命令)
163
+ mtr --no-sync python train.py
164
+
165
+ # 预览将要执行的命令(不实际运行)
166
+ mtr --dry-run python train.py
167
+
168
+ # 强制禁用 TTY(用于日志记录场景)
169
+ mtr --no-tty python train.py
170
+
171
+ # 启用日志记录
172
+ mtr --enable-log python train.py
173
+
174
+ # 从远程下载文件
175
+ mtr --get /remote/path/to/file.txt --to ./local/file.txt
176
+ ```
177
+
178
+ ---
179
+
180
+ mtr-cli 的目标是将远程训练的日常操作自动化,使开发者能够专注于框架本身,而非环境切换的繁琐流程。
@@ -15,6 +15,11 @@ defaults:
15
15
  # 选项: "rsync" (推荐), "sftp"
16
16
  sync: "rsync"
17
17
 
18
+ # 是否尊重 .gitignore 文件(仅 rsync 模式支持)
19
+ # 设置为 true 时,rsync 会自动读取项目根目录的 .gitignore 并排除匹配的文件
20
+ # SFTP 模式不支持此选项,如启用会报错
21
+ respect_gitignore: true
22
+
18
23
  exclude:
19
24
  - ".git/"
20
25
  - "__pycache__/"
@@ -73,7 +78,7 @@ def _init_config():
73
78
  @click.option("--init", is_flag=True, help="Initialize a configuration file in current directory")
74
79
  @click.option("--enable-log", is_flag=True, help="Enable logging to file")
75
80
  @click.option("--log-level", default="INFO", help="Log level (DEBUG/INFO/WARNING/ERROR)")
76
- @click.option("--log-file", help="Path to log file (default: ~/.mtr/logs/mtr_YYYYMMDD_HHMMSS.log)")
81
+ @click.option("--log-file", help="Path to log file (default: ./.mtr/logs/mtr_YYYYMMDD_HHMMSS.log)")
77
82
  @click.option("--get", "remote_get_path", help="Remote path to download from")
78
83
  @click.option("--to", "local_dest_path", help="Local destination path for download (optional)")
79
84
  @click.argument("command", nargs=-1, type=click.UNPROCESSED)
@@ -86,9 +91,9 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
86
91
  # Setup logging if enabled
87
92
  if enable_log:
88
93
  if not log_file:
89
- # Generate default log file path: ~/.mtr/logs/mtr_YYYYMMDD_HHMMSS.log
94
+ # Generate default log file path: ./.mtr/logs/mtr_YYYYMMDD_HHMMSS.log
90
95
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
91
- log_dir = os.path.expanduser("~/.mtr/logs")
96
+ log_dir = os.path.join(os.getcwd(), ".mtr/logs")
92
97
  log_file = os.path.join(log_dir, f"mtr_{timestamp}.log")
93
98
 
94
99
  try:
@@ -186,6 +191,9 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
186
191
  # Resolve exclude
187
192
  exclude = config.global_defaults.get("exclude", []) + server_conf.get("exclude", [])
188
193
 
194
+ # Get respect_gitignore setting
195
+ respect_gitignore = config.get_respect_gitignore()
196
+
189
197
  # Determine engine
190
198
  engine = server_conf.get("sync", config.global_defaults.get("sync", "rsync"))
191
199
 
@@ -199,6 +207,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
199
207
  password=password,
200
208
  port=port,
201
209
  exclude=exclude,
210
+ respect_gitignore=respect_gitignore,
202
211
  )
203
212
  elif engine == "sftp":
204
213
  syncer = SftpSyncer(
@@ -210,6 +219,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
210
219
  password=password,
211
220
  port=port,
212
221
  exclude=exclude,
222
+ respect_gitignore=respect_gitignore,
213
223
  )
214
224
  else:
215
225
  click.secho(
@@ -225,11 +235,28 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
225
235
  logger.info(f"[DryRun] Would sync {local_dir} -> {remote_dir}", module="mtr.sync")
226
236
  else:
227
237
  if is_interactive and console:
228
- with console.status("[bold blue]Syncing code...", spinner="dots"):
229
- syncer.sync()
238
+ # TTY mode: single line real-time update using Rich Live
239
+ from rich.live import Live
240
+ from rich.text import Text
241
+
242
+ with Live(Text("Starting sync...", style="blue"), refresh_per_second=10) as live:
243
+
244
+ def show_sync_progress(filename):
245
+ # Get relative path for cleaner display
246
+ rel_path = os.path.relpath(filename, local_dir)
247
+ live.update(Text(f"Syncing: {rel_path}", style="blue"))
248
+
249
+ syncer.sync(show_progress=True, progress_callback=show_sync_progress)
250
+ live.update(Text("Sync completed!", style="green"))
230
251
  else:
252
+ # no_tty mode: print each file on new line
253
+ def show_sync_progress(filename):
254
+ rel_path = os.path.relpath(filename, local_dir)
255
+ click.echo(f"Syncing: {rel_path}")
256
+
231
257
  click.secho("Syncing code...", fg="blue")
232
- syncer.sync()
258
+ syncer.sync(show_progress=True, progress_callback=show_sync_progress)
259
+ click.secho("Sync completed!", fg="green")
233
260
  logger.info(f"Sync completed: {local_dir} -> {remote_dir}", module="mtr.sync")
234
261
  except SyncError as e:
235
262
  logger.error(f"Sync failed: {e}", module="mtr.sync")
@@ -238,6 +265,13 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
238
265
 
239
266
  # 3. Download from remote (if --get is specified)
240
267
  if remote_get_path:
268
+ # Resolve relative remote path from remote_dir
269
+ if not remote_get_path.startswith("/"):
270
+ if not remote_dir:
271
+ click.secho("Error: 'remote_dir' is required for relative --get path.", fg="red", err=True)
272
+ sys.exit(1)
273
+ remote_get_path = os.path.join(remote_dir, remote_get_path)
274
+
241
275
  # Resolve local destination path
242
276
  if cli_dest:
243
277
  local_dest = cli_dest
@@ -253,6 +287,9 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
253
287
  # Resolve exclude
254
288
  exclude = config.global_defaults.get("exclude", []) + server_conf.get("exclude", [])
255
289
 
290
+ # Get respect_gitignore setting
291
+ respect_gitignore = config.get_respect_gitignore()
292
+
256
293
  # Determine engine
257
294
  engine = server_conf.get("sync", config.global_defaults.get("sync", "rsync"))
258
295
 
@@ -266,6 +303,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
266
303
  password=password,
267
304
  port=port,
268
305
  exclude=exclude,
306
+ respect_gitignore=respect_gitignore,
269
307
  )
270
308
  elif engine == "sftp":
271
309
  syncer = SftpSyncer(
@@ -277,6 +315,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
277
315
  password=password,
278
316
  port=port,
279
317
  exclude=exclude,
318
+ respect_gitignore=respect_gitignore,
280
319
  )
281
320
  else:
282
321
  click.secho(
@@ -291,12 +330,27 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
291
330
  logger.info(f"[DryRun] Would download {remote_get_path} -> {local_dest}", module="mtr.sync")
292
331
  else:
293
332
  if is_interactive and console:
294
- with console.status(f"[bold blue]Downloading {remote_get_path}...", spinner="dots"):
295
- syncer.download(remote_get_path, local_dest)
333
+ # TTY mode: single line real-time update using Rich Live
334
+ from rich.live import Live
335
+ from rich.text import Text
336
+
337
+ with Live(Text("Starting download...", style="blue"), refresh_per_second=10) as live:
338
+
339
+ def show_download_progress(filename):
340
+ live.update(Text(f"Downloading: {filename}", style="blue"))
341
+
342
+ syncer.download(
343
+ remote_get_path, local_dest, show_progress=True, progress_callback=show_download_progress
344
+ )
345
+ live.update(Text("Download completed!", style="green"))
296
346
  console.print(f"✅ [green]Downloaded:[/green] {remote_get_path} -> {local_dest}")
297
347
  else:
348
+ # no_tty mode: print each file on new line
349
+ def show_download_progress(filename):
350
+ click.echo(f"Downloading: {filename}")
351
+
298
352
  click.secho(f"Downloading {remote_get_path}...", fg="blue")
299
- syncer.download(remote_get_path, local_dest)
353
+ syncer.download(remote_get_path, local_dest, show_progress=True, progress_callback=show_download_progress)
300
354
  click.secho(f"Download completed: {local_dest}", fg="green")
301
355
  logger.info(f"Download completed: {remote_get_path} -> {local_dest}", module="mtr.sync")
302
356
  except SyncError as e:
@@ -19,6 +19,20 @@ class Config:
19
19
  server_config: Dict[str, Any]
20
20
  global_defaults: Dict[str, Any]
21
21
 
22
+ def get_respect_gitignore(self) -> bool:
23
+ """Get respect_gitignore setting, default True.
24
+
25
+ Priority: server config > global defaults > True (default)
26
+ """
27
+ # Check server config first
28
+ if "respect_gitignore" in self.server_config:
29
+ return self.server_config["respect_gitignore"]
30
+ # Then check global defaults
31
+ if "respect_gitignore" in self.global_defaults:
32
+ return self.global_defaults["respect_gitignore"]
33
+ # Default to True
34
+ return True
35
+
22
36
 
23
37
  class ConfigLoader:
24
38
  def __init__(self, config_path: Optional[str] = None):