mtr-cli 0.2.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.
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/PKG-INFO +16 -6
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/README.md +15 -5
- mtr_cli-0.3.0/docs/intro-to-mtr.md +180 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/cli.py +53 -6
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/config.py +14 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/sync.py +157 -25
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/pyproject.toml +1 -1
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/test_config.py +56 -0
- mtr_cli-0.3.0/tests/unit/test_sync_rsync.py +368 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/test_sync_sftp.py +28 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/uv.lock +1 -1
- mtr_cli-0.2.0/tests/unit/test_sync_rsync.py +0 -142
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/.gitignore +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/.pre-commit-config.yaml +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/.python-version +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/AGENTS.md +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/LICENSE +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/examples/config.yaml +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/__init__.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/logger.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/ssh.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/__init__.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/conftest.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/integration/__init__.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/integration/test_cli_flow.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/integration/test_cli_phase1.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/__init__.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/test_logger.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/test_ssh.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/test_ssh_interactive.py +0 -0
- {mtr_cli-0.2.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.
|
|
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
|
|
@@ -57,11 +57,21 @@ pip install mtr-cli
|
|
|
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
|
|
|
@@ -30,11 +30,21 @@ pip install mtr-cli
|
|
|
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
|
|
|
@@ -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__/"
|
|
@@ -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
|
-
|
|
229
|
-
|
|
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")
|
|
@@ -260,6 +287,9 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
260
287
|
# Resolve exclude
|
|
261
288
|
exclude = config.global_defaults.get("exclude", []) + server_conf.get("exclude", [])
|
|
262
289
|
|
|
290
|
+
# Get respect_gitignore setting
|
|
291
|
+
respect_gitignore = config.get_respect_gitignore()
|
|
292
|
+
|
|
263
293
|
# Determine engine
|
|
264
294
|
engine = server_conf.get("sync", config.global_defaults.get("sync", "rsync"))
|
|
265
295
|
|
|
@@ -273,6 +303,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
273
303
|
password=password,
|
|
274
304
|
port=port,
|
|
275
305
|
exclude=exclude,
|
|
306
|
+
respect_gitignore=respect_gitignore,
|
|
276
307
|
)
|
|
277
308
|
elif engine == "sftp":
|
|
278
309
|
syncer = SftpSyncer(
|
|
@@ -284,6 +315,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
284
315
|
password=password,
|
|
285
316
|
port=port,
|
|
286
317
|
exclude=exclude,
|
|
318
|
+
respect_gitignore=respect_gitignore,
|
|
287
319
|
)
|
|
288
320
|
else:
|
|
289
321
|
click.secho(
|
|
@@ -298,12 +330,27 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
298
330
|
logger.info(f"[DryRun] Would download {remote_get_path} -> {local_dest}", module="mtr.sync")
|
|
299
331
|
else:
|
|
300
332
|
if is_interactive and console:
|
|
301
|
-
|
|
302
|
-
|
|
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"))
|
|
303
346
|
console.print(f"✅ [green]Downloaded:[/green] {remote_get_path} -> {local_dest}")
|
|
304
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
|
+
|
|
305
352
|
click.secho(f"Downloading {remote_get_path}...", fg="blue")
|
|
306
|
-
syncer.download(remote_get_path, local_dest)
|
|
353
|
+
syncer.download(remote_get_path, local_dest, show_progress=True, progress_callback=show_download_progress)
|
|
307
354
|
click.secho(f"Download completed: {local_dest}", fg="green")
|
|
308
355
|
logger.info(f"Download completed: {remote_get_path} -> {local_dest}", module="mtr.sync")
|
|
309
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):
|