getwebp 1.0.1 → 1.1.1

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 (3) hide show
  1. package/README.md +212 -196
  2. package/dist/index.js +245 -89
  3. package/package.json +9 -4
package/README.md CHANGED
@@ -1,318 +1,334 @@
1
1
  # GetWebP CLI
2
2
 
3
- 一个命令行图片转换工具,将 JPGPNGBMPWebP 批量转为 WebP 格式,支持 Free Pro 两种模式。
3
+ Batch-convert JPG, PNG, BMP, WebP, HEIC, HEIF, and AVIF images to optimized WebP format from the command line.
4
4
 
5
- 官网:[https://getwebp.com](https://getwebp.com) · 定价:[https://getwebp.com/pricing](https://getwebp.com/pricing)
5
+ ![npm version](https://img.shields.io/npm/v/getwebp)
6
+ ![license](https://img.shields.io/npm/l/getwebp)
7
+ ![platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-blue)
8
+ ![downloads](https://img.shields.io/npm/dt/getwebp)
9
+
10
+ **Website:** [getwebp.com](https://getwebp.com) | **Pricing:** [getwebp.com/pricing](https://getwebp.com/pricing) | **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
6
11
 
7
12
  ---
8
13
 
9
- ## 安装
14
+ ## Feature Highlights
15
+
16
+ - **Batch conversion** -- process entire directories of images in one command
17
+ - **Parallel processing** -- concurrent workers scale to your CPU cores (Starter/Pro)
18
+ - **Recursive scanning** -- traverse subdirectories with `-r`
19
+ - **CI/CD ready** -- structured `--json` output with machine-parseable error codes
20
+ - **Offline-first licensing** -- JWT-based activation works without a persistent connection
21
+ - **Cross-platform** -- native binaries for macOS (ARM + Intel), Linux x64, and Windows x64
22
+
23
+ ---
10
24
 
11
- ### 下载二进制文件
25
+ ## Quick Start
26
+
27
+ ```bash
28
+ # 1. Download the binary for your platform (see Installation below)
29
+ # 2. Convert a single image
30
+ getwebp convert photo.jpg
31
+
32
+ # 3. Convert an entire directory
33
+ getwebp convert ./images -o ./output
34
+ ```
35
+
36
+ Output:
37
+
38
+ ```
39
+ ✓ photo.jpg
40
+ Done: 1 succeeded, 0 failed
41
+ Avg saved: 34.2%
42
+ ```
12
43
 
13
- | 平台 | 下载地址 |
14
- |------|----------|
15
- | macOS Apple Silicon (M1/M2/M3) | `getwebp-macos-arm64` |
44
+ ---
45
+
46
+ ## Installation
47
+
48
+ ### Download Binary (Recommended)
49
+
50
+ Download the binary for your platform from the [Releases page](https://getwebp.com/download):
51
+
52
+ | Platform | Binary |
53
+ |---|---|
54
+ | macOS Apple Silicon (M1/M2/M3/M4) | `getwebp-macos-arm64` |
16
55
  | macOS Intel | `getwebp-macos-x64` |
17
56
  | Linux x64 | `getwebp-linux-x64` |
18
57
  | Windows x64 | `getwebp-windows-x64.exe` |
19
58
 
20
- [Releases 页面](https://getwebp.com/download) 下载对应平台的二进制文件。
21
-
22
- ### macOS / Linux 安装
59
+ #### macOS / Linux
23
60
 
24
61
  ```bash
25
- # 重命名并赋予执行权限
26
- mv getwebp-macos-arm64 getwebp # Apple Silicon
27
- # mv getwebp-macos-x64 getwebp # Intel Mac
28
- # mv getwebp-linux-x64 getwebp # Linux
29
-
62
+ # Rename and make executable
63
+ mv getwebp-macos-arm64 getwebp # adjust filename for your platform
30
64
  chmod +x getwebp
31
-
32
- # 移动到 PATH 目录(需要 sudo)
33
65
  sudo mv getwebp /usr/local/bin/
34
66
 
35
- # 验证安装
67
+ # Verify
36
68
  getwebp --version
37
69
  ```
38
70
 
39
- ### Windows 安装
71
+ #### Windows
40
72
 
41
- `getwebp-windows-x64.exe` 重命名为 `getwebp.exe`,然后放到任意已加入 `PATH` 的目录(如 `C:\Windows\System32`),或手动将其所在目录添加到系统环境变量 `PATH`。
73
+ Rename `getwebp-windows-x64.exe` to `getwebp.exe` and place it in a directory on your `PATH` (e.g. `C:\Windows\System32`), or add its directory to the system `PATH` environment variable.
42
74
 
43
- ### macOS Gatekeeper 处理
75
+ ### macOS Gatekeeper
44
76
 
45
- macOS 可能会阻止运行未签名的二进制文件,执行以下命令移除隔离属性:
77
+ macOS may block unsigned binaries. Remove the quarantine attribute:
46
78
 
47
79
  ```bash
48
80
  xattr -cr getwebp
49
81
  ```
50
82
 
51
- ---
52
-
53
- ## 安全验证
54
-
55
- 每个 Release 都附带 SHA256 校验和文件(`*.sha256`),建议安装前验证:
83
+ ### Verify Download (SHA256)
56
84
 
57
- ### macOS / Linux
85
+ Each release includes SHA256 checksum files (`*.sha256`):
58
86
 
59
87
  ```bash
60
- # 下载二进制和校验和文件
61
- curl -LO https://github.com/getwebp/cli/releases/download/v2.1.0/getwebp-macos-arm64
62
- curl -LO https://github.com/getwebp/cli/releases/download/v2.1.0/getwebp-macos-arm64.sha256
63
-
64
- # 验证
88
+ # macOS / Linux
65
89
  shasum -a 256 -c getwebp-macos-arm64.sha256
66
- # 输出:getwebp-macos-arm64: OK
90
+ # Expected: getwebp-macos-arm64: OK
67
91
  ```
68
92
 
69
- ### Windows (PowerShell)
70
-
71
93
  ```powershell
72
- # 下载
73
- Invoke-WebRequest -Uri "https://github.com/getwebp/cli/releases/download/v2.1.0/getwebp-win-x64.exe" -OutFile getwebp.exe
74
- Invoke-WebRequest -Uri "https://github.com/getwebp/cli/releases/download/v2.1.0/getwebp-win-x64.exe.sha256" -OutFile getwebp.exe.sha256
75
-
76
- # 验证
94
+ # Windows (PowerShell)
77
95
  $expected = (Get-Content getwebp.exe.sha256).Split(' ')[0]
78
96
  $actual = (Get-FileHash getwebp.exe -Algorithm SHA256).Hash.ToLower()
79
- if ($expected -eq $actual) { Write-Host " 验证通过" } else { Write-Host "✗ 验证失败" }
97
+ if ($expected -eq $actual) { Write-Host "Verified OK" } else { Write-Host "MISMATCH" }
80
98
  ```
81
99
 
82
100
  ---
83
101
 
84
- ## 命令用法
102
+ ## Commands
103
+
104
+ ### Quick Reference
85
105
 
86
- ### `getwebp convert` 转换图片
106
+ | Command | Description |
107
+ |---|---|
108
+ | `getwebp convert <path>` | Convert images to WebP |
109
+ | `getwebp auth <key>` | Activate a license key |
110
+ | `getwebp status` | Show license and version info |
111
+ | `getwebp logout` | Unbind license from this device |
112
+ | `getwebp --version` | Print CLI version |
113
+ | `getwebp --help` | Show help |
114
+
115
+ ### `getwebp convert`
116
+
117
+ Convert one or more images to WebP format. Original files are never modified or deleted.
87
118
 
88
119
  ```
89
120
  getwebp convert [path] [options]
90
121
  ```
91
122
 
92
- **选项:**
123
+ **Options:**
93
124
 
94
- | 选项 | 说明 | 默认值 |
95
- |------|------|--------|
96
- | `-i, --input <path>` | 输入目录或文件 | |
97
- | `-o, --output <path>` | 输出目录 | 原地覆盖 |
98
- | `-q, --quality <number>` | 质量 1-100 | 80 |
99
- | `--concurrency <number>` | 并发处理数(Pro only) | 4 |
100
- | `-r, --recursive` | 递归处理子目录中的图片(Starter/Pro only) | |
101
- | `--dry-run` | 预览模式,不实际转换 | |
102
- | `--skip-existing` | 跳过已存在的 .webp 文件 | |
103
- | `--json` | JSON 格式输出(适合 AI / CI) | |
125
+ | Option | Description | Default |
126
+ |---|---|---|
127
+ | `-i, --input <path>` | Input file or directory | -- |
128
+ | `-o, --output <path>` | Output directory | Same directory as source |
129
+ | `-q, --quality <n>` | WebP quality, 1--100 | `80` |
130
+ | `-r, --recursive` | Process subdirectories | off |
131
+ | `--concurrency <n>` | Parallel workers, max 32 (Starter/Pro) | CPU cores - 1 |
132
+ | `--dry-run` | Preview files without converting | off |
133
+ | `--skip-existing` | Skip if `.webp` already exists | off |
134
+ | `--json` | JSON output for CI / AI agents | off |
104
135
 
105
- **示例:**
136
+ **Examples:**
106
137
 
107
138
  ```bash
108
- # 转换单张图片
139
+ # Convert a single image
109
140
  getwebp convert photo.jpg
110
141
 
111
- # 转换整个目录(递归)
112
- getwebp convert ./images
113
-
114
- # 指定输入目录和输出目录
142
+ # Convert a directory with custom output
115
143
  getwebp convert -i ./src/images -o ./dist/images
116
144
 
117
- # 设置质量为 90(Pro)
118
- getwebp convert ./images -q 90
145
+ # High quality, skip already-converted files
146
+ getwebp convert ./images -q 95 --skip-existing
119
147
 
120
- # 预览将会转换的文件(不实际执行)
121
- getwebp convert ./images --dry-run
122
-
123
- # 跳过已存在的 WebP 文件
124
- getwebp convert ./images --skip-existing
125
-
126
- # 递归处理子目录中的图片(Starter/Pro)
127
- getwebp convert ./images --recursive
148
+ # Recursive scan of subdirectories
149
+ getwebp convert ./images -r
128
150
 
129
- # 并发 8 线程处理(Pro
151
+ # Parallel processing with 8 workers (Starter/Pro)
130
152
  getwebp convert ./images --concurrency 8
131
153
 
132
- # JSON 输出,适合 CI/CD 流水线
133
- getwebp convert ./images --json
154
+ # Preview what would be converted (no writes)
155
+ getwebp convert ./images --dry-run
134
156
 
135
- # 组合选项:高质量 + 跳过已有文件 + JSON 输出
136
- getwebp convert -i ./images -o ./output -q 90 --skip-existing --json
157
+ # JSON output for CI/CD pipelines
158
+ getwebp convert ./images --json
137
159
  ```
138
160
 
139
- ---
140
-
141
- ### `getwebp auth` — 激活 Pro 授权
142
-
143
- ```bash
144
- getwebp auth <license-key>
145
- ```
161
+ ### `getwebp auth`
146
162
 
147
- **示例:**
163
+ Activate a license key to unlock Starter or Pro features.
148
164
 
149
165
  ```bash
150
- # 使用许可证 Key 激活 Pro
151
166
  getwebp auth XXXX-XXXX-XXXX-XXXX
167
+ ```
152
168
 
153
- # 激活成功后会显示账号信息和到期时间
169
+ ```
170
+ Verifying license...
171
+ ✓ Activated! Starter plan unlocked.
154
172
  ```
155
173
 
156
- 购买 Pro 许可证请访问:[https://getwebp.com/pricing](https://getwebp.com/pricing)
174
+ Purchase a license at [getwebp.com/pricing](https://getwebp.com/pricing).
157
175
 
158
- ---
176
+ ### `getwebp status`
159
177
 
160
- ### `getwebp status` 查看授权状态
178
+ Display current license info, plan, expiry date, and device usage.
161
179
 
162
180
  ```bash
163
- # 查看当前激活状态、许可证信息和到期时间
164
181
  getwebp status
165
-
166
- # 以 JSON 格式输出(适合脚本解析)
167
- getwebp status --json
168
182
  ```
169
183
 
170
- ---
171
-
172
- ### `getwebp logout` — 退出登录
173
-
174
- ```bash
175
- # 清除本地存储的许可证信息
176
- getwebp logout
177
- ```
178
-
179
- ---
180
-
181
- ## JSON 输出格式参考
182
-
183
- 所有命令均支持 `--json` 参数,适用于 AI Agent 和 CI/CD 脚本。
184
-
185
- **成功响应**(`success: true`):
186
- ```json
187
- { "success": true, "data": { ... } }
188
184
  ```
189
-
190
- **错误响应**(`success: false`):
191
- ```json
192
- { "success": false, "status": "error", "error": "<error-code>", "message": "<human-readable>" }
185
+ Version : 1.0.1
186
+ Mode : Starter
187
+ License : xxxx-xxxx-xxxx-A1B2
188
+ Expires : 2027-04-01
189
+ Devices : 1 / 3 used
193
190
  ```
194
191
 
195
- **常见错误码:**
196
-
197
- | error 字段 | 含义 | 是否可重试 |
198
- |------------|------|-----------|
199
- | `missing_input` | 未指定输入路径 | — |
200
- | `network_unreachable` | 无法连接服务器 | ✅ |
201
- | `not_activated` | 未激活 Pro 许可证 | — |
202
- | `device_not_found` | 设备未找到(可能已解绑) | — |
203
- | `invalid_token` | Token 无效或已过期 | — |
204
- | `unknown_error` | 未知错误 | ✅ |
205
-
206
- **convert 成功示例:**
207
- ```json
208
- { "status": "success", "total": 3, "success": 3, "failed": 0, "results": [...] }
192
+ ```bash
193
+ # Machine-readable output
194
+ getwebp status --json
209
195
  ```
210
196
 
211
- **convert 错误示例(缺少路径):**
212
197
  ```json
213
- { "success": false, "status": "error", "error": "missing_input", "message": "Please specify an input path" }
198
+ {
199
+ "success": true,
200
+ "data": {
201
+ "version": "1.0.1",
202
+ "mode": "starter",
203
+ "licenseKeySuffix": "A1B2",
204
+ "expiresAt": "2027-04-01T00:00:00.000Z",
205
+ "devicesUsed": 1,
206
+ "devicesLimit": 3
207
+ }
208
+ }
214
209
  ```
215
210
 
216
- > 注意:`getwebp logout --json` 在脚本中使用时请同时加 `--force`,否则会等待交互式确认。
217
-
218
- ---
219
-
220
- ## 退出码
211
+ ### `getwebp logout`
221
212
 
222
- | 退出码 | 含义 |
223
- |--------|------|
224
- | `0` | 成功 |
225
- | `1` | 失败(详见 stderr 或 JSON 的 `error` 字段) |
213
+ Unbind the license from this device, freeing a device slot. Requires network access.
226
214
 
227
- 使用 `--json` 模式时,通过 `error` 字段区分可重试错误(如 `network_unreachable`)和永久性错误(如 `missing_input`、`not_activated`)。
215
+ ```bash
216
+ getwebp logout # interactive confirmation
217
+ getwebp logout --force # skip confirmation (CI-safe)
218
+ ```
228
219
 
229
220
  ---
230
221
 
231
- ## Free vs Pro
222
+ ## JSON Output
232
223
 
233
- | 功能 | Free | Pro |
234
- |------|:----:|:---:|
235
- | 每次最多处理张数 | 10 张 | 无限制 |
236
- | 处理速度 | 每张等待 3 秒 | 无限制 |
237
- | 并发处理 | ❌ | ✅(最多 4 并发) |
238
- | 递归目录(`-r`) | ❌ | ✅ |
239
- | 自定义质量(`-q`) | ✅ | ✅ |
240
- | JSON 输出(`--json`) | ✅ | ✅ |
241
-
242
- 升级 Pro:[https://getwebp.com/pricing](https://getwebp.com/pricing)
243
-
244
- ---
224
+ All commands support `--json` for structured output on stdout. Human-readable messages go to stderr.
245
225
 
246
- ## FAQ
226
+ **Success:**
247
227
 
248
- **Q:支持哪些图片格式?**
228
+ ```json
229
+ {
230
+ "success": true,
231
+ "status": "success",
232
+ "total": 3,
233
+ "successCount": 3,
234
+ "failedCount": 0,
235
+ "results": [
236
+ { "file": "photo.jpg", "status": "success", "originalSize": 204800, "newSize": 133120, "savedRatio": 0.35, "saved": "35.0%" }
237
+ ]
238
+ }
239
+ ```
249
240
 
250
- A:支持 JPG、PNG、BMP、WebP 作为输入格式,统一输出为 WebP。
241
+ **Error:**
251
242
 
252
- ---
243
+ ```json
244
+ {
245
+ "success": false,
246
+ "status": "error",
247
+ "error": "missing_input",
248
+ "message": "Please specify an input path"
249
+ }
250
+ ```
253
251
 
254
- **Q:macOS 提示"无法打开,因为无法验证开发者"怎么办?**
252
+ **Error codes:**
255
253
 
256
- A:在终端执行以下命令移除隔离属性,然后重试:
254
+ | Code | Meaning | Retryable |
255
+ |---|---|---|
256
+ | `missing_input` | No input path specified | No |
257
+ | `network_unreachable` | Cannot reach API server | Yes |
258
+ | `not_activated` | No active license on device | No |
259
+ | `device_not_found` | Device already unbound | No |
260
+ | `invalid_token` | Token expired or revoked | No |
261
+ | `unknown_error` | Unclassified error | Yes |
257
262
 
258
- ```bash
259
- xattr -cr getwebp
260
- ```
263
+ > **Note:** `getwebp logout --json` automatically skips the interactive confirmation prompt. You can also use `--force` to skip it in non-JSON mode.
261
264
 
262
265
  ---
263
266
 
264
- **Q:如何激活 Pro?**
265
-
266
- A:在 [getwebp.com/pricing](https://getwebp.com/pricing) 购买许可证后,运行:
267
+ ## Exit Codes
267
268
 
268
- ```bash
269
- getwebp auth <你的许可证 Key>
270
- ```
269
+ | Code | Meaning |
270
+ |---|---|
271
+ | `0` | All files processed successfully |
272
+ | `1` | General error |
273
+ | `2` | Partial failure (some files failed) |
274
+ | `3` | License / auth error |
275
+ | `4` | Network error |
271
276
 
272
- 激活成功后即可解锁所有 Pro 功能。
277
+ See [docs/exit-codes.md](./docs/exit-codes.md) for detailed descriptions and retry guidance.
273
278
 
274
279
  ---
275
280
 
276
- **Q:Free 模式有什么限制?**
277
-
278
- A:Free 模式每次最多转换 10 张图片,且每张图片处理前会等待 3 秒。不支持并发处理和递归子目录。升级 Pro 可解锁全部功能。
281
+ ## Free vs Starter vs Pro
279
282
 
280
- ---
281
-
282
- **Q:如何在 CI/CD 流水线中使用?**
283
+ | Feature | Free | Starter | Pro |
284
+ |---|:---:|:---:|:---:|
285
+ | Files per run | 10 | Unlimited | Unlimited |
286
+ | Processing speed | 3s delay per file | Instant | Instant |
287
+ | Parallel workers | 1 (serial) | Auto (CPU cores - 1) | Auto (CPU cores - 1) |
288
+ | Custom `--concurrency` | -- | Up to 32 | Up to 32 |
289
+ | Recursive `-r` | Yes | Yes | Yes |
290
+ | Custom quality `-q` | Yes | Yes | Yes |
291
+ | JSON output `--json` | Yes | Yes | Yes |
292
+ | `--dry-run` / `--skip-existing` | Yes | Yes | Yes |
283
293
 
284
- A:使用 `--json` 参数可以获得结构化的 JSON 输出,方便脚本解析:
294
+ Upgrade at [getwebp.com/pricing](https://getwebp.com/pricing), then activate:
285
295
 
286
296
  ```bash
287
- getwebp convert ./images --json
297
+ getwebp auth <your-license-key>
288
298
  ```
289
299
 
290
- 配合 Pro 许可证,可以在 CI 环境中批量处理大量图片,无数量和速度限制。
291
-
292
- ---
293
-
294
- **Q:支持退款吗?**
295
-
296
- A:购买后如有问题,请联系 [getwebp.com](https://getwebp.com/contact) 客服,我们提供合理的退款支持。
297
-
298
300
  ---
299
301
 
300
- **Q:许可证可以在多台机器上使用吗?**
302
+ ## Supported Formats
301
303
 
302
- A:许可证与设备绑定,具体设备数量限制请参考购买页面的套餐说明:[https://getwebp.com/pricing](https://getwebp.com/pricing)
304
+ | Input | Output |
305
+ |---|---|
306
+ | JPG / JPEG | WebP |
307
+ | PNG | WebP |
308
+ | BMP | WebP |
309
+ | WebP | WebP (re-encoded) |
310
+ | HEIC | WebP |
311
+ | HEIF | WebP |
312
+ | AVIF | WebP |
303
313
 
304
314
  ---
305
315
 
306
- **Q:如何查看当前版本?**
316
+ ## Global Options
307
317
 
308
- ```bash
309
- getwebp --version
310
- ```
318
+ | Option | Description |
319
+ |---|---|
320
+ | `--verbose` | Enable verbose output |
321
+ | `--debug` | Enable debug output (implies `--verbose`) |
322
+ | `--json` | JSON output (available on all commands) |
323
+ | `--version`, `-v` | Print version |
324
+ | `--help` | Show help |
311
325
 
312
326
  ---
313
327
 
314
- ## 版本
315
-
316
- 当前版本:**2.1.0**
328
+ ## Links
317
329
 
318
- 查看更新日志:[https://getwebp.com/changelog](https://getwebp.com/changelog)
330
+ - **Website:** [getwebp.com](https://getwebp.com)
331
+ - **Pricing:** [getwebp.com/pricing](https://getwebp.com/pricing)
332
+ - **Download:** [getwebp.com/download](https://getwebp.com/download)
333
+ - **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
334
+ - **Dashboard:** [getwebp.com/dashboard](https://getwebp.com/dashboard) (manage devices and licenses)
package/dist/index.js CHANGED
@@ -9,31 +9,130 @@ import jwt from "jsonwebtoken";
9
9
 
10
10
  // src/core/config.ts
11
11
  import Conf from "conf";
12
+ import crypto2 from "node:crypto";
13
+ import { unlinkSync } from "node:fs";
14
+ import os2 from "node:os";
15
+ import path from "node:path";
16
+
17
+ // src/utils/debug.ts
18
+ var _verbose = false;
19
+ var _debug = false;
20
+ var _sink = (msg) => process.stderr.write(msg);
21
+ function setVerbose(v) {
22
+ _verbose = v;
23
+ }
24
+ function setDebug(v) {
25
+ _debug = v;
26
+ if (v) _verbose = true;
27
+ }
28
+ function debugLog(...args) {
29
+ if (_debug) _sink("[debug] " + args.join(" ") + "\n");
30
+ }
31
+
32
+ // src/core/machine-id.ts
33
+ import { execSync, exec } from "node:child_process";
12
34
  import crypto from "node:crypto";
13
- import { createRequire } from "node:module";
14
35
  import os from "node:os";
15
- var _require = createRequire(import.meta.url);
16
- var { machineIdSync } = _require("node-machine-id");
36
+ function getRawId() {
37
+ const platform = process.platform;
38
+ try {
39
+ if (platform === "darwin") {
40
+ const out = execSync("ioreg -rd1 -c IOPlatformExpertDevice", { encoding: "utf8" });
41
+ const match = out.match(/IOPlatformUUID[^=]+=\s*"([^"]+)"/);
42
+ if (match) return match[1].toLowerCase();
43
+ } else if (platform === "linux") {
44
+ const out = execSync("( cat /var/lib/dbus/machine-id /etc/machine-id 2>/dev/null || hostname ) | head -n 1", {
45
+ encoding: "utf8",
46
+ shell: "/bin/sh"
47
+ });
48
+ return out.trim();
49
+ } else if (platform === "win32") {
50
+ const out = execSync(
51
+ "REG QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid",
52
+ { encoding: "utf8" }
53
+ );
54
+ const match = out.match(/MachineGuid\s+REG_SZ\s+([^\r\n]+)/);
55
+ if (match) return match[1].trim();
56
+ }
57
+ } catch {
58
+ }
59
+ return `${os.hostname()}:${os.userInfo().username}`;
60
+ }
61
+ function hashId(raw) {
62
+ return crypto.createHash("sha256").update(raw).digest("hex");
63
+ }
64
+ function machineIdSync() {
65
+ return hashId(getRawId());
66
+ }
67
+ function machineId() {
68
+ return new Promise((resolve) => {
69
+ const platform = process.platform;
70
+ if (platform === "darwin") {
71
+ exec("ioreg -rd1 -c IOPlatformExpertDevice", (err, stdout) => {
72
+ if (!err) {
73
+ const match = stdout.match(/IOPlatformUUID[^=]+=\s*"([^"]+)"/);
74
+ if (match) return resolve(hashId(match[1].toLowerCase()));
75
+ }
76
+ resolve(hashId(getRawId()));
77
+ });
78
+ } else if (platform === "linux") {
79
+ exec("( cat /var/lib/dbus/machine-id /etc/machine-id 2>/dev/null || hostname ) | head -n 1", { shell: "/bin/sh" }, (err, stdout) => {
80
+ resolve(hashId(err ? getRawId() : stdout.trim()));
81
+ });
82
+ } else if (platform === "win32") {
83
+ exec("REG QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid", (err, stdout) => {
84
+ if (!err) {
85
+ const match = stdout.match(/MachineGuid\s+REG_SZ\s+([^\r\n]+)/);
86
+ if (match) return resolve(hashId(match[1].trim()));
87
+ }
88
+ resolve(hashId(getRawId()));
89
+ });
90
+ } else {
91
+ resolve(hashId(getRawId()));
92
+ }
93
+ });
94
+ }
95
+
96
+ // src/core/config.ts
17
97
  function getMachineKey() {
18
98
  try {
19
99
  const id = machineIdSync();
20
- return crypto.createHash("sha256").update(id).digest("hex");
100
+ return crypto2.createHash("sha256").update(id).digest("hex");
21
101
  } catch {
22
- process.stderr.write(
23
- "warn: Could not read machine ID, falling back to hostname.\n Token may become invalid if hostname changes.\n"
102
+ debugLog(
103
+ "warn: Could not read machine ID, falling back to hostname.",
104
+ "Token may become invalid if hostname changes."
24
105
  );
25
- return crypto.createHash("sha256").update(`${os.hostname()}:${os.userInfo().username}`).digest("hex");
106
+ return crypto2.createHash("sha256").update(`${os2.hostname()}:${os2.userInfo().username}`).digest("hex");
26
107
  }
27
108
  }
28
- var store = new Conf({
29
- projectName: "getwebp",
30
- encryptionKey: getMachineKey(),
31
- ...process.env.GETWEBP_CONFIG_DIR ? { cwd: process.env.GETWEBP_CONFIG_DIR } : {}
32
- });
109
+ var _store = null;
110
+ function getStore() {
111
+ if (!_store) {
112
+ const confOpts = {
113
+ projectName: "getwebp",
114
+ encryptionKey: getMachineKey(),
115
+ ...process.env.GETWEBP_CONFIG_DIR ? { cwd: process.env.GETWEBP_CONFIG_DIR } : {}
116
+ };
117
+ try {
118
+ _store = new Conf(confOpts);
119
+ } catch {
120
+ debugLog("warn: Corrupt config file detected, clearing and reinitialising.");
121
+ const configDir = process.env.GETWEBP_CONFIG_DIR ?? (process.platform === "darwin" ? path.join(os2.homedir(), "Library", "Preferences", "getwebp-nodejs") : path.join(os2.homedir(), ".config", "getwebp"));
122
+ try {
123
+ unlinkSync(path.join(configDir, "config.json"));
124
+ } catch {
125
+ }
126
+ _store = new Conf(confOpts);
127
+ }
128
+ }
129
+ return _store;
130
+ }
33
131
  function getConfig() {
34
- return store.store;
132
+ return getStore().store;
35
133
  }
36
134
  function saveConfig(data) {
135
+ const store = getStore();
37
136
  for (const [key, value] of Object.entries(data)) {
38
137
  if (value === void 0) {
39
138
  store.delete(key);
@@ -44,25 +143,23 @@ function saveConfig(data) {
44
143
  }
45
144
 
46
145
  // src/core/device.ts
47
- import crypto2 from "node:crypto";
48
- import { createRequire as createRequire2 } from "node:module";
49
- var _require2 = createRequire2(import.meta.url);
50
- var { machineId } = _require2("node-machine-id");
146
+ import crypto3 from "node:crypto";
51
147
  async function getDeviceId() {
52
148
  const id = await machineId();
53
- return crypto2.createHash("sha256").update(id).digest("hex");
149
+ return crypto3.createHash("sha256").update(id).digest("hex");
54
150
  }
55
151
 
56
152
  // src/core/constants.ts
57
- import os2 from "node:os";
153
+ import os3 from "node:os";
58
154
  var FREE_TIER = {
59
155
  FILE_LIMIT: 10,
60
156
  DELAY_MS: 3e3
61
157
  };
62
158
  var DEFAULTS = {
63
159
  QUALITY: 80,
64
- CONCURRENCY: Math.max(1, os2.cpus().length - 1)
160
+ CONCURRENCY: Math.max(1, os3.cpus().length - 1)
65
161
  };
162
+ var AUTO_QUALITY_SENTINEL = -1;
66
163
  var NETWORK = {
67
164
  HEARTBEAT_TIMEOUT_MS: 3e3,
68
165
  API_TIMEOUT_MS: 5e3,
@@ -130,7 +227,7 @@ async function activate(licenseKey) {
130
227
  return { success: false, error: err instanceof Error ? err.message : String(err) };
131
228
  }
132
229
  }
133
- async function checkLicense() {
230
+ async function checkLicense(warn) {
134
231
  const config = getConfig();
135
232
  if (!config?.token) return { valid: false, plan: "free" };
136
233
  try {
@@ -149,7 +246,8 @@ async function checkLicense() {
149
246
  return { valid: false, plan: "free", expired: true };
150
247
  }
151
248
  if (err instanceof Error && err.message.includes("JWT_PUBLIC_KEY")) {
152
- process.stderr.write("warn: JWT public key not found. License validation skipped.\n");
249
+ const w = warn ?? ((msg) => process.stderr.write(msg + "\n"));
250
+ w("warn: JWT public key not found. License validation skipped.");
153
251
  }
154
252
  return { valid: false, plan: "free" };
155
253
  }
@@ -216,44 +314,55 @@ function resolveExitCode(results) {
216
314
  }
217
315
 
218
316
  // src/core/processor.ts
219
- import { encode as encodeWebp } from "@jsquash/webp";
220
- import { unlinkSync, constants as fsConstants } from "node:fs";
317
+ import { encode as encodeWebp, decode as decodeWebp2 } from "@jsquash/webp";
318
+ import { unlinkSync as unlinkSync2, constants as fsConstants } from "node:fs";
221
319
  import fs3 from "node:fs/promises";
222
- import path3 from "node:path";
320
+ import path4 from "node:path";
223
321
  import pLimit from "p-limit";
224
-
225
- // src/utils/debug.ts
226
- var _verbose = false;
227
- var _debug = false;
228
- function setVerbose(v) {
229
- _verbose = v;
230
- }
231
- function setDebug(v) {
232
- _debug = v;
233
- if (v) _verbose = true;
234
- }
235
- function debugLog(...args) {
236
- if (_debug) process.stderr.write("[debug] " + args.join(" ") + "\n");
237
- }
322
+ import { applySizeGuard } from "@getwebp/core/size-guard";
323
+ import { findOptimalQuality } from "@getwebp/core/auto-quality";
238
324
 
239
325
  // src/core/codecs.ts
240
326
  import { decode as decodeJpeg } from "@jsquash/jpeg";
241
327
  import { decode as decodePng } from "@jsquash/png";
242
328
  import { decode as decodeWebp } from "@jsquash/webp";
329
+ import { decode as decodeAvifWasm } from "@jsquash/avif";
243
330
  import Bmp from "bmp-js";
244
331
  import fs from "node:fs/promises";
245
- import path from "node:path";
332
+ import path2 from "node:path";
333
+ async function decodeHeic(buffer) {
334
+ const heicDecode = (await import("heic-decode")).default;
335
+ const { width, height, data } = await heicDecode({ buffer: new Uint8Array(buffer) });
336
+ return new ImageData(
337
+ new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
338
+ width,
339
+ height
340
+ );
341
+ }
246
342
  var decoders = {
247
343
  ".jpg": decodeJpeg,
248
344
  ".jpeg": decodeJpeg,
249
345
  ".png": decodePng,
250
- ".webp": decodeWebp
346
+ ".webp": decodeWebp,
347
+ ".avif": async (buf) => {
348
+ const result = await decodeAvifWasm(buf);
349
+ if (!result) throw new Error("AVIF decode returned null");
350
+ return result;
351
+ },
352
+ ".heic": decodeHeic,
353
+ ".heif": decodeHeic
251
354
  };
252
355
  var MAGIC_VALIDATORS = {
253
356
  ".jpg": (b) => b[0] === 255 && b[1] === 216 && b[2] === 255,
254
357
  ".jpeg": (b) => b[0] === 255 && b[1] === 216 && b[2] === 255,
255
358
  ".png": (b) => b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71,
256
359
  ".webp": (b) => b.length > 11 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80,
360
+ ".heic": (b) => b.length > 11 && b[4] === 102 && b[5] === 116 && b[6] === 121 && b[7] === 112 && (b[8] === 104 && b[9] === 101 && b[10] === 105 && b[11] === 99 || // "heic"
361
+ b[8] === 104 && b[9] === 101 && b[10] === 105 && b[11] === 102 || // "heif"
362
+ b[8] === 109 && b[9] === 105 && b[10] === 102 && b[11] === 49),
363
+ ".heif": (b) => b.length > 11 && b[4] === 102 && b[5] === 116 && b[6] === 121 && b[7] === 112 && (b[8] === 104 && b[9] === 101 && b[10] === 105 && b[11] === 99 || b[8] === 104 && b[9] === 101 && b[10] === 105 && b[11] === 102 || b[8] === 109 && b[9] === 105 && b[10] === 102 && b[11] === 49),
364
+ ".avif": (b) => b.length > 11 && b[4] === 102 && b[5] === 116 && b[6] === 121 && b[7] === 112 && (b[8] === 97 && b[9] === 118 && b[10] === 105 && b[11] === 102 || // "avif"
365
+ b[8] === 97 && b[9] === 118 && b[10] === 105 && b[11] === 115),
257
366
  ".bmp": (b) => b[0] === 66 && b[1] === 77
258
367
  };
259
368
  var BMP_MAX_DIMENSION = 65535;
@@ -276,7 +385,7 @@ async function decodeBmp(buffer) {
276
385
  return new ImageData(new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength), bmp.width, bmp.height);
277
386
  }
278
387
  async function decodeImage(filePath) {
279
- const ext = path.extname(filePath).toLowerCase();
388
+ const ext = path2.extname(filePath).toLowerCase();
280
389
  const buffer = await fs.readFile(filePath);
281
390
  if (buffer.byteLength === 0) {
282
391
  throw new Error(`File is empty (0 bytes): ${filePath}`);
@@ -307,13 +416,13 @@ async function decodeImage(filePath) {
307
416
 
308
417
  // src/core/file-scanner.ts
309
418
  import fs2 from "node:fs/promises";
310
- import path2 from "node:path";
311
- var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".bmp", ".webp"]);
312
- async function collectImageFiles(input, recursive) {
419
+ import path3 from "node:path";
420
+ var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".bmp", ".webp", ".heic", ".heif", ".avif"]);
421
+ var defaultWarn = (msg) => process.stderr.write(msg + "\n");
422
+ async function collectImageFiles(input, recursive, warn = defaultWarn) {
313
423
  const lstat = await fs2.lstat(input);
314
424
  if (lstat.isSymbolicLink()) {
315
- process.stderr.write(`Warning: ${input} is a symlink, skipping
316
- `);
425
+ warn(`Warning: ${input} is a symlink, skipping`);
317
426
  return [];
318
427
  }
319
428
  if (lstat.isFile()) return [input];
@@ -321,18 +430,17 @@ async function collectImageFiles(input, recursive) {
321
430
  try {
322
431
  entries = await fs2.readdir(input, { withFileTypes: true });
323
432
  } catch {
324
- process.stderr.write(`Warning: cannot read directory ${input}, skipping
325
- `);
433
+ warn(`Warning: cannot read directory ${input}, skipping`);
326
434
  return [];
327
435
  }
328
436
  entries.sort((a, b) => a.name.localeCompare(b.name));
329
437
  const results = [];
330
438
  const subdirPromises = [];
331
439
  for (const entry of entries) {
332
- const fullPath = path2.join(input, entry.name);
440
+ const fullPath = path3.join(input, entry.name);
333
441
  if (entry.isDirectory() && recursive && !entry.isSymbolicLink()) {
334
- subdirPromises.push(collectImageFiles(fullPath, recursive));
335
- } else if (entry.isFile() && !entry.isSymbolicLink() && IMAGE_EXTS.has(path2.extname(entry.name).toLowerCase())) {
442
+ subdirPromises.push(collectImageFiles(fullPath, recursive, warn));
443
+ } else if (entry.isFile() && !entry.isSymbolicLink() && IMAGE_EXTS.has(path3.extname(entry.name).toLowerCase())) {
336
444
  results.push(fullPath);
337
445
  }
338
446
  }
@@ -343,27 +451,34 @@ async function collectImageFiles(input, recursive) {
343
451
 
344
452
  // src/core/wasm-init.ts
345
453
  import { readFileSync } from "node:fs";
346
- import { createRequire as createRequire3 } from "node:module";
454
+ import { createRequire } from "node:module";
347
455
  import { init as initPngCodec } from "@jsquash/png/decode.js";
348
456
  import { init as initJpegDec } from "@jsquash/jpeg/decode.js";
349
457
  import { init as initWebpDec } from "@jsquash/webp/decode.js";
350
458
  import { init as initWebpEnc } from "@jsquash/webp/encode.js";
351
- var _require3 = createRequire3(import.meta.url);
459
+ import { init as initAvifDec } from "@jsquash/avif/decode.js";
460
+ var _require = createRequire(import.meta.url);
352
461
  var initialized = false;
353
462
  async function initWasm() {
354
463
  if (initialized) return;
355
464
  initialized = true;
356
- const pngWasm = new WebAssembly.Module(readFileSync(_require3.resolve("@jsquash/png/codec/pkg/squoosh_png_bg.wasm")));
465
+ const pngWasm = new WebAssembly.Module(readFileSync(_require.resolve("@jsquash/png/codec/pkg/squoosh_png_bg.wasm")));
357
466
  await initPngCodec(pngWasm);
358
- const jpegWasm = new WebAssembly.Module(readFileSync(_require3.resolve("@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm")));
467
+ const jpegWasm = new WebAssembly.Module(readFileSync(_require.resolve("@jsquash/jpeg/codec/dec/mozjpeg_dec.wasm")));
359
468
  await initJpegDec(jpegWasm);
360
- const webpDecWasm = new WebAssembly.Module(readFileSync(_require3.resolve("@jsquash/webp/codec/dec/webp_dec.wasm")));
469
+ const webpDecWasm = new WebAssembly.Module(readFileSync(_require.resolve("@jsquash/webp/codec/dec/webp_dec.wasm")));
361
470
  await initWebpDec(webpDecWasm);
362
- const webpEncWasm = new WebAssembly.Module(readFileSync(_require3.resolve("@jsquash/webp/codec/enc/webp_enc.wasm")));
471
+ const webpEncWasm = new WebAssembly.Module(readFileSync(_require.resolve("@jsquash/webp/codec/enc/webp_enc.wasm")));
363
472
  await initWebpEnc(webpEncWasm);
473
+ const avifDecWasm = new WebAssembly.Module(readFileSync(_require.resolve("@jsquash/avif/codec/dec/avif_dec.wasm")));
474
+ await initAvifDec(avifDecWasm);
364
475
  }
365
476
 
366
477
  // src/core/processor.ts
478
+ var HEIC_AVIF_EXTS = /* @__PURE__ */ new Set([".heic", ".heif", ".avif"]);
479
+ function isHeicOrAvif(filePath) {
480
+ return HEIC_AVIF_EXTS.has(path4.extname(filePath).toLowerCase());
481
+ }
367
482
  async function processImages(options) {
368
483
  const { input, output, quality, plan, out } = options;
369
484
  const isFree = plan === "free";
@@ -383,7 +498,7 @@ async function processImages(options) {
383
498
  }
384
499
  await initWasm();
385
500
  const recursive = options.recursive ?? false;
386
- const files = await collectImageFiles(input, recursive);
501
+ const files = await collectImageFiles(input, recursive, (msg) => out.warn(msg));
387
502
  debugLog("collected", files.length, "files");
388
503
  if (isFree && files.length > 0) {
389
504
  out.warn("Free plan: max 10 files, 3s delay between each.");
@@ -393,23 +508,23 @@ async function processImages(options) {
393
508
  const skipped = isFree ? Math.max(0, files.length - FREE_TIER.FILE_LIMIT) : 0;
394
509
  const outputPaths = /* @__PURE__ */ new Map();
395
510
  for (const file of targets) {
396
- const dir = output ?? path3.dirname(file);
397
- const outName = path3.basename(file, path3.extname(file)) + ".webp";
398
- const outPath = path3.resolve(dir, outName);
511
+ const dir = output ?? path4.dirname(file);
512
+ const outName = path4.basename(file, path4.extname(file)) + ".webp";
513
+ const outPath = path4.resolve(dir, outName);
399
514
  const existing = outputPaths.get(outPath) ?? [];
400
515
  existing.push(file);
401
516
  outputPaths.set(outPath, existing);
402
517
  }
403
518
  for (const [outPath, sources] of outputPaths) {
404
519
  if (sources.length > 1) {
405
- out.warn(`Conflict: ${sources.join(", ")} all map to ${path3.basename(outPath)} \u2014 only the last processed will survive`);
520
+ out.warn(`Conflict: ${sources.join(", ")} all map to ${path4.basename(outPath)} \u2014 only the last processed will survive`);
406
521
  }
407
522
  }
408
523
  if (!output && !dryRun) {
409
524
  out.warn("No --output specified: converted files will be written next to source files.");
410
525
  }
411
526
  if (dryRun) {
412
- const webpCount = targets.filter((f) => path3.extname(f).toLowerCase() === ".webp").length;
527
+ const webpCount = targets.filter((f) => path4.extname(f).toLowerCase() === ".webp").length;
413
528
  const nonWebpCount = targets.length - webpCount;
414
529
  out.warn("Dry run \u2014 no files will be converted:");
415
530
  for (const file of targets) {
@@ -424,19 +539,18 @@ async function processImages(options) {
424
539
  }
425
540
  const results = [];
426
541
  const writingFiles = /* @__PURE__ */ new Set();
542
+ const shouldRegisterSigint = options.registerSigint ?? true;
427
543
  const sigintHandler = () => {
428
544
  for (const f of writingFiles) {
429
545
  try {
430
- unlinkSync(f);
546
+ unlinkSync2(f);
431
547
  } catch {
432
548
  }
433
549
  }
434
- process.stderr.write(`
435
- Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progress cleaned up.
436
- `);
550
+ out.warn(`Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progress cleaned up.`);
437
551
  process.exit(130);
438
552
  };
439
- process.once("SIGINT", sigintHandler);
553
+ if (shouldRegisterSigint) process.once("SIGINT", sigintHandler);
440
554
  try {
441
555
  if (isFree) {
442
556
  for (let i = 0; i < targets.length; i++) {
@@ -449,7 +563,7 @@ Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progre
449
563
  await sleep(FREE_TIER.DELAY_MS);
450
564
  }
451
565
  debugLog("converting", file);
452
- const result = await convertFile(file, output ?? path3.dirname(file), quality, out, skipExisting, writingFiles);
566
+ const result = await convertFile(file, output ?? path4.dirname(file), quality, out, skipExisting, writingFiles);
453
567
  debugLog("done", file, result.status);
454
568
  results.push(result);
455
569
  }
@@ -457,11 +571,13 @@ Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progre
457
571
  const MAX_CONCURRENCY = 32;
458
572
  const rawConcurrency = Number(options.concurrency ?? DEFAULTS.CONCURRENCY);
459
573
  const concurrency = Number.isNaN(rawConcurrency) || rawConcurrency < 1 ? DEFAULTS.CONCURRENCY : Math.min(Math.floor(rawConcurrency), MAX_CONCURRENCY);
460
- const limit = pLimit(concurrency);
461
- const uniqueDirs = new Set(targets.map((file) => output ?? path3.dirname(file)));
574
+ const hasHeicOrAvif = targets.some(isHeicOrAvif);
575
+ const effectiveConcurrency = hasHeicOrAvif ? 1 : concurrency;
576
+ const limit = pLimit(effectiveConcurrency);
577
+ const uniqueDirs = new Set(targets.map((file) => output ?? path4.dirname(file)));
462
578
  await Promise.all([...uniqueDirs].map((dir) => fs3.mkdir(dir, { recursive: true })));
463
579
  const tasks = targets.map((file) => {
464
- const outputDir = output ?? path3.dirname(file);
580
+ const outputDir = output ?? path4.dirname(file);
465
581
  return limit(async () => {
466
582
  debugLog("converting", file);
467
583
  const result = await convertFile(file, outputDir, quality, out, skipExisting, writingFiles);
@@ -472,15 +588,15 @@ Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progre
472
588
  results.push(...await Promise.all(tasks));
473
589
  }
474
590
  } finally {
475
- process.removeListener("SIGINT", sigintHandler);
591
+ if (shouldRegisterSigint) process.removeListener("SIGINT", sigintHandler);
476
592
  }
477
593
  out.summary(results, { skipped, totalFound: files.length, plan });
478
594
  return results;
479
595
  }
480
596
  async function convertFile(filePath, outputDir, quality, out, skipExisting, writingFiles) {
481
- const outName = path3.basename(filePath, path3.extname(filePath)) + ".webp";
482
- const outPath = path3.join(outputDir, outName);
483
- if (path3.extname(filePath).toLowerCase() === ".webp" && path3.normalize(outPath) === path3.normalize(filePath)) {
597
+ const outName = path4.basename(filePath, path4.extname(filePath)) + ".webp";
598
+ const outPath = path4.join(outputDir, outName);
599
+ if (path4.extname(filePath).toLowerCase() === ".webp" && path4.normalize(outPath) === path4.normalize(filePath)) {
484
600
  return { file: filePath, status: "skipped", reason: "existing" };
485
601
  }
486
602
  if (skipExisting) {
@@ -502,29 +618,54 @@ async function convertFile(filePath, outputDir, quality, out, skipExisting, writ
502
618
  return { file: filePath, status: "error", error: `Decode failed: ${msg}` };
503
619
  }
504
620
  let webpBuffer;
621
+ let finalQuality;
622
+ let qualityMode;
623
+ let ssimScore;
505
624
  try {
506
- webpBuffer = await encodeWebp(imageData, { quality });
625
+ if (quality === AUTO_QUALITY_SENTINEL) {
626
+ const codec = {
627
+ encode: (img, q) => encodeWebp(img, { quality: q }),
628
+ decode: (buf) => decodeWebp2(buf)
629
+ };
630
+ const result = await findOptimalQuality(imageData, inputStat.size, codec);
631
+ finalQuality = result.quality;
632
+ ssimScore = result.ssim;
633
+ qualityMode = "auto";
634
+ webpBuffer = result.buffer ?? await encodeWebp(imageData, { quality: finalQuality });
635
+ } else {
636
+ finalQuality = quality;
637
+ qualityMode = "fixed";
638
+ webpBuffer = await encodeWebp(imageData, { quality });
639
+ }
507
640
  } catch (err) {
508
641
  spinner.fail();
509
642
  const msg = err instanceof Error ? err.message : String(err);
510
643
  return { file: filePath, status: "error", error: `Encode failed: ${msg}` };
511
644
  }
645
+ const originalBuffer = (await fs3.readFile(filePath)).buffer;
646
+ const isWebP = path4.extname(filePath).toLowerCase() === ".webp";
647
+ const guard = applySizeGuard({ originalBuffer, encodedBuffer: webpBuffer, isWebP });
648
+ const finalBuffer = guard.buffer;
512
649
  const tmpPath = outPath + ".tmp";
513
650
  if (process.platform === "win32" && tmpPath.length >= 260) {
514
651
  spinner.fail();
515
652
  return { file: filePath, status: "error", error: `Output path too long for Windows (${tmpPath.length} chars, max 260). Use a shorter output directory or enable LongPathsEnabled in Windows registry.` };
516
653
  }
517
654
  writingFiles.add(tmpPath);
518
- await fs3.writeFile(tmpPath, new Uint8Array(webpBuffer));
655
+ await fs3.writeFile(tmpPath, new Uint8Array(finalBuffer));
519
656
  await fs3.rename(tmpPath, outPath);
520
657
  writingFiles.delete(tmpPath);
521
- const savedRatio = 1 - webpBuffer.byteLength / inputStat.size;
522
- spinner.succeed();
658
+ const newSize = finalBuffer.byteLength;
659
+ const savedRatio = 1 - newSize / inputStat.size;
660
+ spinner.succeed({ quality: finalQuality, qualityMode });
523
661
  return {
524
662
  file: filePath,
525
663
  originalSize: inputStat.size,
526
- newSize: webpBuffer.byteLength,
664
+ newSize,
527
665
  savedRatio,
666
+ quality: finalQuality,
667
+ qualityMode,
668
+ ssim: ssimScore,
528
669
  status: "success"
529
670
  };
530
671
  } catch (err) {
@@ -545,7 +686,10 @@ var HumanOutput = class {
545
686
  startFile(file) {
546
687
  const spinner = ora(`Processing ${chalk.cyan(file)}`).start();
547
688
  return {
548
- succeed: () => spinner.succeed(chalk.green(`\u2713 ${file}`)),
689
+ succeed: (result) => {
690
+ const qualityInfo = result?.quality !== void 0 ? ` ${chalk.dim(`q=${result.quality}${result.qualityMode === "auto" ? " (auto)" : ""}`)}` : "";
691
+ spinner.succeed(chalk.green(`\u2713 ${file}`) + qualityInfo);
692
+ },
549
693
  fail: () => spinner.fail(chalk.red(`\u2717 ${file}`))
550
694
  };
551
695
  }
@@ -669,7 +813,7 @@ var earlyFlags = {
669
813
 
670
814
  // src/commands/convert.ts
671
815
  function convertCommand(cli2) {
672
- cli2.command("[path]", "Convert images (JPEG/PNG/BMP/WebP) to WebP format").option("-i, --input <path>", "Input file or directory path (e.g. ./images)").option("-o, --output <path>", "Output directory for converted files (default: overwrite source files in place)").option("-q, --quality <number>", "WebP quality 1\u2013100 (default: 80)", { default: void 0 }).option("--json", "Output results as JSON \u2014 useful for CI pipelines and AI agents").option("--concurrency <number>", "Number of parallel workers (default: auto, paid plans only)", { default: void 0 }).option("-r, --recursive", "Recursively process images in subdirectories").option("--dry-run", "Preview which files would be converted without actually converting them").option("--skip-existing", "Skip conversion if a .webp file already exists at the output path").action((argPath, options) => {
816
+ cli2.command("[path]", "Convert images to WebP format. Original files are never modified or deleted.").option("-i, --input <path>", "Input file or directory path (e.g. ./images)").option("-o, --output <path>", "Output directory for converted files (default: write .webp next to source files)").option("-q, --quality <number>", "WebP quality 1\u2013100 (default: 80)", { default: void 0 }).option("--json", "Output results as JSON \u2014 useful for CI pipelines and AI agents").option("--concurrency <number>", "Number of parallel workers (default: auto, paid plans only)", { default: void 0 }).option("-r, --recursive", "Recursively process images in subdirectories").option("--dry-run", "Preview which files would be converted without actually converting them").option("--skip-existing", "Skip conversion if a .webp file already exists at the output path").option("--no-auto-quality", "Disable auto quality, use fixed quality=80").action((argPath, options) => {
673
817
  runAction(async () => {
674
818
  const targetPath = argPath || options.input;
675
819
  if (!targetPath) {
@@ -684,8 +828,20 @@ function convertCommand(cli2) {
684
828
  }
685
829
  const out = createOutput(options.json ?? false);
686
830
  const licenseStatus = await checkLicense();
687
- const rawQuality = Number(options.quality ?? 80);
688
- const quality = Number.isNaN(rawQuality) ? 80 : Math.max(1, Math.min(100, rawQuality));
831
+ if (!licenseStatus.valid && licenseStatus.expired) {
832
+ out.warn("Your license has expired. Please renew to continue using Pro features.");
833
+ } else if (!licenseStatus.valid && !licenseStatus.expired) {
834
+ out.warn("Free mode: conversion is limited. Activate a license for full access.");
835
+ }
836
+ let quality;
837
+ if (options.quality !== void 0) {
838
+ const rawQuality = Number(options.quality);
839
+ quality = Number.isNaN(rawQuality) ? 80 : Math.max(1, Math.min(100, rawQuality));
840
+ } else if (options.autoQuality === false) {
841
+ quality = 80;
842
+ } else {
843
+ quality = AUTO_QUALITY_SENTINEL;
844
+ }
689
845
  const results = await processImages({
690
846
  input: targetPath,
691
847
  output: options.output,
@@ -738,7 +894,7 @@ function authCommand(cli2) {
738
894
  }
739
895
 
740
896
  // src/version.ts
741
- var VERSION = true ? "1.0.1" : await getDevVersion();
897
+ var VERSION = true ? "1.1.0" : await getDevVersion();
742
898
 
743
899
  // src/commands/status.ts
744
900
  function statusCommand(cli2) {
@@ -857,7 +1013,7 @@ function logoutCommand(cli2) {
857
1013
  );
858
1014
  await safeExit(1);
859
1015
  } else if (result.error === "not_activated") {
860
- process.stderr.write("No active license found on this device.\n");
1016
+ process.stderr.write("No active Pro license found on this device.\n");
861
1017
  await safeExit(1);
862
1018
  } else {
863
1019
  process.stderr.write(`\u2717 Logout failed: ${result.error}
@@ -882,7 +1038,7 @@ function errorMessage(error) {
882
1038
  case "network_unreachable":
883
1039
  return "Cannot reach server. Retry later or unbind via dashboard.";
884
1040
  case "not_activated":
885
- return "No active license on this device.";
1041
+ return "No active Pro license on this device.";
886
1042
  case "device_not_found":
887
1043
  return "Device not found. It may have already been unbound.";
888
1044
  case "invalid_token":
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "getwebp",
3
- "version": "1.0.1",
4
- "description": "Convert images to WebP/AVIF from the command line",
3
+ "version": "1.1.1",
4
+ "description": "Convert images to WebP from the command line",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "getwebp": "dist/index.js"
@@ -14,12 +14,15 @@
14
14
  },
15
15
  "scripts": {
16
16
  "dev": "bun run src/index.ts",
17
- "build": "CLI_VERSION=$(node -p \"require('./package.json').version\") bun build src/index.ts --compile --minify --define \"CLI_VERSION='$CLI_VERSION'\" --outfile release/getwebp",
17
+ "build": "export CLI_VERSION=$(node -p \"require('./package.json').version\") && bun build src/index.ts --compile --minify --define \"CLI_VERSION='$CLI_VERSION'\" --outfile release/getwebp",
18
18
  "typecheck": "tsc --noEmit",
19
19
  "test": "bun test --timeout 60000 tests/isolated/ && bun test --timeout 60000 tests/",
20
+ "test:e2e": "bun test --timeout 60000 tests/e2e/",
21
+ "test:e2e:binary": "USE_BINARY=1 bun test --timeout 60000 tests/e2e/",
20
22
  "test:watch": "bun test --timeout 60000 --watch"
21
23
  },
22
24
  "dependencies": {
25
+ "@getwebp/core": "*",
23
26
  "@jsquash/jpeg": "^1.5.0",
24
27
  "@jsquash/png": "^3.1.0",
25
28
  "@jsquash/webp": "^1.4.0",
@@ -30,7 +33,9 @@
30
33
  "jsonwebtoken": "^9.0.2",
31
34
  "node-machine-id": "^1.1.12",
32
35
  "ora": "^8.1.0",
33
- "p-limit": "^6.2.0"
36
+ "p-limit": "^6.2.0",
37
+ "@jsquash/avif": "^2.1.1",
38
+ "heic-decode": "^2.1.0"
34
39
  },
35
40
  "devDependencies": {
36
41
  "@types/jsonwebtoken": "^9.0.7",