juice-email-cli 2.1.10 → 2.1.13

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/context-menu.js +76 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juice-email-cli",
3
- "version": "2.1.10",
3
+ "version": "2.1.13",
4
4
  "description": "CLI tool to generate email-client-compatible HTML with juice (CSS inlining) + Mustache templating + minification",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -14,7 +14,8 @@
14
14
  * └── 📂 打开 PowerShell (可选)
15
15
  */
16
16
 
17
- const { execSync } = require('child_process');
17
+ const { spawnSync } = require('child_process');
18
+ const fs = require('fs');
18
19
  const path = require('path');
19
20
  const chalk = require('chalk');
20
21
 
@@ -34,41 +35,61 @@ function getIconPath() {
34
35
 
35
36
  // ─── 注册表操作封装 ───────────────────────────────────────────────────────────
36
37
 
38
+ const isWindows = process.platform === 'win32';
39
+
37
40
  /**
38
- * 执行 reg add,失败时打印警告而不中断
41
+ * 执行 reg add — 使用 spawnSync 直接调用 reg.exe,避免 cmd.exe 解析特殊字符
39
42
  */
40
43
  function regAdd(key, valueName, type, data) {
41
- const vFlag = valueName === '' ? '/ve' : `/v "${valueName}"`;
42
- const tFlag = type ? `/t ${type}` : '';
43
- const dFlag = data !== undefined ? `/d "${escapeRegData(data)}"` : '';
44
- const cmd = `reg add "${key}" ${vFlag} ${tFlag} ${dFlag} /f`.replace(/\s+/g, ' ').trim();
45
- try {
46
- execSync(cmd, { stdio: 'pipe' });
47
- return true;
48
- } catch (e) {
49
- const msg = e.stderr ? e.stderr.toString().trim() : e.message;
44
+ if (!isWindows) return false;
45
+ const args = ['add', key, '/f'];
46
+ if (valueName === '') {
47
+ args.push('/ve');
48
+ } else {
49
+ args.push('/v', valueName);
50
+ }
51
+ if (type) args.push('/t', type);
52
+ if (data !== undefined) args.push('/d', data);
53
+
54
+ const result = spawnSync('reg', args, { stdio: 'pipe' });
55
+ if (result.status !== 0) {
56
+ const msg = (result.stderr || '').toString().trim();
50
57
  console.warn(chalk.yellow(` ⚠ reg add 失败\n 键:${key}\n 原因:${msg}`));
51
58
  return false;
52
59
  }
60
+ return true;
53
61
  }
54
62
 
55
63
  /**
56
- * 执行 reg delete,键不存在时静默忽略,返回是否实际删除了
64
+ * 执行 reg delete — 键不存在时静默忽略
57
65
  */
58
66
  function regDelete(key) {
59
- try {
60
- execSync(`reg delete "${key}" /f`, { stdio: 'pipe' });
61
- return true;
62
- } catch (_) {
63
- return false;
64
- }
67
+ if (!isWindows) return false;
68
+ const result = spawnSync('reg', ['delete', key, '/f'], { stdio: 'pipe' });
69
+ return result.status === 0;
65
70
  }
66
71
 
67
72
  /**
68
- * 转义注册表数据中的特殊字符
73
+ * 清理旧版本残留在 HKLM 的注册表项(v1 使用 HKCR/HKLM,需要管理员权限)
74
+ *
75
+ * 旧版本用管理员权限安装时写入 HKLM,升级到 HKCU 版本后这些残留不会被自动清理。
76
+ * Windows 合并 HKLM + HKCU 注册表视图时,HKLM 里的旧菜单名可能覆盖 HKCU 的新菜单名,
77
+ * 导致用户更新后重启电脑仍然看到旧版菜单。
78
+ *
79
+ * 没有管理员权限时 reg delete 静默失败,不影响后续 HKCU 写入。
69
80
  */
70
- function escapeRegData(str) {
71
- return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%');
81
+ function cleanLegacyHklmEntries() {
82
+ if (!isWindows) return;
83
+
84
+ // 清理旧版父菜单(HKLM\Software\Classes\SystemFileAssociations\.*\shell\JuiceEmail)
85
+ for (const root of LEGACY_HKLM_ROOTS) {
86
+ regDelete(`${root}\\${PARENT_KEY_NAME}`);
87
+ }
88
+
89
+ // 清理旧版子命令(HKLM\...\CommandStore\shell\JuiceEmail.*)
90
+ for (const name of Object.values(SUBCMDS)) {
91
+ regDelete(`${LEGACY_HKLM_SUBCMD_SPACE}\\${name}`);
92
+ }
72
93
  }
73
94
 
74
95
  // ─── 菜单结构常量 ─────────────────────────────────────────────────────────────
@@ -91,6 +112,17 @@ const YAML_ROOTS = [
91
112
  // CommandStore 子命令注册空间
92
113
  const SUBCMD_SPACE = 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
93
114
 
115
+ // 旧版本可能残留在 HKLM 的路径(v1 使用 HKCR/HKLM,需要管理员权限)
116
+ // 这些残留不清理会导致 Windows 合并注册表视图时显示旧菜单名
117
+ const LEGACY_HKLM_ROOTS = [
118
+ 'HKEY_LOCAL_MACHINE\\Software\\Classes\\SystemFileAssociations\\.html\\shell',
119
+ 'HKEY_LOCAL_MACHINE\\Software\\Classes\\SystemFileAssociations\\.htm\\shell',
120
+ 'HKEY_LOCAL_MACHINE\\Software\\Classes\\SystemFileAssociations\\.yaml\\shell',
121
+ 'HKEY_LOCAL_MACHINE\\Software\\Classes\\SystemFileAssociations\\.yml\\shell',
122
+ ];
123
+
124
+ const LEGACY_HKLM_SUBCMD_SPACE = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
125
+
94
126
  // 子命令名称常量(register / unregister 共用,避免硬编码不一致)
95
127
  const SUBCMDS = {
96
128
  generate: 'JuiceEmail.Generate',
@@ -126,8 +158,16 @@ function wrapInteractive(nodePath, scriptPath, cliArgs) {
126
158
  // ─── 注册 ─────────────────────────────────────────────────────────────────────
127
159
 
128
160
  async function registerContextMenu() {
161
+ if (!isWindows) {
162
+ console.log(chalk.gray(' 右键菜单仅支持 Windows,已跳过。\n'));
163
+ return;
164
+ }
165
+
129
166
  console.log(chalk.cyan('\n 注册 juice 右键菜单(当前用户,无需管理员权限)...\n'));
130
167
 
168
+ // 先清理旧版本可能残留在 HKLM 的注册表项,避免新旧菜单名冲突
169
+ cleanLegacyHklmEntries();
170
+
131
171
  const nodePath = getNodePath();
132
172
  const scriptPath = getJuiceScript();
133
173
  const iconPath = getIconPath();
@@ -204,25 +244,33 @@ async function registerContextMenu() {
204
244
  // ─── 取消注册 ─────────────────────────────────────────────────────────────────
205
245
 
206
246
  async function unregisterContextMenu() {
247
+ if (!isWindows) {
248
+ console.log(chalk.gray(' 右键菜单仅支持 Windows,已跳过。\n'));
249
+ return;
250
+ }
251
+
207
252
  console.log(chalk.cyan('\n 取消注册 juice 右键菜单...\n'));
208
253
 
209
254
  let removed = 0;
210
255
 
211
- // 清理 HTML 父菜单
256
+ // 清理 HKCU 父菜单
212
257
  for (const root of HTML_ROOTS) {
213
258
  if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
214
259
  }
215
260
 
216
- // 清理 YAML 父菜单
261
+ // 清理 HKCU YAML 父菜单
217
262
  for (const root of YAML_ROOTS) {
218
263
  if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
219
264
  }
220
265
 
221
- // 清理子命令
266
+ // 清理 HKCU 子命令
222
267
  for (const name of Object.values(SUBCMDS)) {
223
268
  regDelete(`${SUBCMD_SPACE}\\${name}`);
224
269
  }
225
270
 
271
+ // 同时清理旧版本可能残留在 HKLM 的注册表项
272
+ cleanLegacyHklmEntries();
273
+
226
274
  if (removed > 0) {
227
275
  console.log(chalk.green(` ✔ 已移除 ${removed} 个右键菜单项。\n`));
228
276
  } else {
@@ -239,11 +287,12 @@ function resolvePwsh() {
239
287
  path.join(process.env['LOCALAPPDATA'] || '', 'Microsoft', 'PowerShell', 'pwsh.exe'),
240
288
  ];
241
289
  for (const p of candidates) {
242
- if (require('fs').existsSync(p)) return p;
290
+ if (fs.existsSync(p)) return p;
243
291
  }
244
292
  try {
245
- const result = execSync('where pwsh', { stdio: 'pipe' }).toString().trim().split('\n')[0].trim();
246
- if (result && require('fs').existsSync(result)) return result;
293
+ const result = spawnSync('where', ['pwsh'], { stdio: 'pipe' });
294
+ const line = result.stdout.toString().trim().split('\n')[0].trim();
295
+ if (line && fs.existsSync(line)) return line;
247
296
  } catch (_) {}
248
297
  return null;
249
298
  }