cantarella 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 LeoZhao
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/Readme.md ADDED
@@ -0,0 +1,127 @@
1
+ <div align="center">
2
+
3
+ # 🖼️ cantarella
4
+
5
+ **批量压缩图片的命令行工具**
6
+
7
+ 传入一个文件夹或单张图片,自动压缩覆盖,覆盖前把原图备份到 `.backup/` 目录。
8
+
9
+ [![Node.js](https://img.shields.io/badge/node-%E2%89%A522.12-brightgreen)](https://nodejs.org/)
10
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](./LICENSE)
11
+ [![TypeScript](https://img.shields.io/badge/TypeScript-zero--build-3178C6)](https://www.typescriptlang.org/)
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ ## ✨ 特性
18
+
19
+ - **两种使用方式**:不传参数进入交互问答模式;传参数则为纯 CLI 模式,可写进脚本
20
+ - **安全第一**:覆盖原文件前自动备份到 `.backup/`;重压缩不会变小的文件自动跳过
21
+ - **`--dry` 预览**:真实编码计算压缩后大小,但不写任何文件
22
+ - **`--report` 报告**:在目标目录生成 markdown 压缩报告
23
+ - **格式转换**:jpeg / png / webp / avif / tiff / gif 互转,支持 heic(iPhone 照片)与 svg 输入
24
+ - **并发压缩**:基于 libuv 线程池的 4 路并发 + p-limit 控制
25
+ - **发布纯 JS**:npm 包只含编译后的 JS,不携带 TS 源码;开发时 Node 原生跑 TS,零构建
26
+
27
+ ## 📦 安装
28
+
29
+ ```bash
30
+ npm install -g cantarella
31
+ ```
32
+
33
+ 或从源码运行:
34
+
35
+ ```bash
36
+ git clone https://github.com/zzl110712/cantarella.git
37
+ pnpm install
38
+ pnpm build # 编译 TS 到 dist/(bin 入口指向编译产物)
39
+ npm link # 注册全局命令 cantarella
40
+ ```
41
+
42
+ > 要求 Node.js ≥ 22.12.0(npm 包为编译后的纯 JS,无需构建)
43
+
44
+ ## 🚀 用法
45
+
46
+ ```bash
47
+ cantarella compress # 交互模式:逐项问答
48
+ cantarella compress ./photos # 压缩目录下所有图片(仅当前层)
49
+ cantarella compress ./photos -r # 递归子目录
50
+ cantarella compress ./a.jpg -q 60 # 单文件 + 指定质量
51
+ cantarella compress ./photos -f webp # 全部转成 webp
52
+ cantarella compress ./photos -w 1920 # 限制最大宽度 1920(只缩小不放大)
53
+ cantarella compress ./photos --dry # 预览:真实计算大小但不写任何文件
54
+ cantarella compress ./photos --report # 生成 markdown 压缩报告
55
+ ```
56
+
57
+ 交互模式中直接回车即采用默认值;ESC / Ctrl+C 随时取消。
58
+
59
+ ## ⚙️ 选项
60
+
61
+ | 选项 | 说明 | 默认值 |
62
+ | ------------------------ | ------------------------------------------------- | ------------- |
63
+ | `-q, --quality <number>` | 压缩质量 1-100(png 映射为压缩等级) | `80` |
64
+ | `-f, --format <format>` | 输出格式:`jpeg` `png` `webp` `avif` `tiff` `gif` | 保持原格式 |
65
+ | `-w, --max-width <px>` | 等比缩放的最大宽度,只缩小不放大 | `0`(不缩放) |
66
+ | `-r, --recursive` | 递归处理子目录 | 关闭 |
67
+ | `--dry` | 预览模式:真实计算压缩后大小,但不写任何文件 | 关闭 |
68
+ | `--report` | 在目标目录生成 markdown 压缩报告 | 关闭 |
69
+
70
+ ## 📋 支持格式
71
+
72
+ | 方向 | 格式 | 说明 |
73
+ | ---- | ------------------------------------------- | ----------------------------- |
74
+ | 输入 | jpg / jpeg / png / webp / avif / tiff / gif | 与输出相同 |
75
+ | 输入 | heic / heif | iPhone 照片,自动转 jpeg |
76
+ | 输入 | svg | 矢量图,自动转 png |
77
+ | 输出 | jpeg / png / webp / avif / tiff / gif | heic 预编译版只能解码不能编码 |
78
+
79
+ ## 💡 PNG 压缩提示
80
+
81
+ PNG 是无损格式,默认 `-q 80` 只做无损重编码——对已经优化过的 PNG 基本压不动(会被自动跳过)。想真正压小 PNG:
82
+
83
+ ```bash
84
+ cantarella compress ./images -q 60 # 开启 palette 量化(≤256 色),典型节省 80%+
85
+ cantarella compress ./images -f webp # 照片类收益最大
86
+ ```
87
+
88
+ - **截图 / UI 图形**(大面积纯色):`-q 60` 效果极好
89
+ - **照片**存 PNG:palette 容易出色带,建议转 `webp` 或 `avif`
90
+
91
+ ## 🛠️ 技术栈
92
+
93
+ | 库 | 用途 |
94
+ | -------------------------------------------------------- | ---------------------------------------- |
95
+ | [sharp](https://sharp.pixelplumbing.com/) | 图片解码 / 缩放 / 重编码(基于 libvips) |
96
+ | [p-limit](https://github.com/sindresorhus/p-limit) | 并发控制(同时处理 4 张) |
97
+ | [commander](https://github.com/tj/commander.js) | 子命令与选项解析 |
98
+ | [@clack/prompts](https://github.com/bombshell-dev/clack) | 交互问答 + spinner + 结论行输出 |
99
+ | [chalk](https://github.com/chalk/chalk) | 终端着色 |
100
+
101
+ ## 🔧 开发
102
+
103
+ ```bash
104
+ pnpm install # 安装依赖
105
+ pnpm typecheck # tsc 类型检查
106
+ pnpm start # 直接运行 TS 源码(--conditions=development,无需编译)
107
+ pnpm build # 编译并压缩到 dist/(发布、npm link 前需要)
108
+ ```
109
+
110
+ ## 📜 版本记录
111
+
112
+ ### 🎉 1.0.0 · 2026-09-04
113
+
114
+ 首个版本。
115
+
116
+ - ✨ **新增** `compress` 子命令:批量压缩目录 / 单文件,覆盖前自动备份到 `.backup/`
117
+ - ✨ **新增** 交互问答模式(不传目标路径时)与全参数 CLI 模式(可脚本化)
118
+ - ✨ **新增** `-q` `-f` `-w` `-r` `--dry` `--report` 全套选项与参数校验
119
+ - ✨ **新增** `--dry` 预览与 `--report` markdown 压缩报告
120
+ - ✨ **新增** heic / heif / svg 输入支持(自动转 jpeg / png)
121
+ - ⚡ **优化** mozjpeg 编码器、4 路并发、临时文件 + rename 原子写入
122
+ - 🐛 **修复** 交互问答空输入直接回车无法应用默认值的问题
123
+ - 🐛 **修复** 备份文件路径错误
124
+
125
+ ## 📄 License
126
+
127
+ [ISC](./LICENSE) © LeoZhao
@@ -0,0 +1,2 @@
1
+ #! /usr/bin/env node
2
+ import{program as r}from"commander";import{myCompress as m}from"#lib/core/command";r.name("cantarella"),m(r),r.parse(process.argv);
package/dist/config.js ADDED
@@ -0,0 +1 @@
1
+ var e={compress:{extensions:[".jpg",".jpeg",".png",".webp",".avif",".tiff",".gif",".heic",".heif",".svg"],ignoreDirs:[".backup"],defaultConcurrency:4}};export{e as default};
@@ -0,0 +1 @@
1
+ import{InvalidArgumentError as t,Option as n}from"commander";import{compress as e}from"./compress.js";import{OUTPUT_FORMATS as i}from"./compressImage.js";function m(r){const o=Number.parseInt(r,10);if(Number.isNaN(o)||o<1||o>100)throw new t("\u5FC5\u987B\u662F 1 - 100 \u7684\u6574\u6570");return o}function p(r){const o=Number.parseInt(r,10);if(Number.isNaN(o)||o<=0)throw new t("\u5FC5\u987B\u662F\u6B63\u6574\u6570");return o}const a=new n("-f, --format <format>","\u8F6C\u6362\u8F93\u51FA\u683C\u5F0F\uFF08\u9ED8\u8BA4\u4FDD\u6301\u539F\u683C\u5F0F\uFF09").choices([...i]);function d(r){r.command("compress [target]").description("\u6279\u91CF\u538B\u7F29\u56FE\u7247\uFF08\u8986\u76D6\u539F\u6587\u4EF6\u524D\u81EA\u52A8\u5907\u4EFD\u5230 .backup/ \u76EE\u5F55\uFF09").option("-q, --quality <number>","\u538B\u7F29\u8D28\u91CF 1-100\uFF0C\u9ED8\u8BA4 80\uFF08png \u6620\u5C04\u4E3A\u538B\u7F29\u7B49\u7EA7\uFF09",m,80).addOption(a).option("-w, --max-width <px>","\u7B49\u6BD4\u7F29\u653E\u7684\u6700\u5927\u5BBD\u5EA6\uFF0C\u53EA\u7F29\u5C0F\u4E0D\u653E\u5927",p).option("-r, --recursive","\u9012\u5F52\u5904\u7406\u5B50\u76EE\u5F55",!1).option("--dry","\u9884\u89C8\u6A21\u5F0F\uFF1A\u771F\u5B9E\u8BA1\u7B97\u538B\u7F29\u540E\u5927\u5C0F\uFF0C\u4F46\u4E0D\u5199\u4EFB\u4F55\u6587\u4EF6",!1).option("--report","\u5728\u76EE\u6807\u76EE\u5F55\u751F\u6210 markdown \u538B\u7F29\u62A5\u544A",!1).action(e)}export{d as myCompress};
@@ -0,0 +1 @@
1
+ import*as r from"@clack/prompts";import F from"chalk";import O from"p-limit";import u from"node:path";import{collectImages as R,formatBytes as d,formatPercent as B,resolveRoot as S}from"#lib/utils/fs";import{accent as I}from"#lib/utils/theme";import{compressOne as P,OUTPUT_FORMATS as j}from"./compressImage.js";import{buildReport as A,writeReport as L}from"./report.js";import x from"#config";const J=async(p,i,W)=>{r.intro(I("Cantarella \u56FE\u7247\u538B\u7F29\u5DE5\u5177"));const c=p===void 0?process.cwd():u.resolve(p.trim());let g=i.recursive??!1,h=i.report??!1;if(p===void 0){const o=t=>W.getOptionValueSource(t)==="cli",s=await r.group({...o("quality")?{}:{quality:()=>r.text({message:"\u538B\u7F29\u8D28\u91CF\uFF081-100\uFF09",placeholder:"80",defaultValue:"80",validate:t=>{if(t===void 0||t.trim()==="")return;const n=Number(t);if(!Number.isInteger(n)||n<1||n>100)return"\u8BF7\u8F93\u5165 1 - 100 \u8303\u56F4\u5185\u7684\u6574\u6570"}})},...o("format")?{}:{format:()=>r.select({message:"\u8F93\u51FA\u683C\u5F0F",options:[{value:"",label:"\u4FDD\u6301\u539F\u683C\u5F0F",hint:"heic => jpeg, svg => png \u81EA\u52A8\u8F6C\u6362"},...j.map(t=>({value:t,label:t}))]})},...o("maxWidth")?{}:{maxWidth:()=>r.text({message:"\u6700\u5927\u5BBD\u5EA6 px\uFF080 = \u4E0D\u7F29\u653E\uFF09",placeholder:"0",defaultValue:"0",validate:t=>{if(t===void 0||t.trim()==="")return;const n=Number(t);if(!Number.isInteger(n)||n<0)return"\u8BF7\u8F93\u5165\u4E0D\u5C0F\u4E8E 0 \u7684\u6574\u6570"}})},...o("recursive")?{}:{recursive:()=>r.confirm({message:"\u9012\u5F52\u5904\u7406\u5B50\u76EE\u5F55\uFF1F",initialValue:!1})},...o("report")?{}:{report:()=>r.confirm({message:"\u751F\u6210 markdown \u538B\u7F29\u62A5\u544A\uFF1F",initialValue:!1})}},{onCancel:()=>{r.cancel("\u7528\u6237\u53D6\u6D88\u64CD\u4F5C"),process.exit(0)}});s.quality!==void 0&&(i.quality=Number(s.quality)),s.format!==void 0&&(i.format=s.format),s.maxWidth!==void 0&&Number(s.maxWidth)>0&&(i.maxWidth=Number(s.maxWidth)),s.recursive!==void 0&&(g=s.recursive),s.report!==void 0&&(h=s.report)}const q=i.quality??80,N=i.format===void 0||i.format===""?void 0:i.format,T=i.maxWidth,m=i.dry??!1,l=r.spinner();try{l.start("\u6B63\u5728\u626B\u63CF\u6587\u4EF6...");let o="",s=!1;try{const e=await S(c);o=e.root,s=e.isFile}catch{l.error(`\u8DEF\u5F84\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF1A${c}`),process.exitCode=1;return}let t;if(s){const e=u.extname(c).toLowerCase();if(!x.compress.extensions.includes(e)){l.error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u7C7B\u578B\uFF1A${e===""?"\uFF08\u65E0\u6269\u5C55\u540D\uFF09":e}`),process.exitCode=1;return}t=[c]}else t=await R(o,g);if(t.length===0){l.stop(),r.outro("\u6CA1\u6709\u627E\u5230\u53EF\u5904\u7406\u7684\u56FE\u7247");return}const n={quality:q,format:N,maxWidth:T,dry:m},k=O(x.compress.defaultConcurrency),V=new Set(t);let y=0;l.message(`${m?"\u9884\u89C8":"\u538B\u7F29"} ${y}/${t.length}`);const f=await Promise.all(t.map(e=>k(async()=>{const a=await P(e,n,o,V);return y+=1,l.message(`${m?"\u9884\u89C8":"\u538B\u7F29"} ${y}/${t.length} ${u.basename(e)}`),a}))),b=f.filter(e=>e.status==="skipped"),$=f.filter(e=>e.status==="failed"),w=f.reduce((e,a)=>e+a.beforeBytes,0),v=f.reduce((e,a)=>e+(a.status==="failed"?a.beforeBytes:a.afterBytes),0),C=[`${m?"\u9884\u89C8":"\u538B\u7F29"}\u5B8C\u6210\uFF1A${f.length} \u4E2A\u6587\u4EF6`,`${d(w)} \u2192 ${d(v)}`,`\u8282\u7701 ${B(w,v)}`];if(b.length>0&&C.push(`\u8DF3\u8FC7 ${b.length}`),l.stop(),r.log.success(C.join(",")),$.length>0){r.log.error(`\u5931\u8D25 ${$.length} \u4E2A\uFF1A`);for(const e of $)r.log.error(`${u.relative(o,e.file)} \u2014\u2014 ${e.error??"\u672A\u89E3\u6790\u51FA\u53D1\u751F\u4E86\u4EC0\u4E48"}`);process.exitCode=1}if(m){r.log.message(F.bold("\u9884\u89C8\u660E\u7EC6\uFF08\u672A\u5199\u5165\u4EFB\u4F55\u6587\u4EF6\uFF09\uFF1A"));for(const e of f){const a=u.relative(o,e.file);e.status==="failed"?r.log.error(`${a} \u2014\u2014 ${e.error}`):e.status==="skipped"?r.log.warn(`${a} \u4F1A\u8DF3\u8FC7\uFF08\u91CD\u538B\u7F29\u4E0D\u4F1A\u53D8\u5C0F\uFF09`):r.log.success(`${a} ${d(e.beforeBytes)} => ${d(e.afterBytes)}\uFF08\u8282\u7701 ${B(e.beforeBytes,e.afterBytes)}\uFF09`)}h&&r.log.warn("--dry \u6A21\u5F0F\u627F\u8BFA\u4E0D\u4F1A\u8F93\u51FA\u4EFB\u4F55\u6587\u4EF6\uFF0C\u5DF2\u5FFD\u7565 --report"),r.outro("\u9884\u89C8\u5B8C\u6210");return}if(h){const e={target:c,root:o,recursive:g,params:n,concurrency:x.compress.defaultConcurrency,startedAt:new Date},a=await L(o,A(e,f));r.log.info(`\u62A5\u544A\u5DF2\u751F\u6210\uFF1A${a}`),r.outro("\u5B8C\u6210")}}catch(o){l.stop(),r.log.error(o instanceof Error?o.message:String(o)),process.exitCode=1}};export{J as compress};
@@ -0,0 +1 @@
1
+ import m from"sharp";import w from"path";import{randomUUID as j}from"crypto";import{readFile as v,rename as F,unlink as O,writeFile as T}from"fs/promises";import{backupFile as y,replaceExt as L}from"#lib/utils/fs";const _=["jpeg","png","webp","avif","tiff","gif"],$=["heif","svg"],l={jpeg:".jpg",png:".png",webp:".webp",avif:".avif",tiff:".tiff",gif:".gif"},x={heif:"jpeg",svg:"png"},U={".jpg":"jpeg",".jpeg":"jpeg",".png":"png",".webp":"webp",".avif":"avif",".tiff":"tiff",".gif":"gif",".heic":"heif",".heif":"heif",".svg":"svg"};function k(t){return _.includes(t)}function A(t){const r=Math.round((100-t)/100*9);return t<=60?{compressionLevel:r,palette:!0}:{compressionLevel:r}}function B(t,r,o){switch(r){case"jpeg":return t.jpeg({quality:o,mozjpeg:!0});case"png":return t.png(A(o));case"webp":return t.webp({quality:o});case"avif":return t.avif({quality:o});case"tiff":return t.tiff({quality:o});case"gif":return t.gif()}}async function I(t,r,o,d){const e={file:t,output:t,format:"jpeg",beforeBytes:0,afterBytes:0,status:"failed"};try{const n=await v(t);e.beforeBytes=n.byteLength;const p=w.extname(t).toLowerCase(),f=U[p];if(f===void 0)throw new Error(`\u4E0D\u652F\u6301\u7684\u56FE\u7247\u6269\u5C55\u540D\uFF1A${p===""?"\u65E0\u6269\u5C55\u540D":p}`);const u=await m(n).metadata(),s=r.format??(k(f)?f:x[f]),c=s===f,i=c?t:L(t,l[s]);if(e.output=i,e.format=s,i!==t&&d.has(i))throw new Error(`\u8F93\u51FA ${w.basename(i)} \u548C\u5F85\u5904\u7406\u6587\u4EF6\u540C\u540D\uFF0C\u5DF2\u8DF3\u8FC7\u8F6C\u6362\uFF08\u8BE5\u6587\u4EF6\u81EA\u8EAB\u4F1A\u88AB\u5904\u7406\uFF09`);let a=m(n);r.maxWidth!==void 0&&u.width!==void 0&&u.width>r.maxWidth&&(a=a.resize({width:r.maxWidth,withoutEnlargement:!0})),s==="jpeg"&&u.hasAlpha&&a.flatten({background:"#ffffff"}),a=B(a,s,r.quality);const{data:g}=await a.toBuffer({resolveWithObject:!0});if(e.afterBytes=g.byteLength,c&&g.byteLength>=n.byteLength)return e.status="skipped",e;if(r.dry)return e.status="done",e;c&&await y(o,t);const h=`${i}${j()}.tmp`;try{await T(h,g),await F(h,i)}catch(b){throw await O(h).catch(()=>{}),b}return e.status="done",e}catch(n){return e.error=n instanceof Error?n.message:String(n),e}}export{_ as OUTPUT_FORMATS,I as compressOne};
@@ -0,0 +1,2 @@
1
+ import{writeFile as d}from"fs/promises";import i from"path";import{formatBytes as u,formatPercent as $}from"#lib/utils/fs";function y(e){const s=o=>String(o).padStart(2,"0");return`${e.getFullYear()}${s(e.getMonth()+1)}${s(e.getDate())}-${s(e.getHours())}${s(e.getMinutes())}${s(e.getSeconds())}`}function w(e,s){const o=s.filter(t=>t.status==="done"),f=s.filter(t=>t.status==="skipped"),c=s.filter(t=>t.status==="failed"),p=s.reduce((t,a)=>t+a.beforeBytes,0),n=s.reduce((t,a)=>t+(a.status==="failed"?a.beforeBytes:a.afterBytes),0),r=[];r.push("# \u56FE\u7247\u538B\u7F29\u62A5\u544A",""),r.push(`- \u8FD0\u884C\u65F6\u95F4\uFF1A${e.startedAt.toLocaleString("zh-CN",{hour12:!1})}`),r.push(`- \u76EE\u6807\uFF1A${e.target}${e.recursive?"\uFF08\u9012\u5F52\uFF09":""}`),r.push(`- \u53C2\u6570\uFF1Aquality=${e.params.quality} format=${e.params.format??"\u4FDD\u6301\u539F\u683C\u5F0F"} maxWidth=${e.params.maxWidth??"\u4E0D\u7F29\u653E"} \u5E76\u53D1=${e.concurrency}${e.params.dry?"\uFF08dry \u9884\u89C8\uFF0C\u672A\u5199\u5165\uFF09":""}`,""),r.push("## \u6C47\u603B",""),r.push("| \u6307\u6807 | \u503C |","|---|---|"),r.push(`| \u5904\u7406\u6587\u4EF6\u6570 | ${s.length} |`),r.push(`| \u6210\u529F / \u8DF3\u8FC7 / \u5931\u8D25 | ${o.length} / ${f.length} / ${c.length} |`),r.push(`| \u538B\u7F29\u524D\u603B\u5927\u5C0F | ${u(p)} |`),r.push(`| \u538B\u7F29\u540E\u603B\u5927\u5C0F | ${u(n)} |`),r.push(`| \u8282\u7701 | ${u(p-n)}\uFF08${$(p,n)}\uFF09 |`,""),r.push("## \u660E\u7EC6",""),r.push("| \u6587\u4EF6 | \u8F93\u51FA\u683C\u5F0F | \u539F\u5927\u5C0F | \u538B\u7F29\u540E | \u8282\u7701 | \u72B6\u6001 |"),r.push("|---|---|---|---|---|---|");for(const t of s){const a=i.relative(e.root,t.file),h=t.status==="failed"?"-":u(t.afterBytes),l=t.status==="failed"?"-":$(t.beforeBytes,t.afterBytes),m=t.status==="failed"?`failed: ${t.error??"\u672A\u77E5\u9519\u8BEF"}`:t.status;r.push(`| ${a} | ${t.format} | ${u(t.beforeBytes)} | ${h} | ${l} | ${m} |`)}return r.push(""),r.join(`
2
+ `)}async function x(e,s){const o=i.join(e,`compress-report-${y(new Date)}.md`);return await d(o,s,"utf-8"),o}export{w as buildReport,x as writeReport};
@@ -0,0 +1 @@
1
+ import{readdir as f,copyFile as m,mkdir as p,stat as l}from"fs/promises";import{constants as d}from"fs";import n from"path";import c from"#config";function y(t){if(t<1024)return`${t} B`;const e=t/1024;if(e<1024)return`${e.toFixed(2)} KB`;const r=e/1024;return r<1024?`${r.toFixed(2)} MB`:`${(r/1024).toFixed(2)} GB`}function B(t,e){return t<=0?"-":`${((1-e/t)*100).toFixed(1)}%`}async function $(t){return(await l(t)).isFile()?{root:n.dirname(t),isFile:!0}:{root:t,isFile:!1}}async function b(t,e){const r=await f(t,{withFileTypes:!0,recursive:e}),o=[];for(const i of r){if(!i.isFile())continue;const s=n.join(i.parentPath,i.name),a=n.extname(i.name).toLowerCase();c.compress.extensions.includes(a)&&(e&&n.relative(t,s).split(n.sep).some(u=>c.compress.ignoreDirs.includes(u))||o.push(s))}return o.sort()}function x(t,e){return n.join(t,".backup",n.relative(t,e))}async function P(t,e){const r=x(t,e);await p(n.dirname(r),{recursive:!0});try{return await m(e,r,d.COPYFILE_EXCL),!0}catch(o){if(o instanceof Error&&o.code==="EEXIST")return!1;throw o}}function g(t,e){const r=n.basename(t,n.extname(t));return n.join(n.dirname(t),r+e)}export{P as backupFile,x as backupPathFor,b as collectImages,y as formatBytes,B as formatPercent,g as replaceExt,$ as resolveRoot};
@@ -0,0 +1 @@
1
+ import c from"chalk";const t=c.hex("#0077BE");export{t as accent};
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "cantarella",
3
+ "version": "1.0.0",
4
+ "description": "批量压缩图片的命令行工具",
5
+ "type": "module",
6
+ "bin": {
7
+ "cantarella": "dist/bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node --conditions=development bin/cli.ts --help",
11
+ "typecheck": "tsc",
12
+ "build": "tsc -p tsconfig.build.json && node scripts/minify.mjs",
13
+ "prepack": "npm run build"
14
+ },
15
+ "engines": {
16
+ "node": ">=22.12.0"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/zzl110712/cantarella.git"
21
+ },
22
+ "author": "LeoZhao",
23
+ "license": "ISC",
24
+ "bugs": {
25
+ "url": "https://github.com/zzl110712/cantarella/issues"
26
+ },
27
+ "homepage": "https://github.com/zzl110712/cantarella#readme",
28
+ "keywords": [
29
+ "image",
30
+ "compress",
31
+ "compression",
32
+ "cli",
33
+ "sharp",
34
+ "webp",
35
+ "avif",
36
+ "jpeg",
37
+ "png",
38
+ "batch",
39
+ "image-optimization"
40
+ ],
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "imports": {
45
+ "#config": {
46
+ "development": "./config.ts",
47
+ "default": "./dist/config.js"
48
+ },
49
+ "#lib/*": {
50
+ "development": "./lib/*.ts",
51
+ "default": "./dist/lib/*.js"
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^26.3.0",
56
+ "esbuild": "^0.28.2",
57
+ "typescript": "^7.0.2"
58
+ },
59
+ "dependencies": {
60
+ "@clack/prompts": "^1.7.0",
61
+ "chalk": "^6.0.0",
62
+ "commander": "^15.0.0",
63
+ "p-limit": "^7.3.1",
64
+ "sharp": "^0.35.4"
65
+ }
66
+ }