image-convert-cli 1.1.0 → 1.1.2
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/README.md +5 -3
- package/bin/index.ts +7 -0
- package/dist/index.js +187 -19
- package/package.json +1 -1
- package/src/cli.ts +56 -2
- package/src/converter.ts +108 -1
- package/src/index.ts +1 -1
- package/src/prompts.ts +60 -1
- package/src/types.ts +28 -0
- package/src/utils/path.ts +44 -0
package/README.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# image-convert-cli
|
|
2
2
|
|
|
3
|
-
> A fast, interactive CLI tool for converting images between WebP, JPEG, and
|
|
3
|
+
> A fast, interactive CLI tool for converting images between WebP, JPEG, JPG and PNG formats
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
+
## Demo
|
|
8
|
+
|
|
9
|
+
https://github.com/user-attachments/assets/f3df2efc-5db7-4201-9092-1aa309880380
|
|
10
|
+
|
|
7
11
|
## Quick Start
|
|
8
12
|
|
|
9
13
|
```bash
|
|
@@ -178,8 +182,6 @@ interface ConversionResult {
|
|
|
178
182
|
}
|
|
179
183
|
```
|
|
180
184
|
|
|
181
|
-
See `CLAUDE.md` for detailed development workflow including TDD practices.
|
|
182
|
-
|
|
183
185
|
## Contributing
|
|
184
186
|
|
|
185
187
|
1. Fork the repository
|
package/bin/index.ts
CHANGED
|
@@ -12,11 +12,18 @@ if (args[0] === "update") {
|
|
|
12
12
|
process.exit(0);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Check for version command (positional argument)
|
|
16
|
+
if (args[0] === "version") {
|
|
17
|
+
options.version = true;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
// Parse arguments
|
|
16
21
|
for (let i = 0; i < args.length; i++) {
|
|
17
22
|
const arg = args[i];
|
|
18
23
|
if (arg === "--help" || arg === "-h") {
|
|
19
24
|
options.help = true;
|
|
25
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
26
|
+
options.version = true;
|
|
20
27
|
} else if (arg === "--yes" || arg === "-y") {
|
|
21
28
|
options.yes = true;
|
|
22
29
|
} else if (arg === "--source" || arg === "-s") {
|
package/dist/index.js
CHANGED
|
@@ -6573,8 +6573,8 @@ var require_lib2 = __commonJS((exports, module) => {
|
|
|
6573
6573
|
|
|
6574
6574
|
// src/prompts.ts
|
|
6575
6575
|
import * as readline3 from "readline";
|
|
6576
|
-
import * as
|
|
6577
|
-
import * as
|
|
6576
|
+
import * as fs3 from "fs";
|
|
6577
|
+
import * as path2 from "path";
|
|
6578
6578
|
|
|
6579
6579
|
// node_modules/@inquirer/core/dist/lib/key.js
|
|
6580
6580
|
var isUpKey = (key, keybindings = []) => key.name === "up" || keybindings.includes("vim") && key.name === "k" || keybindings.includes("emacs") && key.ctrl && key.name === "p";
|
|
@@ -8280,7 +8280,7 @@ var dist_default3 = createPrompt((config, done) => {
|
|
|
8280
8280
|
});
|
|
8281
8281
|
// src/converter.ts
|
|
8282
8282
|
var import_sharp = __toESM(require_lib2(), 1);
|
|
8283
|
-
import * as
|
|
8283
|
+
import * as fs2 from "fs";
|
|
8284
8284
|
|
|
8285
8285
|
// src/utils/format.ts
|
|
8286
8286
|
function formatBytes(bytes) {
|
|
@@ -8292,11 +8292,53 @@ function formatBytes(bytes) {
|
|
|
8292
8292
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
8293
8293
|
}
|
|
8294
8294
|
|
|
8295
|
+
// src/utils/path.ts
|
|
8296
|
+
import * as path from "path";
|
|
8297
|
+
import * as fs from "fs";
|
|
8298
|
+
function getDefaultDestinationPath(sourcePath, format) {
|
|
8299
|
+
const parsed = path.parse(sourcePath);
|
|
8300
|
+
return path.join(parsed.dir, `${parsed.name}.${format}`);
|
|
8301
|
+
}
|
|
8302
|
+
function isDirectory(sourcePath) {
|
|
8303
|
+
try {
|
|
8304
|
+
return fs.statSync(sourcePath).isDirectory();
|
|
8305
|
+
} catch {
|
|
8306
|
+
return false;
|
|
8307
|
+
}
|
|
8308
|
+
}
|
|
8309
|
+
function getExtension(filePath) {
|
|
8310
|
+
const ext = path.extname(filePath);
|
|
8311
|
+
return ext.length > 0 ? ext.slice(1).toLowerCase() : "";
|
|
8312
|
+
}
|
|
8313
|
+
function isSameFormat(sourcePath, targetFormat) {
|
|
8314
|
+
const ext = getExtension(sourcePath);
|
|
8315
|
+
if (ext === targetFormat)
|
|
8316
|
+
return true;
|
|
8317
|
+
if ((ext === "jpeg" || ext === "jpg") && (targetFormat === "jpeg" || targetFormat === "jpg")) {
|
|
8318
|
+
return true;
|
|
8319
|
+
}
|
|
8320
|
+
return false;
|
|
8321
|
+
}
|
|
8322
|
+
var SUPPORTED_FORMATS = ["webp", "jpeg", "jpg", "png"];
|
|
8323
|
+
function getImageFilesFromDirectory(dirPath) {
|
|
8324
|
+
try {
|
|
8325
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
8326
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path.join(dirPath, entry.name)).filter((filePath) => SUPPORTED_FORMATS.includes(getExtension(filePath)));
|
|
8327
|
+
} catch {
|
|
8328
|
+
return [];
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
8331
|
+
async function ensureDirectoryExists(dirPath) {
|
|
8332
|
+
if (!fs.existsSync(dirPath)) {
|
|
8333
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
|
|
8295
8337
|
// src/converter.ts
|
|
8296
8338
|
async function convertImage(sourcePath, destinationPath, format, compress) {
|
|
8297
8339
|
const startTime = Date.now();
|
|
8298
8340
|
try {
|
|
8299
|
-
const originalSize =
|
|
8341
|
+
const originalSize = fs2.statSync(sourcePath).size;
|
|
8300
8342
|
let sharpFormat;
|
|
8301
8343
|
let options;
|
|
8302
8344
|
switch (format) {
|
|
@@ -8317,7 +8359,7 @@ async function convertImage(sourcePath, destinationPath, format, compress) {
|
|
|
8317
8359
|
throw new Error(`Unsupported format: ${format}`);
|
|
8318
8360
|
}
|
|
8319
8361
|
await import_sharp.default(sourcePath).toFormat(sharpFormat, options).toFile(destinationPath);
|
|
8320
|
-
const outputSize =
|
|
8362
|
+
const outputSize = fs2.statSync(destinationPath).size;
|
|
8321
8363
|
const elapsed = Date.now() - startTime;
|
|
8322
8364
|
return {
|
|
8323
8365
|
success: true,
|
|
@@ -8354,16 +8396,85 @@ Output saved to: ${result.destinationPath}`);
|
|
|
8354
8396
|
\u2717 Conversion failed:`, result.error);
|
|
8355
8397
|
}
|
|
8356
8398
|
}
|
|
8399
|
+
async function convertBatch(settings) {
|
|
8400
|
+
const startTime = Date.now();
|
|
8401
|
+
const files = getImageFilesFromDirectory(settings.sourceDir);
|
|
8402
|
+
await ensureDirectoryExists(settings.destinationDir);
|
|
8403
|
+
const results = [];
|
|
8404
|
+
let successCount = 0;
|
|
8405
|
+
let failureCount = 0;
|
|
8406
|
+
let skippedCount = 0;
|
|
8407
|
+
for (const sourcePath of files) {
|
|
8408
|
+
const fileName = __require("path").basename(sourcePath);
|
|
8409
|
+
const destPath = __require("path").join(settings.destinationDir, `${__require("path").parse(fileName).name}.${settings.targetFormat}`);
|
|
8410
|
+
if (isSameFormat(sourcePath, settings.targetFormat)) {
|
|
8411
|
+
skippedCount++;
|
|
8412
|
+
continue;
|
|
8413
|
+
}
|
|
8414
|
+
if (settings.yesMode && fs2.existsSync(destPath)) {
|
|
8415
|
+
skippedCount++;
|
|
8416
|
+
continue;
|
|
8417
|
+
}
|
|
8418
|
+
const result = await convertImage(sourcePath, destPath, settings.targetFormat, settings.compress);
|
|
8419
|
+
results.push(result);
|
|
8420
|
+
if (result.success) {
|
|
8421
|
+
successCount++;
|
|
8422
|
+
} else {
|
|
8423
|
+
failureCount++;
|
|
8424
|
+
}
|
|
8425
|
+
}
|
|
8426
|
+
return {
|
|
8427
|
+
totalFiles: files.length,
|
|
8428
|
+
successCount,
|
|
8429
|
+
failureCount,
|
|
8430
|
+
skippedCount,
|
|
8431
|
+
results,
|
|
8432
|
+
totalElapsed: Date.now() - startTime
|
|
8433
|
+
};
|
|
8434
|
+
}
|
|
8435
|
+
function displayBatchResult(summary) {
|
|
8436
|
+
console.log(`
|
|
8437
|
+
Batch conversion complete!`);
|
|
8438
|
+
console.log(` Total files: ${summary.totalFiles}`);
|
|
8439
|
+
console.log(` Converted: ${summary.successCount}`);
|
|
8440
|
+
console.log(` Failed: ${summary.failureCount}`);
|
|
8441
|
+
console.log(` Skipped: ${summary.skippedCount}`);
|
|
8442
|
+
console.log(` Time: ${summary.totalElapsed}ms`);
|
|
8443
|
+
if (summary.successCount > 0) {
|
|
8444
|
+
let totalOriginal = 0;
|
|
8445
|
+
let totalOutput = 0;
|
|
8446
|
+
for (const result of summary.results) {
|
|
8447
|
+
if (result.success) {
|
|
8448
|
+
totalOriginal += result.originalSize;
|
|
8449
|
+
totalOutput += result.outputSize;
|
|
8450
|
+
}
|
|
8451
|
+
}
|
|
8452
|
+
if (totalOriginal > 0) {
|
|
8453
|
+
console.log(` Saved: ${((totalOriginal - totalOutput) / totalOriginal * 100).toFixed(1)}%`);
|
|
8454
|
+
}
|
|
8455
|
+
}
|
|
8456
|
+
console.log(`
|
|
8457
|
+
Output directory: ${summary.results[0]?.destinationPath ? __require("path").dirname(summary.results[0].destinationPath) : "N/A"}`);
|
|
8458
|
+
if (summary.failureCount > 0) {
|
|
8459
|
+
console.log(`
|
|
8460
|
+
Failed files:`);
|
|
8461
|
+
for (const result of summary.results) {
|
|
8462
|
+
if (!result.success) {
|
|
8463
|
+
console.log(` - ${result.sourcePath}: ${result.error}`);
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
8467
|
+
}
|
|
8357
8468
|
|
|
8358
8469
|
// src/prompts.ts
|
|
8359
8470
|
function filePathCompleter(line) {
|
|
8360
8471
|
const trimmed = line.trim();
|
|
8361
8472
|
const input = trimmed.split(" ")[0] || ".";
|
|
8362
8473
|
const isDirectoryInput = input.endsWith("/");
|
|
8363
|
-
const dir = isDirectoryInput ? input.slice(0, -1) || "." :
|
|
8364
|
-
const base = isDirectoryInput ? "" :
|
|
8474
|
+
const dir = isDirectoryInput ? input.slice(0, -1) || "." : path2.dirname(input) || ".";
|
|
8475
|
+
const base = isDirectoryInput ? "" : path2.basename(input) || "";
|
|
8365
8476
|
try {
|
|
8366
|
-
const files =
|
|
8477
|
+
const files = fs3.readdirSync(dir, { withFileTypes: true });
|
|
8367
8478
|
const filtered = base ? files.filter((dirent) => dirent.name.startsWith(base)) : files;
|
|
8368
8479
|
const completions = filtered.map((dirent) => {
|
|
8369
8480
|
const fullPath = dir === "." ? dirent.name : `${dir}/${dirent.name}`;
|
|
@@ -8405,7 +8516,7 @@ class InteractivePromptService {
|
|
|
8405
8516
|
if (!input.trim()) {
|
|
8406
8517
|
return "Please enter a file path";
|
|
8407
8518
|
}
|
|
8408
|
-
if (!
|
|
8519
|
+
if (!fs3.existsSync(input)) {
|
|
8409
8520
|
return "File does not exist";
|
|
8410
8521
|
}
|
|
8411
8522
|
return true;
|
|
@@ -8416,8 +8527,8 @@ class InteractivePromptService {
|
|
|
8416
8527
|
if (!input.trim()) {
|
|
8417
8528
|
return "Please enter a destination path";
|
|
8418
8529
|
}
|
|
8419
|
-
const dir =
|
|
8420
|
-
if (dir !== "." && !
|
|
8530
|
+
const dir = path2.dirname(input);
|
|
8531
|
+
if (dir !== "." && !fs3.existsSync(dir)) {
|
|
8421
8532
|
return "Destination directory does not exist";
|
|
8422
8533
|
}
|
|
8423
8534
|
return true;
|
|
@@ -8446,6 +8557,29 @@ class InteractivePromptService {
|
|
|
8446
8557
|
default: true
|
|
8447
8558
|
});
|
|
8448
8559
|
}
|
|
8560
|
+
async promptForOverwrite(filePath) {
|
|
8561
|
+
return dist_default2({
|
|
8562
|
+
message: `File ${filePath} already exists. Overwrite?`,
|
|
8563
|
+
default: false
|
|
8564
|
+
});
|
|
8565
|
+
}
|
|
8566
|
+
async promptBatchDestination(defaultPath, validate2) {
|
|
8567
|
+
return inputWithPathCompletion("Output directory", defaultPath, validate2 || ((input) => {
|
|
8568
|
+
if (!input.trim()) {
|
|
8569
|
+
return "Please enter an output directory";
|
|
8570
|
+
}
|
|
8571
|
+
if (!fs3.existsSync(input)) {
|
|
8572
|
+
return "Output directory does not exist";
|
|
8573
|
+
}
|
|
8574
|
+
return true;
|
|
8575
|
+
}));
|
|
8576
|
+
}
|
|
8577
|
+
async promptBatchConfirm(fileCount) {
|
|
8578
|
+
return dist_default2({
|
|
8579
|
+
message: `Convert ${fileCount} files?`,
|
|
8580
|
+
default: true
|
|
8581
|
+
});
|
|
8582
|
+
}
|
|
8449
8583
|
showHelp() {
|
|
8450
8584
|
console.log(`Image Converter CLI
|
|
8451
8585
|
|
|
@@ -8454,21 +8588,25 @@ Usage:
|
|
|
8454
8588
|
imgc --source <path> --format <format> [options]
|
|
8455
8589
|
imgc -y --source <path> --format <format>
|
|
8456
8590
|
imgc update # Check for updates
|
|
8591
|
+
imgc version # Show version
|
|
8457
8592
|
|
|
8458
8593
|
Options:
|
|
8459
8594
|
--help, -h Show this help message
|
|
8595
|
+
--version, -v Show version number
|
|
8460
8596
|
--yes, -y Non-interactive mode (use defaults for optional prompts)
|
|
8461
8597
|
--source, -s Source file path (required with -y)
|
|
8462
8598
|
--format, -f Target format: webp, jpeg, jpg, or png (required with -y)
|
|
8463
8599
|
--dest, -d Destination path (optional, auto-generated if not provided)
|
|
8464
|
-
--compress, -c
|
|
8600
|
+
--compress, -c Enable compression (optional, default: false)
|
|
8465
8601
|
|
|
8466
8602
|
Commands:
|
|
8467
8603
|
update Check for the latest version on npm
|
|
8604
|
+
version Show version number
|
|
8468
8605
|
|
|
8469
8606
|
Examples:
|
|
8470
8607
|
imgc # Interactive mode
|
|
8471
8608
|
imgc --help # Show help
|
|
8609
|
+
imgc --version # Show version
|
|
8472
8610
|
imgc -y --source photo.png --format webp
|
|
8473
8611
|
imgc -y --source photo.jpg --format jpeg --compress
|
|
8474
8612
|
imgc update # Check for updates
|
|
@@ -8495,13 +8633,6 @@ Conversion settings:`);
|
|
|
8495
8633
|
}
|
|
8496
8634
|
}
|
|
8497
8635
|
|
|
8498
|
-
// src/utils/path.ts
|
|
8499
|
-
import * as path2 from "path";
|
|
8500
|
-
function getDefaultDestinationPath(sourcePath, format) {
|
|
8501
|
-
const parsed = path2.parse(sourcePath);
|
|
8502
|
-
return path2.join(parsed.dir, `${parsed.name}.${format}`);
|
|
8503
|
-
}
|
|
8504
|
-
|
|
8505
8636
|
// src/cli.ts
|
|
8506
8637
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org/image-convert-cli/latest";
|
|
8507
8638
|
async function handleUpdate(fetchVersion, currentVersion) {
|
|
@@ -8532,6 +8663,11 @@ async function runCli(options, promptService) {
|
|
|
8532
8663
|
prompts.showHelp();
|
|
8533
8664
|
return;
|
|
8534
8665
|
}
|
|
8666
|
+
if (options.version) {
|
|
8667
|
+
const version = process.env.npm_package_version || "1.1.0";
|
|
8668
|
+
console.log(`image-convert-cli v${version}`);
|
|
8669
|
+
return;
|
|
8670
|
+
}
|
|
8535
8671
|
if (options.yes && (!options.source || !options.format)) {
|
|
8536
8672
|
console.error("Error: -y mode requires --source and --format arguments");
|
|
8537
8673
|
prompts.showHelp();
|
|
@@ -8541,6 +8677,11 @@ async function runCli(options, promptService) {
|
|
|
8541
8677
|
`);
|
|
8542
8678
|
const sourcePath = options.source || await prompts.promptSourceFile();
|
|
8543
8679
|
const targetFormat = options.format || await prompts.promptFormat();
|
|
8680
|
+
const isBatchMode = isDirectory(sourcePath);
|
|
8681
|
+
if (isBatchMode) {
|
|
8682
|
+
await runBatchMode(sourcePath, targetFormat, options, prompts);
|
|
8683
|
+
return;
|
|
8684
|
+
}
|
|
8544
8685
|
const destinationPath = options.destination || await prompts.promptDestination(getDefaultDestinationPath(sourcePath, targetFormat));
|
|
8545
8686
|
const compress = options.yes !== undefined ? options.yes : await prompts.promptCompress();
|
|
8546
8687
|
const settings = {
|
|
@@ -8561,6 +8702,28 @@ async function runCli(options, promptService) {
|
|
|
8561
8702
|
process.exit(1);
|
|
8562
8703
|
}
|
|
8563
8704
|
}
|
|
8705
|
+
async function runBatchMode(sourceDir, targetFormat, options, prompts) {
|
|
8706
|
+
const yesMode = options.yes === true;
|
|
8707
|
+
const destinationDir = options.destination || (yesMode ? sourceDir : await prompts.promptBatchDestination(sourceDir));
|
|
8708
|
+
const compress = yesMode ? false : await prompts.promptCompress();
|
|
8709
|
+
const files = getImageFilesFromDirectory(sourceDir);
|
|
8710
|
+
if (!yesMode) {
|
|
8711
|
+
const shouldProceed = await prompts.promptBatchConfirm(files.length);
|
|
8712
|
+
if (!shouldProceed) {
|
|
8713
|
+
console.log("Batch conversion cancelled.");
|
|
8714
|
+
return;
|
|
8715
|
+
}
|
|
8716
|
+
}
|
|
8717
|
+
const batchSettings = {
|
|
8718
|
+
sourceDir,
|
|
8719
|
+
targetFormat,
|
|
8720
|
+
destinationDir,
|
|
8721
|
+
compress,
|
|
8722
|
+
yesMode
|
|
8723
|
+
};
|
|
8724
|
+
const result = await convertBatch(batchSettings);
|
|
8725
|
+
displayBatchResult(result);
|
|
8726
|
+
}
|
|
8564
8727
|
|
|
8565
8728
|
// bin/index.ts
|
|
8566
8729
|
var args = process.argv.slice(2);
|
|
@@ -8569,10 +8732,15 @@ if (args[0] === "update") {
|
|
|
8569
8732
|
await handleUpdate();
|
|
8570
8733
|
process.exit(0);
|
|
8571
8734
|
}
|
|
8735
|
+
if (args[0] === "version") {
|
|
8736
|
+
options.version = true;
|
|
8737
|
+
}
|
|
8572
8738
|
for (let i = 0;i < args.length; i++) {
|
|
8573
8739
|
const arg = args[i];
|
|
8574
8740
|
if (arg === "--help" || arg === "-h") {
|
|
8575
8741
|
options.help = true;
|
|
8742
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
8743
|
+
options.version = true;
|
|
8576
8744
|
} else if (arg === "--yes" || arg === "-y") {
|
|
8577
8745
|
options.yes = true;
|
|
8578
8746
|
} else if (arg === "--source" || arg === "-s") {
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import type { ConvertOptions } from "./types";
|
|
2
3
|
import { IPromptService, InteractivePromptService } from "./prompts";
|
|
3
|
-
import { convertImage, displayConversionResult } from "./converter";
|
|
4
|
-
import { getDefaultDestinationPath } from "./utils/path";
|
|
4
|
+
import { convertImage, displayConversionResult, convertBatch, displayBatchResult } from "./converter";
|
|
5
|
+
import { getDefaultDestinationPath, isDirectory, getImageFilesFromDirectory } from "./utils/path";
|
|
6
|
+
import type { BatchConversionSettings } from "./types";
|
|
5
7
|
|
|
6
8
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/image-convert-cli/latest";
|
|
7
9
|
|
|
@@ -45,6 +47,12 @@ export async function runCli(
|
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
if (options.version) {
|
|
51
|
+
const version = process.env.npm_package_version || "1.1.0";
|
|
52
|
+
console.log(`image-convert-cli v${version}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
if (options.yes && (!options.source || !options.format)) {
|
|
49
57
|
console.error("Error: -y mode requires --source and --format arguments");
|
|
50
58
|
prompts.showHelp();
|
|
@@ -58,6 +66,15 @@ export async function runCli(
|
|
|
58
66
|
const targetFormat =
|
|
59
67
|
options.format || (await prompts.promptFormat());
|
|
60
68
|
|
|
69
|
+
// Check if source is a directory (batch mode)
|
|
70
|
+
const isBatchMode = isDirectory(sourcePath);
|
|
71
|
+
|
|
72
|
+
if (isBatchMode) {
|
|
73
|
+
await runBatchMode(sourcePath, targetFormat, options, prompts);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Single file conversion
|
|
61
78
|
const destinationPath =
|
|
62
79
|
options.destination ||
|
|
63
80
|
(await prompts.promptDestination(
|
|
@@ -97,3 +114,40 @@ export async function runCli(
|
|
|
97
114
|
process.exit(1);
|
|
98
115
|
}
|
|
99
116
|
}
|
|
117
|
+
|
|
118
|
+
async function runBatchMode(
|
|
119
|
+
sourceDir: string,
|
|
120
|
+
targetFormat: string,
|
|
121
|
+
options: ConvertOptions,
|
|
122
|
+
prompts: IPromptService,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
const yesMode = options.yes === true;
|
|
125
|
+
|
|
126
|
+
// Get destination directory - default to same as source
|
|
127
|
+
const destinationDir = options.destination || (yesMode ? sourceDir : await prompts.promptBatchDestination(sourceDir));
|
|
128
|
+
|
|
129
|
+
const compress = yesMode ? false : await prompts.promptCompress();
|
|
130
|
+
|
|
131
|
+
// Count files to convert
|
|
132
|
+
const files = getImageFilesFromDirectory(sourceDir);
|
|
133
|
+
|
|
134
|
+
// In non-yes mode, show batch confirmation
|
|
135
|
+
if (!yesMode) {
|
|
136
|
+
const shouldProceed = await prompts.promptBatchConfirm(files.length);
|
|
137
|
+
if (!shouldProceed) {
|
|
138
|
+
console.log("Batch conversion cancelled.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const batchSettings: BatchConversionSettings = {
|
|
144
|
+
sourceDir,
|
|
145
|
+
targetFormat: targetFormat as any,
|
|
146
|
+
destinationDir,
|
|
147
|
+
compress,
|
|
148
|
+
yesMode,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const result = await convertBatch(batchSettings);
|
|
152
|
+
displayBatchResult(result);
|
|
153
|
+
}
|
package/src/converter.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import sharp from "sharp";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ConversionResult,
|
|
5
|
+
SupportedFormat,
|
|
6
|
+
BatchConversionSettings,
|
|
7
|
+
BatchConversionSummary,
|
|
8
|
+
BatchConversionResult,
|
|
9
|
+
} from "./types";
|
|
4
10
|
import { formatBytes } from "./utils/format";
|
|
11
|
+
import {
|
|
12
|
+
getImageFilesFromDirectory,
|
|
13
|
+
isSameFormat,
|
|
14
|
+
ensureDirectoryExists,
|
|
15
|
+
} from "./utils/path";
|
|
5
16
|
|
|
6
17
|
export async function convertImage(
|
|
7
18
|
sourcePath: string,
|
|
@@ -83,3 +94,99 @@ export function displayConversionResult(result: ConversionResult): void {
|
|
|
83
94
|
console.error("\n✗ Conversion failed:", result.error);
|
|
84
95
|
}
|
|
85
96
|
}
|
|
97
|
+
|
|
98
|
+
export async function convertBatch(
|
|
99
|
+
settings: BatchConversionSettings,
|
|
100
|
+
): Promise<BatchConversionSummary> {
|
|
101
|
+
const startTime = Date.now();
|
|
102
|
+
const files = getImageFilesFromDirectory(settings.sourceDir);
|
|
103
|
+
|
|
104
|
+
await ensureDirectoryExists(settings.destinationDir);
|
|
105
|
+
|
|
106
|
+
const results: BatchConversionResult[] = [];
|
|
107
|
+
let successCount = 0;
|
|
108
|
+
let failureCount = 0;
|
|
109
|
+
let skippedCount = 0;
|
|
110
|
+
|
|
111
|
+
for (const sourcePath of files) {
|
|
112
|
+
const fileName = require("node:path").basename(sourcePath);
|
|
113
|
+
const destPath = require("node:path").join(
|
|
114
|
+
settings.destinationDir,
|
|
115
|
+
`${require("node:path").parse(fileName).name}.${settings.targetFormat}`,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Skip files already in target format
|
|
119
|
+
if (isSameFormat(sourcePath, settings.targetFormat)) {
|
|
120
|
+
skippedCount++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Skip existing files in yes mode
|
|
125
|
+
if (settings.yesMode && fs.existsSync(destPath)) {
|
|
126
|
+
skippedCount++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = await convertImage(
|
|
131
|
+
sourcePath,
|
|
132
|
+
destPath,
|
|
133
|
+
settings.targetFormat,
|
|
134
|
+
settings.compress,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
results.push(result);
|
|
138
|
+
|
|
139
|
+
if (result.success) {
|
|
140
|
+
successCount++;
|
|
141
|
+
} else {
|
|
142
|
+
failureCount++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
totalFiles: files.length,
|
|
148
|
+
successCount,
|
|
149
|
+
failureCount,
|
|
150
|
+
skippedCount,
|
|
151
|
+
results,
|
|
152
|
+
totalElapsed: Date.now() - startTime,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function displayBatchResult(summary: BatchConversionSummary): void {
|
|
157
|
+
console.log("\nBatch conversion complete!");
|
|
158
|
+
console.log(` Total files: ${summary.totalFiles}`);
|
|
159
|
+
console.log(` Converted: ${summary.successCount}`);
|
|
160
|
+
console.log(` Failed: ${summary.failureCount}`);
|
|
161
|
+
console.log(` Skipped: ${summary.skippedCount}`);
|
|
162
|
+
console.log(` Time: ${summary.totalElapsed}ms`);
|
|
163
|
+
|
|
164
|
+
if (summary.successCount > 0) {
|
|
165
|
+
let totalOriginal = 0;
|
|
166
|
+
let totalOutput = 0;
|
|
167
|
+
|
|
168
|
+
for (const result of summary.results) {
|
|
169
|
+
if (result.success) {
|
|
170
|
+
totalOriginal += result.originalSize;
|
|
171
|
+
totalOutput += result.outputSize;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (totalOriginal > 0) {
|
|
176
|
+
console.log(
|
|
177
|
+
` Saved: ${(((totalOriginal - totalOutput) / totalOriginal) * 100).toFixed(1)}%`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`\nOutput directory: ${summary.results[0]?.destinationPath ? require("node:path").dirname(summary.results[0].destinationPath) : "N/A"}`);
|
|
183
|
+
|
|
184
|
+
if (summary.failureCount > 0) {
|
|
185
|
+
console.log("\nFailed files:");
|
|
186
|
+
for (const result of summary.results) {
|
|
187
|
+
if (!result.success) {
|
|
188
|
+
console.log(` - ${result.sourcePath}: ${result.error}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from "./types";
|
|
2
2
|
export * from "./utils/path";
|
|
3
3
|
export * from "./utils/format";
|
|
4
|
-
export { convertImage, displayConversionResult } from "./converter";
|
|
4
|
+
export { convertImage, displayConversionResult, convertBatch, displayBatchResult } from "./converter";
|
|
5
5
|
export { runCli, handleUpdate } from "./cli";
|
|
6
6
|
export {
|
|
7
7
|
type IPromptService,
|
package/src/prompts.ts
CHANGED
|
@@ -72,6 +72,9 @@ export interface IPromptService {
|
|
|
72
72
|
promptFormat(): Promise<SupportedFormat>;
|
|
73
73
|
promptCompress(): Promise<boolean>;
|
|
74
74
|
promptConfirm(settings: ConversionSettings): Promise<boolean>;
|
|
75
|
+
promptForOverwrite(filePath: string): Promise<boolean>;
|
|
76
|
+
promptBatchDestination(defaultPath: string, validate?: (path: string) => string | true): Promise<string>;
|
|
77
|
+
promptBatchConfirm(fileCount: number): Promise<boolean>;
|
|
75
78
|
showHelp(): void;
|
|
76
79
|
showSettings(settings: ConversionSettings): void;
|
|
77
80
|
showResult(result: { success: boolean; destinationPath: string; elapsed: number; originalSize: number; outputSize: number; error?: string }): void;
|
|
@@ -144,6 +147,40 @@ export class InteractivePromptService implements IPromptService {
|
|
|
144
147
|
});
|
|
145
148
|
}
|
|
146
149
|
|
|
150
|
+
async promptForOverwrite(filePath: string): Promise<boolean> {
|
|
151
|
+
return confirm({
|
|
152
|
+
message: `File ${filePath} already exists. Overwrite?`,
|
|
153
|
+
default: false,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async promptBatchDestination(
|
|
158
|
+
defaultPath: string,
|
|
159
|
+
validate?: (path: string) => string | true,
|
|
160
|
+
): Promise<string> {
|
|
161
|
+
return inputWithPathCompletion(
|
|
162
|
+
"Output directory",
|
|
163
|
+
defaultPath,
|
|
164
|
+
validate ||
|
|
165
|
+
((input) => {
|
|
166
|
+
if (!input.trim()) {
|
|
167
|
+
return "Please enter an output directory";
|
|
168
|
+
}
|
|
169
|
+
if (!fs.existsSync(input)) {
|
|
170
|
+
return "Output directory does not exist";
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async promptBatchConfirm(fileCount: number): Promise<boolean> {
|
|
178
|
+
return confirm({
|
|
179
|
+
message: `Convert ${fileCount} files?`,
|
|
180
|
+
default: true,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
147
184
|
showHelp(): void {
|
|
148
185
|
console.log(`Image Converter CLI
|
|
149
186
|
|
|
@@ -152,21 +189,25 @@ Usage:
|
|
|
152
189
|
imgc --source <path> --format <format> [options]
|
|
153
190
|
imgc -y --source <path> --format <format>
|
|
154
191
|
imgc update # Check for updates
|
|
192
|
+
imgc version # Show version
|
|
155
193
|
|
|
156
194
|
Options:
|
|
157
195
|
--help, -h Show this help message
|
|
196
|
+
--version, -v Show version number
|
|
158
197
|
--yes, -y Non-interactive mode (use defaults for optional prompts)
|
|
159
198
|
--source, -s Source file path (required with -y)
|
|
160
199
|
--format, -f Target format: webp, jpeg, jpg, or png (required with -y)
|
|
161
200
|
--dest, -d Destination path (optional, auto-generated if not provided)
|
|
162
|
-
--compress, -c
|
|
201
|
+
--compress, -c Enable compression (optional, default: false)
|
|
163
202
|
|
|
164
203
|
Commands:
|
|
165
204
|
update Check for the latest version on npm
|
|
205
|
+
version Show version number
|
|
166
206
|
|
|
167
207
|
Examples:
|
|
168
208
|
imgc # Interactive mode
|
|
169
209
|
imgc --help # Show help
|
|
210
|
+
imgc --version # Show version
|
|
170
211
|
imgc -y --source photo.png --format webp
|
|
171
212
|
imgc -y --source photo.jpg --format jpeg --compress
|
|
172
213
|
imgc update # Check for updates
|
|
@@ -201,6 +242,9 @@ export class NoopPromptService implements IPromptService {
|
|
|
201
242
|
format?: SupportedFormat;
|
|
202
243
|
compress?: boolean;
|
|
203
244
|
confirm?: boolean;
|
|
245
|
+
overwrite?: boolean;
|
|
246
|
+
batchDestination?: string;
|
|
247
|
+
batchConfirm?: boolean;
|
|
204
248
|
};
|
|
205
249
|
|
|
206
250
|
constructor(responses?: {
|
|
@@ -209,6 +253,9 @@ export class NoopPromptService implements IPromptService {
|
|
|
209
253
|
format?: SupportedFormat;
|
|
210
254
|
compress?: boolean;
|
|
211
255
|
confirm?: boolean;
|
|
256
|
+
overwrite?: boolean;
|
|
257
|
+
batchDestination?: string;
|
|
258
|
+
batchConfirm?: boolean;
|
|
212
259
|
}) {
|
|
213
260
|
this.responses = responses || {};
|
|
214
261
|
}
|
|
@@ -233,6 +280,18 @@ export class NoopPromptService implements IPromptService {
|
|
|
233
280
|
return this.responses.confirm ?? true;
|
|
234
281
|
}
|
|
235
282
|
|
|
283
|
+
async promptForOverwrite(): Promise<boolean> {
|
|
284
|
+
return this.responses.overwrite ?? false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async promptBatchDestination(defaultPath: string): Promise<string> {
|
|
288
|
+
return this.responses.batchDestination || defaultPath;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async promptBatchConfirm(): Promise<boolean> {
|
|
292
|
+
return this.responses.batchConfirm ?? true;
|
|
293
|
+
}
|
|
294
|
+
|
|
236
295
|
showHelp(): void {}
|
|
237
296
|
showSettings(): void {}
|
|
238
297
|
showResult(): void {}
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type SupportedFormat = "webp" | "jpeg" | "jpg" | "png";
|
|
|
2
2
|
|
|
3
3
|
export type ConvertOptions = {
|
|
4
4
|
help?: boolean;
|
|
5
|
+
version?: boolean;
|
|
5
6
|
yes?: boolean;
|
|
6
7
|
source?: string;
|
|
7
8
|
format?: SupportedFormat;
|
|
@@ -25,3 +26,30 @@ export type ConversionSettings = {
|
|
|
25
26
|
destinationPath: string;
|
|
26
27
|
compress: boolean;
|
|
27
28
|
};
|
|
29
|
+
|
|
30
|
+
export type BatchConversionResult = {
|
|
31
|
+
success: boolean;
|
|
32
|
+
sourcePath: string;
|
|
33
|
+
destinationPath: string;
|
|
34
|
+
originalSize: number;
|
|
35
|
+
outputSize: number;
|
|
36
|
+
elapsed: number;
|
|
37
|
+
error?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type BatchConversionSettings = {
|
|
41
|
+
sourceDir: string;
|
|
42
|
+
targetFormat: SupportedFormat;
|
|
43
|
+
destinationDir: string;
|
|
44
|
+
compress: boolean;
|
|
45
|
+
yesMode: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type BatchConversionSummary = {
|
|
49
|
+
totalFiles: number;
|
|
50
|
+
successCount: number;
|
|
51
|
+
failureCount: number;
|
|
52
|
+
skippedCount: number;
|
|
53
|
+
results: BatchConversionResult[];
|
|
54
|
+
totalElapsed: number;
|
|
55
|
+
};
|
package/src/utils/path.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import * as fs from "node:fs";
|
|
2
3
|
import type { SupportedFormat } from "../types";
|
|
3
4
|
|
|
4
5
|
export function getDefaultDestinationPath(
|
|
@@ -8,3 +9,46 @@ export function getDefaultDestinationPath(
|
|
|
8
9
|
const parsed = path.parse(sourcePath);
|
|
9
10
|
return path.join(parsed.dir, `${parsed.name}.${format}`);
|
|
10
11
|
}
|
|
12
|
+
|
|
13
|
+
export function isDirectory(sourcePath: string): boolean {
|
|
14
|
+
try {
|
|
15
|
+
return fs.statSync(sourcePath).isDirectory();
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getExtension(filePath: string): string {
|
|
22
|
+
const ext = path.extname(filePath);
|
|
23
|
+
return ext.length > 0 ? ext.slice(1).toLowerCase() : "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isSameFormat(sourcePath: string, targetFormat: SupportedFormat): boolean {
|
|
27
|
+
const ext = getExtension(sourcePath);
|
|
28
|
+
if (ext === targetFormat) return true;
|
|
29
|
+
// Handle jpeg/jpg equivalence
|
|
30
|
+
if ((ext === "jpeg" || ext === "jpg") && (targetFormat === "jpeg" || targetFormat === "jpg")) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SUPPORTED_FORMATS = ["webp", "jpeg", "jpg", "png"];
|
|
37
|
+
|
|
38
|
+
export function getImageFilesFromDirectory(dirPath: string): string[] {
|
|
39
|
+
try {
|
|
40
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
41
|
+
return entries
|
|
42
|
+
.filter((entry) => entry.isFile())
|
|
43
|
+
.map((entry) => path.join(dirPath, entry.name))
|
|
44
|
+
.filter((filePath) => SUPPORTED_FORMATS.includes(getExtension(filePath)));
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function ensureDirectoryExists(dirPath: string): Promise<void> {
|
|
51
|
+
if (!fs.existsSync(dirPath)) {
|
|
52
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
}
|