juice-email-cli 2.1.12 → 2.1.14
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/package.json +1 -1
- package/src/context-menu.js +120 -84
package/package.json
CHANGED
package/src/context-menu.js
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Windows 右键菜单注册 / 取消注册
|
|
5
5
|
*
|
|
6
|
-
* 使用
|
|
6
|
+
* 使用 ExtendedSubCommandsKey 实现级联菜单(参考 WinRAR 和 Windows Defender 的实现)。
|
|
7
|
+
* 子命令存储在 Classes 根下的共享容器中,各文件类型父菜单通过 ExtendedSubCommandsKey 引用。
|
|
8
|
+
*
|
|
9
|
+
* 与旧版 SubCommands + CommandStore 方案的区别:
|
|
10
|
+
* - 不依赖 CommandStore(HKCU CommandStore 在某些 Windows 版本下不被正确解析)
|
|
11
|
+
* - ExtendedSubCommandsKey 指向 Classes 根下的相对路径,子命令内联存储
|
|
7
12
|
*
|
|
8
13
|
* 菜单结构(所有文件类型统一):
|
|
9
14
|
* .html / .htm / .yaml / .yml:
|
|
@@ -37,9 +42,6 @@ function getIconPath() {
|
|
|
37
42
|
|
|
38
43
|
const isWindows = process.platform === 'win32';
|
|
39
44
|
|
|
40
|
-
/**
|
|
41
|
-
* 执行 reg add — 使用 spawnSync 直接调用 reg.exe,避免 cmd.exe 解析特殊字符
|
|
42
|
-
*/
|
|
43
45
|
function regAdd(key, valueName, type, data) {
|
|
44
46
|
if (!isWindows) return false;
|
|
45
47
|
const args = ['add', key, '/f'];
|
|
@@ -60,9 +62,6 @@ function regAdd(key, valueName, type, data) {
|
|
|
60
62
|
return true;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
/**
|
|
64
|
-
* 执行 reg delete — 键不存在时静默忽略
|
|
65
|
-
*/
|
|
66
65
|
function regDelete(key) {
|
|
67
66
|
if (!isWindows) return false;
|
|
68
67
|
const result = spawnSync('reg', ['delete', key, '/f'], { stdio: 'pipe' });
|
|
@@ -86,10 +85,7 @@ const YAML_ROOTS = [
|
|
|
86
85
|
`${HKCU_SHELL}\\SystemFileAssociations\\.yml\\shell`,
|
|
87
86
|
];
|
|
88
87
|
|
|
89
|
-
//
|
|
90
|
-
const SUBCMD_SPACE = 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
|
|
91
|
-
|
|
92
|
-
// 子命令名称常量(register / unregister 共用,避免硬编码不一致)
|
|
88
|
+
// 子命令名称常量
|
|
93
89
|
const SUBCMDS = {
|
|
94
90
|
generate: 'JuiceEmail.Generate',
|
|
95
91
|
snippet: 'JuiceEmail.Snippet',
|
|
@@ -99,6 +95,48 @@ const SUBCMDS = {
|
|
|
99
95
|
|
|
100
96
|
const PARENT_KEY_NAME = 'JuiceEmail';
|
|
101
97
|
|
|
98
|
+
// ExtendedSubCommandsKey 指向此路径(相对于 HKCU\Software\Classes)
|
|
99
|
+
// 解析结果:HKCU\Software\Classes\JuiceEmail.SubCommands
|
|
100
|
+
const SUB_CMDS_CONTAINER = 'JuiceEmail.SubCommands';
|
|
101
|
+
|
|
102
|
+
// ─── 旧版残留清理 ─────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
// 旧版 HKLM 父菜单路径(v1 使用 HKCR/HKLM,需管理员权限写入)
|
|
105
|
+
const LEGACY_HKLM_ROOTS = [
|
|
106
|
+
'HKEY_LOCAL_MACHINE\\Software\\Classes\\SystemFileAssociations\\.html\\shell',
|
|
107
|
+
'HKEY_LOCAL_MACHINE\\Software\\Classes\\SystemFileAssociations\\.htm\\shell',
|
|
108
|
+
'HKEY_LOCAL_MACHINE\\Software\\Classes\\SystemFileAssociations\\.yaml\\shell',
|
|
109
|
+
'HKEY_LOCAL_MACHINE\\Software\\Classes\\SystemFileAssociations\\.yml\\shell',
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
// 旧版 CommandStore 子命令路径(v1-v2.1.12 使用 SubCommands + CommandStore 方案)
|
|
113
|
+
const LEGACY_HKCU_CMDSTORE = 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
|
|
114
|
+
const LEGACY_HKLM_CMDSTORE = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 清理旧版本残留在 HKLM 的父菜单(v1 使用 HKCR/HKLM,需要管理员权限)
|
|
118
|
+
* 没有管理员权限时静默失败,不影响后续 HKCU 写入
|
|
119
|
+
*/
|
|
120
|
+
function cleanLegacyHklmParents() {
|
|
121
|
+
if (!isWindows) return;
|
|
122
|
+
for (const root of LEGACY_HKLM_ROOTS) {
|
|
123
|
+
regDelete(`${root}\\${PARENT_KEY_NAME}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 清理旧版本 CommandStore 子命令残留(HKCU + HKLM)
|
|
129
|
+
*/
|
|
130
|
+
function cleanLegacyCommandStore() {
|
|
131
|
+
if (!isWindows) return;
|
|
132
|
+
for (const name of Object.values(SUBCMDS)) {
|
|
133
|
+
regDelete(`${LEGACY_HKCU_CMDSTORE}\\${name}`);
|
|
134
|
+
regDelete(`${LEGACY_HKLM_CMDSTORE}\\${name}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── PowerShell 包装 ──────────────────────────────────────────────────────────
|
|
139
|
+
|
|
102
140
|
/**
|
|
103
141
|
* 为交互式命令包一层 PowerShell:
|
|
104
142
|
* - 成功:窗口自动关闭
|
|
@@ -107,7 +145,7 @@ const PARENT_KEY_NAME = 'JuiceEmail';
|
|
|
107
145
|
function wrapInteractive(nodePath, scriptPath, cliArgs) {
|
|
108
146
|
const node = nodePath.replace(/'/g, "''");
|
|
109
147
|
const script = scriptPath.replace(/'/g, "''");
|
|
110
|
-
// %1 由 Windows 在 CreateProcess 前展开为实际文件路径
|
|
148
|
+
// %1 由 Windows Explorer 在 CreateProcess 前展开为实际文件路径
|
|
111
149
|
const ps = [
|
|
112
150
|
`& '${node}' '${script}' ${cliArgs}`,
|
|
113
151
|
`if ($LASTEXITCODE) {`,
|
|
@@ -121,6 +159,56 @@ function wrapInteractive(nodePath, scriptPath, cliArgs) {
|
|
|
121
159
|
return `powershell.exe -Command "${ps}"`;
|
|
122
160
|
}
|
|
123
161
|
|
|
162
|
+
// ─── 工具:查找 PowerShell 7 ─────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
function resolvePwsh() {
|
|
165
|
+
const candidates = [
|
|
166
|
+
'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
|
167
|
+
'C:\\Program Files (x86)\\PowerShell\\7\\pwsh.exe',
|
|
168
|
+
path.join(process.env['LOCALAPPDATA'] || '', 'Microsoft', 'PowerShell', 'pwsh.exe'),
|
|
169
|
+
];
|
|
170
|
+
for (const p of candidates) {
|
|
171
|
+
if (fs.existsSync(p)) return p;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const result = spawnSync('where', ['pwsh'], { stdio: 'pipe' });
|
|
175
|
+
const line = result.stdout.toString().trim().split('\n')[0].trim();
|
|
176
|
+
if (line && fs.existsSync(line)) return line;
|
|
177
|
+
} catch (_) {}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ─── 注册子命令到共享容器 ────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function registerSubCommands(containerPath, nodePath, scriptPath, iconPath, pwshPath) {
|
|
184
|
+
// generate(非交互,无需终端)
|
|
185
|
+
const genKey = `${containerPath}\\shell\\${SUBCMDS.generate}`;
|
|
186
|
+
regAdd(genKey, 'MUIVerb', 'REG_SZ', '📄 作为模板,生成邮件 HTML');
|
|
187
|
+
regAdd(genKey, 'Icon', 'REG_SZ', iconPath);
|
|
188
|
+
regAdd(`${genKey}\\command`, '', 'REG_SZ', `"${nodePath}" "${scriptPath}" -f %1`);
|
|
189
|
+
|
|
190
|
+
// snippet(交互,需要终端)
|
|
191
|
+
const snipKey = `${containerPath}\\shell\\${SUBCMDS.snippet}`;
|
|
192
|
+
regAdd(snipKey, 'MUIVerb', 'REG_SZ', '🧩 作为片段,拼接邮件 HTML');
|
|
193
|
+
regAdd(snipKey, 'Icon', 'REG_SZ', iconPath);
|
|
194
|
+
regAdd(`${snipKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, '-s %1'));
|
|
195
|
+
|
|
196
|
+
// config(交互,需要终端)
|
|
197
|
+
const cfgKey = `${containerPath}\\shell\\${SUBCMDS.config}`;
|
|
198
|
+
regAdd(cfgKey, 'MUIVerb', 'REG_SZ', '⚙️ 作为配置,拼接邮件 HTML');
|
|
199
|
+
regAdd(cfgKey, 'Icon', 'REG_SZ', iconPath);
|
|
200
|
+
regAdd(`${cfgKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, '-c %1'));
|
|
201
|
+
|
|
202
|
+
// pwsh(可选)
|
|
203
|
+
if (pwshPath) {
|
|
204
|
+
const pwshKey = `${containerPath}\\shell\\${SUBCMDS.pwsh}`;
|
|
205
|
+
regAdd(pwshKey, 'MUIVerb', 'REG_SZ', '📂 打开 PowerShell');
|
|
206
|
+
regAdd(pwshKey, 'Icon', 'REG_SZ', pwshPath);
|
|
207
|
+
regAdd(`${pwshKey}\\command`, '', 'REG_SZ',
|
|
208
|
+
`"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath (Split-Path '%1')"`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
124
212
|
// ─── 注册 ─────────────────────────────────────────────────────────────────────
|
|
125
213
|
|
|
126
214
|
async function registerContextMenu() {
|
|
@@ -131,61 +219,30 @@ async function registerContextMenu() {
|
|
|
131
219
|
|
|
132
220
|
console.log(chalk.cyan('\n 注册 juice 右键菜单(当前用户,无需管理员权限)...\n'));
|
|
133
221
|
|
|
222
|
+
// 清理所有旧版残留,避免新旧方案冲突
|
|
223
|
+
cleanLegacyHklmParents();
|
|
224
|
+
cleanLegacyCommandStore();
|
|
225
|
+
|
|
134
226
|
const nodePath = getNodePath();
|
|
135
227
|
const scriptPath = getJuiceScript();
|
|
136
228
|
const iconPath = getIconPath();
|
|
137
|
-
|
|
138
|
-
let ok = true;
|
|
139
|
-
|
|
140
|
-
// ── 子命令 1:普通模式 - juice 生成(非交互,不需终端)─────────────────
|
|
141
|
-
const generateCmd = `"${nodePath}" "${scriptPath}" -f %1`;
|
|
142
|
-
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}`, '', 'REG_SZ', '📄 作为模板,生成邮件 HTML') && ok;
|
|
143
|
-
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}`, 'Icon', 'REG_SZ', iconPath);
|
|
144
|
-
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}\\command`, '', 'REG_SZ', generateCmd);
|
|
145
|
-
|
|
146
|
-
// ── 子命令 2:片段模式 - 片段组装(交互,需要终端)────────────────────────
|
|
147
|
-
const snippetCmd = wrapInteractive(nodePath, scriptPath, '-s %1');
|
|
148
|
-
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, '', 'REG_SZ', '🧩 作为片段,拼接邮件 HTML') && ok;
|
|
149
|
-
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, 'Icon', 'REG_SZ', iconPath);
|
|
150
|
-
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}\\command`, '', 'REG_SZ', snippetCmd);
|
|
151
|
-
|
|
152
|
-
// ── 子命令 3:配置文件模式 - 交互式生成(交互,需要终端)──────────────────
|
|
153
|
-
const configCmd = wrapInteractive(nodePath, scriptPath, '-c %1');
|
|
154
|
-
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, '', 'REG_SZ', '⚙️ 作为配置,拼接邮件 HTML') && ok;
|
|
155
|
-
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, 'Icon', 'REG_SZ', iconPath);
|
|
156
|
-
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}\\command`, '', 'REG_SZ', configCmd);
|
|
157
|
-
|
|
158
|
-
// ── 子命令 4:在文件目录打开 pwsh(可选)──────────────────────────────────
|
|
159
229
|
const pwshPath = resolvePwsh();
|
|
160
|
-
if (pwshPath) {
|
|
161
|
-
const pwshCmd = `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath (Split-Path '%1')"`;
|
|
162
|
-
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}`, '', 'REG_SZ', '📂 打开 PowerShell') && ok;
|
|
163
|
-
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}`, 'Icon', 'REG_SZ', pwshPath);
|
|
164
|
-
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}\\command`, '', 'REG_SZ', pwshCmd);
|
|
165
|
-
}
|
|
166
230
|
|
|
167
|
-
|
|
168
|
-
const allSubs = pwshPath
|
|
169
|
-
? `${SUBCMDS.generate};${SUBCMDS.snippet};${SUBCMDS.config};${SUBCMDS.pwsh}`
|
|
170
|
-
: `${SUBCMDS.generate};${SUBCMDS.snippet};${SUBCMDS.config}`;
|
|
231
|
+
let ok = true;
|
|
171
232
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
ok = regAdd(parentKey, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
|
|
176
|
-
regAdd(parentKey, 'Icon', 'REG_SZ', iconPath);
|
|
177
|
-
regAdd(parentKey, 'SubCommands', 'REG_SZ', allSubs);
|
|
178
|
-
}
|
|
233
|
+
// 子命令注册一次到共享容器(所有文件类型共用)
|
|
234
|
+
const containerPath = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER}`;
|
|
235
|
+
registerSubCommands(containerPath, nodePath, scriptPath, iconPath, pwshPath);
|
|
179
236
|
|
|
180
|
-
//
|
|
181
|
-
|
|
237
|
+
// 为每种文件类型注册父菜单,通过 ExtendedSubCommandsKey 引用共享容器
|
|
238
|
+
const allRoots = [...HTML_ROOTS, ...YAML_ROOTS];
|
|
239
|
+
for (const root of allRoots) {
|
|
182
240
|
const parentKey = `${root}\\${PARENT_KEY_NAME}`;
|
|
183
241
|
ok = regAdd(parentKey, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
|
|
184
242
|
regAdd(parentKey, 'Icon', 'REG_SZ', iconPath);
|
|
185
|
-
regAdd(parentKey, '
|
|
243
|
+
regAdd(parentKey, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER);
|
|
186
244
|
}
|
|
187
245
|
|
|
188
|
-
// ── 输出 ──────────────────────────────────────────────────────────────────
|
|
189
246
|
if (!ok) {
|
|
190
247
|
console.log(chalk.yellow(' ⚠ 部分注册表项写入失败,右键菜单可能不完整。\n'));
|
|
191
248
|
}
|
|
@@ -216,20 +273,18 @@ async function unregisterContextMenu() {
|
|
|
216
273
|
|
|
217
274
|
let removed = 0;
|
|
218
275
|
|
|
219
|
-
// 清理
|
|
220
|
-
|
|
276
|
+
// 清理 HKCU 父菜单(4 个文件类型)
|
|
277
|
+
const allRoots = [...HTML_ROOTS, ...YAML_ROOTS];
|
|
278
|
+
for (const root of allRoots) {
|
|
221
279
|
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
222
280
|
}
|
|
223
281
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
227
|
-
}
|
|
282
|
+
// 清理共享子命令容器
|
|
283
|
+
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER}`)) removed++;
|
|
228
284
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
285
|
+
// 清理旧版残留(HKLM 父菜单 + CommandStore)
|
|
286
|
+
cleanLegacyHklmParents();
|
|
287
|
+
cleanLegacyCommandStore();
|
|
233
288
|
|
|
234
289
|
if (removed > 0) {
|
|
235
290
|
console.log(chalk.green(` ✔ 已移除 ${removed} 个右键菜单项。\n`));
|
|
@@ -238,23 +293,4 @@ async function unregisterContextMenu() {
|
|
|
238
293
|
}
|
|
239
294
|
}
|
|
240
295
|
|
|
241
|
-
// ─── 工具:查找 PowerShell 7 ─────────────────────────────────────────────────
|
|
242
|
-
|
|
243
|
-
function resolvePwsh() {
|
|
244
|
-
const candidates = [
|
|
245
|
-
'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
|
246
|
-
'C:\\Program Files (x86)\\PowerShell\\7\\pwsh.exe',
|
|
247
|
-
path.join(process.env['LOCALAPPDATA'] || '', 'Microsoft', 'PowerShell', 'pwsh.exe'),
|
|
248
|
-
];
|
|
249
|
-
for (const p of candidates) {
|
|
250
|
-
if (fs.existsSync(p)) return p;
|
|
251
|
-
}
|
|
252
|
-
try {
|
|
253
|
-
const result = spawnSync('where', ['pwsh'], { stdio: 'pipe' });
|
|
254
|
-
const line = result.stdout.toString().trim().split('\n')[0].trim();
|
|
255
|
-
if (line && fs.existsSync(line)) return line;
|
|
256
|
-
} catch (_) {}
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
296
|
module.exports = { registerContextMenu, unregisterContextMenu };
|