change-image-suffix 2.0.1 → 2.1.0

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.0](https://github.com/GuoSirius/change-image-suffix/compare/v2.0.2...v2.1.0) (2026-05-21)
6
+
7
+
8
+ ### Features
9
+
10
+ * auto context-menu lifecycle hooks, enable declarations, add dev script ([ab44e37](https://github.com/GuoSirius/change-image-suffix/commit/ab44e374040fbfa6715edacb8fb249eeaf746821))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * rewrite bat script, remove gif/heif/jp2, same-format copy, quality 90 ([085a589](https://github.com/GuoSirius/change-image-suffix/commit/085a589f96682626834ac9715f7f228d52bfe89d))
16
+
17
+
18
+ ### Chores
19
+
20
+ * include .claude/ for cross-device project settings ([c863337](https://github.com/GuoSirius/change-image-suffix/commit/c863337e926826a1799ea0ff4bc6e1f0790afee0))
21
+
22
+
23
+ ### Documentation
24
+
25
+ * add CLAUDE.md for project onboarding on other devices ([c7f06bc](https://github.com/GuoSirius/change-image-suffix/commit/c7f06bc9c670b31e3e9af9329cbce96b5d3ed065))
26
+ * add project memory for context-menu fix and codebase context ([e8fb432](https://github.com/GuoSirius/change-image-suffix/commit/e8fb43252cf1288d503c24737b84b2f687e19968))
27
+
28
+ ### [2.0.2](https://github.com/GuoSirius/change-image-suffix/compare/v2.0.1...v2.0.2) (2026-05-19)
29
+
30
+
31
+ ### CI/CD
32
+
33
+ * add build step to GitHub Actions ([d900c7b](https://github.com/GuoSirius/change-image-suffix/commit/d900c7b69c390554aeb1f76a940e178688f8a6c5))
34
+
5
35
  ### [2.0.1](https://github.com/GuoSirius/change-image-suffix/compare/v2.0.0...v2.0.1) (2026-05-19)
6
36
 
7
37
  ## [2.0.0](https://github.com/GuoSirius/change-image-suffix/compare/v1.18.2...v2.0.0) (2026-05-19)
package/README.md CHANGED
@@ -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
  **适用场景:**
@@ -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
 
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js CHANGED
@@ -42,7 +42,9 @@ 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
- // 默认配置
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'];
46
48
  const DEFAULT_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'webp'];
47
49
  const DEFAULT_TARGET_FORMAT = 'webp';
48
50
  // ─────────────────────────────────────────
@@ -75,17 +77,14 @@ function regDelete(key) {
75
77
  }
76
78
  function installContextMenu() {
77
79
  requireWindows();
78
- // ── 查找 cis.cmd 和 node_modules 路径 ──
80
+ // ── 查找 cis.cmd 路径 ──
79
81
  let cisCmd = '';
80
- let nodeModulesDir = '';
81
82
  try {
82
83
  cisCmd = (0, child_process_1.execSync)('where cis.cmd', { encoding: 'utf8' }).trim().split('\n')[0].trim();
83
- nodeModulesDir = path.dirname(cisCmd);
84
84
  }
85
85
  catch {
86
86
  try {
87
87
  cisCmd = (0, child_process_1.execSync)('where cis', { encoding: 'utf8' }).trim().split('\n')[0].trim();
88
- nodeModulesDir = path.dirname(cisCmd);
89
88
  }
90
89
  catch {
91
90
  console.error('❌ 找不到 cis 命令,请先执行 npm link 或 npm install -g change-image-suffix');
@@ -103,146 +102,60 @@ function installContextMenu() {
103
102
  fs.copyFileSync(icoSource, icoTarget);
104
103
  }
105
104
  const iconPath = fs.existsSync(icoTarget) ? icoTarget : cisCmd;
106
- // ── 辅助脚本路径定义(需要在 batContent 之前,因为 bat 中引用了 ps1Path)──
107
105
  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 需要过滤非图片文件
106
+ // ── bat 脚本:接收格式 + 文件/目录路径,直接调用 cis ──
107
+ // %1 = 格式, %2 %3 ... = Windows 传入的文件/目录路径
141
108
  const batContent = `
142
109
  @echo off
143
110
  chcp 65001 >nul
144
111
  setlocal enabledelayedexpansion
145
112
 
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
113
+ REM Find cis command in PATH
114
+ set "CIS_CMD="
115
+ for /f "delims=" %%c in ('where cis.cmd 2^>nul') do (
116
+ if "!CIS_CMD!"=="" set "CIS_CMD=%%c"
161
117
  )
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"
118
+ if "!CIS_CMD!"=="" (
119
+ for /f "delims=" %%c in ('where cis 2^>nul') do (
120
+ if "!CIS_CMD!"=="" set "CIS_CMD=%%c"
181
121
  )
182
122
  )
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!"
123
+ if "!CIS_CMD!"=="" (
124
+ echo [change-image-suffix] cis command not found. Run: npm install -g change-image-suffix
125
+ pause
126
+ exit /b 1
187
127
  )
188
- goto :done
189
128
 
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
129
+ if "%~1"=="" (
130
+ echo [change-image-suffix] Error: No format specified.
131
+ pause
132
+ exit /b 1
133
+ )
195
134
 
196
- REM Get file extension
197
- for %%E in ("!filePath!") do set "ext=%%~xE"
198
- if "!ext!"=="" exit /b
135
+ set "format=%~1"
136
+ shift
199
137
 
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%%"
138
+ set "args="
139
+ :parse
140
+ if "%~1"=="" goto :run
141
+ if exist "%~1\\*" (
142
+ set "args=!args! -p "%~1""
143
+ ) else (
144
+ set "args=!args! -f "%~1""
145
+ )
146
+ shift
147
+ goto :parse
228
148
 
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!""
149
+ :run
150
+ if "!args!"=="" (
151
+ echo [change-image-suffix] No files or directories to process.
152
+ pause
153
+ exit /b 1
233
154
  )
234
- exit /b
235
155
 
236
- :done
156
+ "!CIS_CMD!" -t !format! !args!
157
+ if !errorlevel! neq 0 pause
237
158
  endlocal
238
- exit
239
-
240
- exit
241
-
242
- exit
243
-
244
- exit
245
-
246
159
  `;
247
160
  fs.writeFileSync(batPath, batContent, 'utf8');
248
161
  // ── 格式列表(webp 排第一,其他按常见程度排序)──
@@ -251,10 +164,7 @@ exit
251
164
  { verb: 'jpg', label: '📷 JPG' },
252
165
  { verb: 'png', label: '🖼 PNG' },
253
166
  { verb: 'avif', label: '📺 AVIF' },
254
- { verb: 'gif', label: '🎞 GIF' },
255
167
  { verb: 'tiff', label: '📋 TIFF' },
256
- { verb: 'heif', label: '🍎 HEIF' },
257
- { verb: 'jp2', label: '📐 JPEG2000' },
258
168
  ];
259
169
  // ── 使用 ExtendedSubCommandsKey 方式(PowerShell 7 同款)──
260
170
  // 主菜单项配置(每个菜单类型有独立的子菜单路径)
@@ -331,18 +241,13 @@ function uninstallContextMenu() {
331
241
  }
332
242
  catch { /* ignore */ }
333
243
  }
334
- // 删除批处理文件和 PowerShell 脚本
244
+ // 删除批处理文件
335
245
  const appDataDir = path.join(os.homedir(), 'AppData', 'Roaming', 'change-image-suffix');
336
246
  const batPath = path.join(appDataDir, 'cis_file.bat');
337
- const ps1Path = path.join(appDataDir, 'cis_getfiles.ps1');
338
247
  try {
339
248
  fs.unlinkSync(batPath);
340
249
  }
341
250
  catch { /* ignore */ }
342
- try {
343
- fs.unlinkSync(ps1Path);
344
- }
345
- catch { /* ignore */ }
346
251
  console.log('✅ 右键菜单已卸载');
347
252
  }
348
253
  // ─────────────────────────────────────────
@@ -400,7 +305,8 @@ function parseArgs() {
400
305
  process.exit(0);
401
306
  }
402
307
  if (arg === '-v' || arg === '--version') {
403
- console.log('change-image-suffix v1.18.0');
308
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
309
+ console.log(`change-image-suffix v${pkg.version}`);
404
310
  process.exit(0);
405
311
  }
406
312
  if (arg === '-p' || arg === '--path') {
@@ -488,12 +394,12 @@ function printHelp() {
488
394
  cis uninstall-menu # 从 Windows 右键菜单移除
489
395
 
490
396
  选项:
491
- -f, --file <file> 转换单个文件(右键文件时自动传入)
397
+ -f, --file <file> 转换指定文件(可多个,空格分隔)
492
398
  -p, --path <dir> 指定工作目录(默认: 当前目录)
493
399
  -r, --recursive 递归搜索子目录
494
400
  -d, --depth <n> 递归深度限制(需要 -r 选项)
495
- -e, --extensions <ext> 指定要转换的后缀,逗号分隔(不含点号)
496
- -t, --to <format> 转换到的目标格式(默认: webp)
401
+ -e, --extensions <ext> 指定源后缀,逗号分隔(不含点号)
402
+ -t, --to <format> 目标格式: webp, jpg, png, avif, tiff(默认: webp
497
403
  -h, --help 显示帮助信息
498
404
  -v, --version 显示版本信息
499
405
 
@@ -536,9 +442,7 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
536
442
  const dir = path.dirname(inputPath);
537
443
  const ext = path.extname(inputPath);
538
444
  const basename = path.basename(inputPath, ext);
539
- const originalExt = ext.slice(1).toLowerCase();
540
445
  const targetExt = targetFormat;
541
- // 源格式与目标格式相同时,直接覆盖
542
446
  let coreName = basename;
543
447
  const outputDir = path.join(dir, 'output');
544
448
  // 检查输入目录中是否有同名(不含扩展名)但不同后缀的文件
@@ -573,34 +477,39 @@ function getOutputPath(inputPath, targetFormat, allInputFiles) {
573
477
  async function convertImage(inputPath, targetFormat, allInputFiles) {
574
478
  try {
575
479
  const outputPath = getOutputPath(inputPath, targetFormat, allInputFiles);
576
- // 确保 output 目录存在
577
480
  const outputDir = path.dirname(outputPath);
578
481
  if (!fs.existsSync(outputDir)) {
579
482
  fs.mkdirSync(outputDir, { recursive: true });
580
483
  }
484
+ const srcExt = path.extname(inputPath).slice(1).toLowerCase();
485
+ const fmt = targetFormat.toLowerCase();
486
+ // 同格式直接复制,避免重新编码导致质量损失
487
+ if (srcExt === fmt || (srcExt === 'jpeg' && fmt === 'jpg') || (srcExt === 'jpg' && fmt === 'jpeg') || (srcExt === 'tif' && fmt === 'tiff') || (srcExt === 'tiff' && fmt === 'tif')) {
488
+ fs.copyFileSync(inputPath, outputPath);
489
+ return { success: true, outputPath };
490
+ }
491
+ if (!SUPPORTED_OUTPUT_FORMATS.includes(fmt)) {
492
+ return { success: false, outputPath: inputPath, error: `不支持的目标格式: ${targetFormat},支持: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}` };
493
+ }
581
494
  const image = (0, sharp_1.default)(inputPath);
582
- switch (targetFormat.toLowerCase()) {
495
+ switch (fmt) {
583
496
  case 'webp':
584
- await image.webp({ quality: 85 }).toFile(outputPath);
497
+ await image.webp({ quality: 90 }).toFile(outputPath);
585
498
  break;
586
499
  case 'jpg':
587
500
  case 'jpeg':
588
- await image.jpeg({ quality: 85 }).toFile(outputPath);
501
+ await image.jpeg({ quality: 90 }).toFile(outputPath);
589
502
  break;
590
503
  case 'png':
591
- await image.png({ quality: 85 }).toFile(outputPath);
592
- break;
593
- case 'gif':
594
- await image.gif().toFile(outputPath);
504
+ await image.png({ compressionLevel: 6 }).toFile(outputPath);
595
505
  break;
596
506
  case 'tiff':
597
507
  case 'tif':
598
- await image.tiff({ quality: 85 }).toFile(outputPath);
508
+ await image.tiff({ quality: 90 }).toFile(outputPath);
599
509
  break;
600
510
  case 'avif':
601
- await image.avif({ quality: 85 }).toFile(outputPath);
511
+ await image.avif({ quality: 90 }).toFile(outputPath);
602
512
  break;
603
- default: await image.toFormat(targetFormat).toFile(outputPath);
604
513
  }
605
514
  return { success: true, outputPath };
606
515
  }
@@ -634,12 +543,11 @@ async function main() {
634
543
  console.log(`🎯 目标格式: ${options.targetFormat}`);
635
544
  console.log(`📦 待处理: ${files.length} 个文件\n`);
636
545
  console.log('----------------------------------------\n');
637
- const supportedExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'avif'];
638
546
  let totalSuccess = 0;
639
547
  let totalFail = 0;
640
548
  for (const filePath of files) {
641
549
  const ext = path.extname(filePath).slice(1).toLowerCase();
642
- if (!supportedExts.includes(ext)) {
550
+ if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
643
551
  console.log(` ⚠️ 跳过(不支持格式): ${filePath}`);
644
552
  totalFail++;
645
553
  continue;
@@ -674,8 +582,7 @@ async function main() {
674
582
  }
675
583
  if (stat.isFile()) {
676
584
  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)) {
585
+ if (!SUPPORTED_INPUT_EXTENSIONS.includes(ext)) {
679
586
  console.log(` ⚠️ 跳过(不支持格式): ${inputPath}`);
680
587
  totalFail++;
681
588
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "change-image-suffix",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
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
+ }