mtr-cli 0.2.0__tar.gz → 2.0.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-2.0.0}/PKG-INFO +37 -25
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/README.md +35 -23
- mtr_cli-2.0.0/mtr/__init__.py +3 -0
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/mtr/cli.py +153 -110
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/mtr/config.py +14 -0
- mtr_cli-2.0.0/mtr/ssh.py +132 -0
- mtr_cli-2.0.0/mtr/sync.py +230 -0
- mtr_cli-2.0.0/mtr/updater.py +130 -0
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/pyproject.toml +2 -2
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/tests/integration/test_cli_flow.py +24 -29
- mtr_cli-2.0.0/tests/integration/test_cli_phase1.py +67 -0
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/tests/unit/test_config.py +56 -0
- mtr_cli-2.0.0/tests/unit/test_ssh.py +25 -0
- mtr_cli-2.0.0/tests/unit/test_sync_rsync.py +368 -0
- mtr_cli-2.0.0/tests/unit/test_updater.py +282 -0
- mtr_cli-2.0.0/uv.lock +424 -0
- mtr_cli-0.2.0/mtr/ssh.py +0 -199
- mtr_cli-0.2.0/mtr/sync.py +0 -338
- mtr_cli-0.2.0/tests/integration/test_cli_phase1.py +0 -75
- mtr_cli-0.2.0/tests/unit/__init__.py +0 -0
- mtr_cli-0.2.0/tests/unit/test_ssh.py +0 -60
- mtr_cli-0.2.0/tests/unit/test_ssh_interactive.py +0 -299
- mtr_cli-0.2.0/tests/unit/test_ssh_pre_cmd.py +0 -43
- mtr_cli-0.2.0/tests/unit/test_sync_rsync.py +0 -142
- mtr_cli-0.2.0/tests/unit/test_sync_sftp.py +0 -178
- mtr_cli-0.2.0/uv.lock +0 -709
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/.gitignore +0 -0
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/.pre-commit-config.yaml +0 -0
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/.python-version +0 -0
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/AGENTS.md +0 -0
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/LICENSE +0 -0
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/examples/config.yaml +0 -0
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/mtr/logger.py +0 -0
- {mtr_cli-0.2.0/mtr → mtr_cli-2.0.0/tests}/__init__.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/tests/conftest.py +0 -0
- {mtr_cli-0.2.0/tests → mtr_cli-2.0.0/tests/integration}/__init__.py +0 -0
- {mtr_cli-0.2.0/tests/integration → mtr_cli-2.0.0/tests/unit}/__init__.py +0 -0
- {mtr_cli-0.2.0 → mtr_cli-2.0.0}/tests/unit/test_logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mtr-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.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
|
|
@@ -20,7 +20,7 @@ Classifier: Topic :: Software Development :: Build Tools
|
|
|
20
20
|
Classifier: Topic :: System :: Systems Administration
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Requires-Dist: click>=8.0.0
|
|
23
|
-
Requires-Dist:
|
|
23
|
+
Requires-Dist: packaging>=21.0
|
|
24
24
|
Requires-Dist: pyyaml>=6.0
|
|
25
25
|
Requires-Dist: rich>=12.0.0
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
@@ -33,8 +33,7 @@ MTRemote 是一个专为 AI Infra 和 Python/C++ 混合开发设计的命令行
|
|
|
33
33
|
|
|
34
34
|
* **多服务器管理**:通过配置文件管理多个 GPU 节点,支持默认服务器 (Implicit/Explicit)。
|
|
35
35
|
* **智能同步引擎**:
|
|
36
|
-
* **Rsync
|
|
37
|
-
* **SFTP (兼容)**:纯 Python 实现,适用于无 `rsync` 的环境,配置简单。
|
|
36
|
+
* **Rsync**:调用系统 `rsync`,支持增量同步,速度极快。支持 `sshpass` 自动处理密码认证。
|
|
38
37
|
* **双向同步**:支持从远端下载文件/文件夹到本地(`--get` 参数)。
|
|
39
38
|
* **双模式交互 (Dual-Mode Interaction)**:
|
|
40
39
|
* **交互模式 (Interactive)**:自动检测 TTY,支持 PTY 分配、Raw Mode、Rich UI 动画。完美支持 `vim`, `ipython`, `pdb`, `htop`。
|
|
@@ -57,11 +56,21 @@ pip install mtr-cli
|
|
|
57
56
|
|
|
58
57
|
MTRemote 需要以下系统命令:
|
|
59
58
|
|
|
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) |
|
|
59
|
+
| 命令 | 用途 | 安装方式 | 版本要求 |
|
|
60
|
+
|------|------|----------|----------|
|
|
61
|
+
| `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` | - |
|
|
62
|
+
| `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 | **≥ 3.1.0** (TTY 进度显示需要) |
|
|
63
|
+
| `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) | - |
|
|
64
|
+
|
|
65
|
+
**注意**:macOS 自带的 rsync 版本较旧(2.6.9),不支持 TTY 模式下的进度显示。建议通过 Homebrew 安装新版:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# macOS 用户建议升级 rsync
|
|
69
|
+
brew install rsync
|
|
70
|
+
|
|
71
|
+
# 验证版本
|
|
72
|
+
rsync --version # 应显示 3.1.0 或更高版本
|
|
73
|
+
```
|
|
65
74
|
|
|
66
75
|
**注意**:交互式 Shell 功能(如 `mtr bash`, `mtr ipython`)**必须**安装 `ssh`。密码认证**必须**安装 `sshpass`。
|
|
67
76
|
|
|
@@ -83,7 +92,7 @@ mtr --init
|
|
|
83
92
|
|
|
84
93
|
```yaml
|
|
85
94
|
defaults:
|
|
86
|
-
sync: "rsync"
|
|
95
|
+
sync: "rsync"
|
|
87
96
|
exclude: [".git/", "__pycache__/"]
|
|
88
97
|
download_dir: "./downloads" # 默认下载位置(可选)
|
|
89
98
|
|
|
@@ -115,6 +124,21 @@ mtr ipython
|
|
|
115
124
|
mtr -s prod-node python train.py
|
|
116
125
|
```
|
|
117
126
|
|
|
127
|
+
### ⚠️ 参数传递注意事项
|
|
128
|
+
|
|
129
|
+
当执行的命令包含以 `-` 开头的参数时(如 `python -c`, `gcc -O2`),建议使用 `--` 作为分隔符,避免被误认为是 `mtr` 的选项:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# ❌ 错误:-s 会被当作 --server 的短选项
|
|
133
|
+
mtr python3 -s
|
|
134
|
+
|
|
135
|
+
# ✅ 正确:使用 -- 分隔符
|
|
136
|
+
mtr -- python3 -s
|
|
137
|
+
|
|
138
|
+
# ✅ 指定服务器时也使用 --
|
|
139
|
+
mtr --server prod-node -- python3 -c "print('hello')"
|
|
140
|
+
```
|
|
141
|
+
|
|
118
142
|
## 📖 命令行选项
|
|
119
143
|
|
|
120
144
|
```bash
|
|
@@ -159,21 +183,9 @@ mtr --enable-log --log-file ./debug.log python train.py
|
|
|
159
183
|
mtr --no-tty python train.py > log.txt
|
|
160
184
|
```
|
|
161
185
|
|
|
162
|
-
### 2.
|
|
163
|
-
如果本地或远程无法使用 rsync,可以在配置中指定 `sync: sftp`:
|
|
164
|
-
|
|
165
|
-
```yaml
|
|
166
|
-
servers:
|
|
167
|
-
win-server:
|
|
168
|
-
host: "10.0.0.9"
|
|
169
|
-
sync: "sftp"
|
|
170
|
-
password: "secret_password"
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### 3. 密码认证
|
|
186
|
+
### 2. 密码认证
|
|
174
187
|
支持 SSH 密码认证,但推荐使用 SSH Key。
|
|
175
188
|
* **交互式 Shell**: 使用 `sshpass` 包装 `ssh -t` 命令。
|
|
176
|
-
* **SFTP**: 原生支持密码。
|
|
177
189
|
* **Rsync**: 需要本地安装 `sshpass` 工具才能使用密码认证。
|
|
178
190
|
|
|
179
191
|
**密码认证依赖**: 使用密码认证时,必须安装 `sshpass`:
|
|
@@ -188,7 +200,7 @@ sudo apt-get install sshpass
|
|
|
188
200
|
sudo yum install sshpass
|
|
189
201
|
```
|
|
190
202
|
|
|
191
|
-
###
|
|
203
|
+
### 3. 从远端下载文件 (--get)
|
|
192
204
|
使用 `--get` 参数可以从远端服务器下载文件或文件夹到本地:
|
|
193
205
|
|
|
194
206
|
```bash
|
|
@@ -231,7 +243,7 @@ servers:
|
|
|
231
243
|
3. 默认配置中的 `download_dir`
|
|
232
244
|
4. 当前工作目录
|
|
233
245
|
|
|
234
|
-
###
|
|
246
|
+
### 4. 调试日志 (--enable-log)
|
|
235
247
|
当遇到问题需要排查时,可以启用文件日志:
|
|
236
248
|
|
|
237
249
|
```bash
|
|
@@ -6,8 +6,7 @@ MTRemote 是一个专为 AI Infra 和 Python/C++ 混合开发设计的命令行
|
|
|
6
6
|
|
|
7
7
|
* **多服务器管理**:通过配置文件管理多个 GPU 节点,支持默认服务器 (Implicit/Explicit)。
|
|
8
8
|
* **智能同步引擎**:
|
|
9
|
-
* **Rsync
|
|
10
|
-
* **SFTP (兼容)**:纯 Python 实现,适用于无 `rsync` 的环境,配置简单。
|
|
9
|
+
* **Rsync**:调用系统 `rsync`,支持增量同步,速度极快。支持 `sshpass` 自动处理密码认证。
|
|
11
10
|
* **双向同步**:支持从远端下载文件/文件夹到本地(`--get` 参数)。
|
|
12
11
|
* **双模式交互 (Dual-Mode Interaction)**:
|
|
13
12
|
* **交互模式 (Interactive)**:自动检测 TTY,支持 PTY 分配、Raw Mode、Rich UI 动画。完美支持 `vim`, `ipython`, `pdb`, `htop`。
|
|
@@ -30,11 +29,21 @@ pip install mtr-cli
|
|
|
30
29
|
|
|
31
30
|
MTRemote 需要以下系统命令:
|
|
32
31
|
|
|
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) |
|
|
32
|
+
| 命令 | 用途 | 安装方式 | 版本要求 |
|
|
33
|
+
|------|------|----------|----------|
|
|
34
|
+
| `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` | - |
|
|
35
|
+
| `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 | **≥ 3.1.0** (TTY 进度显示需要) |
|
|
36
|
+
| `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) | - |
|
|
37
|
+
|
|
38
|
+
**注意**:macOS 自带的 rsync 版本较旧(2.6.9),不支持 TTY 模式下的进度显示。建议通过 Homebrew 安装新版:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# macOS 用户建议升级 rsync
|
|
42
|
+
brew install rsync
|
|
43
|
+
|
|
44
|
+
# 验证版本
|
|
45
|
+
rsync --version # 应显示 3.1.0 或更高版本
|
|
46
|
+
```
|
|
38
47
|
|
|
39
48
|
**注意**:交互式 Shell 功能(如 `mtr bash`, `mtr ipython`)**必须**安装 `ssh`。密码认证**必须**安装 `sshpass`。
|
|
40
49
|
|
|
@@ -56,7 +65,7 @@ mtr --init
|
|
|
56
65
|
|
|
57
66
|
```yaml
|
|
58
67
|
defaults:
|
|
59
|
-
sync: "rsync"
|
|
68
|
+
sync: "rsync"
|
|
60
69
|
exclude: [".git/", "__pycache__/"]
|
|
61
70
|
download_dir: "./downloads" # 默认下载位置(可选)
|
|
62
71
|
|
|
@@ -88,6 +97,21 @@ mtr ipython
|
|
|
88
97
|
mtr -s prod-node python train.py
|
|
89
98
|
```
|
|
90
99
|
|
|
100
|
+
### ⚠️ 参数传递注意事项
|
|
101
|
+
|
|
102
|
+
当执行的命令包含以 `-` 开头的参数时(如 `python -c`, `gcc -O2`),建议使用 `--` 作为分隔符,避免被误认为是 `mtr` 的选项:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# ❌ 错误:-s 会被当作 --server 的短选项
|
|
106
|
+
mtr python3 -s
|
|
107
|
+
|
|
108
|
+
# ✅ 正确:使用 -- 分隔符
|
|
109
|
+
mtr -- python3 -s
|
|
110
|
+
|
|
111
|
+
# ✅ 指定服务器时也使用 --
|
|
112
|
+
mtr --server prod-node -- python3 -c "print('hello')"
|
|
113
|
+
```
|
|
114
|
+
|
|
91
115
|
## 📖 命令行选项
|
|
92
116
|
|
|
93
117
|
```bash
|
|
@@ -132,21 +156,9 @@ mtr --enable-log --log-file ./debug.log python train.py
|
|
|
132
156
|
mtr --no-tty python train.py > log.txt
|
|
133
157
|
```
|
|
134
158
|
|
|
135
|
-
### 2.
|
|
136
|
-
如果本地或远程无法使用 rsync,可以在配置中指定 `sync: sftp`:
|
|
137
|
-
|
|
138
|
-
```yaml
|
|
139
|
-
servers:
|
|
140
|
-
win-server:
|
|
141
|
-
host: "10.0.0.9"
|
|
142
|
-
sync: "sftp"
|
|
143
|
-
password: "secret_password"
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
### 3. 密码认证
|
|
159
|
+
### 2. 密码认证
|
|
147
160
|
支持 SSH 密码认证,但推荐使用 SSH Key。
|
|
148
161
|
* **交互式 Shell**: 使用 `sshpass` 包装 `ssh -t` 命令。
|
|
149
|
-
* **SFTP**: 原生支持密码。
|
|
150
162
|
* **Rsync**: 需要本地安装 `sshpass` 工具才能使用密码认证。
|
|
151
163
|
|
|
152
164
|
**密码认证依赖**: 使用密码认证时,必须安装 `sshpass`:
|
|
@@ -161,7 +173,7 @@ sudo apt-get install sshpass
|
|
|
161
173
|
sudo yum install sshpass
|
|
162
174
|
```
|
|
163
175
|
|
|
164
|
-
###
|
|
176
|
+
### 3. 从远端下载文件 (--get)
|
|
165
177
|
使用 `--get` 参数可以从远端服务器下载文件或文件夹到本地:
|
|
166
178
|
|
|
167
179
|
```bash
|
|
@@ -204,7 +216,7 @@ servers:
|
|
|
204
216
|
3. 默认配置中的 `download_dir`
|
|
205
217
|
4. 当前工作目录
|
|
206
218
|
|
|
207
|
-
###
|
|
219
|
+
### 4. 调试日志 (--enable-log)
|
|
208
220
|
当遇到问题需要排查时,可以启用文件日志:
|
|
209
221
|
|
|
210
222
|
```bash
|
|
@@ -4,17 +4,22 @@ from datetime import datetime
|
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
|
+
from mtr import __version__
|
|
7
8
|
from mtr.config import ConfigError, ConfigLoader
|
|
8
9
|
from mtr.logger import LogLevel, get_logger, setup_logging
|
|
9
|
-
from mtr.ssh import
|
|
10
|
-
from mtr.sync import RsyncSyncer,
|
|
10
|
+
from mtr.ssh import SSHError, run_ssh_command
|
|
11
|
+
from mtr.sync import RsyncSyncer, SyncError
|
|
12
|
+
from mtr.updater import UpdateChecker
|
|
11
13
|
|
|
12
14
|
DEFAULT_CONFIG_TEMPLATE = """# MTRemote Configuration
|
|
13
15
|
defaults:
|
|
14
|
-
# 默认同步引擎
|
|
15
|
-
# 选项: "rsync" (推荐), "sftp"
|
|
16
|
+
# 默认同步引擎 (仅支持 rsync)
|
|
16
17
|
sync: "rsync"
|
|
17
18
|
|
|
19
|
+
# 是否尊重 .gitignore 文件
|
|
20
|
+
# 设置为 true 时,rsync 会自动读取项目根目录的 .gitignore 并排除匹配的文件
|
|
21
|
+
respect_gitignore: true
|
|
22
|
+
|
|
18
23
|
exclude:
|
|
19
24
|
- ".git/"
|
|
20
25
|
- "__pycache__/"
|
|
@@ -34,12 +39,9 @@ servers:
|
|
|
34
39
|
# 预设命令 (可选)
|
|
35
40
|
# pre_cmd: "source ~/.bashrc && conda activate myenv"
|
|
36
41
|
|
|
37
|
-
# 密码认证 (
|
|
42
|
+
# 密码认证 (可选,需要安装 sshpass)
|
|
38
43
|
# password: "secret"
|
|
39
44
|
|
|
40
|
-
# 强制同步引擎 (可选)
|
|
41
|
-
# sync: "sftp"
|
|
42
|
-
|
|
43
45
|
# 该服务器的下载位置(可选,覆盖 defaults)
|
|
44
46
|
# download_dir: "./backups/dev-node"
|
|
45
47
|
"""
|
|
@@ -66,6 +68,7 @@ def _init_config():
|
|
|
66
68
|
|
|
67
69
|
|
|
68
70
|
@click.command(context_settings=dict(ignore_unknown_options=True, allow_extra_args=True))
|
|
71
|
+
@click.version_option(version=__version__, prog_name="mtr-cli", help="Show version and exit.")
|
|
69
72
|
@click.option("-s", "--server", help="Target server alias")
|
|
70
73
|
@click.option("--sync/--no-sync", default=True, help="Enable/Disable code sync")
|
|
71
74
|
@click.option("--dry-run", is_flag=True, help="Print commands without executing")
|
|
@@ -76,10 +79,36 @@ def _init_config():
|
|
|
76
79
|
@click.option("--log-file", help="Path to log file (default: ./.mtr/logs/mtr_YYYYMMDD_HHMMSS.log)")
|
|
77
80
|
@click.option("--get", "remote_get_path", help="Remote path to download from")
|
|
78
81
|
@click.option("--to", "local_dest_path", help="Local destination path for download (optional)")
|
|
82
|
+
@click.option("--no-check-update", is_flag=True, help="Disable update check")
|
|
79
83
|
@click.argument("command", nargs=-1, type=click.UNPROCESSED)
|
|
80
|
-
def cli(
|
|
84
|
+
def cli(
|
|
85
|
+
server,
|
|
86
|
+
sync,
|
|
87
|
+
dry_run,
|
|
88
|
+
tty,
|
|
89
|
+
init,
|
|
90
|
+
enable_log,
|
|
91
|
+
log_level,
|
|
92
|
+
log_file,
|
|
93
|
+
remote_get_path,
|
|
94
|
+
local_dest_path,
|
|
95
|
+
no_check_update,
|
|
96
|
+
command,
|
|
97
|
+
):
|
|
81
98
|
"""MTRemote: Sync and Execute code on remote server."""
|
|
82
99
|
|
|
100
|
+
# Check for updates (async, non-blocking)
|
|
101
|
+
update_message = None
|
|
102
|
+
if not no_check_update and not init:
|
|
103
|
+
checker = UpdateChecker()
|
|
104
|
+
# Try to get cached update message first (from previous check)
|
|
105
|
+
update_message = checker.get_cached_update_message()
|
|
106
|
+
# Trigger background check for next time
|
|
107
|
+
try:
|
|
108
|
+
checker.check()
|
|
109
|
+
except Exception:
|
|
110
|
+
pass # Silently fail update check
|
|
111
|
+
|
|
83
112
|
# Get logger instance (will be no-op if not setup)
|
|
84
113
|
logger = get_logger()
|
|
85
114
|
|
|
@@ -186,55 +215,67 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
186
215
|
# Resolve exclude
|
|
187
216
|
exclude = config.global_defaults.get("exclude", []) + server_conf.get("exclude", [])
|
|
188
217
|
|
|
218
|
+
# Get respect_gitignore setting
|
|
219
|
+
respect_gitignore = config.get_respect_gitignore()
|
|
220
|
+
|
|
189
221
|
# Determine engine
|
|
190
222
|
engine = server_conf.get("sync", config.global_defaults.get("sync", "rsync"))
|
|
191
223
|
|
|
192
|
-
if
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
remote_dir=remote_dir,
|
|
196
|
-
host=host,
|
|
197
|
-
user=user,
|
|
198
|
-
key_filename=key_filename,
|
|
199
|
-
password=password,
|
|
200
|
-
port=port,
|
|
201
|
-
exclude=exclude,
|
|
202
|
-
)
|
|
203
|
-
elif engine == "sftp":
|
|
204
|
-
syncer = SftpSyncer(
|
|
205
|
-
local_dir=local_dir,
|
|
206
|
-
remote_dir=remote_dir,
|
|
207
|
-
host=host,
|
|
208
|
-
user=user,
|
|
209
|
-
key_filename=key_filename,
|
|
210
|
-
password=password,
|
|
211
|
-
port=port,
|
|
212
|
-
exclude=exclude,
|
|
213
|
-
)
|
|
214
|
-
else:
|
|
224
|
+
# Check if SFTP is configured
|
|
225
|
+
if engine == "sftp":
|
|
226
|
+
logger.error("SFTP mode is no longer supported. Please use rsync.", module="mtr.cli")
|
|
215
227
|
click.secho(
|
|
216
|
-
|
|
217
|
-
fg="
|
|
228
|
+
"Error: SFTP mode has been removed. Please update your config to use 'sync: rsync'.",
|
|
229
|
+
fg="red",
|
|
230
|
+
err=True,
|
|
218
231
|
)
|
|
219
|
-
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
|
|
234
|
+
syncer = RsyncSyncer(
|
|
235
|
+
local_dir=local_dir,
|
|
236
|
+
remote_dir=remote_dir,
|
|
237
|
+
host=host,
|
|
238
|
+
user=user,
|
|
239
|
+
key_filename=key_filename,
|
|
240
|
+
password=password,
|
|
241
|
+
port=port,
|
|
242
|
+
exclude=exclude,
|
|
243
|
+
respect_gitignore=respect_gitignore,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
if dry_run:
|
|
248
|
+
click.echo(f"[DryRun] Would sync {local_dir} -> {remote_dir}")
|
|
249
|
+
logger.info(f"[DryRun] Would sync {local_dir} -> {remote_dir}", module="mtr.sync")
|
|
250
|
+
else:
|
|
251
|
+
if is_interactive and console:
|
|
252
|
+
# TTY mode: single line real-time update using Rich Live
|
|
253
|
+
from rich.live import Live
|
|
254
|
+
from rich.text import Text
|
|
255
|
+
|
|
256
|
+
with Live(Text("Starting sync...", style="blue"), refresh_per_second=10) as live:
|
|
220
257
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
258
|
+
def show_sync_progress(filename):
|
|
259
|
+
# Get relative path for cleaner display
|
|
260
|
+
rel_path = os.path.relpath(filename, local_dir)
|
|
261
|
+
live.update(Text(f"Syncing: {rel_path}", style="blue"))
|
|
262
|
+
|
|
263
|
+
syncer.sync(show_progress=True, progress_callback=show_sync_progress)
|
|
264
|
+
live.update(Text("Sync completed!", style="green"))
|
|
226
265
|
else:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
logger.
|
|
236
|
-
|
|
237
|
-
|
|
266
|
+
# no_tty mode: print each file on new line
|
|
267
|
+
def show_sync_progress(filename):
|
|
268
|
+
rel_path = os.path.relpath(filename, local_dir)
|
|
269
|
+
click.echo(f"Syncing: {rel_path}")
|
|
270
|
+
|
|
271
|
+
click.secho("Syncing code...", fg="blue")
|
|
272
|
+
syncer.sync(show_progress=True, progress_callback=show_sync_progress)
|
|
273
|
+
click.secho("Sync completed!", fg="green")
|
|
274
|
+
logger.info(f"Sync completed: {local_dir} -> {remote_dir}", module="mtr.sync")
|
|
275
|
+
except SyncError as e:
|
|
276
|
+
logger.error(f"Sync failed: {e}", module="mtr.sync")
|
|
277
|
+
click.secho(f"Sync Failed: {e}", fg="red", err=True)
|
|
278
|
+
sys.exit(1)
|
|
238
279
|
|
|
239
280
|
# 3. Download from remote (if --get is specified)
|
|
240
281
|
if remote_get_path:
|
|
@@ -260,50 +301,61 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
260
301
|
# Resolve exclude
|
|
261
302
|
exclude = config.global_defaults.get("exclude", []) + server_conf.get("exclude", [])
|
|
262
303
|
|
|
304
|
+
# Get respect_gitignore setting
|
|
305
|
+
respect_gitignore = config.get_respect_gitignore()
|
|
306
|
+
|
|
263
307
|
# Determine engine
|
|
264
308
|
engine = server_conf.get("sync", config.global_defaults.get("sync", "rsync"))
|
|
265
309
|
|
|
266
|
-
if
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
remote_dir=".", # Not used for download
|
|
270
|
-
host=host,
|
|
271
|
-
user=user,
|
|
272
|
-
key_filename=key_filename,
|
|
273
|
-
password=password,
|
|
274
|
-
port=port,
|
|
275
|
-
exclude=exclude,
|
|
276
|
-
)
|
|
277
|
-
elif engine == "sftp":
|
|
278
|
-
syncer = SftpSyncer(
|
|
279
|
-
local_dir=".", # Not used for download
|
|
280
|
-
remote_dir=".", # Not used for download
|
|
281
|
-
host=host,
|
|
282
|
-
user=user,
|
|
283
|
-
key_filename=key_filename,
|
|
284
|
-
password=password,
|
|
285
|
-
port=port,
|
|
286
|
-
exclude=exclude,
|
|
287
|
-
)
|
|
288
|
-
else:
|
|
310
|
+
# Check if SFTP is configured
|
|
311
|
+
if engine == "sftp":
|
|
312
|
+
logger.error("SFTP mode is no longer supported. Please use rsync.", module="mtr.cli")
|
|
289
313
|
click.secho(
|
|
290
|
-
|
|
291
|
-
fg="
|
|
314
|
+
"Error: SFTP mode has been removed. Please update your config to use 'sync: rsync'.",
|
|
315
|
+
fg="red",
|
|
316
|
+
err=True,
|
|
292
317
|
)
|
|
293
318
|
sys.exit(1)
|
|
294
319
|
|
|
320
|
+
syncer = RsyncSyncer(
|
|
321
|
+
local_dir=".", # Not used for download
|
|
322
|
+
remote_dir=".", # Not used for download
|
|
323
|
+
host=host,
|
|
324
|
+
user=user,
|
|
325
|
+
key_filename=key_filename,
|
|
326
|
+
password=password,
|
|
327
|
+
port=port,
|
|
328
|
+
exclude=exclude,
|
|
329
|
+
respect_gitignore=respect_gitignore,
|
|
330
|
+
)
|
|
331
|
+
|
|
295
332
|
try:
|
|
296
333
|
if dry_run:
|
|
297
334
|
click.echo(f"[DryRun] Would download {remote_get_path} -> {local_dest}")
|
|
298
335
|
logger.info(f"[DryRun] Would download {remote_get_path} -> {local_dest}", module="mtr.sync")
|
|
299
336
|
else:
|
|
300
337
|
if is_interactive and console:
|
|
301
|
-
|
|
302
|
-
|
|
338
|
+
# TTY mode: single line real-time update using Rich Live
|
|
339
|
+
from rich.live import Live
|
|
340
|
+
from rich.text import Text
|
|
341
|
+
|
|
342
|
+
with Live(Text("Starting download...", style="blue"), refresh_per_second=10) as live:
|
|
343
|
+
|
|
344
|
+
def show_download_progress(filename):
|
|
345
|
+
live.update(Text(f"Downloading: {filename}", style="blue"))
|
|
346
|
+
|
|
347
|
+
syncer.download(
|
|
348
|
+
remote_get_path, local_dest, show_progress=True, progress_callback=show_download_progress
|
|
349
|
+
)
|
|
350
|
+
live.update(Text("Download completed!", style="green"))
|
|
303
351
|
console.print(f"✅ [green]Downloaded:[/green] {remote_get_path} -> {local_dest}")
|
|
304
352
|
else:
|
|
353
|
+
# no_tty mode: print each file on new line
|
|
354
|
+
def show_download_progress(filename):
|
|
355
|
+
click.echo(f"Downloading: {filename}")
|
|
356
|
+
|
|
305
357
|
click.secho(f"Downloading {remote_get_path}...", fg="blue")
|
|
306
|
-
syncer.download(remote_get_path, local_dest)
|
|
358
|
+
syncer.download(remote_get_path, local_dest, show_progress=True, progress_callback=show_download_progress)
|
|
307
359
|
click.secho(f"Download completed: {local_dest}", fg="green")
|
|
308
360
|
logger.info(f"Download completed: {remote_get_path} -> {local_dest}", module="mtr.sync")
|
|
309
361
|
except SyncError as e:
|
|
@@ -322,43 +374,34 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
322
374
|
click.echo(f"[DryRun] Would run on {host}: {remote_cmd} (workdir={remote_dir})")
|
|
323
375
|
return
|
|
324
376
|
|
|
325
|
-
ssh = SSHClientWrapper(host, user, port=port, key_filename=key_filename, password=password)
|
|
326
377
|
try:
|
|
327
|
-
|
|
328
|
-
logger.info(f"
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
while True:
|
|
347
|
-
line = next(stream)
|
|
348
|
-
click.echo(line, nl=False)
|
|
349
|
-
except StopIteration as e:
|
|
350
|
-
exit_code = e.value
|
|
351
|
-
|
|
352
|
-
logger.info(f"Command completed with exit code: {exit_code}", module="mtr.cli")
|
|
353
|
-
sys.exit(exit_code)
|
|
378
|
+
# Execute command via SSH
|
|
379
|
+
logger.info(f"Executing command: {remote_cmd}", module="mtr.cli")
|
|
380
|
+
exit_code = run_ssh_command(
|
|
381
|
+
host=host,
|
|
382
|
+
user=user,
|
|
383
|
+
command=remote_cmd,
|
|
384
|
+
port=port,
|
|
385
|
+
key_filename=key_filename,
|
|
386
|
+
password=password,
|
|
387
|
+
workdir=remote_dir,
|
|
388
|
+
pre_cmd=pre_cmd,
|
|
389
|
+
tty=is_interactive,
|
|
390
|
+
)
|
|
391
|
+
logger.info(f"Command completed with exit code: {exit_code}", module="mtr.cli")
|
|
392
|
+
|
|
393
|
+
# Show update message if available
|
|
394
|
+
if update_message:
|
|
395
|
+
click.echo(update_message, err=True)
|
|
396
|
+
sys.exit(exit_code)
|
|
354
397
|
|
|
355
398
|
except SSHError as e:
|
|
356
399
|
logger.error(f"SSH error: {e}", module="mtr.ssh")
|
|
357
400
|
click.secho(f"SSH Error: {e}", fg="red", err=True)
|
|
401
|
+
# Show update message if available even on error
|
|
402
|
+
if update_message:
|
|
403
|
+
click.echo(update_message, err=True)
|
|
358
404
|
sys.exit(1)
|
|
359
|
-
finally:
|
|
360
|
-
logger.info("Closing SSH connection", module="mtr.ssh")
|
|
361
|
-
ssh.close()
|
|
362
405
|
|
|
363
406
|
|
|
364
407
|
if __name__ == "__main__":
|
|
@@ -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):
|