change-image-suffix 2.1.3 → 2.1.5

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 CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [2.1.5](https://github.com/GuoSirius/change-image-suffix/compare/v2.1.4...v2.1.5) (2026-05-23)
6
+
7
+
8
+ ### Chores
9
+
10
+ * upgrade deps (sharp 0.34, TS6, @types/node 25), migrate to ESM, require node >=24 ([d3a3234](https://github.com/GuoSirius/change-image-suffix/commit/d3a323410ccd1f5ed9f04443adc25c6edd96f7df))
11
+
12
+ ### [2.1.4](https://github.com/GuoSirius/change-image-suffix/compare/v2.1.3...v2.1.4) (2026-05-23)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * output to <format>/ dir, command injection, recursive nesting, and other issues ([32da38a](https://github.com/GuoSirius/change-image-suffix/commit/32da38aef36a7d736e91e0375573a905d565d864))
18
+
19
+
20
+ ### Chores
21
+
22
+ * update local settings permissions ([1a86130](https://github.com/GuoSirius/change-image-suffix/commit/1a86130de6005348d6ba8aaba26500d7688be996))
23
+
24
+
25
+ ### Documentation
26
+
27
+ * update README output convention, add .claude/ commit policy memory ([b689bc6](https://github.com/GuoSirius/change-image-suffix/commit/b689bc60ed397eb826954e289c3f89488bd97449))
28
+
5
29
  ### [2.1.3](https://github.com/GuoSirius/change-image-suffix/compare/v2.1.2...v2.1.3) (2026-05-21)
6
30
 
7
31
 
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  - 📏 支持递归深度限制
10
10
  - 🎯 支持指定源文件后缀(png, jpg, gif 等)
11
11
  - 🎨 支持多种目标格式(webp, jpg, png, avif, tiff)
12
- - 📤 输出到 `output/` 子目录
12
+ - 📤 输出到 `<目标格式>/` 子目录(如 `webp/`, `jpg/`)
13
13
  - 🔢 同名不同后缀文件自动编号(`_01`, `_02`)
14
14
  - 🖱️ Windows 右键菜单集成
15
15
  - ⚡ 基于 [sharp](https://sharp.pixel.glass/) 高性能图片处理
@@ -101,7 +101,7 @@ cis uninstall-menu
101
101
 
102
102
  ## 文件命名规范
103
103
 
104
- 转换后的文件输出到 **`output/`** 子目录下,命名规则如下:
104
+ 转换后的文件输出到 **`<目标格式>/`** 子目录下(例如转换为 webp 则输出到 `webp/`,转换为 jpg 则输出到 `jpg/`),命名规则如下:
105
105
 
106
106
  ### 同名不同后缀 → 自动编号
107
107
 
@@ -109,34 +109,36 @@ cis uninstall-menu
109
109
 
110
110
  ```
111
111
  源目录: photo.png + photo.jpg + photo.gif
112
- 输出: output/photo_01.webp
113
- output/photo_02.webp
114
- output/photo_03.webp
112
+ 输出: webp/photo_01.webp
113
+ webp/photo_02.webp
114
+ webp/photo_03.webp
115
115
  ```
116
116
 
117
117
  ### 不同名或不同文件 → 直接覆盖
118
118
 
119
119
  ```
120
120
  源目录: banner.png + logo.jpg
121
- 输出: output/banner.webp
122
- output/logo.webp
121
+ 输出: webp/banner.webp
122
+ webp/logo.webp
123
123
  ```
124
124
 
125
125
  ### 同格式转换 → 直接覆盖
126
126
 
127
127
  ```
128
128
  源目录: photo.webp
129
- 输出: output/photo.webp (直接覆盖,无双重后缀)
129
+ 输出: webp/photo.webp (直接复制,无双重后缀)
130
130
  ```
131
131
 
132
132
  ### 输出目录结构
133
133
 
134
134
  ```
135
135
  📁 原目录/
136
- ├── 📁 output/ 转换后的文件
136
+ ├── 📁 webp/ 转换后的 webp 文件
137
137
  │ ├── photo.webp
138
- │ ├── banner.jpg
139
- │ └── photo_01.png
138
+ │ ├── banner.webp
139
+ │ └── photo_01.webp
140
+ ├── 📁 jpg/ ← 转换后的 jpg 文件
141
+ │ └── ...
140
142
  ├── photo.png
141
143
  ├── banner.jpg
142
144
  └── logo.gif
@@ -193,14 +195,14 @@ cis -f ./avatar.png -t webp
193
195
  ### 示例 6:多选文件/目录批量转换
194
196
 
195
197
  ```bash
196
- # 多选文件(空格分隔),每个文件的输出在其所在目录的 output/
198
+ # 多选文件(空格分隔),每个文件的输出在其所在目录的 <目标格式>/
197
199
  cis ./photo1.png ./photo2.jpg ./folder3
198
200
 
199
201
  # 多选多个目录
200
202
  cis ./images ./icons ./logos
201
203
  ```
202
204
 
203
- > 💡 **多选时**:每个文件/目录的输出结果放在**各自所在目录的 `output/` 子目录**中,互不干扰。
205
+ > 💡 **多选时**:每个文件/目录的输出结果放在**各自所在目录的 `<目标格式>/` 子目录**中(如 `webp/`, `jpg/`),互不干扰。
204
206
 
205
207
  ---
206
208
 
package/dist/index.js CHANGED
@@ -1,47 +1,12 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || (function () {
20
- var ownKeys = function(o) {
21
- ownKeys = Object.getOwnPropertyNames || function (o) {
22
- var ar = [];
23
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
- return ar;
25
- };
26
- return ownKeys(o);
27
- };
28
- return function (mod) {
29
- if (mod && mod.__esModule) return mod;
30
- var result = {};
31
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
- __setModuleDefault(result, mod);
33
- return result;
34
- };
35
- })();
36
- var __importDefault = (this && this.__importDefault) || function (mod) {
37
- return (mod && mod.__esModule) ? mod : { "default": mod };
38
- };
39
- Object.defineProperty(exports, "__esModule", { value: true });
40
- const fs = __importStar(require("fs"));
41
- const path = __importStar(require("path"));
42
- const os = __importStar(require("os"));
43
- const child_process_1 = require("child_process");
44
- const sharp_1 = __importDefault(require("sharp"));
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { execSync } from 'child_process';
6
+ import { fileURLToPath } from 'url';
7
+ import sharp from 'sharp';
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
45
10
  // 支持的输入/输出格式
46
11
  const SUPPORTED_INPUT_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'avif'];
47
12
  const SUPPORTED_OUTPUT_FORMATS = ['webp', 'jpg', 'jpeg', 'png', 'avif', 'tiff', 'tif'];
@@ -58,7 +23,7 @@ function requireWindows() {
58
23
  }
59
24
  function regDelete(key) {
60
25
  try {
61
- (0, child_process_1.execSync)(`reg delete "${key}" /f`, { stdio: 'ignore' });
26
+ execSync(`reg delete "${key}" /f`, { stdio: 'ignore' });
62
27
  }
63
28
  catch {
64
29
  // 忽略不存在的键
@@ -69,11 +34,11 @@ function installContextMenu() {
69
34
  // ── 查找 cis.cmd 路径 ──
70
35
  let cisCmd = '';
71
36
  try {
72
- cisCmd = (0, child_process_1.execSync)('where cis.cmd', { encoding: 'utf8' }).trim().split('\n')[0].trim();
37
+ cisCmd = execSync('where cis.cmd', { encoding: 'utf8' }).trim().split('\n')[0].trim();
73
38
  }
74
39
  catch {
75
40
  try {
76
- cisCmd = (0, child_process_1.execSync)('where cis', { encoding: 'utf8' }).trim().split('\n')[0].trim();
41
+ cisCmd = execSync('where cis', { encoding: 'utf8' }).trim().split('\n')[0].trim();
77
42
  }
78
43
  catch {
79
44
  console.error('❌ 找不到 cis 命令,请先执行 npm link 或 npm install -g change-image-suffix');
@@ -176,21 +141,21 @@ endlocal
176
141
  else {
177
142
  cmd = `"${cisCmd}" -t ${fmt.verb} ${menu.arg}`;
178
143
  }
179
- (0, child_process_1.execSync)(`reg add "${shellKey}" /ve /d "${fmt.label}" /f`, { stdio: 'ignore' });
180
- (0, child_process_1.execSync)(`reg add "${shellKey}" /v Icon /d "${iconPath}" /f`, { stdio: 'ignore' });
144
+ execSync(`reg add "${shellKey}" /ve /d "${fmt.label}" /f`, { stdio: 'ignore' });
145
+ execSync(`reg add "${shellKey}" /v Icon /d "${iconPath}" /f`, { stdio: 'ignore' });
181
146
  // 直接调用 bat,不需要 cmd /c,Windows 会自动追加文件路径
182
- (0, child_process_1.execSync)(`reg add "${shellKey}\\command" /ve /d "${cmd}" /f`, { stdio: 'ignore' });
147
+ execSync(`reg add "${shellKey}\\command" /ve /d "${cmd}" /f`, { stdio: 'ignore' });
183
148
  }
184
149
  }
185
150
  // 2. 注册主菜单项(使用 ExtendedSubCommandsKey 关联各自的子菜单)
186
151
  for (const menu of menuBases) {
187
- (0, child_process_1.execSync)(`reg add "${menu.base}" /ve /d "🖼 转换图片 (cis)" /f`, { stdio: 'ignore' });
188
- (0, child_process_1.execSync)(`reg add "${menu.base}" /v Icon /d "${iconPath}" /f`, { stdio: 'ignore' });
189
- (0, child_process_1.execSync)(`reg add "${menu.base}" /v ExtendedSubCommandsKey /d "${menu.subMenu}" /f`, { stdio: 'ignore' });
152
+ execSync(`reg add "${menu.base}" /ve /d "🖼 转换图片 (cis)" /f`, { stdio: 'ignore' });
153
+ execSync(`reg add "${menu.base}" /v Icon /d "${iconPath}" /f`, { stdio: 'ignore' });
154
+ execSync(`reg add "${menu.base}" /v ExtendedSubCommandsKey /d "${menu.subMenu}" /f`, { stdio: 'ignore' });
190
155
  // 使用 bat 的菜单(文件右键和目录右键):设置 command 接收文件路径 %1
191
156
  // Windows 会将父命令收到的 %1 自动传递给子命令
192
157
  if (menu.useBat) {
193
- (0, child_process_1.execSync)(`reg add "${menu.base}\\command" /ve /d "cmd /c echo %1 > nul" /f`, { stdio: 'ignore' });
158
+ execSync(`reg add "${menu.base}\\command" /ve /d "cmd /c echo %1 > nul" /f`, { stdio: 'ignore' });
194
159
  // 注意:不添加 AppliesTo 限制,让菜单始终显示
195
160
  // bat 脚本会检查文件扩展名,自动忽略非图片文件
196
161
  }
@@ -203,7 +168,7 @@ endlocal
203
168
  console.log(' 📁 文件夹空白处/图标右键 → 悬停展开格式子菜单');
204
169
  console.log(' 🖼 图片文件上右键 → 悬停展开格式子菜单');
205
170
  console.log(' ⚠️ 非图片文件右键 → 菜单显示但不处理');
206
- console.log(` 📂 输出目录: <原目录>/output/`);
171
+ console.log(` 📂 输出目录: <原目录>/<目标格式>/`);
207
172
  console.log('\n💡 提示:如需卸载,执行 cis uninstall-menu');
208
173
  }
209
174
  function uninstallContextMenu() {
@@ -216,7 +181,7 @@ function uninstallContextMenu() {
216
181
  ];
217
182
  for (const key of mainKeys) {
218
183
  try {
219
- (0, child_process_1.execSync)(`reg delete "${key}" /f`, { stdio: 'ignore' });
184
+ execSync(`reg delete "${key}" /f`, { stdio: 'ignore' });
220
185
  }
221
186
  catch { /* ignore */ }
222
187
  }
@@ -228,22 +193,32 @@ function uninstallContextMenu() {
228
193
  ];
229
194
  for (const root of subMenuRoots) {
230
195
  try {
231
- (0, child_process_1.execSync)(`reg delete "${root}" /f`, { stdio: 'ignore' });
196
+ execSync(`reg delete "${root}" /f`, { stdio: 'ignore' });
232
197
  }
233
198
  catch { /* ignore */ }
234
199
  }
235
- // 删除批处理文件和版本标记
200
+ // 删除批处理文件、图标和版本标记
236
201
  const appDataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'change-image-suffix');
237
202
  const batPath = path.join(appDataDir, 'cis_file.bat');
238
203
  try {
239
204
  fs.unlinkSync(batPath);
240
205
  }
241
206
  catch { /* ignore */ }
207
+ const iconPath = path.join(appDataDir, 'icon.ico');
208
+ try {
209
+ fs.unlinkSync(iconPath);
210
+ }
211
+ catch { /* ignore */ }
242
212
  const versionFile = path.join(appDataDir, 'version.json');
243
213
  try {
244
214
  fs.unlinkSync(versionFile);
245
215
  }
246
216
  catch { /* ignore */ }
217
+ // 尝试删除目录(仅当为空时),失败也不影响
218
+ try {
219
+ fs.rmdirSync(appDataDir);
220
+ }
221
+ catch { /* ignore */ }
247
222
  console.log('✅ 右键菜单已卸载');
248
223
  }
249
224
  // 自动检测版本变化并更新右键菜单(解决 npm update 不触发 postinstall 的问题)
@@ -252,20 +227,23 @@ function autoUpdateContextMenu() {
252
227
  return;
253
228
  const appDataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'change-image-suffix');
254
229
  const versionFile = path.join(appDataDir, 'version.json');
230
+ // 如果从未安装过右键菜单,跳过自动更新(postinstall 负责首次安装)
231
+ if (!fs.existsSync(versionFile))
232
+ return;
255
233
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
256
234
  const currentVersion = pkg.version;
257
235
  let installedVersion = '';
258
236
  try {
259
- if (fs.existsSync(versionFile)) {
260
- installedVersion = JSON.parse(fs.readFileSync(versionFile, 'utf8')).version || '';
261
- }
237
+ installedVersion = JSON.parse(fs.readFileSync(versionFile, 'utf8')).version || '';
262
238
  }
263
239
  catch { /* ignore */ }
264
240
  if (installedVersion !== currentVersion) {
265
241
  try {
266
242
  installContextMenu();
267
243
  }
268
- catch { /* menu update failed silently, will retry next invocation */ }
244
+ catch {
245
+ console.warn('⚠️ 右键菜单自动更新失败,请手动执行 cis install-menu');
246
+ }
269
247
  }
270
248
  }
271
249
  // ─────────────────────────────────────────
@@ -294,27 +272,40 @@ function parseArgs() {
294
272
  continue;
295
273
  }
296
274
  if (arg === '-d' || arg === '--depth') {
297
- if (i + 1 < args.length) {
298
- options.maxDepth = parseInt(args[++i], 10);
299
- if (isNaN(options.maxDepth) || options.maxDepth < 1) {
275
+ if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
276
+ const val = parseInt(args[++i], 10);
277
+ if (isNaN(val) || val < 1) {
300
278
  console.error('❌ 深度必须是正整数');
301
279
  process.exit(1);
302
280
  }
281
+ options.maxDepth = val;
282
+ }
283
+ else {
284
+ console.error('❌ -d/--depth 需要指定一个正整数参数');
285
+ process.exit(1);
303
286
  }
304
287
  i++;
305
288
  continue;
306
289
  }
307
290
  if (arg === '-e' || arg === '--extensions') {
308
- if (i + 1 < args.length) {
291
+ if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
309
292
  options.extensions = args[++i].split(',').map(e => e.trim().toLowerCase().replace(/^\./, ''));
310
293
  }
294
+ else {
295
+ console.error('❌ -e/--extensions 需要指定后缀参数');
296
+ process.exit(1);
297
+ }
311
298
  i++;
312
299
  continue;
313
300
  }
314
301
  if (arg === '-t' || arg === '--to') {
315
- if (i + 1 < args.length) {
302
+ if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
316
303
  options.targetFormat = args[++i].trim().toLowerCase().replace(/^\./, '');
317
304
  }
305
+ else {
306
+ console.error('❌ -t/--to 需要指定目标格式');
307
+ process.exit(1);
308
+ }
318
309
  i++;
319
310
  continue;
320
311
  }
@@ -343,10 +334,15 @@ function parseArgs() {
343
334
  }
344
335
  if (arg === '-f' || arg === '--file') {
345
336
  // 收集 -f 后的所有文件
337
+ const start = i + 1;
346
338
  while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
347
339
  i++;
348
340
  filesFromFlag.push(path.resolve(args[i]));
349
341
  }
342
+ if (start > i) {
343
+ console.error('❌ -f/--file 需要指定至少一个文件路径');
344
+ process.exit(1);
345
+ }
350
346
  i++;
351
347
  continue;
352
348
  }
@@ -434,14 +430,16 @@ function printHelp() {
434
430
  // ─────────────────────────────────────────
435
431
  // 图片处理
436
432
  // ─────────────────────────────────────────
437
- function getAllFiles(dir, extensions, recursive, currentDepth, maxDepth) {
433
+ function getAllFiles(dir, extensions, recursive, currentDepth, maxDepth, excludeDirName) {
438
434
  const files = [];
439
435
  try {
440
436
  const entries = fs.readdirSync(dir, { withFileTypes: true });
441
437
  for (const entry of entries) {
442
438
  const fullPath = path.join(dir, entry.name);
443
439
  if (entry.isDirectory() && recursive && currentDepth < maxDepth) {
444
- files.push(...getAllFiles(fullPath, extensions, recursive, currentDepth + 1, maxDepth));
440
+ if (excludeDirName && entry.name === excludeDirName)
441
+ continue;
442
+ files.push(...getAllFiles(fullPath, extensions, recursive, currentDepth + 1, maxDepth, excludeDirName));
445
443
  }
446
444
  else if (entry.isFile()) {
447
445
  const ext = path.extname(entry.name).slice(1).toLowerCase();
@@ -462,7 +460,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
462
460
  const basename = path.basename(inputPath, ext);
463
461
  const targetExt = targetFormat;
464
462
  let coreName = basename;
465
- const outputDir = path.join(dir, 'output');
463
+ const outputDir = path.join(dir, targetFormat);
466
464
  // 检查输入目录中是否有同名(不含扩展名)但不同后缀的文件
467
465
  // 如 photo.png 和 photo.jpg 会被判定为同名冲突
468
466
  const hasNameConflict = allInputFiles.some(f => {
@@ -471,7 +469,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
471
469
  const fDir = path.dirname(f);
472
470
  const fExt = path.extname(f);
473
471
  const fBasename = path.basename(f, fExt);
474
- return fDir === dir && fBasename === basename && fExt.toLowerCase() !== ext.toLowerCase();
472
+ return fDir === dir && fBasename.toLowerCase() === basename.toLowerCase() && fExt.toLowerCase() !== ext.toLowerCase();
475
473
  });
476
474
  if (hasNameConflict) {
477
475
  // 找所有同basename的文件的序号
@@ -481,7 +479,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
481
479
  const fDir = path.dirname(f);
482
480
  const fExt = path.extname(f);
483
481
  const fBasename = path.basename(f, fExt);
484
- return fDir === dir && fBasename === basename;
482
+ return fDir === dir && fBasename.toLowerCase() === basename.toLowerCase();
485
483
  });
486
484
  // 当前文件在所有同名文件中的索引(从1开始)
487
485
  const sortedMatches = [...allBasenameMatches, inputPath].sort();
@@ -493,23 +491,24 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
493
491
  return path.join(outputDir, filename);
494
492
  }
495
493
  async function convertImage(inputPath, targetFormat, allInputFiles) {
494
+ const outputPath = getOutputPath(inputPath, targetFormat, allInputFiles);
495
+ const srcExt = path.extname(inputPath).slice(1).toLowerCase();
496
+ const fmt = targetFormat.toLowerCase();
497
+ // 先验证格式,避免在无效路径上创建目录
498
+ if (!SUPPORTED_OUTPUT_FORMATS.includes(fmt)) {
499
+ return { success: false, outputPath, error: `不支持的目标格式: ${targetFormat},支持: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}` };
500
+ }
496
501
  try {
497
- const outputPath = getOutputPath(inputPath, targetFormat, allInputFiles);
498
502
  const outputDir = path.dirname(outputPath);
499
503
  if (!fs.existsSync(outputDir)) {
500
504
  fs.mkdirSync(outputDir, { recursive: true });
501
505
  }
502
- const srcExt = path.extname(inputPath).slice(1).toLowerCase();
503
- const fmt = targetFormat.toLowerCase();
504
506
  // 同格式直接复制,避免重新编码导致质量损失
505
507
  if (srcExt === fmt || (srcExt === 'jpeg' && fmt === 'jpg') || (srcExt === 'jpg' && fmt === 'jpeg') || (srcExt === 'tif' && fmt === 'tiff') || (srcExt === 'tiff' && fmt === 'tif')) {
506
508
  fs.copyFileSync(inputPath, outputPath);
507
509
  return { success: true, outputPath };
508
510
  }
509
- if (!SUPPORTED_OUTPUT_FORMATS.includes(fmt)) {
510
- return { success: false, outputPath: inputPath, error: `不支持的目标格式: ${targetFormat},支持: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}` };
511
- }
512
- const image = (0, sharp_1.default)(inputPath);
511
+ const image = sharp(inputPath);
513
512
  switch (fmt) {
514
513
  case 'webp':
515
514
  await image.webp({ quality: 90 }).toFile(outputPath);
@@ -534,7 +533,7 @@ async function convertImage(inputPath, targetFormat, allInputFiles) {
534
533
  catch (err) {
535
534
  return {
536
535
  success: false,
537
- outputPath: inputPath,
536
+ outputPath,
538
537
  error: err instanceof Error ? err.message : '未知错误'
539
538
  };
540
539
  }
@@ -565,16 +564,18 @@ async function main() {
565
564
  console.log('----------------------------------------\n');
566
565
  let totalSuccess = 0;
567
566
  let totalFail = 0;
567
+ const failures = [];
568
568
  for (const filePath of files) {
569
569
  const ext = path.extname(filePath).slice(1).toLowerCase();
570
570
  if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
571
571
  console.log(` ⚠️ 跳过(不支持格式): ${filePath}`);
572
572
  totalFail++;
573
+ failures.push(filePath);
573
574
  continue;
574
575
  }
575
576
  console.log(` 📄 文件: ${filePath}`);
576
577
  process.stdout.write(` 处理中: ${path.basename(filePath)} ... `);
577
- const result = await convertImage(filePath, options.targetFormat, [filePath]);
578
+ const result = await convertImage(filePath, options.targetFormat, files);
578
579
  if (result.success) {
579
580
  console.log(`✅ -> ${path.relative(path.dirname(filePath), result.outputPath)}`);
580
581
  totalSuccess++;
@@ -582,6 +583,13 @@ async function main() {
582
583
  else {
583
584
  console.log(`❌ 失败 (${result.error})`);
584
585
  totalFail++;
586
+ failures.push(filePath);
587
+ }
588
+ }
589
+ if (failures.length > 0) {
590
+ console.log('\n❌ 失败的文件:');
591
+ for (const f of failures) {
592
+ console.log(` - ${f}`);
585
593
  }
586
594
  }
587
595
  return { success: totalSuccess, fail: totalFail };
@@ -593,11 +601,13 @@ async function main() {
593
601
  console.log('----------------------------------------\n');
594
602
  let totalSuccess = 0;
595
603
  let totalFail = 0;
604
+ const failures = [];
596
605
  for (const inputPath of dirs) {
597
606
  const stat = fs.existsSync(inputPath) ? fs.statSync(inputPath) : null;
598
607
  if (!stat) {
599
608
  console.log(` ⚠️ 跳过(不存在): ${inputPath}`);
600
609
  totalFail++;
610
+ failures.push(inputPath);
601
611
  continue;
602
612
  }
603
613
  if (stat.isFile()) {
@@ -605,11 +615,12 @@ async function main() {
605
615
  if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
606
616
  console.log(` ⚠️ 跳过(不支持格式): ${inputPath}`);
607
617
  totalFail++;
618
+ failures.push(inputPath);
608
619
  continue;
609
620
  }
610
621
  console.log(` 📄 文件: ${inputPath}`);
611
622
  process.stdout.write(` 处理中: ${path.basename(inputPath)} ... `);
612
- const result = await convertImage(inputPath, options.targetFormat, [inputPath]);
623
+ const result = await convertImage(inputPath, options.targetFormat, dirs);
613
624
  if (result.success) {
614
625
  console.log(`✅ -> ${path.relative(path.dirname(inputPath), result.outputPath)}`);
615
626
  totalSuccess++;
@@ -617,10 +628,11 @@ async function main() {
617
628
  else {
618
629
  console.log(`❌ 失败 (${result.error})`);
619
630
  totalFail++;
631
+ failures.push(inputPath);
620
632
  }
621
633
  }
622
634
  else {
623
- const files = getAllFiles(inputPath, options.extensions, options.recursive, 0, options.maxDepth);
635
+ const files = getAllFiles(inputPath, options.extensions, options.recursive, 0, options.maxDepth, options.targetFormat);
624
636
  console.log(` 📁 目录: ${inputPath} (${files.length} 个文件)`);
625
637
  if (files.length === 0) {
626
638
  console.log(' ✅ 没有找到图片文件');
@@ -636,10 +648,17 @@ async function main() {
636
648
  else {
637
649
  console.log(`❌ (${result.error})`);
638
650
  totalFail++;
651
+ failures.push(file);
639
652
  }
640
653
  }
641
654
  }
642
655
  }
656
+ if (failures.length > 0) {
657
+ console.log('\n❌ 失败的文件:');
658
+ for (const f of failures) {
659
+ console.log(` - ${f}`);
660
+ }
661
+ }
643
662
  return { success: totalSuccess, fail: totalFail };
644
663
  }
645
664
  // ─── 混合模式:同时有文件和目录 ───
@@ -673,7 +692,7 @@ async function main() {
673
692
  console.log(`📄 后缀: ${options.extensions.join(', ')}`);
674
693
  console.log(`🎯 目标格式: ${options.targetFormat}`);
675
694
  console.log('\n----------------------------------------\n');
676
- const files = getAllFiles(options.directory, options.extensions, options.recursive, 0, options.maxDepth);
695
+ const files = getAllFiles(options.directory, options.extensions, options.recursive, 0, options.maxDepth, options.targetFormat);
677
696
  if (files.length === 0) {
678
697
  console.log('✅ 没有找到需要转换的图片文件。');
679
698
  return;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "change-image-suffix",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
+ "type": "module",
4
5
  "description": "批量转换图片格式的CLI工具,支持递归搜索、深度限制、指定后缀、Windows右键菜单等功能",
5
6
  "main": "dist/index.js",
6
7
  "files": [
@@ -9,8 +10,8 @@
9
10
  "README.md",
10
11
  "LICENSE",
11
12
  "CHANGELOG.md",
12
- "scripts/postinstall.js",
13
- "scripts/preuninstall.js"
13
+ "scripts/postinstall.cjs",
14
+ "scripts/preuninstall.cjs"
14
15
  ],
15
16
  "bin": {
16
17
  "change-image-suffix": "./dist/index.js",
@@ -24,13 +25,13 @@
24
25
  "clean": "rimraf dist",
25
26
  "build": "tsc",
26
27
  "dev": "tsc --watch",
27
- "postinstall": "node scripts/postinstall.js",
28
- "preuninstall": "node scripts/preuninstall.js",
28
+ "postinstall": "node scripts/postinstall.cjs",
29
+ "preuninstall": "node scripts/preuninstall.cjs",
29
30
  "prepublishOnly": "npm run clean && npm run build",
30
- "release": "node scripts/release.js",
31
- "release:patch": "node scripts/release.js --patch",
32
- "release:minor": "node scripts/release.js --minor",
33
- "release:major": "node scripts/release.js --major",
31
+ "release": "node scripts/release.cjs",
32
+ "release:patch": "node scripts/release.cjs --patch",
33
+ "release:minor": "node scripts/release.cjs --minor",
34
+ "release:major": "node scripts/release.cjs --major",
34
35
  "lint": "tsc --noEmit",
35
36
  "typecheck": "tsc --noEmit"
36
37
  },
@@ -56,19 +57,19 @@
56
57
  },
57
58
  "homepage": "https://github.com/GuoSirius/change-image-suffix#readme",
58
59
  "dependencies": {
59
- "sharp": "^0.33.2"
60
+ "sharp": "^0.34.5"
60
61
  },
61
62
  "devDependencies": {
62
63
  "@commitlint/cli": "^21.0.1",
63
64
  "@commitlint/config-conventional": "^21.0.1",
64
- "@types/node": "^20.11.0",
65
+ "@types/node": "^25.9.1",
65
66
  "enquirer": "^2.4.1",
66
67
  "husky": "^9.1.7",
67
68
  "rimraf": "^6.1.3",
68
69
  "standard-version": "^9.5.0",
69
- "typescript": "^5.3.3"
70
+ "typescript": "^6.0.3"
70
71
  },
71
72
  "engines": {
72
- "node": ">=16.0.0"
73
+ "node": ">=24.0.0"
73
74
  }
74
75
  }
File without changes
File without changes