pix-cli 0.1.2__tar.gz → 0.1.3__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 (26) hide show
  1. {pix_cli-0.1.2 → pix_cli-0.1.3}/AGENTS.md +4 -4
  2. {pix_cli-0.1.2 → pix_cli-0.1.3}/PKG-INFO +20 -20
  3. {pix_cli-0.1.2 → pix_cli-0.1.3}/README.md +18 -18
  4. {pix_cli-0.1.2 → pix_cli-0.1.3}/README.zh-CN.md +18 -18
  5. {pix_cli-0.1.2 → pix_cli-0.1.3}/docs/architecture.md +21 -21
  6. {pix_cli-0.1.2 → pix_cli-0.1.3}/docs/decisions.md +17 -9
  7. {pix_cli-0.1.2 → pix_cli-0.1.3}/docs/development.md +9 -9
  8. {pix_cli-0.1.2 → pix_cli-0.1.3}/docs/roadmap.md +9 -9
  9. {pix_cli-0.1.2 → pix_cli-0.1.3}/pyproject.toml +2 -2
  10. {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/__init__.py +1 -1
  11. {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/cli.py +11 -11
  12. {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/config.py +2 -2
  13. {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/default_config.toml +1 -1
  14. {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/sync.py +3 -3
  15. {pix_cli-0.1.2 → pix_cli-0.1.3}/tests/test_cli.py +18 -18
  16. {pix_cli-0.1.2 → pix_cli-0.1.3}/uv.lock +1 -1
  17. {pix_cli-0.1.2 → pix_cli-0.1.3}/.github/workflows/ci.yml +0 -0
  18. {pix_cli-0.1.2 → pix_cli-0.1.3}/.github/workflows/publish.yml +0 -0
  19. {pix_cli-0.1.2 → pix_cli-0.1.3}/.gitignore +0 -0
  20. {pix_cli-0.1.2 → pix_cli-0.1.3}/.python-version +0 -0
  21. {pix_cli-0.1.2 → pix_cli-0.1.3}/LICENSE +0 -0
  22. {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/doctor.py +0 -0
  23. {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/logger.py +0 -0
  24. {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/py.typed +0 -0
  25. {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/rclone.py +0 -0
  26. {pix_cli-0.1.2 → pix_cli-0.1.3}/src/pix/shrink.py +0 -0
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md
2
2
 
3
- `pix` 是批量图片压缩(AVIF)与 rclone 图床同步 CLI。已实现 `shrink` / `backup` / `publish` / `doctor` / `config init`;行为规格见 `docs/architecture.md`。
3
+ `pix` 是批量图片压缩(AVIF)与 rclone 图床同步 CLI。已实现 `shrink` / `mirror` / `publish` / `doctor` / `config init`;行为规格见 `docs/architecture.md`。
4
4
 
5
5
  ## 文档
6
6
 
@@ -31,9 +31,9 @@ uv build
31
31
  - `pyvips` 只在 `shrink` / `doctor` 内延迟导入。
32
32
  - 不通过 shell 调用 `vips` 或 `rclone`。
33
33
  - `-q` 是 quality,`--quiet` 是静默。
34
- - 退出码 0/1/2,日志走 stderr;`backup` / `publish` 透传 rclone 退出码。
35
- - `backup` / `publish` 使用 `rclone bisync`;首次运行必须显式 `--resync`,不要自动加。它们是双向镜像,不是单向备份。
36
- - `--max-delete` 默认 10(删除百分比,rclone bisync 语义);`backup` / `publish` 源为空时默认拒绝,`--allow-empty` 加 `--resync` 才放行。
34
+ - 退出码 0/1/2,日志走 stderr;`mirror` / `publish` 透传 rclone 退出码。
35
+ - `mirror` / `publish` 使用 `rclone bisync`;首次运行必须显式 `--resync`,不要自动加。它们是双向镜像,不是单向备份。
36
+ - `--max-delete` 默认 10(删除百分比,rclone bisync 语义);`mirror` / `publish` 源为空时默认拒绝,`--allow-empty` 加 `--resync` 才放行。
37
37
  - 修改排除项后必须提示用户先 `--resync`;`--exclude` 是配置在前、命令行追加并去重。
38
38
  - 增量状态固定在 `OUT/.pix/state.json`,每 200 个成功文件刷新并在 `finally` 落盘;`publish` 必须排除 `.pix/**`。
39
39
  - `shrink` 必须拒绝目标冲突,`--prune` 必须拒绝空源,非 dry-run 必须持有 `OUT/.pix/lock`。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pix-cli
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Batch AVIF shrink and image-host sync CLI
5
5
  Project-URL: Homepage, https://github.com/uzvg/pix
6
6
  Project-URL: Repository, https://github.com/uzvg/pix
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Programming Language :: Python :: 3.14
23
23
  Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
24
- Classifier: Topic :: System :: Archiving :: Backup
24
+ Classifier: Topic :: System :: Archiving :: Mirroring
25
25
  Requires-Python: >=3.11
26
26
  Requires-Dist: pyvips[binary]>=3.2
27
27
  Description-Content-Type: text/markdown
@@ -41,7 +41,7 @@ Description-Content-Type: text/markdown
41
41
  | Command | Purpose |
42
42
  |---|---|
43
43
  | `pix shrink` | Recursively compress images to AVIF, re-encoding only changed files |
44
- | `pix backup` | Bidirectionally mirror an original photo directory to a cloud drive |
44
+ | `pix mirror` | Bidirectionally mirror an original photo directory to a cloud drive |
45
45
  | `pix publish` | Bidirectionally mirror AVIF output to an image host; optionally emit public URLs |
46
46
  | `pix doctor` | Check Python, libvips, rclone and configuration |
47
47
  | `pix config init` | Write a commented default configuration file |
@@ -58,25 +58,25 @@ Description-Content-Type: text/markdown
58
58
  photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
59
59
  │
60
60
  └── publish (rclone bisync) ──> r2:images
61
- photos/ ── backup (rclone bisync) ────────────────────────────────> onedrive:photos
61
+ photos/ ── mirror (rclone bisync) ────────────────────────────────> onedrive:photos
62
62
  ```
63
63
 
64
64
  | Command | What actually runs | Who does the work |
65
65
  |---|---|---|
66
66
  | `shrink` | pyvips thumbnail + AVIF encode | pix |
67
- | `backup` | `rclone bisync SRC REMOTE` | pix composes arguments, rclone executes |
67
+ | `mirror` | `rclone bisync SRC REMOTE` | pix composes arguments, rclone executes |
68
68
  | `publish` | `rclone bisync OUT REMOTE`, excluding `.pix/**` | pix + rclone |
69
69
  | `doctor` / `config init` | Dependency checks / config scaffold | pix |
70
70
 
71
- **Sync is rclone.** `backup` and `publish` only compose `rclone bisync` arguments; rclone handles authentication, transfers and conflict handling. Configure remotes with `rclone config` before using them. `pix` never reads or writes `rclone.conf`.
71
+ **Sync is rclone.** `mirror` and `publish` only compose `rclone bisync` arguments; rclone handles authentication, transfers and conflict handling. Configure remotes with `rclone config` before using them. `pix` never reads or writes `rclone.conf`.
72
72
 
73
- `backup` / `publish` are **bidirectional mirrors**, not one-way backups. Deletions propagate in both directions.
73
+ `mirror` / `publish` are **bidirectional mirrors**, not one-way copies. Deletions propagate in both directions.
74
74
 
75
75
  ## Requirements
76
76
 
77
77
  - Python 3.11+
78
78
  - [uv](https://docs.astral.sh/uv/)
79
- - rclone >= 1.66 for `backup` / `publish`
79
+ - rclone >= 1.66 for `mirror` / `publish`
80
80
  - At least one remote already configured in rclone
81
81
 
82
82
  `pyvips[binary]` bundles libvips, so no separate vips installation is required.
@@ -96,7 +96,7 @@ Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pi
96
96
 
97
97
  ## Configure rclone
98
98
 
99
- Only needed for `backup` / `publish`:
99
+ Only needed for `mirror` / `publish`:
100
100
 
101
101
  ```bash
102
102
  rclone config # create a remote, for example onedrive or r2
@@ -129,12 +129,12 @@ Common keys:
129
129
  | | `effort` | `5` | libvips effort, 0–9 |
130
130
  | | `min_size` | `0` | Skip sources smaller than this; accepts `500K`, `1M`, `2G` |
131
131
  | | `include` | `jpg`, `jpeg`, `png`, `webp` | Extensions to scan, without dots |
132
- | `[backup]` | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
132
+ | `[mirror]` | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
133
133
  | | `max_delete` | `10` | Delete limit as a percentage; `-1` disables, `0` forbids |
134
134
  | `[publish]` | `base_url` | `""` | Public URL prefix; must start with `http://` or `https://` |
135
135
  | | `urls` | `false` | Emit a JSON URL manifest |
136
- | | `exclude` | `[]` | Same as backup |
137
- | | `max_delete` | `10` | Same as backup |
136
+ | | `exclude` | `[]` | Same as mirror |
137
+ | | `max_delete` | `10` | Same as mirror |
138
138
  | `[rclone]` | `binary` | `rclone` | rclone executable name or path |
139
139
 
140
140
  Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality; silence is `--quiet` (no short form).
@@ -156,11 +156,11 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
156
156
 
157
157
  The first run encodes everything; later runs skip unchanged files. State lives in `OUT/.pix/state.json`. Changing `width`, `quality` or `effort` triggers a full re-encode. `--dry-run` only prints the plan.
158
158
 
159
- 3. Back up originals:
159
+ 3. Mirror originals:
160
160
 
161
161
  ```bash
162
- pix backup -s ~/Pictures -d onedrive:photos --dry-run
163
- pix backup -s ~/Pictures -d onedrive:photos --resync
162
+ pix mirror -s ~/Pictures -d onedrive:photos --dry-run
163
+ pix mirror -s ~/Pictures -d onedrive:photos --resync
164
164
  ```
165
165
 
166
166
  `--resync` is required on the first run. Always preview with `--dry-run` first.
@@ -178,7 +178,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
178
178
 
179
179
  ```bash
180
180
  pix shrink -s ~/Pictures -d ~/Pictures-avif
181
- pix backup -s ~/Pictures -d onedrive:photos
181
+ pix mirror -s ~/Pictures -d onedrive:photos
182
182
  pix publish -s ~/Pictures-avif -d r2:images --urls
183
183
  ```
184
184
 
@@ -191,8 +191,8 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
191
191
  | Preview compression | `pix shrink -s SRC -d OUT --dry-run` |
192
192
  | Re-encode everything | `pix shrink -s SRC -d OUT --force` |
193
193
  | Remove outputs whose source is gone | `pix shrink -s SRC -d OUT --prune` |
194
- | Preview a sync | `pix backup -s SRC -d REMOTE --dry-run` |
195
- | First run, or after changing excludes | `pix backup -s SRC -d REMOTE --resync` |
194
+ | Preview a sync | `pix mirror -s SRC -d REMOTE --dry-run` |
195
+ | First run, or after changing excludes | `pix mirror -s SRC -d REMOTE --resync` |
196
196
  | Emit a URL manifest | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
197
197
  | Diagnose the environment | `pix doctor` |
198
198
  | Generate a config file | `pix config init` |
@@ -204,7 +204,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
204
204
  | Deletions on either side | Propagated in both directions; accidental deletion is irreversible | Preview with `--dry-run`; default `--max-delete 10` (percentage) |
205
205
  | First sync | rclone has no baseline and errors | Add `--resync` explicitly |
206
206
  | Excludes changed | The old baseline may treat excluded files as deletions | Run with `--resync` again after changing excludes |
207
- | Empty source | `backup` / `publish` refuse; `shrink --prune` also refuses | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
207
+ | Empty source | `mirror` / `publish` refuse; `shrink --prune` also refuses | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
208
208
  | `a.jpg` and `a.jpeg` both exist | Both map to the same output | Detected before encoding; exits 2 |
209
209
  | Two concurrent shrinks | They would write the same state and outputs | `OUT/.pix/lock` blocks the second process |
210
210
  | State lost or corrupt | Warning, then a full rebuild | Only causes re-encoding; outputs stay valid |
@@ -214,7 +214,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
214
214
  - Logs and progress go to stderr.
215
215
  - stdout carries `doctor` reports, `--dry-run` plans and `--urls` JSON.
216
216
  - Exit codes: `0` success, `1` runtime failure, `2` argument or configuration error.
217
- - `backup` / `publish` return rclone's exit code unchanged.
217
+ - `mirror` / `publish` return rclone's exit code unchanged.
218
218
 
219
219
  URL manifest example:
220
220
 
@@ -13,7 +13,7 @@
13
13
  | Command | Purpose |
14
14
  |---|---|
15
15
  | `pix shrink` | Recursively compress images to AVIF, re-encoding only changed files |
16
- | `pix backup` | Bidirectionally mirror an original photo directory to a cloud drive |
16
+ | `pix mirror` | Bidirectionally mirror an original photo directory to a cloud drive |
17
17
  | `pix publish` | Bidirectionally mirror AVIF output to an image host; optionally emit public URLs |
18
18
  | `pix doctor` | Check Python, libvips, rclone and configuration |
19
19
  | `pix config init` | Write a commented default configuration file |
@@ -30,25 +30,25 @@
30
30
  photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
31
31
  │
32
32
  └── publish (rclone bisync) ──> r2:images
33
- photos/ ── backup (rclone bisync) ────────────────────────────────> onedrive:photos
33
+ photos/ ── mirror (rclone bisync) ────────────────────────────────> onedrive:photos
34
34
  ```
35
35
 
36
36
  | Command | What actually runs | Who does the work |
37
37
  |---|---|---|
38
38
  | `shrink` | pyvips thumbnail + AVIF encode | pix |
39
- | `backup` | `rclone bisync SRC REMOTE` | pix composes arguments, rclone executes |
39
+ | `mirror` | `rclone bisync SRC REMOTE` | pix composes arguments, rclone executes |
40
40
  | `publish` | `rclone bisync OUT REMOTE`, excluding `.pix/**` | pix + rclone |
41
41
  | `doctor` / `config init` | Dependency checks / config scaffold | pix |
42
42
 
43
- **Sync is rclone.** `backup` and `publish` only compose `rclone bisync` arguments; rclone handles authentication, transfers and conflict handling. Configure remotes with `rclone config` before using them. `pix` never reads or writes `rclone.conf`.
43
+ **Sync is rclone.** `mirror` and `publish` only compose `rclone bisync` arguments; rclone handles authentication, transfers and conflict handling. Configure remotes with `rclone config` before using them. `pix` never reads or writes `rclone.conf`.
44
44
 
45
- `backup` / `publish` are **bidirectional mirrors**, not one-way backups. Deletions propagate in both directions.
45
+ `mirror` / `publish` are **bidirectional mirrors**, not one-way copies. Deletions propagate in both directions.
46
46
 
47
47
  ## Requirements
48
48
 
49
49
  - Python 3.11+
50
50
  - [uv](https://docs.astral.sh/uv/)
51
- - rclone >= 1.66 for `backup` / `publish`
51
+ - rclone >= 1.66 for `mirror` / `publish`
52
52
  - At least one remote already configured in rclone
53
53
 
54
54
  `pyvips[binary]` bundles libvips, so no separate vips installation is required.
@@ -68,7 +68,7 @@ Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pi
68
68
 
69
69
  ## Configure rclone
70
70
 
71
- Only needed for `backup` / `publish`:
71
+ Only needed for `mirror` / `publish`:
72
72
 
73
73
  ```bash
74
74
  rclone config # create a remote, for example onedrive or r2
@@ -101,12 +101,12 @@ Common keys:
101
101
  | | `effort` | `5` | libvips effort, 0–9 |
102
102
  | | `min_size` | `0` | Skip sources smaller than this; accepts `500K`, `1M`, `2G` |
103
103
  | | `include` | `jpg`, `jpeg`, `png`, `webp` | Extensions to scan, without dots |
104
- | `[backup]` | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
104
+ | `[mirror]` | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
105
105
  | | `max_delete` | `10` | Delete limit as a percentage; `-1` disables, `0` forbids |
106
106
  | `[publish]` | `base_url` | `""` | Public URL prefix; must start with `http://` or `https://` |
107
107
  | | `urls` | `false` | Emit a JSON URL manifest |
108
- | | `exclude` | `[]` | Same as backup |
109
- | | `max_delete` | `10` | Same as backup |
108
+ | | `exclude` | `[]` | Same as mirror |
109
+ | | `max_delete` | `10` | Same as mirror |
110
110
  | `[rclone]` | `binary` | `rclone` | rclone executable name or path |
111
111
 
112
112
  Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality; silence is `--quiet` (no short form).
@@ -128,11 +128,11 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
128
128
 
129
129
  The first run encodes everything; later runs skip unchanged files. State lives in `OUT/.pix/state.json`. Changing `width`, `quality` or `effort` triggers a full re-encode. `--dry-run` only prints the plan.
130
130
 
131
- 3. Back up originals:
131
+ 3. Mirror originals:
132
132
 
133
133
  ```bash
134
- pix backup -s ~/Pictures -d onedrive:photos --dry-run
135
- pix backup -s ~/Pictures -d onedrive:photos --resync
134
+ pix mirror -s ~/Pictures -d onedrive:photos --dry-run
135
+ pix mirror -s ~/Pictures -d onedrive:photos --resync
136
136
  ```
137
137
 
138
138
  `--resync` is required on the first run. Always preview with `--dry-run` first.
@@ -150,7 +150,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
150
150
 
151
151
  ```bash
152
152
  pix shrink -s ~/Pictures -d ~/Pictures-avif
153
- pix backup -s ~/Pictures -d onedrive:photos
153
+ pix mirror -s ~/Pictures -d onedrive:photos
154
154
  pix publish -s ~/Pictures-avif -d r2:images --urls
155
155
  ```
156
156
 
@@ -163,8 +163,8 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
163
163
  | Preview compression | `pix shrink -s SRC -d OUT --dry-run` |
164
164
  | Re-encode everything | `pix shrink -s SRC -d OUT --force` |
165
165
  | Remove outputs whose source is gone | `pix shrink -s SRC -d OUT --prune` |
166
- | Preview a sync | `pix backup -s SRC -d REMOTE --dry-run` |
167
- | First run, or after changing excludes | `pix backup -s SRC -d REMOTE --resync` |
166
+ | Preview a sync | `pix mirror -s SRC -d REMOTE --dry-run` |
167
+ | First run, or after changing excludes | `pix mirror -s SRC -d REMOTE --resync` |
168
168
  | Emit a URL manifest | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
169
169
  | Diagnose the environment | `pix doctor` |
170
170
  | Generate a config file | `pix config init` |
@@ -176,7 +176,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
176
176
  | Deletions on either side | Propagated in both directions; accidental deletion is irreversible | Preview with `--dry-run`; default `--max-delete 10` (percentage) |
177
177
  | First sync | rclone has no baseline and errors | Add `--resync` explicitly |
178
178
  | Excludes changed | The old baseline may treat excluded files as deletions | Run with `--resync` again after changing excludes |
179
- | Empty source | `backup` / `publish` refuse; `shrink --prune` also refuses | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
179
+ | Empty source | `mirror` / `publish` refuse; `shrink --prune` also refuses | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
180
180
  | `a.jpg` and `a.jpeg` both exist | Both map to the same output | Detected before encoding; exits 2 |
181
181
  | Two concurrent shrinks | They would write the same state and outputs | `OUT/.pix/lock` blocks the second process |
182
182
  | State lost or corrupt | Warning, then a full rebuild | Only causes re-encoding; outputs stay valid |
@@ -186,7 +186,7 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
186
186
  - Logs and progress go to stderr.
187
187
  - stdout carries `doctor` reports, `--dry-run` plans and `--urls` JSON.
188
188
  - Exit codes: `0` success, `1` runtime failure, `2` argument or configuration error.
189
- - `backup` / `publish` return rclone's exit code unchanged.
189
+ - `mirror` / `publish` return rclone's exit code unchanged.
190
190
 
191
191
  URL manifest example:
192
192
 
@@ -13,7 +13,7 @@
13
13
  | 命令 | 作用 |
14
14
  |---|---|
15
15
  | `pix shrink` | 递归压缩图片为 AVIF,只重新编码变化的文件 |
16
- | `pix backup` | 把原图目录与云盘做双向镜像 |
16
+ | `pix mirror` | 把原图目录与云盘做双向镜像 |
17
17
  | `pix publish` | 把 AVIF 输出目录与图床做双向镜像,可选输出公开 URL |
18
18
  | `pix doctor` | 检查 Python、libvips、rclone 与配置 |
19
19
  | `pix config init` | 生成带注释的默认配置文件 |
@@ -30,25 +30,25 @@
30
30
  photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
31
31
  │
32
32
  └── publish (rclone bisync) ──> r2:images
33
- photos/ ── backup (rclone bisync) ────────────────────────────────> onedrive:photos
33
+ photos/ ── mirror (rclone bisync) ────────────────────────────────> onedrive:photos
34
34
  ```
35
35
 
36
36
  | 命令 | 实际执行的内容 | 谁负责 |
37
37
  |---|---|---|
38
38
  | `shrink` | pyvips 缩放并编码 AVIF | pix |
39
- | `backup` | `rclone bisync SRC REMOTE` | pix 拼参数,rclone 执行 |
39
+ | `mirror` | `rclone bisync SRC REMOTE` | pix 拼参数,rclone 执行 |
40
40
  | `publish` | `rclone bisync OUT REMOTE`,排除 `.pix/**` | pix + rclone |
41
41
  | `doctor` / `config init` | 依赖检查 / 配置脚手架 | pix |
42
42
 
43
- **同步由 rclone 执行。** `backup` / `publish` 只负责拼装 `rclone bisync` 参数;认证、传输和冲突处理都由 rclone 完成。使用前请先用 `rclone config` 配好 remote。`pix` 不读取也不写入 `rclone.conf`。
43
+ **同步由 rclone 执行。** `mirror` / `publish` 只负责拼装 `rclone bisync` 参数;认证、传输和冲突处理都由 rclone 完成。使用前请先用 `rclone config` 配好 remote。`pix` 不读取也不写入 `rclone.conf`。
44
44
 
45
- `backup` / `publish` 是**双向镜像**,不是单向备份。删除会双向传播。
45
+ `mirror` / `publish` 是**双向镜像**,不是单向备份。删除会双向传播。
46
46
 
47
47
  ## 前置条件
48
48
 
49
49
  - Python 3.11+
50
50
  - [uv](https://docs.astral.sh/uv/)
51
- - `backup` / `publish` 需要 rclone >= 1.66
51
+ - `mirror` / `publish` 需要 rclone >= 1.66
52
52
  - 已在 rclone 中配置好至少一个 remote
53
53
 
54
54
  `pyvips[binary]` 自带 libvips,不需要单独安装 vips。
@@ -68,7 +68,7 @@ Linux 安装到 `~/.local/bin/pix`,Windows 为 `%USERPROFILE%\.local\bin\pix.e
68
68
 
69
69
  ## 配置 rclone
70
70
 
71
- 仅 `backup` / `publish` 需要:
71
+ 仅 `mirror` / `publish` 需要:
72
72
 
73
73
  ```bash
74
74
  rclone config # 新建 remote,例如 onedrive 或 r2
@@ -101,12 +101,12 @@ pix config init
101
101
  | | `effort` | `5` | libvips effort,0–9 |
102
102
  | | `min_size` | `0` | 小于该值的源文件跳过;支持 `500K`、`1M`、`2G` |
103
103
  | | `include` | `jpg`、`jpeg`、`png`、`webp` | 扫描扩展名,不带点 |
104
- | `[backup]` | `exclude` | `[]` | rclone 排除 glob;命令行项会追加 |
104
+ | `[mirror]` | `exclude` | `[]` | rclone 排除 glob;命令行项会追加 |
105
105
  | | `max_delete` | `10` | 删除百分比上限;`-1` 不限制,`0` 禁止删除 |
106
106
  | `[publish]` | `base_url` | `""` | 公开 URL 前缀,必须以 `http://` 或 `https://` 开头 |
107
107
  | | `urls` | `false` | 是否输出 JSON URL 清单 |
108
- | | `exclude` | `[]` | 同 `backup` |
109
- | | `max_delete` | `10` | 同 `backup` |
108
+ | | `exclude` | `[]` | 同 `mirror` |
109
+ | | `max_delete` | `10` | 同 `mirror` |
110
110
  | `[rclone]` | `binary` | `rclone` | rclone 可执行文件名或路径 |
111
111
 
112
112
  优先级为**命令行 > pix 配置 > 内置默认**。`--exclude` 是追加而非覆盖配置项。`-q` 表示 quality,静默模式用 `--quiet`(无短选项)。
@@ -128,11 +128,11 @@ pix config init
128
128
 
129
129
  首次全量编码,之后跳过未变化的文件。状态保存在 `OUT/.pix/state.json`。修改 `width`、`quality` 或 `effort` 会触发全量重编码。`--dry-run` 只打印计划。
130
130
 
131
- 3. 备份原图:
131
+ 3. 镜像原图:
132
132
 
133
133
  ```bash
134
- pix backup -s ~/Pictures -d onedrive:photos --dry-run
135
- pix backup -s ~/Pictures -d onedrive:photos --resync
134
+ pix mirror -s ~/Pictures -d onedrive:photos --dry-run
135
+ pix mirror -s ~/Pictures -d onedrive:photos --resync
136
136
  ```
137
137
 
138
138
  首次运行必须显式加 `--resync`。建议先跑 `--dry-run` 预览。
@@ -150,7 +150,7 @@ pix config init
150
150
 
151
151
  ```bash
152
152
  pix shrink -s ~/Pictures -d ~/Pictures-avif
153
- pix backup -s ~/Pictures -d onedrive:photos
153
+ pix mirror -s ~/Pictures -d onedrive:photos
154
154
  pix publish -s ~/Pictures-avif -d r2:images --urls
155
155
  ```
156
156
 
@@ -163,8 +163,8 @@ pix config init
163
163
  | 预览压缩 | `pix shrink -s SRC -d OUT --dry-run` |
164
164
  | 强制全部重压 | `pix shrink -s SRC -d OUT --force` |
165
165
  | 删除源已消失的输出 | `pix shrink -s SRC -d OUT --prune` |
166
- | 预览同步 | `pix backup -s SRC -d REMOTE --dry-run` |
167
- | 首次运行或修改排除项后 | `pix backup -s SRC -d REMOTE --resync` |
166
+ | 预览同步 | `pix mirror -s SRC -d REMOTE --dry-run` |
167
+ | 首次运行或修改排除项后 | `pix mirror -s SRC -d REMOTE --resync` |
168
168
  | 输出 URL 清单 | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
169
169
  | 诊断环境 | `pix doctor` |
170
170
  | 生成配置文件 | `pix config init` |
@@ -176,7 +176,7 @@ pix config init
176
176
  | 任意一侧删除文件 | 删除双向传播,误删不可逆 | 先 `--dry-run`;默认 `--max-delete 10`(百分比) |
177
177
  | 首次同步 | rclone 没有基线,直接报错 | 显式加 `--resync` |
178
178
  | 修改排除项 | 旧基线可能把被排除的文件当成删除传播 | 修改后必须重新 `--resync` |
179
- | 源目录为空 | `backup` / `publish` 默认拒绝;`shrink --prune` 同样拒绝 | 仅在确认要清空时用 `--allow-empty`(同步还需 `--resync`) |
179
+ | 源目录为空 | `mirror` / `publish` 默认拒绝;`shrink --prune` 同样拒绝 | 仅在确认要清空时用 `--allow-empty`(同步还需 `--resync`) |
180
180
  | `a.jpg` 与 `a.jpeg` 同时存在 | 映射到同一输出 | 编码前检测并以 2 退出 |
181
181
  | 同时运行两个 shrink | 会写同一份状态和输出 | `OUT/.pix/lock` 阻止第二个进程 |
182
182
  | 状态文件丢失或损坏 | 告警后全量重建 | 只会重新编码,不会产生错误结果 |
@@ -186,7 +186,7 @@ pix config init
186
186
  - 日志和进度输出到 stderr。
187
187
  - stdout 用于 `doctor` 报告、`--dry-run` 计划和 `--urls` JSON。
188
188
  - 退出码:`0` 成功,`1` 运行失败,`2` 参数或配置错误。
189
- - `backup` / `publish` 原样返回 rclone 的退出码。
189
+ - `mirror` / `publish` 原样返回 rclone 的退出码。
190
190
 
191
191
  URL 清单示例:
192
192
 
@@ -7,7 +7,7 @@
7
7
  `pix` 是独立的图床流水线 CLI:
8
8
 
9
9
  - 把本地图片目录增量压缩为 AVIF。
10
- - 把原始图片目录备份到 OneDrive 等 rclone 兼容云盘。
10
+ - 把原始图片目录双向镜像到 OneDrive 等 rclone 兼容云盘。
11
11
  - 把压缩结果双向同步到 Cloudflare R2 等 rclone 兼容图床。
12
12
 
13
13
  `pix` 只处理用户显式传入的本地路径和 rclone 远端,不感知项目类型或版本控制状态。所有路径和远端都由命令行或用户级配置提供。
@@ -24,7 +24,7 @@
24
24
  第一版做:
25
25
 
26
26
  - `pix shrink`:递归扫描图片,按宽度等比缩放并输出 AVIF,带增量状态。
27
- - `pix backup`:把原始图片目录与 rclone 远端双向同步。
27
+ - `pix mirror`:把原始图片目录与 rclone 远端双向同步。
28
28
  - `pix publish`:把压缩目录与图床远端双向同步,可选输出公开 URL 清单。
29
29
  - `pix doctor`:检查运行依赖、rclone bisync 能力和配置。
30
30
  - `pix config init`:把带注释的默认配置写到默认路径,供用户修改。
@@ -44,7 +44,7 @@
44
44
 
45
45
  - Python `>=3.11`。
46
46
  - 仅标准库 + `pyvips[binary]`。
47
- - 运行时依赖外部 `rclone >= 1.66`(bisync 稳定版),由 `backup` / `publish` 调用。
47
+ - 运行时依赖外部 `rclone >= 1.66`(bisync 稳定版),由 `mirror` / `publish` 调用。
48
48
 
49
49
  ```text
50
50
  src/pix/
@@ -56,7 +56,7 @@ src/pix/
56
56
  ├── logger.py # stderr 日志
57
57
  ├── shrink.py # shrink 子命令
58
58
  ├── rclone.py # rclone 探测、版本检查与执行
59
- ├── sync.py # backup / publish 子命令
59
+ ├── sync.py # mirror / publish 子命令
60
60
  └── doctor.py # doctor 子命令
61
61
  tests/
62
62
  └── test_cli.py
@@ -77,7 +77,7 @@ tests/
77
77
  | `config` | `parse_size(value: str \| int) -> int` | 尺寸字符串转字节 |
78
78
  | `config` | `init_config(args) -> int` | `config init` 子命令:写入默认配置 |
79
79
  | `shrink` | `shrink(args, settings, log) -> int` | `shrink` 子命令 |
80
- | `sync` | `backup(args, settings, log) -> int` | `backup` 子命令 |
80
+ | `sync` | `mirror(args, settings, log) -> int` | `mirror` 子命令 |
81
81
  | `sync` | `publish(args, settings, log) -> int` | `publish` 子命令 |
82
82
  | `doctor` | `doctor(args, settings, log) -> int` | `doctor` 子命令 |
83
83
  | `rclone` | `run_rclone(cmd: list[str]) -> int` | 调用 rclone 并返回其退出码 |
@@ -95,13 +95,13 @@ pix.rclone ──> subprocess
95
95
  ## 3. CLI 契约
96
96
 
97
97
  ```bash
98
- pix [--config PATH] [--version] {shrink,backup,publish,doctor,config}
98
+ pix [--config PATH] [--version] {shrink,mirror,publish,doctor,config}
99
99
 
100
100
  pix shrink -s SRC -d OUT
101
101
  [-w 1920] [-q 85] [-e 5] [-m 0]
102
102
  [--force] [--prune] [--allow-empty] [--dry-run] [-v|--quiet]
103
103
 
104
- pix backup -s SRC -d REMOTE
104
+ pix mirror -s SRC -d REMOTE
105
105
  [--exclude GLOB ...] [--max-delete PERCENT] [--allow-empty] [--resync]
106
106
  [--dry-run] [-v|--quiet]
107
107
 
@@ -120,15 +120,15 @@ pix config init [--force]
120
120
  - `--config` 属于主 parser,写在子命令之前。
121
121
  - `--version` 输出 `pix <version>` 后以 0 退出。
122
122
  - `-s` 为源,`-d` 为目标。
123
- - `shrink` 的 `-d` 是本地目录;`backup` / `publish` 的 `-d` 是 rclone 路径,可以是 remote,也可以是本地目录。
123
+ - `shrink` 的 `-d` 是本地目录;`mirror` / `publish` 的 `-d` 是 rclone 路径,可以是 remote,也可以是本地目录。
124
124
  - `-q` 只表示 quality;安静模式使用 `--quiet`,不提供短选项。
125
125
  - `-m/--min-size` 表示“小于该大小的源文件跳过”,`0` 表示不限制;支持 `500K` / `1M` / `2G`,按 1024 进制。
126
126
  - `-v` 与 `--quiet` 互斥。
127
- - `--dry-run` 不产生本地写入;`backup` / `publish` 中原样传给 rclone。与 `shrink --prune` 同用时只列出待删除项。
127
+ - `--dry-run` 不产生本地写入;`mirror` / `publish` 中原样传给 rclone。与 `shrink --prune` 同用时只列出待删除项。
128
128
  - `--exclude` 可重复。配置中的排除项在前,命令行排除项追加在后,保留顺序并去重。
129
- - `--max-delete` 只用于 `backup` / `publish`,按删除百分比限制(rclone bisync 语义,不是数量),默认 `10`(即 10%);`0` 禁止删除,`-1` 不限制,最大 `100`。超过比例时 rclone 拒绝运行。
130
- - `--allow-empty` 只用于 `shrink --prune`、`backup` 和 `publish`:允许源为空时继续执行破坏性操作。默认拒绝;`backup` / `publish` 还必须同时带 `--resync`。
131
- - `--resync` 只用于 `backup` / `publish`,原样传给 rclone。
129
+ - `--max-delete` 只用于 `mirror` / `publish`,按删除百分比限制(rclone bisync 语义,不是数量),默认 `10`(即 10%);`0` 禁止删除,`-1` 不限制,最大 `100`。超过比例时 rclone 拒绝运行。
130
+ - `--allow-empty` 只用于 `shrink --prune`、`mirror` 和 `publish`:允许源为空时继续执行破坏性操作。默认拒绝;`mirror` / `publish` 还必须同时带 `--resync`。
131
+ - `--resync` 只用于 `mirror` / `publish`,原样传给 rclone。
132
132
  - `--force` 只用于 `shrink`。
133
133
  - `--prune` 只用于 `shrink`,默认关闭。
134
134
  - `--base-url` / `--urls` / `--no-urls` 只用于 `publish`;`--no-urls` 可临时关闭配置中的 `[publish].urls = true`。
@@ -160,7 +160,7 @@ effort = 5
160
160
  min_size = 0
161
161
  include = ["jpg", "jpeg", "png", "webp"]
162
162
 
163
- [backup]
163
+ [mirror]
164
164
  exclude = []
165
165
  max_delete = 10
166
166
 
@@ -266,9 +266,9 @@ binary = "rclone"
266
266
  - 删除失败记录错误,最后返回 1。
267
267
  - 默认关闭,避免误删。
268
268
 
269
- ## 6. backup 行为
269
+ ## 6. mirror 行为
270
270
 
271
- `backup` 把原始图片目录与远端做 **rclone bisync 双向同步**。它是双向镜像,不是单向备份。
271
+ `mirror` 把原始图片目录与远端做 **rclone bisync 双向同步**。它是双向镜像,不是单向备份。
272
272
 
273
273
  命令拼装:
274
274
 
@@ -287,7 +287,7 @@ cmd.append("--progress" if verbose else "--quiet")
287
287
  - 首次运行必须显式加 `--resync`。`pix` 不自动添加,避免两边都有内容时悄悄覆盖一侧。
288
288
  - 不加 `--resync` 且 bisync 基线不存在时,rclone 会报错,`pix` 原样返回其退出码。
289
289
  - `-v` 映射 `--progress`,默认映射 `--quiet`。
290
- - `--exclude` 按配置在前、命令行在后合并,逐条传给 rclone;`pix` 不内置任何备份排除规则。
290
+ - `--exclude` 按配置在前、命令行在后合并,逐条传给 rclone;`pix` 不内置任何镜像排除规则。
291
291
  - **修改排除项后必须先 `--resync`**:rclone 要求过滤器变更后重建基线,否则被排除的文件可能被当作删除传播。先跑 `--dry-run`。
292
292
  - `--max-delete` 默认 `10`,是删除百分比;一次运行要删除的比例超过 10% 时 rclone 会拒绝。确认要大量删除时显式调高,或传 `-1` 取消限制。
293
293
  - 双向同步意味着删除会传播:本地删除会同步到远端,远端删除也会同步到本地。首次操作建议先 `--dry-run`。
@@ -298,11 +298,11 @@ cmd.append("--progress" if verbose else "--quiet")
298
298
 
299
299
  ## 7. publish 行为
300
300
 
301
- `publish` 把 shrink 输出目录与图床远端做 **rclone bisync 双向同步**,参数与 `backup` 相同,另加:
301
+ `publish` 把 shrink 输出目录与图床远端做 **rclone bisync 双向同步**,参数与 `mirror` 相同,另加:
302
302
 
303
303
  - `-s` 必须是已存在的本地目录,且至少包含一个 `.avif`。源为空时默认退出 2;只有显式 `--allow-empty` 且带 `--resync` 时才允许。
304
304
  - 固定追加 `--exclude .pix/**`,防止状态文件被同步到图床。配置排除项随后,命令行排除项最后,去重。
305
- - `--max-delete` 与 `backup` 相同,默认 `10`,按删除百分比计。
305
+ - `--max-delete` 与 `mirror` 相同,默认 `10`,按删除百分比计。
306
306
  - `--base-url` 优先取命令行,其次取 `[publish].base_url`。值必须以 `http://` 或 `https://` 开头,否则退出 2。
307
307
  - `--urls` / `--no-urls` 优先取命令行,其次取 `[publish].urls`,默认关闭;`--no-urls` 可临时关闭配置中的 `true`。
308
308
  - 开启 URL 清单但没有可用的 `base_url` 时,在调用 rclone 前以退出码 2 拒绝。
@@ -318,7 +318,7 @@ cmd.append("--progress" if verbose else "--quiet")
318
318
  - 生成清单时递归收集 OUT 下所有 `.avif`(大小写不敏感),路径段用 `urllib.parse.quote(..., safe="/")` 编码。
319
319
  - `base_url` 末尾的 `/` 在拼接时去掉。
320
320
  - `--dry-run` 不生成 URL 清单。
321
- - 修改排除项后必须先 `--resync`,规则同 `backup`。
321
+ - 修改排除项后必须先 `--resync`,规则同 `mirror`。
322
322
  - 双向同步同样会传播删除,首次操作建议先 `--dry-run`。
323
323
 
324
324
  ## 8. doctor
@@ -337,7 +337,7 @@ config /home/uzvg/.config/pix/config.toml (ok)
337
337
 
338
338
  - 缺少 pyvips/libvips:打印安装提示(文案见 [development.md](development.md#7-缺少-libvips-时)),退出 1。
339
339
  - 缺少 rclone:提示官方下载地址或 `winget search rclone`,退出 1。
340
- - rclone 版本低于 1.66:提示升级,退出 1。`backup` / `publish` 启动时也执行同样的版本检查,不依赖用户先跑 `doctor`。
340
+ - rclone 版本低于 1.66:提示升级,退出 1。`mirror` / `publish` 启动时也执行同样的版本检查,不依赖用户先跑 `doctor`。
341
341
  - 配置文件解析失败:指出文件和错误位置,退出 2。
342
342
  - 配置文件不存在:显示 `missing (using defaults)`,属于正常状态,退出 0。
343
343
  - 多个问题同时存在时,配置错误(2)优先于依赖缺失(1)。
@@ -349,7 +349,7 @@ config /home/uzvg/.config/pix/config.toml (ok)
349
349
  - `--quiet`:只输出错误。
350
350
  - `doctor` 报告输出到 stdout;`--dry-run` 的计划文件列表和 `--urls` 的 JSON 输出到 stdout。
351
351
  - 退出码:`0` 成功,`1` 运行失败,`2` 参数/配置错误。
352
- - 例外:`backup` / `publish` 直接返回 rclone 的退出码,不归一化;调用方按非零即失败处理。
352
+ - 例外:`mirror` / `publish` 直接返回 rclone 的退出码,不归一化;调用方按非零即失败处理。
353
353
  - 不引入 colorama、rich、click;仅标准库 + pyvips。
354
354
 
355
355
  ## 10. 跨平台约定
@@ -38,7 +38,7 @@
38
38
 
39
39
  **决策**:`import pyvips` 只出现在 `shrink` / `doctor` 内部。
40
40
 
41
- **原因**:`backup` / `publish` 不需要图像后端;缺少 libvips 时同步仍应可用。导入失败只在真正需要图像能力时提示。
41
+ **原因**:`mirror` / `publish` 不需要图像后端;缺少 libvips 时同步仍应可用。导入失败只在真正需要图像能力时提示。
42
42
 
43
43
  ## D7. 配置优先级:命令行 > 配置文件 > 内置默认
44
44
 
@@ -66,15 +66,15 @@
66
66
 
67
67
  ## D11. 使用 rclone bisync 双向同步
68
68
 
69
- **决策**:`backup` / `publish` 都调用 `rclone bisync`,不使用单向 `copy` 或 `sync`。
69
+ **决策**:`mirror` / `publish` 都调用 `rclone bisync`,不使用单向 `copy` 或 `sync`。
70
70
 
71
71
  **原因**:两边都可能新增、修改或删除文件,单向同步会让另一侧的改动被覆盖或永远无法回流。
72
72
 
73
- **影响**:`backup` / `publish` 是双向镜像,不是单向备份;删除会双向传播,误删不可逆。用 `--dry-run` 预览、`--resync` 显式重建基线,并默认 `--max-delete 10`(rclone bisync 的删除百分比,`0` 禁止删除,`-1` 不限制)来控制风险;`pix` 不自动 resync。修改排除项后也必须先 `--resync`。
73
+ **影响**:`mirror` / `publish` 是双向镜像,不是单向备份;删除会双向传播,误删不可逆。用 `--dry-run` 预览、`--resync` 显式重建基线,并默认 `--max-delete 10`(rclone bisync 的删除百分比,`0` 禁止删除,`-1` 不限制)来控制风险;`pix` 不自动 resync。修改排除项后也必须先 `--resync`。
74
74
 
75
75
  ## D12. 不内置 rclone 排除规则
76
76
 
77
- **决策**:`pix` 不硬编码备份排除项;排除全部由 `--exclude` 传入。配置排除项在前,命令行排除项追加在后并去重。唯一例外是 `publish` 固定排除 `.pix/**`。
77
+ **决策**:`pix` 不硬编码镜像排除项;排除全部由 `--exclude` 传入。配置排除项在前,命令行排除项追加在后并去重。唯一例外是 `publish` 固定排除 `.pix/**`。
78
78
 
79
79
  **原因**:排除项属于调用方的同步策略。`.pix/**` 是 `pix` 自己的状态文件,不属于用户内容,必须排除,否则会被传到图床。合并而不是覆盖,避免加一条规则时丢掉另一条。
80
80
 
@@ -86,7 +86,7 @@
86
86
 
87
87
  ## D14. `subprocess.run(list)`,退出码透传
88
88
 
89
- **决策**:不经过 shell,`backup` / `publish` 直接返回 rclone 的退出码。
89
+ **决策**:不经过 shell,`mirror` / `publish` 直接返回 rclone 的退出码。
90
90
 
91
91
  **原因**:避免 shell 引号和 Windows 路径转义问题;透传退出码便于脚本判断 bisync 的具体失败原因。这是全局 `0/1/2` 约定的明确例外。
92
92
 
@@ -128,11 +128,11 @@
128
128
 
129
129
  **影响**:首次运行没有状态,会全量编码一次;状态丢失只会导致重编码,不会产生错误结果。
130
130
 
131
- ## D21. backup 与 publish 分离
131
+ ## D21. mirror 与 publish 分离
132
132
 
133
133
  **决策**:两个子命令,而不是一个带模式开关的 `sync`。
134
134
 
135
- **原因**:备份原图和发布压缩图的目标、源、默认排除不同。分开后每个命令只有一个明确意图,也不需要 `--delete` 这类容易误用的开关。
135
+ **原因**:镜像原图和发布压缩图的目标、源、默认排除不同。分开后每个命令只有一个明确意图,也不需要 `--delete` 这类容易误用的开关。
136
136
 
137
137
  ## D22. URL 清单默认关闭
138
138
 
@@ -148,7 +148,7 @@
148
148
 
149
149
  ## D24. 破坏性操作先拒绝空输入与目标冲突
150
150
 
151
- **决策**:`shrink --prune` 扫描到 0 个源图片但状态仍有条目时拒绝删除;`backup` / `publish` 源为空时要求显式 `--allow-empty` 且同时带 `--resync`;两个源文件映射到同一输出时退出 2。
151
+ **决策**:`shrink --prune` 扫描到 0 个源图片但状态仍有条目时拒绝删除;`mirror` / `publish` 源为空时要求显式 `--allow-empty` 且同时带 `--resync`;两个源文件映射到同一输出时退出 2。
152
152
 
153
153
  **原因**:空目录最常见的来源是挂载失败、路径写错或上游失败,而不是用户真的想删除全部。目标冲突(如 `a.jpg` 与 `a.jpeg`)会让后一个编码静默覆盖前一个。这些情况都应在写入或删除前拒绝。
154
154
 
@@ -160,7 +160,7 @@
160
160
 
161
161
  ## D26. rclone 版本在命令入口检查
162
162
 
163
- **决策**:`backup` / `publish` 启动时执行 `rclone version` 并要求 `>= 1.66`;`doctor` 保留同样的检查作为诊断。
163
+ **决策**:`mirror` / `publish` 启动时执行 `rclone version` 并要求 `>= 1.66`;`doctor` 保留同样的检查作为诊断。
164
164
 
165
165
  **原因**:旧版 rclone 的 bisync 参数或行为不同,只靠 `doctor` 会让直接运行同步命令的用户遇到晦涩错误。版本检查放在真正调用 rclone 之前,失败信息才可操作。
166
166
 
@@ -175,3 +175,11 @@
175
175
  **决策**:随包分发的 `src/pix/default_config.toml` 是默认值的唯一来源:`load_settings` 通过 `load_defaults()` 解析它,`pix config init` 直接复制它(保留注释)。目标文件已存在时拒绝覆盖并退出 2,`--force` 才覆盖;该命令在 `load_settings` 之前分派。
176
176
 
177
177
  **原因**:Python 3.11 标准库只有 `tomllib`,没有 TOML 写入器;引入 `tomli-w` 违反“仅标准库 + pyvips”的约束。独立文件可以直接带注释,用户打开就能改,编辑器也能按 TOML 高亮;默认值只在这一处维护,避免代码常量与模板漂移。拒绝覆盖保护用户已经修改过的配置,误覆盖的代价远高于多敲一个 `--force`。提前分派让 `--config` 指向新路径时也能创建工作,而不是被“config file not found”挡住。
178
+
179
+ ## D29. `backup` 改名为 `mirror`
180
+
181
+ **决策**:原 `backup` 子命令改名为 `mirror`,配置节 `[backup]` 同步改为 `[mirror]`,`publish` 保持不变;不保留旧名别名。
182
+
183
+ **原因**:`backup` 暗示单向、只增、可回滚,但实现是 `rclone bisync` 双向镜像,删除会双向传播且不可逆,命令名本身就在误导用户。`mirror` 准确描述机制。`publish` 描述的是目的,不隐含“远端只增”,取消发布时删除传播符合直觉,因此保留。处于测试阶段且未发布,改名成本最低。
184
+
185
+ **影响**:破坏性改名,不提供旧命令名或旧配置节兼容。
@@ -7,7 +7,7 @@
7
7
  - [uv](https://docs.astral.sh/uv/)。
8
8
  - Python `>=3.11`。仓库的 `.python-version` 和本地虚拟环境是 3.14,但代码必须兼容 3.11,不能使用 3.12+ 语法。
9
9
  - 图像后端 `pyvips[binary]` 由依赖自动安装,不需要单独安装 libvips。
10
- - `backup` / `publish` 需要外部 `rclone >= 1.66`;单元测试不需要安装真实 rclone。
10
+ - `mirror` / `publish` 需要外部 `rclone >= 1.66`;单元测试不需要安装真实 rclone。
11
11
 
12
12
  ## 2. 常用命令
13
13
 
@@ -55,16 +55,16 @@ editable 安装后修改源码立即生效。升级依赖后可用 `uv tool inst
55
55
  | 原子写入 | 编码失败时临时文件被清理 |
56
56
  | 并发锁 | 第二个 `shrink` 遇到 `OUT/.pix/lock` 时返回 1 |
57
57
  | `min_size` 类型 | 配置写整数 `0` 或字符串 `"500K"` 均可解析 |
58
- | `backup` 命令拼装 | `bisync`、`--exclude`、`--max-delete`、`--resync`、`--dry-run`、`-v` / `--quiet` |
59
- | `backup` / `publish` 空源 | 默认退出 2;`--allow-empty` 加 `--resync` 才放行 |
58
+ | `mirror` 命令拼装 | `bisync`、`--exclude`、`--max-delete`、`--resync`、`--dry-run`、`-v` / `--quiet` |
59
+ | `mirror` / `publish` 空源 | 默认退出 2;`--allow-empty` 加 `--resync` 才放行 |
60
60
  | `publish` 命令拼装 | 固定带 `--exclude .pix/**`,配置排除项随后,命令行追加 |
61
61
  | `publish` 无 AVIF | 源目录没有 `.avif` 时退出 2,不调用 rclone |
62
62
  | `publish --urls` | 输出排序后的 JSON;路径段正确编码 |
63
63
  | `--urls` / `--no-urls` | 配置 `urls = true` 时 `--no-urls` 能单次关闭 |
64
64
  | `--urls` 缺少 base_url | 退出 2,且不调用 rclone |
65
65
  | `base_url` 协议 | 非 `http(s)://` 时退出 2 |
66
- | 退出码透传 | mock 返回非零时 `backup` / `publish` 原样返回 |
67
- | rclone 缺失/过旧 | `backup` / `publish` 启动时检查版本,失败返回 1 |
66
+ | 退出码透传 | mock 返回非零时 `mirror` / `publish` 原样返回 |
67
+ | rclone 缺失/过旧 | `mirror` / `publish` 启动时检查版本,失败返回 1 |
68
68
  | 真实 shrink | 生成小 PNG,运行 `pix shrink`,断言输出存在且宽度正确 |
69
69
 
70
70
  mtime 测试用 `os.utime` 控制时间,避免依赖真实文件系统精度。状态测试直接读写 `.pix/state.json`,不要依赖真实 rclone。
@@ -111,7 +111,7 @@ vips thumbnail src 'out.avif[Q=85,strip,effort=5]' 1920 --size down --linear
111
111
  - `thumbnail(..., size="down", linear=True)` 的默认旋转行为与 CLI 一致。
112
112
  - libvips 8.18.7 没有 `avifsave` action;AVIF 通过 `heifsave`(`.avif` 后缀自动选择)保存。
113
113
  - `effort` 是有效参数。
114
- - `import pyvips` 延迟到 `shrink` / `doctor` 内部,保证 `backup` / `publish` 在图像后端缺失时仍可用。
114
+ - `import pyvips` 延迟到 `shrink` / `doctor` 内部,保证 `mirror` / `publish` 在图像后端缺失时仍可用。
115
115
 
116
116
  复验方法:对同一张真实图片分别用上述两种方式生成 AVIF,比较文件大小和 MD5/内容哈希。
117
117
 
@@ -136,11 +136,11 @@ pyvips/libvips 不可用。
136
136
 
137
137
  ## 8. rclone 与 bisync
138
138
 
139
- - 需要 `rclone >= 1.66`。`backup` / `publish` 启动时解析 `rclone version` 并检查;`doctor` 保留同样的检查。
140
- - 首次运行 `pix backup` / `pix publish` 必须加 `--resync`,否则 rclone 会因缺少基线报错。
139
+ - 需要 `rclone >= 1.66`。`mirror` / `publish` 启动时解析 `rclone version` 并检查;`doctor` 保留同样的检查。
140
+ - 首次运行 `pix mirror` / `pix publish` 必须加 `--resync`,否则 rclone 会因缺少基线报错。
141
141
  - 修改 `--exclude` 或配置排除项后,必须重新带 `--resync` 运行,否则被排除的文件可能被当作删除传播。
142
142
  - `--max-delete` 默认 `10`,是 rclone bisync 的删除百分比(不是数量);`0` 禁止删除,`-1` 不限制。超过比例时 rclone 拒绝执行,这是默认保护,不要静默改成 `-1`。
143
- - `backup` / `publish` 是双向镜像,不是单向备份,删除会双向传播。手动验证时先用 `--dry-run`,再在临时目录里测试。
143
+ - `mirror` / `publish` 是双向镜像,不是单向备份,删除会双向传播。手动验证时先用 `--dry-run`,再在临时目录里测试。
144
144
  - 源目录为空时默认拒绝;确实要清空远端时才用 `--allow-empty`(`publish` 还需 `--resync`)。
145
145
  - bisync 基线在 `~/.cache/rclone/bisync/`,由 rclone 管理;排查问题时可以用 `rclone bisync --help` 查看当前版本的参数。
146
146
  - 单元测试只断言 argv,不访问网络,也不需要真实 remote。
@@ -6,7 +6,7 @@
6
6
 
7
7
  截至 2026-11-04:
8
8
 
9
- - 四个子命令 `shrink` / `backup` / `publish` / `doctor` 已实现,`uv run pytest` 通过。
9
+ - 四个子命令 `shrink` / `mirror` / `publish` / `doctor` 已实现,`uv run pytest` 通过。
10
10
  - `pix config init` 可生成带注释的默认配置;`--force` 覆盖已有文件。
11
11
  - 真实 rclone 的本地目录端到端(首次 `--resync`、二次增量、双向删除)已验证;`pix shrink` 与 `vips thumbnail` 的 AVIF 字节一致性已验证。真实 R2 与 Windows 原生库仍待手工验证。
12
12
  - 文档已按新定位重写,并完成 2026-11-04 审计修复。
@@ -15,7 +15,7 @@
15
15
  ## M0. 文档重写(完成)
16
16
 
17
17
  - [x] 移除旧项目的全部残留内容。
18
- - [x] 定义 `shrink` / `backup` / `publish` / `doctor` 四个子命令。
18
+ - [x] 定义 `shrink` / `mirror` / `publish` / `doctor` 四个子命令。
19
19
  - [x] 定义 `.pix/state.json` 增量状态。
20
20
  - [x] 定义 bisync 双向同步与 `--resync` 规则。
21
21
  - [x] 更新 README、AGENTS.md、pyproject 描述。
@@ -25,7 +25,7 @@
25
25
  按 2026-11-04 审计逐项修正规格与脚手架:
26
26
 
27
27
  - [x] `--prune` 空源保护与 `--allow-empty`。
28
- - [x] `backup` 明确为双向镜像,新增 `--max-delete`(默认 10%,rclone bisync 的删除百分比)。
28
+ - [x] `mirror` 明确为双向镜像,新增 `--max-delete`(默认 10%,rclone bisync 的删除百分比)。
29
29
  - [x] 目标路径冲突检测(`a.jpg` / `a.jpeg` 等)。
30
30
  - [x] 状态每 200 个文件刷新,`finally` 落盘;`out_mtime_ns` 退出跳过条件。
31
31
  - [x] `publish` 源校验:必须是含 `.avif` 的目录。
@@ -36,13 +36,13 @@
36
36
  - [x] `min_size` 接受整数或尺寸字符串。
37
37
  - [x] 修改排除项后必须 `--resync`。
38
38
  - [x] 删除模板死代码;拆分规则改为按职责拆分。
39
- - [x] rclone 版本在 `backup` / `publish` 入口检查。
39
+ - [x] rclone 版本在 `mirror` / `publish` 入口检查。
40
40
  - [x] `shrink` 单实例锁 `OUT/.pix/lock`。
41
41
  - [x] `base_url` 必须带 `http(s)://`。
42
42
 
43
43
  ## M1. CLI 骨架
44
44
 
45
- - [x] argparse 主 parser + `shrink` / `backup` / `publish` / `doctor` 子命令。
45
+ - [x] argparse 主 parser + `shrink` / `mirror` / `publish` / `doctor` 子命令。
46
46
  - [x] 全局 `--config`、`--version`。
47
47
  - [x] 配置加载与三层合并,argparse 默认值使用哨兵。
48
48
  - [x] 日志级别:默认、`-v`、`--quiet`。
@@ -74,7 +74,7 @@
74
74
  - 改参数后全量重编码;改单个源文件只重编码该文件。
75
75
  - 中断或编码失败不会留下半截 AVIF。
76
76
 
77
- ## M3. backup
77
+ ## M3. mirror
78
78
 
79
79
  - [x] rclone 路径解析与本地源目录校验;空源默认拒绝,`--allow-empty` 加 `--resync` 才放行。
80
80
  - [x] bisync 命令拼装:`--exclude`(合并去重)、`--max-delete`(默认 10%,百分比)、`--resync`、`--dry-run`、`-v` / `--quiet`。
@@ -93,7 +93,7 @@
93
93
 
94
94
  - [x] bisync 命令固定追加 `--exclude .pix/**`,配置排除项随后,命令行追加。
95
95
  - [x] `publish` 源必须是含至少一个 `.avif` 的目录;空源需 `--allow-empty` 加 `--resync`。
96
- - [x] `--max-delete` 默认 10%,与 `backup` 一致。
96
+ - [x] `--max-delete` 默认 10%,与 `mirror` 一致。
97
97
  - [x] `--base-url` / `[publish].base_url` 合并,协议必须是 `http(s)://`。
98
98
  - [x] `--urls` / `--no-urls` 合并与 JSON 输出,路径段编码正确。
99
99
  - [x] 缺少 base_url 时退出 2,且在调用 rclone 前拒绝。
@@ -122,7 +122,7 @@
122
122
 
123
123
  ## Backlog(第一版不做)
124
124
 
125
- - 一键流水线 `pix run`(shrink → backup → publish)。
125
+ - 一键流水线 `pix run`(shrink → mirror → publish)。
126
126
  - 自动 `--resync`。
127
127
  - 远端可达性探测。
128
128
  - 图片裁剪(crop)。
@@ -135,7 +135,7 @@
135
135
  | 风险 | 处理 |
136
136
  |---|---|
137
137
  | bisync 双向删除不可逆 | 默认 `--max-delete 10`(10% 删除比例);显式 `--resync`;先 `--dry-run` |
138
- | 空目录/挂载失败被当成“删除全部” | `--prune` 空源拒绝;`backup` / `publish` 空源需 `--allow-empty` |
138
+ | 空目录/挂载失败被当成“删除全部” | `--prune` 空源拒绝;`mirror` / `publish` 空源需 `--allow-empty` |
139
139
  | `a.jpg` 与 `a.jpeg` 争用同一输出 | 编码前冲突检测,退出 2 |
140
140
  | 修改排除项后未 resync 导致误删 | 文档与错误提示要求先 `--resync` |
141
141
  | `pyvips[binary]` 与系统 libvips 版本不一致导致 AVIF 差异 | 已在 8.18.7 验证字节一致;升级 libvips 后重跑抽样对比 |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pix-cli"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Batch AVIF shrink and image-host sync CLI"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -26,7 +26,7 @@ classifiers = [
26
26
  "Programming Language :: Python :: 3.13",
27
27
  "Programming Language :: Python :: 3.14",
28
28
  "Topic :: Multimedia :: Graphics :: Graphics Conversion",
29
- "Topic :: System :: Archiving :: Backup",
29
+ "Topic :: System :: Archiving :: Mirroring",
30
30
  ]
31
31
  requires-python = ">=3.11"
32
32
  dependencies = [
@@ -6,4 +6,4 @@ from importlib.metadata import version as _package_version
6
6
  try:
7
7
  __version__ = _package_version("pix-cli")
8
8
  except PackageNotFoundError: # pragma: no cover - only when running uninstalled
9
- __version__ = "0.1.0"
9
+ __version__ = "0.1.3"
@@ -11,7 +11,7 @@ from .config import ConfigError, init_config, load_settings
11
11
  from .doctor import doctor
12
12
  from .logger import Logger
13
13
  from .shrink import shrink
14
- from .sync import backup, publish
14
+ from .sync import mirror, publish
15
15
 
16
16
 
17
17
  def _add_logging_args(parser: argparse.ArgumentParser) -> None:
@@ -39,21 +39,21 @@ def build_parser() -> argparse.ArgumentParser:
39
39
  shrink.add_argument("--dry-run", action="store_true")
40
40
  _add_logging_args(shrink)
41
41
 
42
- backup = subparsers.add_parser("backup", help="bidirectionally mirror originals")
43
- backup.add_argument("-s", "--source", type=Path, required=True)
44
- backup.add_argument("-d", "--dest", required=True)
45
- backup.add_argument("--exclude", action="append", default=None, metavar="GLOB")
46
- backup.add_argument(
42
+ mirror = subparsers.add_parser("mirror", help="bidirectionally mirror originals")
43
+ mirror.add_argument("-s", "--source", type=Path, required=True)
44
+ mirror.add_argument("-d", "--dest", required=True)
45
+ mirror.add_argument("--exclude", action="append", default=None, metavar="GLOB")
46
+ mirror.add_argument(
47
47
  "--max-delete",
48
48
  type=int,
49
49
  default=None,
50
50
  metavar="PERCENT",
51
51
  help="abort if more than PERCENT%% of files are deleted (default: 10)",
52
52
  )
53
- backup.add_argument("--allow-empty", action="store_true")
54
- backup.add_argument("--resync", action="store_true")
55
- backup.add_argument("--dry-run", action="store_true")
56
- _add_logging_args(backup)
53
+ mirror.add_argument("--allow-empty", action="store_true")
54
+ mirror.add_argument("--resync", action="store_true")
55
+ mirror.add_argument("--dry-run", action="store_true")
56
+ _add_logging_args(mirror)
57
57
 
58
58
  publish = subparsers.add_parser("publish", help="bidirectionally mirror compressed images")
59
59
  publish.add_argument("-s", "--source", type=Path, required=True)
@@ -105,7 +105,7 @@ def main(argv: list[str] | None = None) -> int:
105
105
  )
106
106
  handlers = {
107
107
  "shrink": shrink,
108
- "backup": backup,
108
+ "mirror": mirror,
109
109
  "publish": publish,
110
110
  "doctor": doctor,
111
111
  }
@@ -108,7 +108,7 @@ def _require_excludes(name: str, value: Any) -> list[str]:
108
108
 
109
109
 
110
110
  def validate_settings(settings: dict[str, Any]) -> None:
111
- for section in ("shrink", "backup", "publish", "rclone"):
111
+ for section in ("shrink", "mirror", "publish", "rclone"):
112
112
  if not isinstance(settings.get(section), dict):
113
113
  raise ConfigError(f"[{section}] must be a table")
114
114
 
@@ -127,7 +127,7 @@ def validate_settings(settings: dict[str, Any]) -> None:
127
127
  ):
128
128
  raise ConfigError("shrink.include must be a list of extensions without dots")
129
129
 
130
- for section in ("backup", "publish"):
130
+ for section in ("mirror", "publish"):
131
131
  _require_excludes(f"{section}.exclude", settings[section]["exclude"])
132
132
  require_int(f"{section}.max_delete", settings[section]["max_delete"], -1, 100)
133
133
 
@@ -13,7 +13,7 @@ min_size = 0
13
13
  # Extensions to scan, written without dots.
14
14
  include = ["jpg", "jpeg", "png", "webp"]
15
15
 
16
- [backup]
16
+ [mirror]
17
17
  # rclone exclude globs; command-line --exclude is appended after these.
18
18
  exclude = []
19
19
  # Maximum percent of files rclone may delete (-1 disables, 0 forbids).
@@ -1,4 +1,4 @@
1
- """backup and publish subcommands."""
1
+ """mirror and publish subcommands."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -13,8 +13,8 @@ from .logger import Logger
13
13
  from .rclone import check_rclone, has_files, merge_excludes, run_rclone
14
14
 
15
15
 
16
- def backup(args: argparse.Namespace, settings: dict[str, Any], log: Logger) -> int:
17
- config = settings["backup"]
16
+ def mirror(args: argparse.Namespace, settings: dict[str, Any], log: Logger) -> int:
17
+ config = settings["mirror"]
18
18
  source = args.source
19
19
  if not source.is_dir():
20
20
  log.error(f"source is not a directory: {source}")
@@ -407,11 +407,11 @@ def make_source(tmp_path: Path) -> Path:
407
407
  return source
408
408
 
409
409
 
410
- def test_backup_builds_bisync_command(tmp_path: Path, monkeypatch):
410
+ def test_mirror_builds_bisync_command(tmp_path: Path, monkeypatch):
411
411
  source = make_source(tmp_path)
412
412
  config = tmp_path / "config.toml"
413
413
  config.write_text(
414
- '[backup]\nexclude = ["cfg", "dup"]\nmax_delete = 9\n', encoding="utf-8"
414
+ '[mirror]\nexclude = ["cfg", "dup"]\nmax_delete = 9\n', encoding="utf-8"
415
415
  )
416
416
  calls = fake_rclone(monkeypatch)
417
417
 
@@ -419,7 +419,7 @@ def test_backup_builds_bisync_command(tmp_path: Path, monkeypatch):
419
419
  [
420
420
  "--config",
421
421
  str(config),
422
- "backup",
422
+ "mirror",
423
423
  "-s",
424
424
  str(source),
425
425
  "-d",
@@ -450,39 +450,39 @@ def test_backup_builds_bisync_command(tmp_path: Path, monkeypatch):
450
450
  ]
451
451
 
452
452
 
453
- def test_backup_defaults_to_quiet_and_max_delete_ten(tmp_path: Path, monkeypatch):
453
+ def test_mirror_defaults_to_quiet_and_max_delete_ten(tmp_path: Path, monkeypatch):
454
454
  source = make_source(tmp_path)
455
455
  calls = fake_rclone(monkeypatch)
456
456
 
457
- assert cli.main(["backup", "-s", str(source), "-d", "remote:photos"]) == 0
457
+ assert cli.main(["mirror", "-s", str(source), "-d", "remote:photos"]) == 0
458
458
 
459
459
  assert calls[1][-3:] == ["--max-delete", "10", "--quiet"]
460
460
 
461
461
 
462
- def test_backup_empty_source_refuses(tmp_path: Path, monkeypatch, capsys):
462
+ def test_mirror_empty_source_refuses(tmp_path: Path, monkeypatch, capsys):
463
463
  source = tmp_path / "src"
464
464
  source.mkdir()
465
465
  calls = fake_rclone(monkeypatch)
466
466
 
467
- assert cli.main(["backup", "-s", str(source), "-d", "remote:photos"]) == 2
467
+ assert cli.main(["mirror", "-s", str(source), "-d", "remote:photos"]) == 2
468
468
 
469
469
  assert calls == []
470
470
  assert "source is empty" in capsys.readouterr().err
471
471
 
472
472
 
473
- def test_backup_allow_empty_requires_resync(tmp_path: Path, monkeypatch):
473
+ def test_mirror_allow_empty_requires_resync(tmp_path: Path, monkeypatch):
474
474
  source = tmp_path / "src"
475
475
  source.mkdir()
476
476
  calls = fake_rclone(monkeypatch)
477
477
 
478
478
  assert cli.main(
479
- ["backup", "-s", str(source), "-d", "remote:photos", "--allow-empty"]
479
+ ["mirror", "-s", str(source), "-d", "remote:photos", "--allow-empty"]
480
480
  ) == 2
481
481
  assert calls == []
482
482
 
483
483
  assert cli.main(
484
484
  [
485
- "backup",
485
+ "mirror",
486
486
  "-s",
487
487
  str(source),
488
488
  "-d",
@@ -494,38 +494,38 @@ def test_backup_allow_empty_requires_resync(tmp_path: Path, monkeypatch):
494
494
  assert "--resync" in calls[1]
495
495
 
496
496
 
497
- def test_backup_dry_run_and_passthrough_exit_code(tmp_path: Path, monkeypatch):
497
+ def test_mirror_dry_run_and_passthrough_exit_code(tmp_path: Path, monkeypatch):
498
498
  source = make_source(tmp_path)
499
499
  calls = fake_rclone(monkeypatch, bisync_code=7)
500
500
 
501
501
  assert cli.main(
502
- ["backup", "-s", str(source), "-d", "remote:photos", "--dry-run", "--quiet"]
502
+ ["mirror", "-s", str(source), "-d", "remote:photos", "--dry-run", "--quiet"]
503
503
  ) == 7
504
504
 
505
505
  assert calls[1][-2:] == ["--dry-run", "--quiet"]
506
506
 
507
507
 
508
- def test_backup_rejects_old_or_missing_rclone(tmp_path: Path, monkeypatch, capsys):
508
+ def test_mirror_rejects_old_or_missing_rclone(tmp_path: Path, monkeypatch, capsys):
509
509
  source = make_source(tmp_path)
510
510
 
511
511
  fake_rclone(monkeypatch, version="rclone v1.65.2\n")
512
- assert cli.main(["backup", "-s", str(source), "-d", "remote:photos"]) == 1
512
+ assert cli.main(["mirror", "-s", str(source), "-d", "remote:photos"]) == 1
513
513
  assert "too old" in capsys.readouterr().err
514
514
 
515
515
  fake_rclone(monkeypatch, version=FileNotFoundError())
516
- assert cli.main(["backup", "-s", str(source), "-d", "remote:photos"]) == 1
516
+ assert cli.main(["mirror", "-s", str(source), "-d", "remote:photos"]) == 1
517
517
  assert "not found" in capsys.readouterr().err
518
518
 
519
519
 
520
- def test_backup_rejects_invalid_max_delete(tmp_path: Path, monkeypatch):
520
+ def test_mirror_rejects_invalid_max_delete(tmp_path: Path, monkeypatch):
521
521
  source = make_source(tmp_path)
522
522
  calls = fake_rclone(monkeypatch)
523
523
 
524
524
  assert cli.main(
525
- ["backup", "-s", str(source), "-d", "remote:photos", "--max-delete", "-2"]
525
+ ["mirror", "-s", str(source), "-d", "remote:photos", "--max-delete", "-2"]
526
526
  ) == 2
527
527
  assert cli.main(
528
- ["backup", "-s", str(source), "-d", "remote:photos", "--max-delete", "101"]
528
+ ["mirror", "-s", str(source), "-d", "remote:photos", "--max-delete", "101"]
529
529
  ) == 2
530
530
  assert calls == []
531
531
 
@@ -129,7 +129,7 @@ wheels = [
129
129
 
130
130
  [[package]]
131
131
  name = "pix-cli"
132
- version = "0.1.1"
132
+ version = "0.1.3"
133
133
  source = { editable = "." }
134
134
  dependencies = [
135
135
  { name = "pyvips", extra = ["binary"] },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes