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 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
- > **注意**:注册/卸载需要**管理员权限**。注册成功后如菜单未立即出现,重启文件资源管理器(`explorer.exe`)即可。
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 style="padding-bottoms: 24px;" class="h-0">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juice-email-cli",
3
- "version": "2.1.8",
3
+ "version": "2.1.9",
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": {
@@ -3,18 +3,18 @@
3
3
  /**
4
4
  * Windows 右键菜单注册 / 取消注册
5
5
  *
6
- * 风格参考 PowerShell 7 的右键菜单实现:
7
- * - 使用 MUIVerb(菜单显示文字)+ Icon 键
8
- * - 通过 ExtendedSubCommandsKey 实现子菜单("用 juice 处理" → 子项)
9
- * - 注册到 SystemFileAssociations,兼容任意关联方式打开的 .html/.htm
6
+ * 使用 HKEY_CURRENT_USER,无需管理员权限,仅对当前用户生效。
10
7
  *
11
8
  * 菜单结构:
12
- * 📧 juice 生成邮件 HTML
13
- * ├── 生成邮件 HTML(标准 + 压缩)
14
- * ├── 🧩 邮件片段组装(交互选择模板)
15
- * └── 📂 在此目录打开 PowerShell
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 失败(可能需要管理员权限)\n 键:${key}\n 原因:${msg}`));
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
- } catch (_) {}
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
- // reg.exe 中反斜杠需要双写,双引号需要转义
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
- // 根键:SystemFileAssociations 对 .html/.htm 通用
76
- const ROOTS = [
77
- 'HKEY_CLASSES_ROOT\\SystemFileAssociations\\.html\\shell',
78
- 'HKEY_CLASSES_ROOT\\SystemFileAssociations\\.htm\\shell',
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 右键菜单(PowerShell 7 风格)...\n'));
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
- // 子命令 1:生成(标准 + 压缩)
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
- // ── 注册子命令到 CommandStore ─────────────────────────────────────────────
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
- // 子命令 1:普通模式 - juice 生成
106
- const generateCmd = `"${nodePath}" "${scriptPath}" -f "%1"`;
107
- regAdd(`${SUBCMD_SPACE}\\${subCmd1}`, '', 'REG_SZ', ' 生成邮件 HTML(标准 + 压缩)');
108
- regAdd(`${SUBCMD_SPACE}\\${subCmd1}`, 'Icon', 'REG_SZ', iconPath);
109
- regAdd(`${SUBCMD_SPACE}\\${subCmd1}\\command`, '', 'REG_SZ', generateCmd);
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
- // 子命令 2:片段模式 - 片段组装(交互选择模板)
112
- const snippetCmd = `"${nodePath}" "${scriptPath}" -s "%1"`;
113
- regAdd(`${SUBCMD_SPACE}\\${subCmd2}`, '', 'REG_SZ', '🧩 邮件片段组装(交互选择模板)');
114
- regAdd(`${SUBCMD_SPACE}\\${subCmd2}`, 'Icon', 'REG_SZ', iconPath);
115
- regAdd(`${SUBCMD_SPACE}\\${subCmd2}\\command`, '', 'REG_SZ', snippetCmd);
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
- // 子命令 3:在文件目录打开 pwsh(如果安装了 PowerShell 7)
143
+ // ── 子命令 4:在文件目录打开 pwsh(可选)──────────────────────────────────
118
144
  const pwshPath = resolvePwsh();
119
145
  if (pwshPath) {
120
- const pwshCmd = `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '%W'"`;
121
- regAdd(`${SUBCMD_SPACE}\\${subCmd3}`, '', 'REG_SZ', '📂 在此目录打开 PowerShell 7');
122
- regAdd(`${SUBCMD_SPACE}\\${subCmd3}`, 'Icon', 'REG_SZ', pwshPath);
123
- regAdd(`${SUBCMD_SPACE}\\${subCmd3}\\command`, '', 'REG_SZ', pwshCmd);
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
- // ── 注册父菜单到 SystemFileAssociations ──────────────────────────────────
152
+ // ── 注册 HTML 父菜单(二级子菜单)────────────────────────────────────────
153
+ const htmlSubs = pwshPath
154
+ ? `${SUBCMDS.generate};${SUBCMDS.snippet};${SUBCMDS.pwsh}`
155
+ : `${SUBCMDS.generate};${SUBCMDS.snippet}`;
127
156
 
128
- let subCommands = `${subCmd1};${subCmd2}`;
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
- // MUIVerb:菜单显示文字
135
- regAdd(parentKey, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML');
136
- // Icon:使用自定义图标
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
- // SubCommands:引用 CommandStore 中的子命令(分号分隔)
139
- regAdd(parentKey, 'SubCommands', 'REG_SZ', subCommands);
140
- // Extended:取消注释可让菜单仅在按住 Shift 时显示(默认不加)
141
- // regAdd(parentKey, 'Extended', 'REG_SZ', '');
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
- ` ${chalk.cyan('.html')} / ${chalk.cyan('.htm')} 文件上右键即可看到:\n` +
148
- ` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
149
- ` ├── ⚡ 生成邮件 HTML(标准 + 压缩) → juice -f\n` +
150
- ` ├── 🧩 邮件片段组装(交互选择模板) → juice -s\n` +
151
- (pwshPath ? ` └── 📂 在此目录打开 PowerShell 7\n` : '') +
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
- for (const root of ROOTS) {
163
- regDelete(`${root}\\${PARENT_KEY_NAME}`);
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
- regDelete(`${SUBCMD_SPACE}\\JuiceEmail.Generate`);
167
- regDelete(`${SUBCMD_SPACE}\\JuiceEmail.Snippet`);
168
- regDelete(`${SUBCMD_SPACE}\\JuiceEmail.OpenPwsh`);
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
- console.log(chalk.green(' ✔ 右键菜单已移除。\n'));
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
- console.log(chalk.gray(` 请在 edm/${brand.name}/ 下创建片段文件夹(含 HTML 和 YAML 文件)。`));
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);