juice-email-cli 2.1.8 → 2.1.9
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/README.md +2 -3
- package/bin/juice.js +4 -4
- package/edm/elabscience/literature/snippet.html +1 -1
- package/package.json +1 -1
- package/src/context-menu.js +118 -70
- package/src/snippet.js +28 -2
package/README.md
CHANGED
|
@@ -322,12 +322,11 @@ edm/
|
|
|
322
322
|
```
|
|
323
323
|
|
|
324
324
|
```bash
|
|
325
|
-
#
|
|
326
|
-
juice --install # 注册
|
|
325
|
+
juice --install # 注册(当前用户,无需管理员)
|
|
327
326
|
juice --uninstall # 卸载
|
|
328
327
|
```
|
|
329
328
|
|
|
330
|
-
>
|
|
329
|
+
> **注意**:注册到当前用户(HKCU),无需管理员权限。注册成功后如菜单未立即出现,重启文件资源管理器(`explorer.exe`)即可。
|
|
331
330
|
|
|
332
331
|
---
|
|
333
332
|
|
package/bin/juice.js
CHANGED
|
@@ -14,8 +14,8 @@ program
|
|
|
14
14
|
.option('-s, --snippet <path>', '片段 HTML 文件路径:将片段内容插入模板 <tbody id="content"> 中')
|
|
15
15
|
.option('-c, --config <path>', '配置文件路径')
|
|
16
16
|
.option('-n, --name <name>', '片段模式输出文件名(不含扩展名)')
|
|
17
|
-
.option('--install', '注册 Windows
|
|
18
|
-
.option('--uninstall', '取消 Windows
|
|
17
|
+
.option('--install', '注册 Windows 右键菜单(当前用户,无需管理员)')
|
|
18
|
+
.option('--uninstall', '取消 Windows 右键菜单注册')
|
|
19
19
|
.addHelpText('before', `
|
|
20
20
|
╔════════════════════════════════════════════════════════════════╗
|
|
21
21
|
║ juice-email-cli v${pkg.version} ║
|
|
@@ -64,8 +64,8 @@ program
|
|
|
64
64
|
════════════════════════════════════════════════════════════════
|
|
65
65
|
|
|
66
66
|
在 .html/.htm 文件上右键即可使用:
|
|
67
|
-
juice --install
|
|
68
|
-
juice --uninstall
|
|
67
|
+
juice --install (当前用户,注册右键菜单)
|
|
68
|
+
juice --uninstall (卸载右键菜单)
|
|
69
69
|
|
|
70
70
|
更多信息:https://gitee.com/siriussupreme/juice-cli
|
|
71
71
|
`)
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
<td rowspan="2" style="padding-right: 12px;" class="image-box align-middle">
|
|
66
66
|
<img src="{{information.image}}" />
|
|
67
67
|
</td>
|
|
68
|
-
<td
|
|
68
|
+
<td class="h-0 {{#information.link}}pb-24{{/information.link}}">
|
|
69
69
|
<p>{{information.content}}</p>
|
|
70
70
|
</td>
|
|
71
71
|
</tr>
|
package/package.json
CHANGED
package/src/context-menu.js
CHANGED
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Windows 右键菜单注册 / 取消注册
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* - 使用 MUIVerb(菜单显示文字)+ Icon 键
|
|
8
|
-
* - 通过 ExtendedSubCommandsKey 实现子菜单("用 juice 处理" → 子项)
|
|
9
|
-
* - 注册到 SystemFileAssociations,兼容任意关联方式打开的 .html/.htm
|
|
6
|
+
* 使用 HKEY_CURRENT_USER,无需管理员权限,仅对当前用户生效。
|
|
10
7
|
*
|
|
11
8
|
* 菜单结构:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* .html / .htm:
|
|
10
|
+
* 📧 用 juice 生成邮件 HTML
|
|
11
|
+
* ├── ⚡ 生成邮件 HTML(标准 + 压缩) → juice -f
|
|
12
|
+
* ├── 🧩 邮件片段组装(交互选择模板) → juice -s
|
|
13
|
+
* └── 📂 在此目录打开 PowerShell 7 (可选)
|
|
16
14
|
*
|
|
17
|
-
*
|
|
15
|
+
* .yaml / .yml:
|
|
16
|
+
* 📧 用 juice 生成邮件 HTML
|
|
17
|
+
* └── 📋 交互式生成邮件(使用此配置) → juice -c
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
const { execSync } = require('child_process');
|
|
@@ -47,108 +47,143 @@ function regAdd(key, valueName, type, data) {
|
|
|
47
47
|
const cmd = `reg add "${key}" ${vFlag} ${tFlag} ${dFlag} /f`.replace(/\s+/g, ' ').trim();
|
|
48
48
|
try {
|
|
49
49
|
execSync(cmd, { stdio: 'pipe' });
|
|
50
|
+
return true;
|
|
50
51
|
} catch (e) {
|
|
51
52
|
const msg = e.stderr ? e.stderr.toString().trim() : e.message;
|
|
52
|
-
console.warn(chalk.yellow(` ⚠ reg add
|
|
53
|
+
console.warn(chalk.yellow(` ⚠ reg add 失败\n 键:${key}\n 原因:${msg}`));
|
|
54
|
+
return false;
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
|
-
* 执行 reg delete
|
|
59
|
+
* 执行 reg delete,键不存在时静默忽略,返回是否实际删除了
|
|
58
60
|
*/
|
|
59
61
|
function regDelete(key) {
|
|
60
62
|
try {
|
|
61
63
|
execSync(`reg delete "${key}" /f`, { stdio: 'pipe' });
|
|
62
|
-
|
|
64
|
+
return true;
|
|
65
|
+
} catch (_) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
66
|
-
*
|
|
71
|
+
* 转义注册表数据中的特殊字符
|
|
67
72
|
*/
|
|
68
73
|
function escapeRegData(str) {
|
|
69
|
-
|
|
70
|
-
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
74
|
+
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%');
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
// ─── 菜单结构常量 ─────────────────────────────────────────────────────────────
|
|
74
78
|
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
// 使用 HKCU,无需管理员权限
|
|
80
|
+
const HKCU_SHELL = 'HKEY_CURRENT_USER\\Software\\Classes';
|
|
81
|
+
|
|
82
|
+
// HTML 文件右键
|
|
83
|
+
const HTML_ROOTS = [
|
|
84
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.html\\shell`,
|
|
85
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.htm\\shell`,
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// YAML 文件右键
|
|
89
|
+
const YAML_ROOTS = [
|
|
90
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.yaml\\shell`,
|
|
91
|
+
`${HKCU_SHELL}\\SystemFileAssociations\\.yml\\shell`,
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// CommandStore 子命令注册空间
|
|
95
|
+
const SUBCMD_SPACE = 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
|
|
96
|
+
|
|
97
|
+
// 子命令名称常量(register / unregister 共用,避免硬编码不一致)
|
|
98
|
+
const SUBCMDS = {
|
|
99
|
+
generate: 'JuiceEmail.Generate',
|
|
100
|
+
snippet: 'JuiceEmail.Snippet',
|
|
101
|
+
config: 'JuiceEmail.WithConfig',
|
|
102
|
+
pwsh: 'JuiceEmail.OpenPwsh',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// 旧版本可能残留的子命令名(升级时清理)
|
|
106
|
+
const LEGACY_SUBCMDS = [
|
|
107
|
+
'JuiceEmail.Generate',
|
|
108
|
+
'JuiceEmail.Snippet',
|
|
109
|
+
'JuiceEmail.OpenPwsh',
|
|
79
110
|
];
|
|
80
111
|
|
|
81
|
-
// 父菜单键名(与 PowerShell 7 风格保持一致:不含特殊字符的英文键名)
|
|
82
112
|
const PARENT_KEY_NAME = 'JuiceEmail';
|
|
83
|
-
// 子命令注册表空间(SubCommands 引用的命令存放在 shell\JuiceEmail.SubCommands 下)
|
|
84
|
-
// PowerShell 7 的做法:在 shell 同级放一个专用 SubCommands 键
|
|
85
|
-
const SUBCMD_SPACE = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
|
|
86
113
|
|
|
87
114
|
// ─── 注册 ─────────────────────────────────────────────────────────────────────
|
|
88
115
|
|
|
89
116
|
async function registerContextMenu() {
|
|
90
|
-
console.log(chalk.cyan('\n 注册 juice
|
|
117
|
+
console.log(chalk.cyan('\n 注册 juice 右键菜单(当前用户,无需管理员权限)...\n'));
|
|
91
118
|
|
|
92
119
|
const nodePath = getNodePath();
|
|
93
120
|
const scriptPath = getJuiceScript();
|
|
94
121
|
const iconPath = getIconPath();
|
|
95
122
|
|
|
96
|
-
|
|
97
|
-
const subCmd1 = `JuiceEmail.Generate`;
|
|
98
|
-
// 子命令 2:片段组装
|
|
99
|
-
const subCmd2 = `JuiceEmail.Snippet`;
|
|
100
|
-
// 子命令 3:在此打开 PowerShell(可选便利项)
|
|
101
|
-
const subCmd3 = `JuiceEmail.OpenPwsh`;
|
|
123
|
+
let ok = true;
|
|
102
124
|
|
|
103
|
-
// ──
|
|
125
|
+
// ── 子命令 1:普通模式 - juice 生成 ──────────────────────────────────────
|
|
126
|
+
const generateCmd = `"${nodePath}" "${scriptPath}" -f %1`;
|
|
127
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}`, '', 'REG_SZ', '⚡ 生成邮件 HTML(标准 + 压缩)') && ok;
|
|
128
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}`, 'Icon', 'REG_SZ', iconPath);
|
|
129
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.generate}\\command`, '', 'REG_SZ', generateCmd);
|
|
104
130
|
|
|
105
|
-
// 子命令
|
|
106
|
-
const
|
|
107
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
108
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
109
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
131
|
+
// ── 子命令 2:片段模式 - 片段组装 ────────────────────────────────────────
|
|
132
|
+
const snippetCmd = `"${nodePath}" "${scriptPath}" -s %1`;
|
|
133
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, '', 'REG_SZ', '🧩 邮件片段组装(交互选择模板)') && ok;
|
|
134
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}`, 'Icon', 'REG_SZ', iconPath);
|
|
135
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.snippet}\\command`, '', 'REG_SZ', snippetCmd);
|
|
110
136
|
|
|
111
|
-
// 子命令
|
|
112
|
-
const
|
|
113
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
114
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
115
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
137
|
+
// ── 子命令 3:配置文件模式 - 交互式生成 ──────────────────────────────────
|
|
138
|
+
const configCmd = `"${nodePath}" "${scriptPath}" -c %1`;
|
|
139
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, '', 'REG_SZ', '📋 交互式生成邮件(使用此配置)') && ok;
|
|
140
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}`, 'Icon', 'REG_SZ', iconPath);
|
|
141
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.config}\\command`, '', 'REG_SZ', configCmd);
|
|
116
142
|
|
|
117
|
-
// 子命令
|
|
143
|
+
// ── 子命令 4:在文件目录打开 pwsh(可选)──────────────────────────────────
|
|
118
144
|
const pwshPath = resolvePwsh();
|
|
119
145
|
if (pwshPath) {
|
|
120
|
-
const pwshCmd = `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '%
|
|
121
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
122
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
123
|
-
regAdd(`${SUBCMD_SPACE}\\${
|
|
146
|
+
const pwshCmd = `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath (Split-Path '%1')"`;
|
|
147
|
+
ok = regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}`, '', 'REG_SZ', '📂 在此目录打开 PowerShell 7') && ok;
|
|
148
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}`, 'Icon', 'REG_SZ', pwshPath);
|
|
149
|
+
regAdd(`${SUBCMD_SPACE}\\${SUBCMDS.pwsh}\\command`, '', 'REG_SZ', pwshCmd);
|
|
124
150
|
}
|
|
125
151
|
|
|
126
|
-
// ──
|
|
152
|
+
// ── 注册 HTML 父菜单(二级子菜单)────────────────────────────────────────
|
|
153
|
+
const htmlSubs = pwshPath
|
|
154
|
+
? `${SUBCMDS.generate};${SUBCMDS.snippet};${SUBCMDS.pwsh}`
|
|
155
|
+
: `${SUBCMDS.generate};${SUBCMDS.snippet}`;
|
|
127
156
|
|
|
128
|
-
|
|
129
|
-
if (pwshPath) subCommands += `;${subCmd3}`;
|
|
130
|
-
|
|
131
|
-
for (const root of ROOTS) {
|
|
157
|
+
for (const root of HTML_ROOTS) {
|
|
132
158
|
const parentKey = `${root}\\${PARENT_KEY_NAME}`;
|
|
159
|
+
ok = regAdd(parentKey, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
|
|
160
|
+
regAdd(parentKey, 'Icon', 'REG_SZ', iconPath);
|
|
161
|
+
regAdd(parentKey, 'SubCommands', 'REG_SZ', htmlSubs);
|
|
162
|
+
}
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
164
|
+
// ── 注册 YAML 父菜单(二级子菜单)────────────────────────────────────────
|
|
165
|
+
for (const root of YAML_ROOTS) {
|
|
166
|
+
const parentKey = `${root}\\${PARENT_KEY_NAME}`;
|
|
167
|
+
ok = regAdd(parentKey, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
|
|
137
168
|
regAdd(parentKey, 'Icon', 'REG_SZ', iconPath);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
169
|
+
regAdd(parentKey, 'SubCommands', 'REG_SZ', SUBCMDS.config);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── 输出 ──────────────────────────────────────────────────────────────────
|
|
173
|
+
if (!ok) {
|
|
174
|
+
console.log(chalk.yellow(' ⚠ 部分注册表项写入失败,右键菜单可能不完整。\n'));
|
|
142
175
|
}
|
|
143
176
|
|
|
144
177
|
console.log(
|
|
145
178
|
'\n' +
|
|
146
179
|
chalk.green(' ✔ 右键菜单注册完成!') + '\n\n' +
|
|
147
|
-
`
|
|
148
|
-
`
|
|
149
|
-
`
|
|
150
|
-
`
|
|
151
|
-
(pwshPath ? `
|
|
180
|
+
` ${chalk.bold('.html / .htm')} 文件右键:\n` +
|
|
181
|
+
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
182
|
+
` ├── ⚡ 生成邮件 HTML(标准 + 压缩) → juice -f\n` +
|
|
183
|
+
` ├── 🧩 邮件片段组装(交互选择模板) → juice -s\n` +
|
|
184
|
+
(pwshPath ? ` └── 📂 在此目录打开 PowerShell 7\n` : '') +
|
|
185
|
+
`\n ${chalk.bold('.yaml / .yml')} 文件右键:\n` +
|
|
186
|
+
` └── 📧 交互式生成邮件(使用此配置) → juice -c\n` +
|
|
152
187
|
'\n' +
|
|
153
188
|
chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
|
|
154
189
|
);
|
|
@@ -159,15 +194,29 @@ async function registerContextMenu() {
|
|
|
159
194
|
async function unregisterContextMenu() {
|
|
160
195
|
console.log(chalk.cyan('\n 取消注册 juice 右键菜单...\n'));
|
|
161
196
|
|
|
162
|
-
|
|
163
|
-
|
|
197
|
+
let removed = 0;
|
|
198
|
+
|
|
199
|
+
// 清理 HTML 父菜单
|
|
200
|
+
for (const root of HTML_ROOTS) {
|
|
201
|
+
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
164
202
|
}
|
|
165
203
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
204
|
+
// 清理 YAML 父菜单
|
|
205
|
+
for (const root of YAML_ROOTS) {
|
|
206
|
+
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 清理子命令(当前版本 + 旧版本残留)
|
|
210
|
+
const allSubcmds = [...new Set([...Object.values(SUBCMDS), ...LEGACY_SUBCMDS])];
|
|
211
|
+
for (const name of allSubcmds) {
|
|
212
|
+
regDelete(`${SUBCMD_SPACE}\\${name}`);
|
|
213
|
+
}
|
|
169
214
|
|
|
170
|
-
|
|
215
|
+
if (removed > 0) {
|
|
216
|
+
console.log(chalk.green(` ✔ 已移除 ${removed} 个右键菜单项。\n`));
|
|
217
|
+
} else {
|
|
218
|
+
console.log(chalk.gray(' ℹ 未找到已注册的右键菜单,无需卸载。\n'));
|
|
219
|
+
}
|
|
171
220
|
}
|
|
172
221
|
|
|
173
222
|
// ─── 工具:查找 PowerShell 7 ─────────────────────────────────────────────────
|
|
@@ -181,7 +230,6 @@ function resolvePwsh() {
|
|
|
181
230
|
for (const p of candidates) {
|
|
182
231
|
if (require('fs').existsSync(p)) return p;
|
|
183
232
|
}
|
|
184
|
-
// 尝试 where.exe
|
|
185
233
|
try {
|
|
186
234
|
const result = execSync('where pwsh', { stdio: 'pipe' }).toString().trim().split('\n')[0].trim();
|
|
187
235
|
if (result && require('fs').existsSync(result)) return result;
|
package/src/snippet.js
CHANGED
|
@@ -479,6 +479,32 @@ async function promptOutputName(defaultBaseName, cwd) {
|
|
|
479
479
|
return baseName;
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
/**
|
|
483
|
+
* 无片段可用时,将模板拷贝到当前目录作为快捷副本
|
|
484
|
+
*/
|
|
485
|
+
async function copyTemplateToCwd(templatePath) {
|
|
486
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
487
|
+
|
|
488
|
+
const templateName = path.parse(templatePath).name;
|
|
489
|
+
console.log(chalk.cyan(`\n 可将模板「${path.basename(templatePath)}」拷贝到当前目录作为快捷副本。`));
|
|
490
|
+
|
|
491
|
+
const proceed = await confirm({
|
|
492
|
+
message: '是否拷贝模板到当前目录?',
|
|
493
|
+
default: true,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
if (!proceed) {
|
|
497
|
+
console.log(chalk.gray('已取消。'));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const baseName = await promptOutputName(templateName, process.cwd());
|
|
502
|
+
const destPath = path.join(process.cwd(), baseName + '.html');
|
|
503
|
+
|
|
504
|
+
fs.copyFileSync(templatePath, destPath);
|
|
505
|
+
console.log(chalk.green(`\n✔ 模板已拷贝到:${destPath}`));
|
|
506
|
+
}
|
|
507
|
+
|
|
482
508
|
async function promptConfirm(summary) {
|
|
483
509
|
const { confirm } = await import('@inquirer/prompts');
|
|
484
510
|
const outPaths = resolveSnippetOutputPaths(summary.outputBaseName, summary.outputDir);
|
|
@@ -621,8 +647,8 @@ async function runInteractiveMode({ config: cliConfigPath }) {
|
|
|
621
647
|
// 3. 选择片段文件夹
|
|
622
648
|
const snippetFolders = findSnippetFolders(brand.path);
|
|
623
649
|
if (snippetFolders.length === 0) {
|
|
624
|
-
console.log(chalk.yellow(`\n ⚠ 品牌「${brand.name}
|
|
625
|
-
|
|
650
|
+
console.log(chalk.yellow(`\n ⚠ 品牌「${brand.name}」下暂无片段系列。`));
|
|
651
|
+
await copyTemplateToCwd(templateChoice.path);
|
|
626
652
|
return;
|
|
627
653
|
}
|
|
628
654
|
const folder = await promptSnippetFolder(snippetFolders);
|