pix-cli 0.1.2__tar.gz → 0.1.4__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 (27) hide show
  1. {pix_cli-0.1.2 → pix_cli-0.1.4}/AGENTS.md +6 -5
  2. {pix_cli-0.1.2 → pix_cli-0.1.4}/PKG-INFO +54 -38
  3. {pix_cli-0.1.2 → pix_cli-0.1.4}/README.md +52 -36
  4. {pix_cli-0.1.2 → pix_cli-0.1.4}/README.zh-CN.md +52 -36
  5. {pix_cli-0.1.2 → pix_cli-0.1.4}/docs/architecture.md +77 -31
  6. {pix_cli-0.1.2 → pix_cli-0.1.4}/docs/decisions.md +37 -13
  7. {pix_cli-0.1.2 → pix_cli-0.1.4}/docs/development.md +12 -9
  8. {pix_cli-0.1.2 → pix_cli-0.1.4}/docs/roadmap.md +25 -15
  9. {pix_cli-0.1.2 → pix_cli-0.1.4}/pyproject.toml +2 -2
  10. {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/__init__.py +1 -1
  11. {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/cli.py +11 -11
  12. {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/config.py +21 -2
  13. {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/default_config.toml +7 -1
  14. {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/logger.py +3 -0
  15. pix_cli-0.1.4/src/pix/rclone.py +162 -0
  16. {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/sync.py +112 -32
  17. {pix_cli-0.1.2 → pix_cli-0.1.4}/tests/test_cli.py +269 -38
  18. {pix_cli-0.1.2 → pix_cli-0.1.4}/uv.lock +1 -1
  19. pix_cli-0.1.2/src/pix/rclone.py +0 -72
  20. {pix_cli-0.1.2 → pix_cli-0.1.4}/.github/workflows/ci.yml +0 -0
  21. {pix_cli-0.1.2 → pix_cli-0.1.4}/.github/workflows/publish.yml +0 -0
  22. {pix_cli-0.1.2 → pix_cli-0.1.4}/.gitignore +0 -0
  23. {pix_cli-0.1.2 → pix_cli-0.1.4}/.python-version +0 -0
  24. {pix_cli-0.1.2 → pix_cli-0.1.4}/LICENSE +0 -0
  25. {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/doctor.py +0 -0
  26. {pix_cli-0.1.2 → pix_cli-0.1.4}/src/pix/py.typed +0 -0
  27. {pix_cli-0.1.2 → pix_cli-0.1.4}/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,10 +31,11 @@ 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` 才放行。
37
- - 修改排除项后必须提示用户先 `--resync`;`--exclude` 是配置在前、命令行追加并去重。
34
+ - 退出码 0/1/2,日志走 stderr;`mirror` / `publish` 透传 rclone 退出码。
35
+ - `mirror` / `publish` 使用 `rclone bisync`;调用前先 `rclone mkdir` 目标并用 `rclone lsjson --max-depth 1` 判空。目标为空时自动加 `--resync`,目标非空时必须显式 `--resync`。用户不需要手动运行 rclone。它们是双向镜像,不是单向备份。
36
+ - `--max-delete` 默认 10(删除百分比,rclone bisync 语义);`mirror` / `publish` 源为空时默认拒绝,`--allow-empty` 加 `--resync` 才放行。
37
+ - `mirror` 默认只同步 `[mirror].include` 中的图片扩展名,`include = []` 关闭白名单;`publish` 固定只同步 `.avif`。两者用 rclone filters 文件(bisync `--filters-file`、判空 `--filter-from`)并加 `--ignore-case`。
38
+ - 修改 include/exclude 后必须提示用户先 `--resync`;升级到带过滤的版本后首次运行也需要 `--resync`。`--exclude` 是配置在前、命令行追加并去重。
38
39
  - 增量状态固定在 `OUT/.pix/state.json`,每 200 个成功文件刷新并在 `finally` 落盘;`publish` 必须排除 `.pix/**`。
39
40
  - `shrink` 必须拒绝目标冲突,`--prune` 必须拒绝空源,非 dry-run 必须持有 `OUT/.pix/lock`。
40
41
  - `publish` 源必须至少有一个 `.avif`;`base_url` 必须以 `http://` 或 `https://` 开头。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pix-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
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,9 +41,9 @@ 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
- | `pix doctor` | Check Python, libvips, rclone and configuration |
46
+ | `pix doctor` | Report Python, libvips and rclone versions, plus the config path |
47
47
  | `pix config init` | Write a commented default configuration file |
48
48
 
49
49
  `pix` is deliberately narrow:
@@ -58,32 +58,41 @@ 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
- | `doctor` / `config init` | Dependency checks / config scaffold | pix |
69
+ | `doctor` / `config init` | Runtime report / 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` (and for `pix doctor` to report a clean environment)
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.
83
83
 
84
84
  ## Install
85
85
 
86
- Not yet published on PyPI; install from source:
86
+ Install from PyPI:
87
+
88
+ ```bash
89
+ uv tool install pix-cli
90
+ pix --version
91
+ ```
92
+
93
+ Upgrade with `uv tool upgrade pix-cli`; remove with `uv tool uninstall pix-cli`. Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pix.exe`.
94
+
95
+ To work on pix itself, install the checkout in editable mode:
87
96
 
88
97
  ```bash
89
98
  git clone https://github.com/uzvg/pix.git
@@ -92,11 +101,11 @@ uv tool install --editable .
92
101
  pix --version
93
102
  ```
94
103
 
95
- Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pix.exe`. An editable install picks up source changes immediately. Reinstall with `uv tool install --force --editable .`, or remove it with `uv tool uninstall pix-cli`.
104
+ An editable install picks up source changes immediately. Reinstall with `uv tool install --force --editable .`.
96
105
 
97
106
  ## Configure rclone
98
107
 
99
- Only needed for `backup` / `publish`:
108
+ Only needed for `mirror` / `publish`:
100
109
 
101
110
  ```bash
102
111
  rclone config # create a remote, for example onedrive or r2
@@ -118,7 +127,7 @@ If the file is missing, built-in defaults are used. Generate a commented file wi
118
127
  pix config init
119
128
  ```
120
129
 
121
- Existing files are not overwritten; pass `--force` to replace one. Use `--config PATH` before the subcommand to point at another file.
130
+ Existing files are not overwritten; pass `--force` to replace one. Use `--config PATH` before the subcommand to point at another file; an explicit path must exist, while a missing default path is fine.
122
131
 
123
132
  Common keys:
124
133
 
@@ -129,15 +138,18 @@ Common keys:
129
138
  | | `effort` | `5` | libvips effort, 0–9 |
130
139
  | | `min_size` | `0` | Skip sources smaller than this; accepts `500K`, `1M`, `2G` |
131
140
  | | `include` | `jpg`, `jpeg`, `png`, `webp` | Extensions to scan, without dots |
132
- | `[backup]` | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
141
+ | `[mirror]` | `include` | common image extensions | Extensions to mirror, without dots; `[]` disables the whitelist |
142
+ | | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
133
143
  | | `max_delete` | `10` | Delete limit as a percentage; `-1` disables, `0` forbids |
134
144
  | `[publish]` | `base_url` | `""` | Public URL prefix; must start with `http://` or `https://` |
135
145
  | | `urls` | `false` | Emit a JSON URL manifest |
136
- | | `exclude` | `[]` | Same as backup |
137
- | | `max_delete` | `10` | Same as backup |
146
+ | | `exclude` | `[]` | Same as mirror; `.pix/**` is always excluded first |
147
+ | | `max_delete` | `10` | Same as mirror |
138
148
  | `[rclone]` | `binary` | `rclone` | rclone executable name or path |
139
149
 
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).
150
+ Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality; `--quiet` is errors-only; `-v` / `--verbose` adds per-file detail and rclone progress. `--no-urls` overrides `[publish].urls = true` for one run.
151
+
152
+ The default `[mirror].include` is `jpg`, `jpeg`, `png`, `webp`, `gif`, `bmp`, `tif`, `tiff`, `heic`, `heif`, `avif`, `jxl`, `svg`, `ico`, `cr2`, `cr3`, `nef`, `arw`, `dng`, `orf`, `raf`, `rw2`. Set it to `[]` to mirror every file.
141
153
 
142
154
  ## Quick start
143
155
 
@@ -154,16 +166,16 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
154
166
  pix shrink -s ~/Pictures -d ~/Pictures-avif -w 1920 -q 85
155
167
  ```
156
168
 
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.
169
+ The first run encodes everything; later runs skip unchanged files. State lives in `OUT/.pix/state.json`. `-w` is a maximum long edge, so smaller images are not upscaled; relative directories are preserved under OUT. Change detection uses source size/mtime and output size, not content hashes. Changing `width`, `quality` or `effort` triggers a full re-encode. `--dry-run` only prints the plan. `--prune` removes only outputs recorded in state; extra files in OUT are left alone.
158
170
 
159
- 3. Back up originals:
171
+ 3. Mirror originals:
160
172
 
161
173
  ```bash
162
- pix backup -s ~/Pictures -d onedrive:photos --dry-run
163
- pix backup -s ~/Pictures -d onedrive:photos --resync
174
+ pix mirror -s ~/Pictures -d onedrive:photos --dry-run
175
+ pix mirror -s ~/Pictures -d onedrive:photos
164
176
  ```
165
177
 
166
- `--resync` is required on the first run. Always preview with `--dry-run` first.
178
+ The destination is created automatically. Only files matching `[mirror].include` are mirrored; other source files are ignored, and non-matching files already on the destination are left untouched. If no source file matches the filter, the source counts as empty and is refused unless you pass `--allow-empty --resync`. An empty destination initializes its own baseline, so no `--resync` is needed on the first run. Use `--resync` to rebuild the baseline when the destination already has matching content, or after changing `include`/`exclude`; `--dry-run` may create an empty destination but does not transfer or delete. After upgrading from a version without include filtering, the first run also needs `--resync`; preview with `--dry-run --resync` first (the preview does not write the baseline).
167
179
 
168
180
  4. Publish the AVIF output:
169
181
 
@@ -172,17 +184,18 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
172
184
  --base-url https://img.example.com --urls > urls.json
173
185
  ```
174
186
 
175
- `publish` requires at least one `.avif` in the source and always excludes `.pix/**`. The URL manifest is printed to stdout as JSON only after a successful, non-dry-run sync.
187
+ `publish` requires at least one `.avif` in the source unless you pass `--allow-empty --resync`; it syncs only `.avif` files and always excludes `.pix/**`. The URL manifest is printed to stdout as JSON only after a successful, non-dry-run sync. It lists every `.avif` under OUT, not just files changed in this run, and is not filtered by `[publish].exclude` or `--exclude`; excluded files can still appear in it. If you rely on excludes to hide files, do not publish the manifest until this behavior is fixed.
176
188
 
177
189
  5. Re-run later:
178
190
 
179
191
  ```bash
180
192
  pix shrink -s ~/Pictures -d ~/Pictures-avif
181
- pix backup -s ~/Pictures -d onedrive:photos
182
- pix publish -s ~/Pictures-avif -d r2:images --urls
193
+ pix mirror -s ~/Pictures -d onedrive:photos
194
+ pix publish -s ~/Pictures-avif -d r2:images \
195
+ --base-url https://img.example.com --urls
183
196
  ```
184
197
 
185
- Only changes are encoded and transferred.
198
+ Only changes are encoded and transferred. `--base-url` is not persisted between runs; set `[publish].base_url` in config to avoid repeating it.
186
199
 
187
200
  ## Command cheat sheet
188
201
 
@@ -190,9 +203,10 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
190
203
  |---|---|
191
204
  | Preview compression | `pix shrink -s SRC -d OUT --dry-run` |
192
205
  | Re-encode everything | `pix shrink -s SRC -d OUT --force` |
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` |
206
+ | Remove tracked outputs whose source is gone | `pix shrink -s SRC -d OUT --prune` |
207
+ | Preview a sync | `pix mirror -s SRC -d REMOTE --dry-run` |
208
+ | First sync (new or empty destination) | `pix mirror -s SRC -d REMOTE` |
209
+ | Rebuild the baseline (non-empty destination, or after changing include/exclude) | `pix mirror -s SRC -d REMOTE --resync` |
196
210
  | Emit a URL manifest | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
197
211
  | Diagnose the environment | `pix doctor` |
198
212
  | Generate a config file | `pix config init` |
@@ -202,19 +216,21 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
202
216
  | Situation | What happens | Protection |
203
217
  |---|---|---|
204
218
  | Deletions on either side | Propagated in both directions; accidental deletion is irreversible | Preview with `--dry-run`; default `--max-delete 10` (percentage) |
205
- | First sync | rclone has no baseline and errors | Add `--resync` explicitly |
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 |
219
+ | First sync (destination empty) | `pix` creates the destination and initializes the baseline | Preview with `--dry-run`; it may create an empty destination but does not transfer or delete. No manual rclone needed |
220
+ | Destination has matching content but no baseline | `pix` does not auto-resync; bisync fails instead of overwriting | Confirm which side is authoritative, then add `--resync` |
221
+ | Include/exclude filters changed | When filters are active, rclone detects the changed filters file and exits 7 | Run with `--resync` after changing `include` or `exclude`; `--dry-run --resync` previews but does not write the baseline |
222
+ | Non-image files in the source | `mirror` ignores them by default; `publish` ignores anything that is not `.avif`. If no file matches, the source counts as empty and is refused | Add extensions to `[mirror].include` if they should be mirrored; use `--allow-empty --resync` only for an intentional empty sync |
223
+ | Empty source | `mirror` / `publish` refuse. `shrink --prune` refuses only while state still has entries; empty state is a no-op | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
208
224
  | `a.jpg` and `a.jpeg` both exist | Both map to the same output | Detected before encoding; exits 2 |
209
225
  | Two concurrent shrinks | They would write the same state and outputs | `OUT/.pix/lock` blocks the second process |
210
- | State lost or corrupt | Warning, then a full rebuild | Only causes re-encoding; outputs stay valid |
226
+ | State missing or corrupt | Full rebuild; corrupt state logs a warning | Only causes re-encoding; outputs stay valid |
211
227
 
212
228
  ## Output and exit codes
213
229
 
214
230
  - Logs and progress go to stderr.
215
- - stdout carries `doctor` reports, `--dry-run` plans and `--urls` JSON.
231
+ - stdout carries `doctor` reports, `shrink --dry-run` plans, `config init`'s `wrote <path>`, and `--urls` JSON. `mirror` / `publish --dry-run` output comes from rclone.
216
232
  - Exit codes: `0` success, `1` runtime failure, `2` argument or configuration error.
217
- - `backup` / `publish` return rclone's exit code unchanged.
233
+ - `mirror` / `publish` return rclone's bisync exit code unchanged; destination preflight failures return 1.
218
234
 
219
235
  URL manifest example:
220
236
 
@@ -230,9 +246,9 @@ URL manifest example:
230
246
  |---|---|
231
247
  | `pyvips/libvips unavailable` | Reinstall to bring libvips: `uv tool install --force --editable /path/to/pix`; or install libvips manually (Linux: `apt install libvips42`, Windows: libvips release zip on `PATH`) |
232
248
  | rclone is older than 1.66 | Upgrade rclone |
233
- | bisync reports a missing baseline | Add `--resync` on the first run |
249
+ | bisync reports a missing baseline or changed filters file | The destination has matching content, or the include/exclude rules changed (including the first run after upgrading). Confirm local should win, then add `--resync`; an empty destination initializes automatically. `--dry-run --resync` previews but does not write the baseline. |
234
250
  | `OUT/.pix/lock` already exists | Check for another `shrink`, then remove a stale lock |
235
- | Configuration fails to parse | Run `pix doctor` to locate the file; fix the TOML or remove the file to fall back to defaults |
251
+ | Configuration fails to parse | The error names the file and location; fix the TOML or remove the file to fall back to defaults. `pix doctor` cannot run until the config parses |
236
252
  | `publish` says the source has no `.avif` | Run `pix shrink` first |
237
253
  | URL-related error | `base_url` must start with `http://` or `https://`, and `--urls` requires it |
238
254
 
@@ -13,9 +13,9 @@
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
- | `pix doctor` | Check Python, libvips, rclone and configuration |
18
+ | `pix doctor` | Report Python, libvips and rclone versions, plus the config path |
19
19
  | `pix config init` | Write a commented default configuration file |
20
20
 
21
21
  `pix` is deliberately narrow:
@@ -30,32 +30,41 @@
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
- | `doctor` / `config init` | Dependency checks / config scaffold | pix |
41
+ | `doctor` / `config init` | Runtime report / 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` (and for `pix doctor` to report a clean environment)
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.
55
55
 
56
56
  ## Install
57
57
 
58
- Not yet published on PyPI; install from source:
58
+ Install from PyPI:
59
+
60
+ ```bash
61
+ uv tool install pix-cli
62
+ pix --version
63
+ ```
64
+
65
+ Upgrade with `uv tool upgrade pix-cli`; remove with `uv tool uninstall pix-cli`. Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pix.exe`.
66
+
67
+ To work on pix itself, install the checkout in editable mode:
59
68
 
60
69
  ```bash
61
70
  git clone https://github.com/uzvg/pix.git
@@ -64,11 +73,11 @@ uv tool install --editable .
64
73
  pix --version
65
74
  ```
66
75
 
67
- Linux installs `~/.local/bin/pix`; Windows installs `%USERPROFILE%\.local\bin\pix.exe`. An editable install picks up source changes immediately. Reinstall with `uv tool install --force --editable .`, or remove it with `uv tool uninstall pix-cli`.
76
+ An editable install picks up source changes immediately. Reinstall with `uv tool install --force --editable .`.
68
77
 
69
78
  ## Configure rclone
70
79
 
71
- Only needed for `backup` / `publish`:
80
+ Only needed for `mirror` / `publish`:
72
81
 
73
82
  ```bash
74
83
  rclone config # create a remote, for example onedrive or r2
@@ -90,7 +99,7 @@ If the file is missing, built-in defaults are used. Generate a commented file wi
90
99
  pix config init
91
100
  ```
92
101
 
93
- Existing files are not overwritten; pass `--force` to replace one. Use `--config PATH` before the subcommand to point at another file.
102
+ Existing files are not overwritten; pass `--force` to replace one. Use `--config PATH` before the subcommand to point at another file; an explicit path must exist, while a missing default path is fine.
94
103
 
95
104
  Common keys:
96
105
 
@@ -101,15 +110,18 @@ Common keys:
101
110
  | | `effort` | `5` | libvips effort, 0–9 |
102
111
  | | `min_size` | `0` | Skip sources smaller than this; accepts `500K`, `1M`, `2G` |
103
112
  | | `include` | `jpg`, `jpeg`, `png`, `webp` | Extensions to scan, without dots |
104
- | `[backup]` | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
113
+ | `[mirror]` | `include` | common image extensions | Extensions to mirror, without dots; `[]` disables the whitelist |
114
+ | | `exclude` | `[]` | rclone exclude globs; command-line values are appended |
105
115
  | | `max_delete` | `10` | Delete limit as a percentage; `-1` disables, `0` forbids |
106
116
  | `[publish]` | `base_url` | `""` | Public URL prefix; must start with `http://` or `https://` |
107
117
  | | `urls` | `false` | Emit a JSON URL manifest |
108
- | | `exclude` | `[]` | Same as backup |
109
- | | `max_delete` | `10` | Same as backup |
118
+ | | `exclude` | `[]` | Same as mirror; `.pix/**` is always excluded first |
119
+ | | `max_delete` | `10` | Same as mirror |
110
120
  | `[rclone]` | `binary` | `rclone` | rclone executable name or path |
111
121
 
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).
122
+ Precedence is **command line > pix config > built-in defaults**. `--exclude` is appended to config excludes rather than replacing them. `-q` means quality; `--quiet` is errors-only; `-v` / `--verbose` adds per-file detail and rclone progress. `--no-urls` overrides `[publish].urls = true` for one run.
123
+
124
+ The default `[mirror].include` is `jpg`, `jpeg`, `png`, `webp`, `gif`, `bmp`, `tif`, `tiff`, `heic`, `heif`, `avif`, `jxl`, `svg`, `ico`, `cr2`, `cr3`, `nef`, `arw`, `dng`, `orf`, `raf`, `rw2`. Set it to `[]` to mirror every file.
113
125
 
114
126
  ## Quick start
115
127
 
@@ -126,16 +138,16 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
126
138
  pix shrink -s ~/Pictures -d ~/Pictures-avif -w 1920 -q 85
127
139
  ```
128
140
 
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.
141
+ The first run encodes everything; later runs skip unchanged files. State lives in `OUT/.pix/state.json`. `-w` is a maximum long edge, so smaller images are not upscaled; relative directories are preserved under OUT. Change detection uses source size/mtime and output size, not content hashes. Changing `width`, `quality` or `effort` triggers a full re-encode. `--dry-run` only prints the plan. `--prune` removes only outputs recorded in state; extra files in OUT are left alone.
130
142
 
131
- 3. Back up originals:
143
+ 3. Mirror originals:
132
144
 
133
145
  ```bash
134
- pix backup -s ~/Pictures -d onedrive:photos --dry-run
135
- pix backup -s ~/Pictures -d onedrive:photos --resync
146
+ pix mirror -s ~/Pictures -d onedrive:photos --dry-run
147
+ pix mirror -s ~/Pictures -d onedrive:photos
136
148
  ```
137
149
 
138
- `--resync` is required on the first run. Always preview with `--dry-run` first.
150
+ The destination is created automatically. Only files matching `[mirror].include` are mirrored; other source files are ignored, and non-matching files already on the destination are left untouched. If no source file matches the filter, the source counts as empty and is refused unless you pass `--allow-empty --resync`. An empty destination initializes its own baseline, so no `--resync` is needed on the first run. Use `--resync` to rebuild the baseline when the destination already has matching content, or after changing `include`/`exclude`; `--dry-run` may create an empty destination but does not transfer or delete. After upgrading from a version without include filtering, the first run also needs `--resync`; preview with `--dry-run --resync` first (the preview does not write the baseline).
139
151
 
140
152
  4. Publish the AVIF output:
141
153
 
@@ -144,17 +156,18 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
144
156
  --base-url https://img.example.com --urls > urls.json
145
157
  ```
146
158
 
147
- `publish` requires at least one `.avif` in the source and always excludes `.pix/**`. The URL manifest is printed to stdout as JSON only after a successful, non-dry-run sync.
159
+ `publish` requires at least one `.avif` in the source unless you pass `--allow-empty --resync`; it syncs only `.avif` files and always excludes `.pix/**`. The URL manifest is printed to stdout as JSON only after a successful, non-dry-run sync. It lists every `.avif` under OUT, not just files changed in this run, and is not filtered by `[publish].exclude` or `--exclude`; excluded files can still appear in it. If you rely on excludes to hide files, do not publish the manifest until this behavior is fixed.
148
160
 
149
161
  5. Re-run later:
150
162
 
151
163
  ```bash
152
164
  pix shrink -s ~/Pictures -d ~/Pictures-avif
153
- pix backup -s ~/Pictures -d onedrive:photos
154
- pix publish -s ~/Pictures-avif -d r2:images --urls
165
+ pix mirror -s ~/Pictures -d onedrive:photos
166
+ pix publish -s ~/Pictures-avif -d r2:images \
167
+ --base-url https://img.example.com --urls
155
168
  ```
156
169
 
157
- Only changes are encoded and transferred.
170
+ Only changes are encoded and transferred. `--base-url` is not persisted between runs; set `[publish].base_url` in config to avoid repeating it.
158
171
 
159
172
  ## Command cheat sheet
160
173
 
@@ -162,9 +175,10 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
162
175
  |---|---|
163
176
  | Preview compression | `pix shrink -s SRC -d OUT --dry-run` |
164
177
  | Re-encode everything | `pix shrink -s SRC -d OUT --force` |
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` |
178
+ | Remove tracked outputs whose source is gone | `pix shrink -s SRC -d OUT --prune` |
179
+ | Preview a sync | `pix mirror -s SRC -d REMOTE --dry-run` |
180
+ | First sync (new or empty destination) | `pix mirror -s SRC -d REMOTE` |
181
+ | Rebuild the baseline (non-empty destination, or after changing include/exclude) | `pix mirror -s SRC -d REMOTE --resync` |
168
182
  | Emit a URL manifest | `pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls` |
169
183
  | Diagnose the environment | `pix doctor` |
170
184
  | Generate a config file | `pix config init` |
@@ -174,19 +188,21 @@ Precedence is **command line > pix config > built-in defaults**. `--exclude` is
174
188
  | Situation | What happens | Protection |
175
189
  |---|---|---|
176
190
  | Deletions on either side | Propagated in both directions; accidental deletion is irreversible | Preview with `--dry-run`; default `--max-delete 10` (percentage) |
177
- | First sync | rclone has no baseline and errors | Add `--resync` explicitly |
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 |
191
+ | First sync (destination empty) | `pix` creates the destination and initializes the baseline | Preview with `--dry-run`; it may create an empty destination but does not transfer or delete. No manual rclone needed |
192
+ | Destination has matching content but no baseline | `pix` does not auto-resync; bisync fails instead of overwriting | Confirm which side is authoritative, then add `--resync` |
193
+ | Include/exclude filters changed | When filters are active, rclone detects the changed filters file and exits 7 | Run with `--resync` after changing `include` or `exclude`; `--dry-run --resync` previews but does not write the baseline |
194
+ | Non-image files in the source | `mirror` ignores them by default; `publish` ignores anything that is not `.avif`. If no file matches, the source counts as empty and is refused | Add extensions to `[mirror].include` if they should be mirrored; use `--allow-empty --resync` only for an intentional empty sync |
195
+ | Empty source | `mirror` / `publish` refuse. `shrink --prune` refuses only while state still has entries; empty state is a no-op | Use `--allow-empty` (plus `--resync` for sync) only when intentional |
180
196
  | `a.jpg` and `a.jpeg` both exist | Both map to the same output | Detected before encoding; exits 2 |
181
197
  | Two concurrent shrinks | They would write the same state and outputs | `OUT/.pix/lock` blocks the second process |
182
- | State lost or corrupt | Warning, then a full rebuild | Only causes re-encoding; outputs stay valid |
198
+ | State missing or corrupt | Full rebuild; corrupt state logs a warning | Only causes re-encoding; outputs stay valid |
183
199
 
184
200
  ## Output and exit codes
185
201
 
186
202
  - Logs and progress go to stderr.
187
- - stdout carries `doctor` reports, `--dry-run` plans and `--urls` JSON.
203
+ - stdout carries `doctor` reports, `shrink --dry-run` plans, `config init`'s `wrote <path>`, and `--urls` JSON. `mirror` / `publish --dry-run` output comes from rclone.
188
204
  - Exit codes: `0` success, `1` runtime failure, `2` argument or configuration error.
189
- - `backup` / `publish` return rclone's exit code unchanged.
205
+ - `mirror` / `publish` return rclone's bisync exit code unchanged; destination preflight failures return 1.
190
206
 
191
207
  URL manifest example:
192
208
 
@@ -202,9 +218,9 @@ URL manifest example:
202
218
  |---|---|
203
219
  | `pyvips/libvips unavailable` | Reinstall to bring libvips: `uv tool install --force --editable /path/to/pix`; or install libvips manually (Linux: `apt install libvips42`, Windows: libvips release zip on `PATH`) |
204
220
  | rclone is older than 1.66 | Upgrade rclone |
205
- | bisync reports a missing baseline | Add `--resync` on the first run |
221
+ | bisync reports a missing baseline or changed filters file | The destination has matching content, or the include/exclude rules changed (including the first run after upgrading). Confirm local should win, then add `--resync`; an empty destination initializes automatically. `--dry-run --resync` previews but does not write the baseline. |
206
222
  | `OUT/.pix/lock` already exists | Check for another `shrink`, then remove a stale lock |
207
- | Configuration fails to parse | Run `pix doctor` to locate the file; fix the TOML or remove the file to fall back to defaults |
223
+ | Configuration fails to parse | The error names the file and location; fix the TOML or remove the file to fall back to defaults. `pix doctor` cannot run until the config parses |
208
224
  | `publish` says the source has no `.avif` | Run `pix shrink` first |
209
225
  | URL-related error | `base_url` must start with `http://` or `https://`, and `--urls` requires it |
210
226