getwebp 1.0.0 → 1.1.0

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 +243 -83
  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,29 +9,130 @@ import jwt from "jsonwebtoken";
9
9
 
10
10
  // src/core/config.ts
11
11
  import Conf from "conf";
12
- import { machineIdSync } from "node-machine-id";
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";
13
34
  import crypto from "node:crypto";
14
35
  import os from "node:os";
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
15
97
  function getMachineKey() {
16
98
  try {
17
99
  const id = machineIdSync();
18
- return crypto.createHash("sha256").update(id).digest("hex");
100
+ return crypto2.createHash("sha256").update(id).digest("hex");
19
101
  } catch {
20
- process.stderr.write(
21
- "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."
22
105
  );
23
- 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");
24
107
  }
25
108
  }
26
- var store = new Conf({
27
- projectName: "getwebp",
28
- encryptionKey: getMachineKey(),
29
- ...process.env.GETWEBP_CONFIG_DIR ? { cwd: process.env.GETWEBP_CONFIG_DIR } : {}
30
- });
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
+ }
31
131
  function getConfig() {
32
- return store.store;
132
+ return getStore().store;
33
133
  }
34
134
  function saveConfig(data) {
135
+ const store = getStore();
35
136
  for (const [key, value] of Object.entries(data)) {
36
137
  if (value === void 0) {
37
138
  store.delete(key);
@@ -42,23 +143,23 @@ function saveConfig(data) {
42
143
  }
43
144
 
44
145
  // src/core/device.ts
45
- import { machineId } from "node-machine-id";
46
- import crypto2 from "node:crypto";
146
+ import crypto3 from "node:crypto";
47
147
  async function getDeviceId() {
48
148
  const id = await machineId();
49
- return crypto2.createHash("sha256").update(id).digest("hex");
149
+ return crypto3.createHash("sha256").update(id).digest("hex");
50
150
  }
51
151
 
52
152
  // src/core/constants.ts
53
- import os2 from "node:os";
153
+ import os3 from "node:os";
54
154
  var FREE_TIER = {
55
155
  FILE_LIMIT: 10,
56
156
  DELAY_MS: 3e3
57
157
  };
58
158
  var DEFAULTS = {
59
159
  QUALITY: 80,
60
- CONCURRENCY: Math.max(1, os2.cpus().length - 1)
160
+ CONCURRENCY: Math.max(1, os3.cpus().length - 1)
61
161
  };
162
+ var AUTO_QUALITY_SENTINEL = -1;
62
163
  var NETWORK = {
63
164
  HEARTBEAT_TIMEOUT_MS: 3e3,
64
165
  API_TIMEOUT_MS: 5e3,
@@ -126,7 +227,7 @@ async function activate(licenseKey) {
126
227
  return { success: false, error: err instanceof Error ? err.message : String(err) };
127
228
  }
128
229
  }
129
- async function checkLicense() {
230
+ async function checkLicense(warn) {
130
231
  const config = getConfig();
131
232
  if (!config?.token) return { valid: false, plan: "free" };
132
233
  try {
@@ -145,7 +246,8 @@ async function checkLicense() {
145
246
  return { valid: false, plan: "free", expired: true };
146
247
  }
147
248
  if (err instanceof Error && err.message.includes("JWT_PUBLIC_KEY")) {
148
- 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.");
149
251
  }
150
252
  return { valid: false, plan: "free" };
151
253
  }
@@ -212,44 +314,55 @@ function resolveExitCode(results) {
212
314
  }
213
315
 
214
316
  // src/core/processor.ts
215
- import { encode as encodeWebp } from "@jsquash/webp";
216
- 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";
217
319
  import fs3 from "node:fs/promises";
218
- import path3 from "node:path";
320
+ import path4 from "node:path";
219
321
  import pLimit from "p-limit";
220
-
221
- // src/utils/debug.ts
222
- var _verbose = false;
223
- var _debug = false;
224
- function setVerbose(v) {
225
- _verbose = v;
226
- }
227
- function setDebug(v) {
228
- _debug = v;
229
- if (v) _verbose = true;
230
- }
231
- function debugLog(...args) {
232
- if (_debug) process.stderr.write("[debug] " + args.join(" ") + "\n");
233
- }
322
+ import { applySizeGuard } from "@getwebp/core/size-guard";
323
+ import { findOptimalQuality } from "@getwebp/core/auto-quality";
234
324
 
235
325
  // src/core/codecs.ts
236
326
  import { decode as decodeJpeg } from "@jsquash/jpeg";
237
327
  import { decode as decodePng } from "@jsquash/png";
238
328
  import { decode as decodeWebp } from "@jsquash/webp";
329
+ import { decode as decodeAvifWasm } from "@jsquash/avif";
239
330
  import Bmp from "bmp-js";
240
331
  import fs from "node:fs/promises";
241
- 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
+ }
242
342
  var decoders = {
243
343
  ".jpg": decodeJpeg,
244
344
  ".jpeg": decodeJpeg,
245
345
  ".png": decodePng,
246
- ".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
247
354
  };
248
355
  var MAGIC_VALIDATORS = {
249
356
  ".jpg": (b) => b[0] === 255 && b[1] === 216 && b[2] === 255,
250
357
  ".jpeg": (b) => b[0] === 255 && b[1] === 216 && b[2] === 255,
251
358
  ".png": (b) => b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71,
252
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),
253
366
  ".bmp": (b) => b[0] === 66 && b[1] === 77
254
367
  };
255
368
  var BMP_MAX_DIMENSION = 65535;
@@ -272,7 +385,7 @@ async function decodeBmp(buffer) {
272
385
  return new ImageData(new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength), bmp.width, bmp.height);
273
386
  }
274
387
  async function decodeImage(filePath) {
275
- const ext = path.extname(filePath).toLowerCase();
388
+ const ext = path2.extname(filePath).toLowerCase();
276
389
  const buffer = await fs.readFile(filePath);
277
390
  if (buffer.byteLength === 0) {
278
391
  throw new Error(`File is empty (0 bytes): ${filePath}`);
@@ -303,13 +416,13 @@ async function decodeImage(filePath) {
303
416
 
304
417
  // src/core/file-scanner.ts
305
418
  import fs2 from "node:fs/promises";
306
- import path2 from "node:path";
307
- var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".bmp", ".webp"]);
308
- 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) {
309
423
  const lstat = await fs2.lstat(input);
310
424
  if (lstat.isSymbolicLink()) {
311
- process.stderr.write(`Warning: ${input} is a symlink, skipping
312
- `);
425
+ warn(`Warning: ${input} is a symlink, skipping`);
313
426
  return [];
314
427
  }
315
428
  if (lstat.isFile()) return [input];
@@ -317,18 +430,17 @@ async function collectImageFiles(input, recursive) {
317
430
  try {
318
431
  entries = await fs2.readdir(input, { withFileTypes: true });
319
432
  } catch {
320
- process.stderr.write(`Warning: cannot read directory ${input}, skipping
321
- `);
433
+ warn(`Warning: cannot read directory ${input}, skipping`);
322
434
  return [];
323
435
  }
324
436
  entries.sort((a, b) => a.name.localeCompare(b.name));
325
437
  const results = [];
326
438
  const subdirPromises = [];
327
439
  for (const entry of entries) {
328
- const fullPath = path2.join(input, entry.name);
440
+ const fullPath = path3.join(input, entry.name);
329
441
  if (entry.isDirectory() && recursive && !entry.isSymbolicLink()) {
330
- subdirPromises.push(collectImageFiles(fullPath, recursive));
331
- } 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())) {
332
444
  results.push(fullPath);
333
445
  }
334
446
  }
@@ -340,10 +452,11 @@ async function collectImageFiles(input, recursive) {
340
452
  // src/core/wasm-init.ts
341
453
  import { readFileSync } from "node:fs";
342
454
  import { createRequire } from "node:module";
343
- import { init as initPngCodec } from "@jsquash/png/decode";
344
- import { init as initJpegDec } from "@jsquash/jpeg/decode";
345
- import { init as initWebpDec } from "@jsquash/webp/decode";
346
- import { init as initWebpEnc } from "@jsquash/webp/encode";
455
+ import { init as initPngCodec } from "@jsquash/png/decode.js";
456
+ import { init as initJpegDec } from "@jsquash/jpeg/decode.js";
457
+ import { init as initWebpDec } from "@jsquash/webp/decode.js";
458
+ import { init as initWebpEnc } from "@jsquash/webp/encode.js";
459
+ import { init as initAvifDec } from "@jsquash/avif/decode.js";
347
460
  var _require = createRequire(import.meta.url);
348
461
  var initialized = false;
349
462
  async function initWasm() {
@@ -357,9 +470,15 @@ async function initWasm() {
357
470
  await initWebpDec(webpDecWasm);
358
471
  const webpEncWasm = new WebAssembly.Module(readFileSync(_require.resolve("@jsquash/webp/codec/enc/webp_enc.wasm")));
359
472
  await initWebpEnc(webpEncWasm);
473
+ const avifDecWasm = new WebAssembly.Module(readFileSync(_require.resolve("@jsquash/avif/codec/dec/avif_dec.wasm")));
474
+ await initAvifDec(avifDecWasm);
360
475
  }
361
476
 
362
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
+ }
363
482
  async function processImages(options) {
364
483
  const { input, output, quality, plan, out } = options;
365
484
  const isFree = plan === "free";
@@ -379,7 +498,7 @@ async function processImages(options) {
379
498
  }
380
499
  await initWasm();
381
500
  const recursive = options.recursive ?? false;
382
- const files = await collectImageFiles(input, recursive);
501
+ const files = await collectImageFiles(input, recursive, (msg) => out.warn(msg));
383
502
  debugLog("collected", files.length, "files");
384
503
  if (isFree && files.length > 0) {
385
504
  out.warn("Free plan: max 10 files, 3s delay between each.");
@@ -389,23 +508,23 @@ async function processImages(options) {
389
508
  const skipped = isFree ? Math.max(0, files.length - FREE_TIER.FILE_LIMIT) : 0;
390
509
  const outputPaths = /* @__PURE__ */ new Map();
391
510
  for (const file of targets) {
392
- const dir = output ?? path3.dirname(file);
393
- const outName = path3.basename(file, path3.extname(file)) + ".webp";
394
- 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);
395
514
  const existing = outputPaths.get(outPath) ?? [];
396
515
  existing.push(file);
397
516
  outputPaths.set(outPath, existing);
398
517
  }
399
518
  for (const [outPath, sources] of outputPaths) {
400
519
  if (sources.length > 1) {
401
- 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`);
402
521
  }
403
522
  }
404
523
  if (!output && !dryRun) {
405
524
  out.warn("No --output specified: converted files will be written next to source files.");
406
525
  }
407
526
  if (dryRun) {
408
- const webpCount = targets.filter((f) => path3.extname(f).toLowerCase() === ".webp").length;
527
+ const webpCount = targets.filter((f) => path4.extname(f).toLowerCase() === ".webp").length;
409
528
  const nonWebpCount = targets.length - webpCount;
410
529
  out.warn("Dry run \u2014 no files will be converted:");
411
530
  for (const file of targets) {
@@ -420,19 +539,18 @@ async function processImages(options) {
420
539
  }
421
540
  const results = [];
422
541
  const writingFiles = /* @__PURE__ */ new Set();
542
+ const shouldRegisterSigint = options.registerSigint ?? true;
423
543
  const sigintHandler = () => {
424
544
  for (const f of writingFiles) {
425
545
  try {
426
- unlinkSync(f);
546
+ unlinkSync2(f);
427
547
  } catch {
428
548
  }
429
549
  }
430
- process.stderr.write(`
431
- Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progress cleaned up.
432
- `);
550
+ out.warn(`Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progress cleaned up.`);
433
551
  process.exit(130);
434
552
  };
435
- process.once("SIGINT", sigintHandler);
553
+ if (shouldRegisterSigint) process.once("SIGINT", sigintHandler);
436
554
  try {
437
555
  if (isFree) {
438
556
  for (let i = 0; i < targets.length; i++) {
@@ -445,7 +563,7 @@ Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progre
445
563
  await sleep(FREE_TIER.DELAY_MS);
446
564
  }
447
565
  debugLog("converting", file);
448
- 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);
449
567
  debugLog("done", file, result.status);
450
568
  results.push(result);
451
569
  }
@@ -453,11 +571,13 @@ Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progre
453
571
  const MAX_CONCURRENCY = 32;
454
572
  const rawConcurrency = Number(options.concurrency ?? DEFAULTS.CONCURRENCY);
455
573
  const concurrency = Number.isNaN(rawConcurrency) || rawConcurrency < 1 ? DEFAULTS.CONCURRENCY : Math.min(Math.floor(rawConcurrency), MAX_CONCURRENCY);
456
- const limit = pLimit(concurrency);
457
- 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)));
458
578
  await Promise.all([...uniqueDirs].map((dir) => fs3.mkdir(dir, { recursive: true })));
459
579
  const tasks = targets.map((file) => {
460
- const outputDir = output ?? path3.dirname(file);
580
+ const outputDir = output ?? path4.dirname(file);
461
581
  return limit(async () => {
462
582
  debugLog("converting", file);
463
583
  const result = await convertFile(file, outputDir, quality, out, skipExisting, writingFiles);
@@ -468,15 +588,15 @@ Interrupted. ${results.length} file(s) completed, ${writingFiles.size} in-progre
468
588
  results.push(...await Promise.all(tasks));
469
589
  }
470
590
  } finally {
471
- process.removeListener("SIGINT", sigintHandler);
591
+ if (shouldRegisterSigint) process.removeListener("SIGINT", sigintHandler);
472
592
  }
473
593
  out.summary(results, { skipped, totalFound: files.length, plan });
474
594
  return results;
475
595
  }
476
596
  async function convertFile(filePath, outputDir, quality, out, skipExisting, writingFiles) {
477
- const outName = path3.basename(filePath, path3.extname(filePath)) + ".webp";
478
- const outPath = path3.join(outputDir, outName);
479
- 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)) {
480
600
  return { file: filePath, status: "skipped", reason: "existing" };
481
601
  }
482
602
  if (skipExisting) {
@@ -498,29 +618,54 @@ async function convertFile(filePath, outputDir, quality, out, skipExisting, writ
498
618
  return { file: filePath, status: "error", error: `Decode failed: ${msg}` };
499
619
  }
500
620
  let webpBuffer;
621
+ let finalQuality;
622
+ let qualityMode;
623
+ let ssimScore;
501
624
  try {
502
- 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
+ }
503
640
  } catch (err) {
504
641
  spinner.fail();
505
642
  const msg = err instanceof Error ? err.message : String(err);
506
643
  return { file: filePath, status: "error", error: `Encode failed: ${msg}` };
507
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;
508
649
  const tmpPath = outPath + ".tmp";
509
650
  if (process.platform === "win32" && tmpPath.length >= 260) {
510
651
  spinner.fail();
511
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.` };
512
653
  }
513
654
  writingFiles.add(tmpPath);
514
- await fs3.writeFile(tmpPath, new Uint8Array(webpBuffer));
655
+ await fs3.writeFile(tmpPath, new Uint8Array(finalBuffer));
515
656
  await fs3.rename(tmpPath, outPath);
516
657
  writingFiles.delete(tmpPath);
517
- const savedRatio = 1 - webpBuffer.byteLength / inputStat.size;
518
- spinner.succeed();
658
+ const newSize = finalBuffer.byteLength;
659
+ const savedRatio = 1 - newSize / inputStat.size;
660
+ spinner.succeed({ quality: finalQuality, qualityMode });
519
661
  return {
520
662
  file: filePath,
521
663
  originalSize: inputStat.size,
522
- newSize: webpBuffer.byteLength,
664
+ newSize,
523
665
  savedRatio,
666
+ quality: finalQuality,
667
+ qualityMode,
668
+ ssim: ssimScore,
524
669
  status: "success"
525
670
  };
526
671
  } catch (err) {
@@ -541,7 +686,10 @@ var HumanOutput = class {
541
686
  startFile(file) {
542
687
  const spinner = ora(`Processing ${chalk.cyan(file)}`).start();
543
688
  return {
544
- 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
+ },
545
693
  fail: () => spinner.fail(chalk.red(`\u2717 ${file}`))
546
694
  };
547
695
  }
@@ -665,7 +813,7 @@ var earlyFlags = {
665
813
 
666
814
  // src/commands/convert.ts
667
815
  function convertCommand(cli2) {
668
- 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) => {
669
817
  runAction(async () => {
670
818
  const targetPath = argPath || options.input;
671
819
  if (!targetPath) {
@@ -680,8 +828,20 @@ function convertCommand(cli2) {
680
828
  }
681
829
  const out = createOutput(options.json ?? false);
682
830
  const licenseStatus = await checkLicense();
683
- const rawQuality = Number(options.quality ?? 80);
684
- 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
+ }
685
845
  const results = await processImages({
686
846
  input: targetPath,
687
847
  output: options.output,
@@ -734,7 +894,7 @@ function authCommand(cli2) {
734
894
  }
735
895
 
736
896
  // src/version.ts
737
- var VERSION = true ? "1.0.0" : await getDevVersion();
897
+ var VERSION = true ? "1.1.0" : await getDevVersion();
738
898
 
739
899
  // src/commands/status.ts
740
900
  function statusCommand(cli2) {
@@ -853,7 +1013,7 @@ function logoutCommand(cli2) {
853
1013
  );
854
1014
  await safeExit(1);
855
1015
  } else if (result.error === "not_activated") {
856
- process.stderr.write("No active license found on this device.\n");
1016
+ process.stderr.write("No active Pro license found on this device.\n");
857
1017
  await safeExit(1);
858
1018
  } else {
859
1019
  process.stderr.write(`\u2717 Logout failed: ${result.error}
@@ -878,7 +1038,7 @@ function errorMessage(error) {
878
1038
  case "network_unreachable":
879
1039
  return "Cannot reach server. Retry later or unbind via dashboard.";
880
1040
  case "not_activated":
881
- return "No active license on this device.";
1041
+ return "No active Pro license on this device.";
882
1042
  case "device_not_found":
883
1043
  return "Device not found. It may have already been unbound.";
884
1044
  case "invalid_token":
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "getwebp",
3
- "version": "1.0.0",
4
- "description": "Convert images to WebP/AVIF from the command line",
3
+ "version": "1.1.0",
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",