change-image-suffix 2.0.2 → 2.1.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 CHANGED
@@ -2,6 +2,36 @@
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.1](https://github.com/GuoSirius/change-image-suffix/compare/v2.1.0...v2.1.1) (2026-05-21)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * unify format constants, remove dead code, sync docs with source ([c1e7700](https://github.com/GuoSirius/change-image-suffix/commit/c1e7700ee5f5a0142c98f5a1e95bc6453317a4a9))
11
+
12
+ ## [2.1.0](https://github.com/GuoSirius/change-image-suffix/compare/v2.0.2...v2.1.0) (2026-05-21)
13
+
14
+
15
+ ### Features
16
+
17
+ * auto context-menu lifecycle hooks, enable declarations, add dev script ([ab44e37](https://github.com/GuoSirius/change-image-suffix/commit/ab44e374040fbfa6715edacb8fb249eeaf746821))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * rewrite bat script, remove gif/heif/jp2, same-format copy, quality 90 ([085a589](https://github.com/GuoSirius/change-image-suffix/commit/085a589f96682626834ac9715f7f228d52bfe89d))
23
+
24
+
25
+ ### Chores
26
+
27
+ * include .claude/ for cross-device project settings ([c863337](https://github.com/GuoSirius/change-image-suffix/commit/c863337e926826a1799ea0ff4bc6e1f0790afee0))
28
+
29
+
30
+ ### Documentation
31
+
32
+ * add CLAUDE.md for project onboarding on other devices ([c7f06bc](https://github.com/GuoSirius/change-image-suffix/commit/c7f06bc9c670b31e3e9af9329cbce96b5d3ed065))
33
+ * add project memory for context-menu fix and codebase context ([e8fb432](https://github.com/GuoSirius/change-image-suffix/commit/e8fb43252cf1288d503c24737b84b2f687e19968))
34
+
5
35
  ### [2.0.2](https://github.com/GuoSirius/change-image-suffix/compare/v2.0.1...v2.0.2) (2026-05-19)
6
36
 
7
37
 
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  - 🔄 支持递归搜索子目录
9
9
  - 📏 支持递归深度限制
10
10
  - 🎯 支持指定源文件后缀(png, jpg, gif 等)
11
- - 🎨 支持多种目标格式(webp, jpg, png, avif, gif
11
+ - 🎨 支持多种目标格式(webp, jpg, png, avif, tiff
12
12
  - 📤 输出到 `output/` 子目录
13
13
  - 🔢 同名不同后缀文件自动编号(`_01`, `_02`)
14
14
  - 🖱️ Windows 右键菜单集成
@@ -68,10 +68,7 @@ cis uninstall-menu
68
68
  ├── 📷 JPG
69
69
  ├── 🖼 PNG
70
70
  ├── 📺 AVIF
71
- ├── 🎞 GIF
72
- ├── 📋 TIFF
73
- ├── 🍎 HEIF
74
- └── 📐 JPEG2000
71
+ └── 📋 TIFF
75
72
  ```
76
73
 
77
74
  **适用场景:**
@@ -94,7 +91,7 @@ cis uninstall-menu
94
91
  | `-p, --path <dir>` | 指定工作目录 | 当前目录 |
95
92
  | `-r, --recursive` | 递归搜索子目录 | 否 |
96
93
  | `-d, --depth <n>` | 递归深度限制 | 无限制 |
97
- | `-e, --extensions` | 指定源后缀,逗号分隔 | png,jpg,jpeg,gif,bmp,tiff,webp |
94
+ | `-e, --extensions` | 指定源后缀,逗号分隔 | png,jpg,jpeg,gif,bmp,tiff,tif,webp,avif |
98
95
  | `-t, --to <format>` | 目标格式 | webp |
99
96
  | `-f, --file <file>` | 指定单个文件转换 | - |
100
97
  | `-h, --help` | 显示帮助 | - |
@@ -152,7 +149,7 @@ cis uninstall-menu
152
149
  | 类型 | 格式 |
153
150
  |------|------|
154
151
  | **输入** | png, jpg, jpeg, gif, bmp, tiff, webp, avif |
155
- | **输出** | webp, jpg, png, avif, gif, tiff, heif, jp2 |
152
+ | **输出** | webp, jpg, png, avif, tiff |
156
153
 
157
154
  ---
158
155
 
@@ -282,14 +279,21 @@ npm run release:major # 重大变更
282
279
  ## 项目结构
283
280
 
284
281
  ```
282
+ ├── .claude/ # Claude Code 项目记忆与设置
283
+ │ ├── MEMORY.md
284
+ │ ├── memory/
285
+ │ └── settings.local.json
285
286
  ├── .github/workflows/ # GitHub Actions 工作流
286
287
  │ └── release.yml # 自动发布工作流
287
288
  ├── .husky/ # Git 钩子
288
289
  │ └── commit-msg # 提交信息校验钩子
289
290
  ├── scripts/ # 脚本目录
290
- └── release.js # 一键发布脚本
291
+ ├── release.js # 一键发布脚本
292
+ │ ├── postinstall.js # npm postinstall 钩子(自动注册右键菜单)
293
+ │ └── preuninstall.js # npm preuninstall 钩子(自动清理右键菜单)
291
294
  ├── src/ # 源代码
292
295
  │ └── index.ts # 主入口
296
+ ├── CLAUDE.md # 项目上下文文档
293
297
  ├── .commitlintrc.json # commitlint 配置
294
298
  ├── CHANGELOG.md # 变更日志
295
299
  └── package.json # 项目配置
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js CHANGED
@@ -42,8 +42,10 @@ const path = __importStar(require("path"));
42
42
  const os = __importStar(require("os"));
43
43
  const child_process_1 = require("child_process");
44
44
  const sharp_1 = __importDefault(require("sharp"));
45
- // 默认配置
46
- const DEFAULT_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'webp'];
45
+ // 支持的输入/输出格式
46
+ const SUPPORTED_INPUT_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'avif'];
47
+ const SUPPORTED_OUTPUT_FORMATS = ['webp', 'jpg', 'jpeg', 'png', 'avif', 'tiff', 'tif'];
48
+ const DEFAULT_EXTENSIONS = [...SUPPORTED_INPUT_EXTENSIONS];
47
49
  const DEFAULT_TARGET_FORMAT = 'webp';
48
50
  // ─────────────────────────────────────────
49
51
  // 右键菜单管理(仅 Windows)
@@ -54,17 +56,6 @@ function requireWindows() {
54
56
  process.exit(1);
55
57
  }
56
58
  }
57
- /**
58
- * 写注册表 key(调用 reg.exe,无需管理员权限写 HKCU)
59
- */
60
- function regAdd(key, name, value, type = 'REG_SZ') {
61
- const cmd = `reg add "${key}" /v "${name}" /t ${type} /d "${value}" /f`;
62
- (0, child_process_1.execSync)(cmd, { stdio: 'ignore' });
63
- }
64
- function regAddDefault(key, value) {
65
- const cmd = `reg add "${key}" /ve /d "${value}" /f`;
66
- (0, child_process_1.execSync)(cmd, { stdio: 'ignore' });
67
- }
68
59
  function regDelete(key) {
69
60
  try {
70
61
  (0, child_process_1.execSync)(`reg delete "${key}" /f`, { stdio: 'ignore' });
@@ -75,17 +66,14 @@ function regDelete(key) {
75
66
  }
76
67
  function installContextMenu() {
77
68
  requireWindows();
78
- // ── 查找 cis.cmd 和 node_modules 路径 ──
69
+ // ── 查找 cis.cmd 路径 ──
79
70
  let cisCmd = '';
80
- let nodeModulesDir = '';
81
71
  try {
82
72
  cisCmd = (0, child_process_1.execSync)('where cis.cmd', { encoding: 'utf8' }).trim().split('\n')[0].trim();
83
- nodeModulesDir = path.dirname(cisCmd);
84
73
  }
85
74
  catch {
86
75
  try {
87
76
  cisCmd = (0, child_process_1.execSync)('where cis', { encoding: 'utf8' }).trim().split('\n')[0].trim();
88
- nodeModulesDir = path.dirname(cisCmd);
89
77
  }
90
78
  catch {
91
79
  console.error('❌ 找不到 cis 命令,请先执行 npm link 或 npm install -g change-image-suffix');
@@ -103,146 +91,60 @@ function installContextMenu() {
103
91
  fs.copyFileSync(icoSource, icoTarget);
104
92
  }
105
93
  const iconPath = fs.existsSync(icoTarget) ? icoTarget : cisCmd;
106
- // ── 辅助脚本路径定义(需要在 batContent 之前,因为 bat 中引用了 ps1Path)──
107
94
  const batPath = path.join(appDataDir, 'cis_file.bat');
108
- const ps1Path = path.join(appDataDir, 'cis_getfiles.ps1');
109
- // cis_getfiles.ps1: 通过 Shell.Application COM 获取 Explorer 选中文件
110
- const cisGetfilesContent = `
111
- Add-Type -AssemblyName Microsoft.VisualBasic
112
- Add-Type -AssemblyName UIAutomationClient
113
- $files = @()
114
- try {
115
- $shell = New-Object -ComObject Shell.Application
116
- $windows = $shell.Windows()
117
- foreach ($win in $windows) {
118
- if ($win -and $win.FullName -like "*explorer.exe") {
119
- $selected = $win.Document.SelectedItems()
120
- foreach ($item in $selected) {
121
- if ($item -and $item.Path) {
122
- $files += $item.Path
123
- }
124
- }
125
- }
126
- }
127
- } catch {}
128
- if ($files.Count -gt 0) {
129
- $files | ForEach-Object { $_ }
130
- } else {
131
- Write-Output "NO_FILES"
132
- }
133
- `;
134
- fs.writeFileSync(ps1Path, cisGetfilesContent, 'utf8');
135
- // ── bat 脚本:接收 Windows 传递的文件路径和格式参数 ──
136
- // 根据 Windows ExtendedSubCommandsKey 机制:
137
- // - 子命令的 command 参数(格式)在前
138
- // - Windows 自动将父命令收到的文件路径追加在末尾
139
- // - 最终执行: cmd /c "bat" "格式" "文件路径"
140
- // 移除 AppliesTo 限制后,bat 需要过滤非图片文件
95
+ // ── bat 脚本:接收格式 + 文件/目录路径,直接调用 cis ──
96
+ // %1 = 格式, %2 %3 ... = Windows 传入的文件/目录路径
141
97
  const batContent = `
142
98
  @echo off
143
99
  chcp 65001 >nul
144
100
  setlocal enabledelayedexpansion
145
101
 
146
- REM Get this script's directory (no trailing backslash)
147
- set "SCRIPT_DIR=%~dp0"
148
- set "SCRIPT_DIR=!SCRIPT_DIR:~0,-1!"
149
-
150
- REM Get cis.cmd path
151
- for /f "delims=" %%c in ('where cis.cmd 2^>nul') do set "CIS_CMD=%%c"
152
-
153
- REM Supported image extensions
154
- set "SUPPORTED_EXT=.png;.jpg;.jpeg;.gif;.bmp;.tiff;.tif;.webp;.avif"
155
-
156
- REM %1 = format (from subcommand), %2 = file path (from Windows)
157
- if "%~1"=="" (
158
- echo Error: No format specified.
159
- timeout /t 2 >nul
160
- goto :done
102
+ REM Find cis command in PATH
103
+ set "CIS_CMD="
104
+ for /f "delims=" %%c in ('where cis.cmd 2^>nul') do (
105
+ if "!CIS_CMD!"=="" set "CIS_CMD=%%c"
161
106
  )
162
- set "format=%~1"
163
-
164
- REM Collect all image files
165
- set "fileList="
166
-
167
- REM Windows passes file path as %2. If multiple files selected, use PowerShell.
168
- REM Otherwise use direct argument.
169
- if "%~2"=="" (
170
- REM Multi-file via PowerShell (with retries for reliability)
171
- for /f "delims=" %%i in ('powershell -ExecutionPolicy Bypass -File "!SCRIPT_DIR!\\cis_getfiles.ps1"') do (
172
- if not "%%i"=="NO_FILES" (
173
- call :add_if_image "%%i"
174
- )
175
- )
176
- ) else (
177
- REM Single file or multiple via %2 (space-separated)
178
- REM Split space-separated paths
179
- for %%F in (%~2) do (
180
- call :add_if_image "%%F"
107
+ if "!CIS_CMD!"=="" (
108
+ for /f "delims=" %%c in ('where cis 2^>nul') do (
109
+ if "!CIS_CMD!"=="" set "CIS_CMD=%%c"
181
110
  )
182
111
  )
183
-
184
- REM Process all collected files - use start /b to avoid new window
185
- if not "!fileList!"=="" (
186
- start "" /b cmd /c "!CIS_CMD! -t !format! !fileList!"
112
+ if "!CIS_CMD!"=="" (
113
+ echo [change-image-suffix] cis command not found. Run: npm install -g change-image-suffix
114
+ pause
115
+ exit /b 1
187
116
  )
188
- goto :done
189
117
 
190
- :add_if_image
191
- set "filePath=%~1"
192
- REM Skip if empty or NO_FILES
193
- if "!filePath!"=="" exit /b
194
- if "!filePath!"=="NO_FILES" exit /b
118
+ if "%~1"=="" (
119
+ echo [change-image-suffix] Error: No format specified.
120
+ pause
121
+ exit /b 1
122
+ )
195
123
 
196
- REM Get file extension
197
- for %%E in ("!filePath!") do set "ext=%%~xE"
198
- if "!ext!"=="" exit /b
124
+ set "format=%~1"
125
+ shift
199
126
 
200
- REM Convert to lowercase
201
- set "ext_lower=!ext!"
202
- call set "ext_lower=%%ext_lower:A=a%%
203
- call set "ext_lower=%%ext_lower:B=b%%
204
- call set "ext_lower=%%ext_lower:C=c%%
205
- call set "ext_lower=%%ext_lower:D=d%%
206
- call set "ext_lower=%%ext_lower:E=e%%
207
- call set "ext_lower=%%ext_lower:F=f%%
208
- call set "ext_lower=%%ext_lower:G=g%%
209
- call set "ext_lower=%%ext_lower:H=h%%
210
- call set "ext_lower=%%ext_lower:I=i%%
211
- call set "ext_lower=%%ext_lower:J=j%%
212
- call set "ext_lower=%%ext_lower:K=k%%
213
- call set "ext_lower=%%ext_lower:L=l%%
214
- call set "ext_lower=%%ext_lower:M=m%%
215
- call set "ext_lower=%%ext_lower:N=n%%
216
- call set "ext_lower=%%ext_lower:O=o%%
217
- call set "ext_lower=%%ext_lower:P=p%%
218
- call set "ext_lower=%%ext_lower:Q=q%%
219
- call set "ext_lower=%%ext_lower:R=r%%
220
- call set "ext_lower=%%ext_lower:S=s%%
221
- call set "ext_lower=%%ext_lower:T=t%%
222
- call set "ext_lower=%%ext_lower:U=u%%
223
- call set "ext_lower=%%ext_lower:V=v%%
224
- call set "ext_lower=%%ext_lower:W=w%%
225
- call set "ext_lower=%%ext_lower:X=x%%
226
- call set "ext_lower=%%ext_lower:Y=y%%
227
- call set "ext_lower=%%ext_lower:Z=z%%"
127
+ set "args="
128
+ :parse
129
+ if "%~1"=="" goto :run
130
+ if exist "%~1\\*" (
131
+ set "args=!args! -p "%~1""
132
+ ) else (
133
+ set "args=!args! -f "%~1""
134
+ )
135
+ shift
136
+ goto :parse
228
137
 
229
- REM Check if extension is supported
230
- echo !SUPPORTED_EXT! | findstr /i /c:"!ext_lower!" >nul 2>&1
231
- if !errorlevel!==0 (
232
- set "fileList=!fileList! -f "!filePath!""
138
+ :run
139
+ if "!args!"=="" (
140
+ echo [change-image-suffix] No files or directories to process.
141
+ pause
142
+ exit /b 1
233
143
  )
234
- exit /b
235
144
 
236
- :done
145
+ "!CIS_CMD!" -t !format! !args!
146
+ if !errorlevel! neq 0 pause
237
147
  endlocal
238
- exit
239
-
240
- exit
241
-
242
- exit
243
-
244
- exit
245
-
246
148
  `;
247
149
  fs.writeFileSync(batPath, batContent, 'utf8');
248
150
  // ── 格式列表(webp 排第一,其他按常见程度排序)──
@@ -251,15 +153,10 @@ exit
251
153
  { verb: 'jpg', label: '📷 JPG' },
252
154
  { verb: 'png', label: '🖼 PNG' },
253
155
  { verb: 'avif', label: '📺 AVIF' },
254
- { verb: 'gif', label: '🎞 GIF' },
255
156
  { verb: 'tiff', label: '📋 TIFF' },
256
- { verb: 'heif', label: '🍎 HEIF' },
257
- { verb: 'jp2', label: '📐 JPEG2000' },
258
157
  ];
259
- // ── 使用 ExtendedSubCommandsKey 方式(PowerShell 7 同款)──
260
- // 主菜单项配置(每个菜单类型有独立的子菜单路径)
261
- // 注意:文件右键和目录右键都使用 bat + PowerShell 获取选中文件
262
- // 这样混合选择时也能处理所有选中项
158
+ // ── 使用 ExtendedSubCommandsKey 方式 ──
159
+ // 文件右键和目录右键均使用 bat 脚本,混合选择时自动分类文件和目录
263
160
  const menuBases = [
264
161
  { base: 'HKCU\\Software\\Classes\\Directory\\Background\\shell\\cis', subMenu: 'Directory\\ContextMenus\\cis', arg: '-p "%V"' },
265
162
  { base: 'HKCU\\Software\\Classes\\Directory\\shell\\cis', subMenu: 'Directory\\ContextMenus\\cis_dir', useBat: true },
@@ -331,18 +228,13 @@ function uninstallContextMenu() {
331
228
  }
332
229
  catch { /* ignore */ }
333
230
  }
334
- // 删除批处理文件和 PowerShell 脚本
231
+ // 删除批处理文件
335
232
  const appDataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'change-image-suffix');
336
233
  const batPath = path.join(appDataDir, 'cis_file.bat');
337
- const ps1Path = path.join(appDataDir, 'cis_getfiles.ps1');
338
234
  try {
339
235
  fs.unlinkSync(batPath);
340
236
  }
341
237
  catch { /* ignore */ }
342
- try {
343
- fs.unlinkSync(ps1Path);
344
- }
345
- catch { /* ignore */ }
346
238
  console.log('✅ 右键菜单已卸载');
347
239
  }
348
240
  // ─────────────────────────────────────────
@@ -400,7 +292,8 @@ function parseArgs() {
400
292
  process.exit(0);
401
293
  }
402
294
  if (arg === '-v' || arg === '--version') {
403
- console.log('change-image-suffix v1.18.0');
295
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
296
+ console.log(`change-image-suffix v${pkg.version}`);
404
297
  process.exit(0);
405
298
  }
406
299
  if (arg === '-p' || arg === '--path') {
@@ -488,12 +381,12 @@ function printHelp() {
488
381
  cis uninstall-menu # 从 Windows 右键菜单移除
489
382
 
490
383
  选项:
491
- -f, --file <file> 转换单个文件(右键文件时自动传入)
384
+ -f, --file <file> 转换指定文件(可多个,空格分隔)
492
385
  -p, --path <dir> 指定工作目录(默认: 当前目录)
493
386
  -r, --recursive 递归搜索子目录
494
387
  -d, --depth <n> 递归深度限制(需要 -r 选项)
495
- -e, --extensions <ext> 指定要转换的后缀,逗号分隔(不含点号)
496
- -t, --to <format> 转换到的目标格式(默认: webp)
388
+ -e, --extensions <ext> 指定源后缀,逗号分隔(不含点号)
389
+ -t, --to <format> 目标格式: webp, jpg/jpeg, png, avif, tiff/tif(默认: webp
497
390
  -h, --help 显示帮助信息
498
391
  -v, --version 显示版本信息
499
392
 
@@ -536,9 +429,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
536
429
  const dir = path.dirname(inputPath);
537
430
  const ext = path.extname(inputPath);
538
431
  const basename = path.basename(inputPath, ext);
539
- const originalExt = ext.slice(1).toLowerCase();
540
432
  const targetExt = targetFormat;
541
- // 源格式与目标格式相同时,直接覆盖
542
433
  let coreName = basename;
543
434
  const outputDir = path.join(dir, 'output');
544
435
  // 检查输入目录中是否有同名(不含扩展名)但不同后缀的文件
@@ -573,34 +464,39 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
573
464
  async function convertImage(inputPath, targetFormat, allInputFiles) {
574
465
  try {
575
466
  const outputPath = getOutputPath(inputPath, targetFormat, allInputFiles);
576
- // 确保 output 目录存在
577
467
  const outputDir = path.dirname(outputPath);
578
468
  if (!fs.existsSync(outputDir)) {
579
469
  fs.mkdirSync(outputDir, { recursive: true });
580
470
  }
471
+ const srcExt = path.extname(inputPath).slice(1).toLowerCase();
472
+ const fmt = targetFormat.toLowerCase();
473
+ // 同格式直接复制,避免重新编码导致质量损失
474
+ if (srcExt === fmt || (srcExt === 'jpeg' && fmt === 'jpg') || (srcExt === 'jpg' && fmt === 'jpeg') || (srcExt === 'tif' && fmt === 'tiff') || (srcExt === 'tiff' && fmt === 'tif')) {
475
+ fs.copyFileSync(inputPath, outputPath);
476
+ return { success: true, outputPath };
477
+ }
478
+ if (!SUPPORTED_OUTPUT_FORMATS.includes(fmt)) {
479
+ return { success: false, outputPath: inputPath, error: `不支持的目标格式: ${targetFormat},支持: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}` };
480
+ }
581
481
  const image = (0, sharp_1.default)(inputPath);
582
- switch (targetFormat.toLowerCase()) {
482
+ switch (fmt) {
583
483
  case 'webp':
584
- await image.webp({ quality: 85 }).toFile(outputPath);
484
+ await image.webp({ quality: 90 }).toFile(outputPath);
585
485
  break;
586
486
  case 'jpg':
587
487
  case 'jpeg':
588
- await image.jpeg({ quality: 85 }).toFile(outputPath);
488
+ await image.jpeg({ quality: 90 }).toFile(outputPath);
589
489
  break;
590
490
  case 'png':
591
- await image.png({ quality: 85 }).toFile(outputPath);
592
- break;
593
- case 'gif':
594
- await image.gif().toFile(outputPath);
491
+ await image.png({ compressionLevel: 6 }).toFile(outputPath);
595
492
  break;
596
493
  case 'tiff':
597
494
  case 'tif':
598
- await image.tiff({ quality: 85 }).toFile(outputPath);
495
+ await image.tiff({ quality: 90 }).toFile(outputPath);
599
496
  break;
600
497
  case 'avif':
601
- await image.avif({ quality: 85 }).toFile(outputPath);
498
+ await image.avif({ quality: 90 }).toFile(outputPath);
602
499
  break;
603
- default: await image.toFormat(targetFormat).toFile(outputPath);
604
500
  }
605
501
  return { success: true, outputPath };
606
502
  }
@@ -634,12 +530,11 @@ async function main() {
634
530
  console.log(`🎯 目标格式: ${options.targetFormat}`);
635
531
  console.log(`📦 待处理: ${files.length} 个文件\n`);
636
532
  console.log('----------------------------------------\n');
637
- const supportedExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'avif'];
638
533
  let totalSuccess = 0;
639
534
  let totalFail = 0;
640
535
  for (const filePath of files) {
641
536
  const ext = path.extname(filePath).slice(1).toLowerCase();
642
- if (!supportedExts.includes(ext)) {
537
+ if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
643
538
  console.log(` ⚠️ 跳过(不支持格式): ${filePath}`);
644
539
  totalFail++;
645
540
  continue;
@@ -674,8 +569,7 @@ async function main() {
674
569
  }
675
570
  if (stat.isFile()) {
676
571
  const ext = path.extname(inputPath).slice(1).toLowerCase();
677
- const supportedExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'avif'];
678
- if (!supportedExts.includes(ext)) {
572
+ if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
679
573
  console.log(` ⚠️ 跳过(不支持格式): ${inputPath}`);
680
574
  totalFail++;
681
575
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "change-image-suffix",
3
- "version": "2.0.2",
3
+ "version": "2.1.1",
4
4
  "description": "批量转换图片格式的CLI工具,支持递归搜索、深度限制、指定后缀、Windows右键菜单等功能",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -8,7 +8,9 @@
8
8
  "assets/",
9
9
  "README.md",
10
10
  "LICENSE",
11
- "CHANGELOG.md"
11
+ "CHANGELOG.md",
12
+ "scripts/postinstall.js",
13
+ "scripts/preuninstall.js"
12
14
  ],
13
15
  "bin": {
14
16
  "change-image-suffix": "./dist/index.js",
@@ -21,13 +23,15 @@
21
23
  "scripts": {
22
24
  "clean": "rimraf dist",
23
25
  "build": "tsc",
26
+ "dev": "tsc --watch",
27
+ "postinstall": "node scripts/postinstall.js",
28
+ "preuninstall": "node scripts/preuninstall.js",
24
29
  "prepublishOnly": "npm run clean && npm run build",
25
30
  "release": "node scripts/release.js",
26
31
  "release:patch": "node scripts/release.js --patch",
27
32
  "release:minor": "node scripts/release.js --minor",
28
33
  "release:major": "node scripts/release.js --major",
29
- "lint": "echo \"Linting not configured yet\"",
30
- "lint:fix": "echo \"Linting fix not configured yet\"",
34
+ "lint": "tsc --noEmit",
31
35
  "typecheck": "tsc --noEmit"
32
36
  },
33
37
  "keywords": [
@@ -0,0 +1,14 @@
1
+ // npm lifecycle hook: runs after npm install/update
2
+ // Auto-registers Windows context menu on global install
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+ const path = require('path');
6
+
7
+ if (os.platform() === 'win32' && process.env.npm_config_global === 'true') {
8
+ try {
9
+ const indexJs = path.join(__dirname, '..', 'dist', 'index.js');
10
+ execSync(`node "${indexJs}" install-menu`, { stdio: 'inherit' });
11
+ } catch (e) {
12
+ console.warn('⚠️ Context menu auto-register failed. Run "cis install-menu" manually.');
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ // npm lifecycle hook: runs before npm uninstall
2
+ // Cleans up Windows context menu to avoid leftover registry entries
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+ const path = require('path');
6
+
7
+ if (os.platform() === 'win32') {
8
+ try {
9
+ const indexJs = path.join(__dirname, '..', 'dist', 'index.js');
10
+ execSync(`node "${indexJs}" uninstall-menu`, { stdio: 'inherit' });
11
+ } catch (e) {
12
+ // Ignore cleanup errors — the menu will be orphaned but harmless
13
+ }
14
+ }