micropng-cli 0.1.0 β†’ 0.3.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0] - 2026-01-30
9
+
10
+ ### Added
11
+ - Added `--ignore` (`-i`) flag to exclude specific files or directories using glob patterns.
12
+ - Added `--keep-metadata` flag to preserve EXIF and other image metadata during compression.
13
+ - Enhanced CLI tests to cover ignore patterns and metadata handling.
14
+
15
+ ### Changed
16
+ - Major version bump to reflect significant new feature additions.
17
+
18
+ ## [0.2.4] - 2026-01-30
19
+
20
+ ### Changed
21
+ - **Breaking Change**: Removed the resize feature (`-w`, `--width`) to keep the tool focused strictly on high-performance compression.
22
+ - Updated documentation to reflect the streamlined feature set.
23
+
24
+ ## [0.2.3] - 2026-01-30
25
+
26
+ ### Fixed
27
+ - Fixed GitHub Actions publishing issue by correctly configuring registry authentication.
28
+ - Corrected package name and scope.
29
+
30
+ ## [0.1.0] - 2026-01-29
31
+
32
+ ### Added
33
+ - Initial release.
34
+ - Support for JPEG, PNG, WebP, and AVIF compression.
35
+ - Recursive directory processing.
36
+ - Atomic safe-replace functionality.
37
+ - Parallel processing with concurrency control.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sahil
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,96 +1,91 @@
1
1
  # MicroPng CLI
2
2
 
3
- A high-performance, local-first CLI image compressor built with Node.js and libvips (via `sharp`). Supports recursive processing, atomic overwrites, and format conversion.
3
+ A high-performance, local-first CLI image compressor built with Node.js and libvips (via `sharp`). Designed for developers who need fast, reliable, and recursive image optimization without sending data to a cloud service.
4
4
 
5
- [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
5
+ [![npm version](https://img.shields.io/npm/v/micropng-cli.svg)](https://www.npmjs.com/package/micropng-cli)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
7
 
7
- ## Features
8
+ ## πŸš€ Features
8
9
 
9
- - πŸš€ **Fast**: Parallel processing with smart concurrency control.
10
- - πŸ“‚ **Recursive**: Deeply scans folders and subfolders.
11
- - πŸ”„ **Safe Overwrite**: Atomic replacements to prevent data loss.
12
- - πŸ–ΌοΈ **Multi-Format**: Supports JPEG, PNG, WebP, AVIF.
13
- - πŸ”’ **Local-First**: No data leaves your machine.
10
+ - **Blazing Fast**: Uses parallel processing with smart concurrency control to saturate your CPU without crashing.
11
+ - **Deeply Recursive**: Scans folders and subfolders, maintaining your directory structure perfectly.
12
+ - **Safety First**: Implements atomic overwrites using the `--replace` flagβ€”original files are only replaced if the compressed version is actually smaller.
13
+ - **Universal Formats**: Full support for JPEG, PNG, WebP, and AVIF conversion and compression.
14
+ - **Local-First**: No data ever leaves your machine. Your privacy is guaranteed.
15
+ - **Smarter Scanning**: Native support for ignore patterns to skip `node_modules`, `.git`, or specific assets.
16
+ - **Metadata Control**: Choose whether to strip or keep EXIF information (GPS, camera settings, etc.).
14
17
 
15
- ## Installation
16
-
17
- ### For Users
18
-
19
- You can run **MicroPng** directly without installation using `npx`:
18
+ ## πŸ“¦ Installation
20
19
 
20
+ ### Global Installation
21
+ Install it once and use the `micropng-cli` command anywhere:
21
22
  ```bash
22
- npx micropng-cli --help
23
+ npm install -g micropng-cli
23
24
  ```
24
25
 
25
- Or install it globally to use the `micropng` command anywhere:
26
-
26
+ ### Run without Installation
27
+ Use `npx` to run it instantly without cluttering your system:
27
28
  ```bash
28
- npm install -g micropng-cli
29
+ npx micropng-cli --help
29
30
  ```
30
31
 
31
- ### For Developers
32
-
33
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for local development setup.
34
-
35
- ## Usage
36
-
37
- ### Basic Usage
32
+ ---
38
33
 
39
- Compress a single file:
34
+ ## πŸ›  Usage Examples
40
35
 
36
+ ### 1. Basic Compression
37
+ Compress a single file and save it as a new file:
41
38
  ```bash
42
- micropng-cli input.png --output compressed.png
39
+ micropng-cli input.png --output optimized.png
43
40
  ```
44
41
 
45
- Compress a directory of images:
46
-
42
+ ### 2. High-Performance Bulk Processing
43
+ Optimize an entire directory and maintain the structure in an output folder:
47
44
  ```bash
48
- micropng-cli ./images --output ./compressed-images
45
+ micropng-cli ./raw-assets --output ./dist/assets --recursive
49
46
  ```
50
47
 
51
- ### Recursive Processing
52
-
53
- Process all images in a folder and its subfolders, maintaining the directory structure:
54
-
48
+ ### 3. Safe In-Place Replacement
49
+ The most popular way to use MicroPng. This will search through your project and optimize all images, replacing them **only if** size is saved:
55
50
  ```bash
56
- micropng-cli ./photos --recursive --output ./optimized-photos
51
+ micropng-cli ./src --recursive --replace --quality 85
57
52
  ```
58
53
 
59
- ### In-Place Replacement (Overwrite)
60
-
61
- ⚠️ **Warning**: This will replace your original files!
62
-
63
- Compress and overwrite images in-place safely:
64
-
54
+ ### 4. Advanced: Modern Web Formats
55
+ Convert all images in a folder to WebP for modern web performance:
65
56
  ```bash
66
- micropng-cli ./project-assets --recursive --replace
57
+ micropng-cli ./images --format webp --output ./webp-bundle --recursive
67
58
  ```
68
59
 
69
- ### Format Conversion
70
-
71
- Convert all PNGs to WebP:
72
-
60
+ ### 5. Advanced: Complex Ignores
61
+ Ignore specific directories or patterns while processing:
73
62
  ```bash
74
- micropng-cli ./images --format webp --output ./webp-images --recursive
63
+ micropng-cli . --recursive --replace --ignore "node_modules/**" "**/previews/**" "*.tmp"
75
64
  ```
76
65
 
77
- ### Options
66
+ ---
67
+
68
+ ## βš™οΈ Options
78
69
 
79
70
  | Option | Alias | Description | Default |
80
71
  | :--- | :--- | :--- | :--- |
81
- | `--output <dir>` | `-o` | Output directory | (Current dir) |
82
- | `--recursive` | `-r` | Process subfolders deeply | `false` |
83
- | `--replace` | | Overwrite original files | `false` |
84
- | `--quality <number>` | `-q` | Compression quality (1-100) | `80` |
85
- | `--width <number>` | `-w` | Resize width in pixels | (Original) |
86
- | `--format <type>` | `-f` | Output format (jpeg, png, webp, avif) | (Original) |
87
- | `--concurrency <number>` | `-c` | Number of concurrent tasks | `5` |
72
+ | `--output <dir>` | `-o` | Target directory for compressed files; for a single file input, can be an output file path (e.g. `optimized.png`) | (Current Dir) |
73
+ | `--recursive` | `-r` | Deep scan folders and subfolders | `false` |
74
+ | `--replace` | | Replace originals (Atomic safety enabled) | `false` |
75
+ | `--quality <n>` | `-q` | Compression quality (1-100) | `80` |
76
+ | `--format <type>` | `-f` | Output format (jpeg, png, webp, avif) | (Source Ext) |
77
+ | `--concurrency <n>` | `-c` | Max simultaneous tasks | `5` |
78
+ | `--ignore <glob>` | `-i` | Patterns to exclude (supports multiple) | - |
79
+ | `--keep-metadata`| | Preserves EXIF/GPS/IPTC data | `false` |
80
+
81
+ ---
88
82
 
89
- ## Documentation
83
+ ## πŸ“– Additional Docs
90
84
 
91
- - [Contributing Guide](./CONTRIBUTING.md): Setup local dev environment and run tests.
92
- - [Distribution Guide](./DISTRIBUTION.md): How to publish and release this package.
85
+ - **[Changelog](./CHANGELOG.md)**: See what's new in each version.
86
+ - **[Contributing](./CONTRIBUTING.md)**: Learn how to set up the dev environment and add new features.
87
+ - **[Distribution](./DISTRIBUTION.md)**: Details on the build and release pipeline.
93
88
 
94
- ## License
89
+ ## βš–οΈ License
95
90
 
96
- ISC
91
+ MIT Β© Sahil Fruitwala
package/dist/index.cjs CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var k=Object.create;var h=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var z=Object.getPrototypeOf,R=Object.prototype.hasOwnProperty;var N=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of L(e))!R.call(r,s)&&s!==t&&h(r,s,{get:()=>e[s],enumerable:!(i=F(e,s))||i.enumerable});return r};var c=(r,e,t)=>(t=r!=null?k(z(r)):{},N(e||!r||!r.__esModule?h(t,"default",{value:r,enumerable:!0}):t,r));var I=require("commander"),C=c(require("fast-glob"),1),q=c(require("p-limit"),1),a=c(require("path"),1),l=c(require("chalk"),1),E=c(require("fs-extra"),1);var x=c(require("sharp"),1),d=c(require("fs-extra"),1),$=c(require("path"),1);async function j(r,e,t){let{quality:i=80,width:s,format:f}=t,n=(0,x.default)(r);s&&(n=n.resize(s));let y=$.default.extname(r).toLowerCase(),m=f||y.slice(1);m==="jpeg"||m==="jpg"?n=n.jpeg({quality:i}):m==="png"?n=n.png({quality:i,compressionLevel:9}):m==="webp"?n=n.webp({quality:i}):m==="avif"&&(n=n.avif({quality:i}));let p=t.replace?`${r}.tmp_${Date.now()}`:e;try{await d.default.ensureDir($.default.dirname(p)),await n.toFile(p),t.replace&&await d.default.move(p,r,{overwrite:!0})}catch(o){throw t.replace&&await d.default.pathExists(p)&&await d.default.remove(p),o}}var O=new I.Command;O.name("micropng-cli").description("High-performance CLI image compressor").version("0.1.0").argument("<input>","Input file or directory").option("-o, --output <dir>","Output directory").option("-r, --recursive","Process subfolders deeply").option("--replace","OVERWRITE original files (Caution!)").option("-q, --quality <number>","Compression quality (1-100)","80").option("-w, --width <number>","Resize width in pixels").option("-f, --format <type>","Output format (jpeg, png, webp, avif)").option("-c, --concurrency <number>","Number of concurrent tasks","5").action(async(r,e)=>{try{let t=a.default.resolve(r),i=(await E.default.stat(t)).isDirectory(),s;if(i){let o=t.replace(/\\/g,"/");s=e.recursive?`${o}/**/*.{jpg,jpeg,png,webp}`:`${o}/*.{jpg,jpeg,png,webp}`}else s=t.replace(/\\/g,"/");let f=await(0,C.default)(s,{absolute:!0});if(f.length===0){console.log(l.default.yellow("No images found to process."));return}console.log(l.default.blue(`Found ${f.length} images. Processing...`));let n=(0,q.default)(parseInt(e.concurrency)),y=parseInt(e.quality),m=e.width?parseInt(e.width):void 0,p=f.map(o=>n(async()=>{try{let g;if(e.replace)g=o;else if(e.output){let u=i?a.default.relative(t,o):a.default.basename(o),b=a.default.dirname(u),w=a.default.extname(u),v=a.default.basename(u,w),D=e.format?`.${e.format}`:w;g=a.default.join(a.default.resolve(e.output),b,`${v}${D}`)}else{let u=a.default.extname(o),b=a.default.basename(o,u),w=a.default.dirname(o),v=e.format?`.${e.format}`:u;g=a.default.join(w,`${b}_compressed${v}`)}await j(o,g,{quality:y,width:m,format:e.format,replace:e.replace}),console.log(l.default.green(`\u2713 Processed: ${a.default.relative(process.cwd(),o)}`))}catch(g){console.error(l.default.red(`\u2717 Failed: ${o} - ${g.message}`))}}));await Promise.all(p),console.log(l.default.bold.cyan(`
3
- Compression complete!`))}catch(t){console.error(l.default.red.bold(`Error: ${t.message}`)),process.exit(1)}});O.parse(process.argv);
2
+ "use strict";var H=Object.create;var E=Object.defineProperty;var K=Object.getOwnPropertyDescriptor;var X=Object.getOwnPropertyNames;var Y=Object.getPrototypeOf,A=Object.prototype.hasOwnProperty;var G=(o,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of X(e))!A.call(o,i)&&i!==r&&E(o,i,{get:()=>e[i],enumerable:!(n=K(e,i))||n.enumerable});return o};var u=(o,e,r)=>(r=o!=null?H(Y(o)):{},G(e||!o||!o.__esModule?E(r,"default",{value:o,enumerable:!0}):r,o));var Q=()=>typeof document>"u"?new URL(`file:${__filename}`).href:document.currentScript&&document.currentScript.tagName.toUpperCase()==="SCRIPT"?document.currentScript.src:new URL("main.js",document.baseURI).href,k=Q();var L=require("commander"),O=u(require("fast-glob"),1),U=u(require("p-limit"),1),a=u(require("path"),1),s=u(require("chalk"),1),F=u(require("fs-extra"),1),z=u(require("cli-progress"),1);var N=u(require("sharp"),1),g=u(require("fs-extra"),1),M=u(require("path"),1);N.default.cache(!1);async function R(o,e,r){let{quality:n=80,format:i,keepMetadata:h}=r,l=(await g.default.stat(o)).size,t=(0,N.default)(o);h&&(t=t.withMetadata());let f=M.default.extname(o).toLowerCase(),m=i||f.slice(1);m==="jpeg"||m==="jpg"?t=t.jpeg({quality:n}):m==="png"?t=t.png({quality:n,compressionLevel:9}):m==="webp"?t=t.webp({quality:n}):m==="avif"&&(t=t.avif({quality:n}));let c=r.replace?`${o}.tmp_${Date.now()}`:e;try{await g.default.ensureDir(M.default.dirname(c));let x=(await t.toFile(c)).size,b=!1;return r.replace?x<l?(await g.default.move(c,o,{overwrite:!0}),b=!0):(await g.default.remove(c),b=!1):b=!0,{inputSize:l,outputSize:x,saved:Math.max(0,l-x),replaced:b}}catch(d){throw r.replace&&await g.default.pathExists(c)&&await g.default.remove(c),d}}var q=F.default.readJsonSync(new URL("../package.json",k)),B=new L.Command;B.name("micropng-cli").description("High-performance CLI image compressor").version(q.version).argument("<input>","Input file or directory").option("-o, --output <dir>","Output directory").option("-r, --recursive","Process subfolders deeply").option("--replace","Replace original files ONLY if compressed is smaller").option("-q, --quality <number>","Compression quality (1-100)","80").option("-f, --format <type>","Output format (jpeg, png, webp, avif)").option("-c, --concurrency <number>","Number of concurrent tasks","5").option("-i, --ignore <patterns...>","Ignore patterns (glob)").option("--keep-metadata","Keep image metadata (EXIF, etc.)",!1).action(async(o,e)=>{try{let r=parseInt(e.quality,10),n=parseInt(e.concurrency,10);(Number.isNaN(r)||r<1||r>100)&&(console.error(s.default.red.bold("Error: --quality must be a number between 1 and 100.")),process.exit(1)),(Number.isNaN(n)||n<1)&&(console.error(s.default.red.bold("Error: --concurrency must be a positive number.")),process.exit(1));let i=a.default.resolve(o),h=(await F.default.stat(i)).isDirectory(),C;if(h){let v=i.replace(/\\/g,"/");C=e.recursive?`${v}/**/*.{jpg,jpeg,png,webp,avif}`:`${v}/*.{jpg,jpeg,png,webp,avif}`}else C=i.replace(/\\/g,"/");console.log(s.default.bold.cyan(`Micropng v${q.version}`)),console.log(s.default.dim(`Scanning: ${i}
3
+ `));let l=[],t=new z.default.SingleBar({format:"{bar} {percentage}% | {value}/{total} Files | {saved}MB",barCompleteChar:"\u2588",barIncompleteChar:"\u2591",hideCursor:!0},z.default.Presets.shades_grey),f=0,m=0,c=0,d=0,x=(0,U.default)(n);t.start(0,0,{file:"Scanning...",saved:"0.00"});let b=O.default.stream(C,{absolute:!0,ignore:e.ignore}),P=[];for await(let v of b){let p=v.toString();l.push(p),t.setTotal(l.length),P.push(x(async()=>{try{let y;if(e.replace)y=p;else if(e.output){let w=a.default.resolve(e.output);if(!h&&a.default.extname(w)!=="")y=w;else{let $=h?a.default.relative(i,p):a.default.basename(p),I=a.default.dirname($),j=a.default.extname($),T=a.default.basename($,j),J=e.format?`.${e.format}`:j;y=a.default.join(w,I,`${T}${J}`)}}else{let w=a.default.extname(p),$=a.default.basename(p,w),I=a.default.dirname(p),j=e.format?`.${e.format}`:w;y=a.default.join(I,`${$}_compressed${j}`)}let S=await R(p,y,{quality:r,format:e.format,replace:e.replace,keepMetadata:e.keepMetadata});f+=S.saved,m+=S.inputSize,c++;let D=S.inputSize>0?(S.saved/S.inputSize*100).toFixed(1):"0.0",_=a.default.basename(p);t.stop(),console.log(s.default.green(" \u2713 ")+s.default.white(_.padEnd(25))+s.default.dim(` saved ${D}%`)),t.start(l.length,c,{saved:(f/(1024*1024)).toFixed(2)})}catch{d++,t.stop(),console.log(s.default.red(" \u2717 ")+s.default.white(a.default.basename(p))),t.start(l.length,c,{saved:(f/(1024*1024)).toFixed(2)})}}))}if(l.length===0){t.stop(),console.log(s.default.yellow("No images found to process."));return}if(await Promise.all(P),t.stop(),console.log(s.default.bold.cyan(`
4
+ Compression complete!`)),c>0){let v=(f/1048576).toFixed(2),p=m>0?(f/m*100).toFixed(1):"0.0";console.log(s.default.green(`\u2713 Successfully processed ${c} images.`)),console.log(s.default.green(`Total space saved: ${v} MB (${p}%)`))}d>0&&console.log(s.default.red(`\u2717 Failed to process ${d} images.`))}catch(r){let n=r instanceof Error?r.message:String(r);console.error(s.default.red.bold(`Error: ${n}`)),process.exit(1)}});B.parse(process.argv);
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import{Command as q}from"commander";import E from"fast-glob";import O from"p-limit";import o from"path";import g from"chalk";import D from"fs-extra";import C from"sharp";import d from"fs-extra";import v from"path";async function $(c,e,a){let{quality:n=80,width:m,format:u}=a,t=C(c);m&&(t=t.resize(m));let w=v.extname(c).toLowerCase(),s=u||w.slice(1);s==="jpeg"||s==="jpg"?t=t.jpeg({quality:n}):s==="png"?t=t.png({quality:n,compressionLevel:9}):s==="webp"?t=t.webp({quality:n}):s==="avif"&&(t=t.avif({quality:n}));let i=a.replace?`${c}.tmp_${Date.now()}`:e;try{await d.ensureDir(v.dirname(i)),await t.toFile(i),a.replace&&await d.move(i,c,{overwrite:!0})}catch(r){throw a.replace&&await d.pathExists(i)&&await d.remove(i),r}}var h=new q;h.name("micropng-cli").description("High-performance CLI image compressor").version("0.1.0").argument("<input>","Input file or directory").option("-o, --output <dir>","Output directory").option("-r, --recursive","Process subfolders deeply").option("--replace","OVERWRITE original files (Caution!)").option("-q, --quality <number>","Compression quality (1-100)","80").option("-w, --width <number>","Resize width in pixels").option("-f, --format <type>","Output format (jpeg, png, webp, avif)").option("-c, --concurrency <number>","Number of concurrent tasks","5").action(async(c,e)=>{try{let a=o.resolve(c),n=(await D.stat(a)).isDirectory(),m;if(n){let r=a.replace(/\\/g,"/");m=e.recursive?`${r}/**/*.{jpg,jpeg,png,webp}`:`${r}/*.{jpg,jpeg,png,webp}`}else m=a.replace(/\\/g,"/");let u=await E(m,{absolute:!0});if(u.length===0){console.log(g.yellow("No images found to process."));return}console.log(g.blue(`Found ${u.length} images. Processing...`));let t=O(parseInt(e.concurrency)),w=parseInt(e.quality),s=e.width?parseInt(e.width):void 0,i=u.map(r=>t(async()=>{try{let p;if(e.replace)p=r;else if(e.output){let l=n?o.relative(a,r):o.basename(r),y=o.dirname(l),f=o.extname(l),b=o.basename(l,f),x=e.format?`.${e.format}`:f;p=o.join(o.resolve(e.output),y,`${b}${x}`)}else{let l=o.extname(r),y=o.basename(r,l),f=o.dirname(r),b=e.format?`.${e.format}`:l;p=o.join(f,`${y}_compressed${b}`)}await $(r,p,{quality:w,width:s,format:e.format,replace:e.replace}),console.log(g.green(`\u2713 Processed: ${o.relative(process.cwd(),r)}`))}catch(p){console.error(g.red(`\u2717 Failed: ${r} - ${p.message}`))}}));await Promise.all(i),console.log(g.bold.cyan(`
3
- Compression complete!`))}catch(a){console.error(g.red.bold(`Error: ${a.message}`)),process.exit(1)}});h.parse(process.argv);
2
+ import{Command as _}from"commander";import J from"fast-glob";import T from"p-limit";import r from"path";import o from"chalk";import E from"fs-extra";import M from"cli-progress";import F from"sharp";import y from"fs-extra";import z from"path";F.cache(!1);async function I(p,t,a){let{quality:i=80,format:m,keepMetadata:w}=a,c=(await y.stat(p)).size,e=F(p);w&&(e=e.withMetadata());let u=z.extname(p).toLowerCase(),l=m||u.slice(1);l==="jpeg"||l==="jpg"?e=e.jpeg({quality:i}):l==="png"?e=e.png({quality:i,compressionLevel:9}):l==="webp"?e=e.webp({quality:i}):l==="avif"&&(e=e.avif({quality:i}));let s=a.replace?`${p}.tmp_${Date.now()}`:t;try{await y.ensureDir(z.dirname(s));let h=(await e.toFile(s)).size,f=!1;return a.replace?h<c?(await y.move(s,p,{overwrite:!0}),f=!0):(await y.remove(s),f=!1):f=!0,{inputSize:c,outputSize:h,saved:Math.max(0,c-h),replaced:f}}catch(g){throw a.replace&&await y.pathExists(s)&&await y.remove(s),g}}var N=E.readJsonSync(new URL("../package.json",import.meta.url)),P=new _;P.name("micropng-cli").description("High-performance CLI image compressor").version(N.version).argument("<input>","Input file or directory").option("-o, --output <dir>","Output directory").option("-r, --recursive","Process subfolders deeply").option("--replace","Replace original files ONLY if compressed is smaller").option("-q, --quality <number>","Compression quality (1-100)","80").option("-f, --format <type>","Output format (jpeg, png, webp, avif)").option("-c, --concurrency <number>","Number of concurrent tasks","5").option("-i, --ignore <patterns...>","Ignore patterns (glob)").option("--keep-metadata","Keep image metadata (EXIF, etc.)",!1).action(async(p,t)=>{try{let a=parseInt(t.quality,10),i=parseInt(t.concurrency,10);(Number.isNaN(a)||a<1||a>100)&&(console.error(o.red.bold("Error: --quality must be a number between 1 and 100.")),process.exit(1)),(Number.isNaN(i)||i<1)&&(console.error(o.red.bold("Error: --concurrency must be a positive number.")),process.exit(1));let m=r.resolve(p),w=(await E.stat(m)).isDirectory(),$;if(w){let d=m.replace(/\\/g,"/");$=t.recursive?`${d}/**/*.{jpg,jpeg,png,webp,avif}`:`${d}/*.{jpg,jpeg,png,webp,avif}`}else $=m.replace(/\\/g,"/");console.log(o.bold.cyan(`Micropng v${N.version}`)),console.log(o.dim(`Scanning: ${m}
3
+ `));let c=[],e=new M.SingleBar({format:"{bar} {percentage}% | {value}/{total} Files | {saved}MB",barCompleteChar:"\u2588",barIncompleteChar:"\u2591",hideCursor:!0},M.Presets.shades_grey),u=0,l=0,s=0,g=0,h=T(i);e.start(0,0,{file:"Scanning...",saved:"0.00"});let f=J.stream($,{absolute:!0,ignore:t.ignore}),k=[];for await(let d of f){let n=d.toString();c.push(n),e.setTotal(c.length),k.push(h(async()=>{try{let b;if(t.replace)b=n;else if(t.output){let v=r.resolve(t.output);if(!w&&r.extname(v)!=="")b=v;else{let S=w?r.relative(m,n):r.basename(n),j=r.dirname(S),C=r.extname(S),L=r.basename(S,C),B=t.format?`.${t.format}`:C;b=r.join(v,j,`${L}${B}`)}}else{let v=r.extname(n),S=r.basename(n,v),j=r.dirname(n),C=t.format?`.${t.format}`:v;b=r.join(j,`${S}_compressed${C}`)}let x=await I(n,b,{quality:a,format:t.format,replace:t.replace,keepMetadata:t.keepMetadata});u+=x.saved,l+=x.inputSize,s++;let q=x.inputSize>0?(x.saved/x.inputSize*100).toFixed(1):"0.0",O=r.basename(n);e.stop(),console.log(o.green(" \u2713 ")+o.white(O.padEnd(25))+o.dim(` saved ${q}%`)),e.start(c.length,s,{saved:(u/(1024*1024)).toFixed(2)})}catch{g++,e.stop(),console.log(o.red(" \u2717 ")+o.white(r.basename(n))),e.start(c.length,s,{saved:(u/(1024*1024)).toFixed(2)})}}))}if(c.length===0){e.stop(),console.log(o.yellow("No images found to process."));return}if(await Promise.all(k),e.stop(),console.log(o.bold.cyan(`
4
+ Compression complete!`)),s>0){let d=(u/1048576).toFixed(2),n=l>0?(u/l*100).toFixed(1):"0.0";console.log(o.green(`\u2713 Successfully processed ${s} images.`)),console.log(o.green(`Total space saved: ${d} MB (${n}%)`))}g>0&&console.log(o.red(`\u2717 Failed to process ${g} images.`))}catch(a){let i=a instanceof Error?a.message:String(a);console.error(o.red.bold(`Error: ${i}`)),process.exit(1)}});P.parse(process.argv);
package/package.json CHANGED
@@ -1,12 +1,27 @@
1
1
  {
2
2
  "name": "micropng-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/SahilFruitwala/micropng-cli.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/SahilFruitwala/micropng-cli/issues"
13
+ },
14
+ "homepage": "https://github.com/SahilFruitwala/micropng-cli#readme",
4
15
  "description": "High-performance CLI image compressor using sharp and libvips",
5
16
  "files": [
6
17
  "dist",
7
- "README.md"
18
+ "README.md",
19
+ "CHANGELOG.md"
8
20
  ],
9
- "main": "index.js",
21
+ "main": "dist/index.js",
22
+ "exports": {
23
+ ".": "./dist/index.js"
24
+ },
10
25
  "type": "module",
11
26
  "bin": {
12
27
  "micropng-cli": "./dist/index.js"
@@ -16,7 +31,8 @@
16
31
  "dev": "tsup --watch",
17
32
  "test": "vitest run",
18
33
  "test:coverage": "vitest run --coverage",
19
- "prepublishOnly": "npm run build"
34
+ "prepublishOnly": "npm run build",
35
+ "build:bin": "tsup && pkg ."
20
36
  },
21
37
  "keywords": [
22
38
  "image-compression",
@@ -26,9 +42,10 @@
26
42
  "optimization"
27
43
  ],
28
44
  "author": "Sahil",
29
- "license": "ISC",
45
+ "license": "MIT",
30
46
  "dependencies": {
31
47
  "chalk": "^5.6.2",
48
+ "cli-progress": "^3.12.0",
32
49
  "commander": "^14.0.2",
33
50
  "fast-glob": "^3.3.3",
34
51
  "fs-extra": "^11.3.3",
@@ -36,6 +53,7 @@
36
53
  "sharp": "^0.34.5"
37
54
  },
38
55
  "devDependencies": {
56
+ "@types/cli-progress": "^3.11.6",
39
57
  "@types/fs-extra": "^11.0.4",
40
58
  "@types/node": "^25.1.0",
41
59
  "@types/sharp": "^0.31.1",