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 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 JPG formats
3
+ > A fast, interactive CLI tool for converting images between WebP, JPEG, JPG and PNG formats
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 fs2 from "fs";
6577
- import * as path from "path";
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 fs from "fs";
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 = fs.statSync(sourcePath).size;
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 = fs.statSync(destinationPath).size;
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) || "." : path.dirname(input) || ".";
8364
- const base = isDirectoryInput ? "" : path.basename(input) || "";
8474
+ const dir = isDirectoryInput ? input.slice(0, -1) || "." : path2.dirname(input) || ".";
8475
+ const base = isDirectoryInput ? "" : path2.basename(input) || "";
8365
8476
  try {
8366
- const files = fs2.readdirSync(dir, { withFileTypes: true });
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 (!fs2.existsSync(input)) {
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 = path.dirname(input);
8420
- if (dir !== "." && !fs2.existsSync(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 Enable compression (optional, default: false)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-convert-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "imgc": "./dist/index.js"
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 { ConversionResult, SupportedFormat } from "./types";
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 Enable compression (optional, default: false)
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
+ }