juice-email-cli 2.3.3 → 2.3.5

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
@@ -351,34 +351,46 @@ edm/
351
351
 
352
352
  ## Windows 右键菜单
353
353
 
354
- ### 文件右键(.html / .htm
354
+ ### .html / .htm 文件右键
355
355
 
356
356
  ```
357
357
  📧 用 juice 生成邮件 HTML
358
- ├── 📄 作为模板,生成邮件 HTML → juice -f %1(后台执行)
359
- ├── 🧩 作为片段,拼接邮件 HTML → juice -s %1(交互选择模板)
360
- ├── 📦 查看资源列表 → juice view
361
- ├── 📋 浏览资源库 → juice view -i
358
+ ├── 📄 作为模板,生成邮件 HTML → juice -f %1(后台执行)
359
+ ├── 🧩 作为片段,拼接邮件 HTML → juice -s %1(交互选择模板)
360
+ ├── 📋 查看可用资源 → juice view
361
+ ├── 📦 拷贝全部资源 → juice init --all
362
+ ├── 📥 选择资源拷贝 → juice init
362
363
  └── 📂 打开 PowerShell ← 仅已安装 pwsh 时出现
363
364
  ```
364
365
 
365
- ### 文件右键(.yaml / .yml
366
+ ### .yaml / .yml 文件右键
366
367
 
367
368
  ```
368
369
  📧 用 juice 生成邮件 HTML
369
- ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c %1(交互选择品牌/模板/片段)
370
- ├── 📦 查看资源列表 → juice view
371
- ├── 📋 浏览资源库 → juice view -i
370
+ ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c %1(交互选择品牌/模板/片段)
371
+ ├── 📋 查看可用资源
372
+ ├── 📦 拷贝全部资源
373
+ ├── 📥 选择资源拷贝
372
374
  └── 📂 打开 PowerShell
373
375
  ```
374
376
 
375
- ### 文件夹 / 空白处右键
377
+ ### 文件夹右键
376
378
 
377
379
  ```
378
- 📧 juice 邮件工具
379
- ├── 📥 从资源库拷贝到此处 → juice init
380
- ├── 📦 查看资源列表 → juice view
381
- ├── 📋 浏览资源库 → juice view -i
380
+ 📧 juice 生成邮件 HTML
381
+ ├── 📋 查看可用资源
382
+ ├── 📦 拷贝全部资源
383
+ ├── 📥 选择资源拷贝
384
+ └── 📂 在此打开终端
385
+ ```
386
+
387
+ ### 空白处右键
388
+
389
+ ```
390
+ 📧 用 juice 生成邮件 HTML
391
+ ├── 📋 查看可用资源
392
+ ├── 📦 拷贝全部资源
393
+ ├── 📥 选择资源拷贝
382
394
  └── 📂 在此打开终端
383
395
  ```
384
396
 
package/bin/juice.js CHANGED
@@ -67,11 +67,13 @@ program
67
67
  juice --install (当前用户,无需管理员)
68
68
  juice --uninstall (卸载右键菜单)
69
69
 
70
- 菜单结构(所有文件类型统一):
70
+ 菜单结构:
71
71
  📧 用 juice 生成邮件 HTML
72
- ├── 📄 作为模板,生成邮件 HTML → juice -f %1(后台执行)
73
- ├── 🧩 作为片段,拼接邮件 HTML → juice -s %1(交互选择模板)
74
- ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c %1(交互选择品牌/模板/片段)
72
+ ├── 📄 作为模板,生成邮件 HTML → juice -f(后台执行)
73
+ ├── 🧩 作为片段,拼接邮件 HTML → juice -s(交互选择模板)
74
+ ├── 📋 查看可用资源 → juice view
75
+ ├── 📦 拷贝全部资源 → juice init --all
76
+ ├── 📥 选择资源拷贝 → juice init
75
77
  └── 📂 打开 PowerShell (仅已安装 pwsh 时出现)
76
78
 
77
79
  更多信息:https://gitee.com/siriussupreme/juice-cli
@@ -103,6 +105,7 @@ program
103
105
  .option('-t, --template <file-path>', '仅拷贝模板 HTML 文件')
104
106
  .option('-s, --snippet <file-path>', '仅拷贝片段 HTML 文件')
105
107
  .option('-c, --config <file-path>', '仅拷贝配置 YAML 文件')
108
+ .option('--all [target]', '拷贝整个 EDM 资源目录到当前或指定目录')
106
109
  .action(async (initPath, options) => {
107
110
  const { runInitMode } = require('../src/init');
108
111
  await runInitMode({
@@ -110,6 +113,7 @@ program
110
113
  template: options.template || null,
111
114
  snippet: options.snippet || null,
112
115
  config: options.config || null,
116
+ all: options.all !== undefined ? (options.all || true) : null,
113
117
  });
114
118
  });
115
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juice-email-cli",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
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": {
@@ -6,20 +6,30 @@
6
6
  * 使用 ExtendedSubCommandsKey 实现级联菜单(参考 WinRAR 和 Windows Defender 的实现)。
7
7
  * 子命令存储在 Classes 根下的共享容器中,各文件类型父菜单通过 ExtendedSubCommandsKey 引用。
8
8
  *
9
- * 与旧版 SubCommands + CommandStore 方案的区别:
10
- * - 不依赖 CommandStore(HKCU CommandStore 在某些 Windows 版本下不被正确解析)
11
- * - ExtendedSubCommandsKey 指向 Classes 根下的相对路径,子命令内联存储
12
- *
13
- * 菜单结构(按文件类型区分):
9
+ * 菜单结构:
14
10
  * .html / .htm:
15
11
  * 📧 用 juice 生成邮件 HTML
16
12
  * ├── 📄 作为模板,生成邮件 HTML → juice -f(后台执行)
17
13
  * ├── 🧩 作为片段,拼接邮件 HTML → juice -s(交互选择模板)
14
+ * ├── 📋 查看可用资源 → juice view
15
+ * ├── 📦 拷贝全部资源 → juice init --all
16
+ * ├── 📥 选择资源拷贝 → juice init
18
17
  * └── 📂 打开 PowerShell (可选)
19
18
  *
20
19
  * .yaml / .yml:
21
20
  * 📧 用 juice 生成邮件 HTML
22
- * └── ⚙️ 作为配置,拼接邮件 HTML → juice -c(交互选择品牌/模板/片段)
21
+ * ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c(交互选择品牌/模板/片段)
22
+ * ├── 📋 查看可用资源
23
+ * ├── 📦 拷贝全部资源
24
+ * ├── 📥 选择资源拷贝
25
+ * └── 📂 打开 PowerShell (可选)
26
+ *
27
+ * 文件夹 / 空白处:
28
+ * 📧 用 juice 生成邮件 HTML
29
+ * ├── 📋 查看可用资源
30
+ * ├── 📦 拷贝全部资源
31
+ * ├── 📥 选择资源拷贝
32
+ * └── 📂 在此打开终端 (可选)
23
33
  */
24
34
 
25
35
  const { spawnSync } = require('child_process');
@@ -103,7 +113,6 @@ const SUBCMDS = {
103
113
  snippet: 'JuiceEmail.Snippet',
104
114
  config: 'JuiceEmail.WithConfig',
105
115
  viewEdm: 'JuiceEmail.ViewEdm',
106
- viewEdmInt:'JuiceEmail.ViewEdmInteractive',
107
116
  initEdm: 'JuiceEmail.InitEdm',
108
117
  pwsh: 'JuiceEmail.OpenPwsh',
109
118
  };
@@ -118,7 +127,6 @@ const SUB_CMDS_CONTAINER_BG = 'JuiceEmail.SubCommands.Bg';
118
127
 
119
128
  // ─── 旧版残留清理 ─────────────────────────────────────────────────────────────
120
129
 
121
- // 旧版 HKLM 父菜单路径(v1 使用 HKCR/HKLM,需管理员权限写入)
122
130
  const LEGACY_HKLM_ROOTS = [
123
131
  'HKEY_LOCAL_MACHINE\\Software\\Classes\\SystemFileAssociations\\.html\\shell',
124
132
  'HKEY_LOCAL_MACHINE\\Software\\Classes\\SystemFileAssociations\\.htm\\shell',
@@ -126,14 +134,9 @@ const LEGACY_HKLM_ROOTS = [
126
134
  'HKEY_LOCAL_MACHINE\\Software\\Classes\\SystemFileAssociations\\.yml\\shell',
127
135
  ];
128
136
 
129
- // 旧版 CommandStore 子命令路径(v1-v2.1.12 使用 SubCommands + CommandStore 方案)
130
137
  const LEGACY_HKCU_CMDSTORE = 'HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
131
138
  const LEGACY_HKLM_CMDSTORE = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CommandStore\\shell';
132
139
 
133
- /**
134
- * 清理旧版本残留在 HKLM 的父菜单(v1 使用 HKCR/HKLM,需要管理员权限)
135
- * 没有管理员权限时静默失败,不影响后续 HKCU 写入
136
- */
137
140
  function cleanLegacyHklmParents() {
138
141
  if (!isWindows) return;
139
142
  for (const root of LEGACY_HKLM_ROOTS) {
@@ -141,28 +144,24 @@ function cleanLegacyHklmParents() {
141
144
  }
142
145
  }
143
146
 
144
- /**
145
- * 清理旧版本 CommandStore 子命令残留(HKCU + HKLM)
146
- */
147
147
  function cleanLegacyCommandStore() {
148
148
  if (!isWindows) return;
149
149
  for (const name of Object.values(SUBCMDS)) {
150
150
  regDelete(`${LEGACY_HKCU_CMDSTORE}\\${name}`);
151
151
  regDelete(`${LEGACY_HKLM_CMDSTORE}\\${name}`);
152
152
  }
153
+ // Also clean up old Dir CommandStore entries from v2.3.x SubCommands attempt
154
+ for (const name of ['JuiceEmailDir.View', 'JuiceEmailDir.CopyAll', 'JuiceEmailDir.CopyPick', 'JuiceEmailDir.Pwsh']) {
155
+ regDelete(`${LEGACY_HKCU_CMDSTORE}\\${name}`);
156
+ regDelete(`${LEGACY_HKLM_CMDSTORE}\\${name}`);
157
+ }
153
158
  }
154
159
 
155
160
  // ─── PowerShell 包装 ──────────────────────────────────────────────────────────
156
161
 
157
- /**
158
- * 为交互式命令包一层 PowerShell:
159
- * - 成功:窗口自动关闭
160
- * - 失败:窗口保持开启,显示重新执行的命令
161
- */
162
162
  function wrapInteractive(nodePath, scriptPath, cliArgs) {
163
163
  const node = nodePath.replace(/'/g, "''");
164
164
  const script = scriptPath.replace(/'/g, "''");
165
- // %1 由 Windows Explorer 在 CreateProcess 前展开为实际文件路径
166
165
  const ps = [
167
166
  `& '${node}' '${script}' ${cliArgs}`,
168
167
  `if ($LASTEXITCODE) {`,
@@ -200,115 +199,105 @@ function resolvePwsh() {
200
199
  /**
201
200
  * 向指定容器注册子命令
202
201
  * @param {string} containerPath - 容器完整注册表路径
203
- * @param {'html'|'yaml'} kind - 子命令集合类型
202
+ * @param {'html'|'yaml'|'dir'|'bg'} kind - 子命令集合类型
204
203
  */
205
204
  function registerSubCommands(containerPath, kind, nodePath, scriptPath, iconPath, pwshPath) {
205
+ const node = nodePath.replace(/'/g, "''");
206
+ const script = scriptPath.replace(/'/g, "''");
207
+
208
+ // Shared resource commands (added to all containers)
209
+ function regResourceCmds(dirPrefix) {
210
+ const cd = dirPrefix || '';
211
+
212
+ // 📋 查看可用资源
213
+ const vKey = `${containerPath}\\shell\\JuiceEmail.ViewRes`;
214
+ regAdd(vKey, 'MUIVerb', 'REG_SZ', '📋 查看可用资源');
215
+ regAdd(vKey, 'Icon', 'REG_SZ', iconPath);
216
+ const vPs = `${cd}& '${node}' '${script}' view; Write-Host ''; Read-Host 'Press Enter to close'`;
217
+ regAdd(`${vKey}\\command`, '', 'REG_SZ', `powershell.exe -Command "${vPs}"`);
218
+
219
+ // 📦 拷贝全部资源
220
+ const aKey = `${containerPath}\\shell\\JuiceEmail.CopyAll`;
221
+ regAdd(aKey, 'MUIVerb', 'REG_SZ', '📦 拷贝全部资源');
222
+ regAdd(aKey, 'Icon', 'REG_SZ', iconPath);
223
+ const aPs = `${cd}& '${node}' '${script}' init --all .; Write-Host ''; Read-Host 'Press Enter to close'`;
224
+ regAdd(`${aKey}\\command`, '', 'REG_SZ', `powershell.exe -Command "${aPs}"`);
225
+
226
+ // 📥 选择资源拷贝
227
+ const pKey = `${containerPath}\\shell\\JuiceEmail.CopyPick`;
228
+ regAdd(pKey, 'MUIVerb', 'REG_SZ', '📥 选择资源拷贝');
229
+ regAdd(pKey, 'Icon', 'REG_SZ', iconPath);
230
+ const pPs = `${cd}& '${node}' '${script}' init; Write-Host ''; Read-Host 'Press Enter to close'`;
231
+ regAdd(`${pKey}\\command`, '', 'REG_SZ', `powershell.exe -Command "${pPs}"`);
232
+ }
233
+
206
234
  if (kind === 'html') {
207
- // HTML:作为模板生成
208
235
  const genKey = `${containerPath}\\shell\\${SUBCMDS.generate}`;
209
236
  regAdd(genKey, 'MUIVerb', 'REG_SZ', '📄 作为模板,生成邮件 HTML');
210
237
  regAdd(genKey, 'Icon', 'REG_SZ', iconPath);
211
238
  regAdd(`${genKey}\\command`, '', 'REG_SZ', `"${nodePath}" "${scriptPath}" -f %1`);
212
239
 
213
- // HTML:作为片段拼接
214
240
  const snipKey = `${containerPath}\\shell\\${SUBCMDS.snippet}`;
215
241
  regAdd(snipKey, 'MUIVerb', 'REG_SZ', '🧩 作为片段,拼接邮件 HTML');
216
242
  regAdd(snipKey, 'Icon', 'REG_SZ', iconPath);
217
243
  regAdd(`${snipKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, '-s %1'));
218
- } else {
219
- // YAML:作为配置拼接
244
+
245
+ regResourceCmds(`Set-Location (Split-Path '%1'); `);
246
+
247
+ if (pwshPath) {
248
+ const pwshKey = `${containerPath}\\shell\\${SUBCMDS.pwsh}`;
249
+ regAdd(pwshKey, 'MUIVerb', 'REG_SZ', '📂 打开 PowerShell');
250
+ regAdd(pwshKey, 'Icon', 'REG_SZ', pwshPath);
251
+ regAdd(`${pwshKey}\\command`, '', 'REG_SZ',
252
+ `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath (Split-Path '%1')"`);
253
+ }
254
+ } else if (kind === 'yaml') {
220
255
  const cfgKey = `${containerPath}\\shell\\${SUBCMDS.config}`;
221
256
  regAdd(cfgKey, 'MUIVerb', 'REG_SZ', '⚙️ 作为配置,拼接邮件 HTML');
222
257
  regAdd(cfgKey, 'Icon', 'REG_SZ', iconPath);
223
258
  regAdd(`${cfgKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, '-c %1'));
224
- }
225
-
226
- // Common items for file-type containers (html + yaml)
227
- if (kind === 'html' || kind === 'yaml') {
228
- const viewKey = `${containerPath}\\shell\\${SUBCMDS.viewEdm}`;
229
- regAdd(viewKey, 'MUIVerb', 'REG_SZ', '📦 查看资源列表');
230
- regAdd(viewKey, 'Icon', 'REG_SZ', iconPath);
231
- regAdd(`${viewKey}\\command`, '', 'REG_SZ', wrapWithPause(nodePath, scriptPath, 'view'));
232
-
233
- const viewIntKey = `${containerPath}\\shell\\${SUBCMDS.viewEdmInt}`;
234
- regAdd(viewIntKey, 'MUIVerb', 'REG_SZ', '📋 浏览资源库');
235
- regAdd(viewIntKey, 'Icon', 'REG_SZ', iconPath);
236
- regAdd(`${viewIntKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view -i'));
237
- }
238
-
239
- // pwsh — 始终显示(可选)
240
- if (pwshPath) {
241
- const pwshKey = `${containerPath}\\shell\\${SUBCMDS.pwsh}`;
242
- regAdd(pwshKey, 'MUIVerb', 'REG_SZ', '📂 打开 PowerShell');
243
- regAdd(pwshKey, 'Icon', 'REG_SZ', pwshPath);
244
- regAdd(`${pwshKey}\\command`, '', 'REG_SZ',
245
- `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath (Split-Path '%1')"`);
246
- }
247
- }
248
259
 
249
- // ─── 非交互命令包装(始终暂停以便用户查看输出)─────────────────────────────────
260
+ regResourceCmds(`Set-Location (Split-Path '%1'); `);
250
261
 
251
- function wrapWithPause(nodePath, scriptPath, cliArgs) {
252
- const node = nodePath.replace(/'/g, "''");
253
- const script = scriptPath.replace(/'/g, "''");
254
- const ps = [
255
- `& '${node}' '${script}' ${cliArgs}`,
256
- `Write-Host ''`,
257
- `Read-Host 'Press Enter to close'`,
258
- ].join('; ');
259
- return `powershell.exe -Command "${ps}"`;
260
- }
261
-
262
- // ─── Directory / Background 菜单注册 ──────────────────────────────────────────
263
-
264
- /**
265
- * Register individual shell commands for Directory and Directory\Background.
266
- * ExtendedSubCommandsKey doesn't work reliably for non-file-type associations,
267
- * so we register each item as a standalone shell verb.
268
- */
269
- function registerDirBgMenus(nodePath, scriptPath, iconPath, pwshPath) {
270
- const roots = [
271
- `${HKCU_SHELL}\\Directory\\shell`,
272
- `${HKCU_SHELL}\\Directory\\Background\\shell`,
273
- ];
274
-
275
- // First, clean up old keys from all previous versions to prevent stale entries
276
- for (const root of roots) {
277
- // Old parent key (ExtendedSubCommandsKey approach, v2.x commit 3fc761e)
278
- regDelete(`${root}\\${PARENT_KEY_NAME}`);
279
- // Old individual entries from any prior version
280
- regDelete(`${root}\\JuiceEmail.Init`);
281
- regDelete(`${root}\\JuiceEmail.View`);
282
- regDelete(`${root}\\JuiceEmail.Browse`);
283
- regDelete(`${root}\\JuiceEmail.Pwsh`);
284
- }
262
+ if (pwshPath) {
263
+ const pwshKey = `${containerPath}\\shell\\${SUBCMDS.pwsh}`;
264
+ regAdd(pwshKey, 'MUIVerb', 'REG_SZ', '📂 打开 PowerShell');
265
+ regAdd(pwshKey, 'Icon', 'REG_SZ', pwshPath);
266
+ regAdd(`${pwshKey}\\command`, '', 'REG_SZ',
267
+ `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath (Split-Path '%1')"`);
268
+ }
269
+ } else if (kind === 'dir' || kind === 'bg') {
270
+ // Directory / Background: cd to the target directory first
271
+ const dirRef = kind === 'dir' ? '%1' : '%V';
272
+ const cd = `Set-Location '${dirRef}'; `;
273
+
274
+ // 📋 查看可用资源
275
+ const vKey = `${containerPath}\\shell\\JuiceEmail.ViewRes`;
276
+ regAdd(vKey, 'MUIVerb', 'REG_SZ', '📋 查看可用资源');
277
+ regAdd(vKey, 'Icon', 'REG_SZ', iconPath);
278
+ const vPs = `${cd}& '${node}' '${script}' view; Write-Host ''; Read-Host 'Press Enter to close'`;
279
+ regAdd(`${vKey}\\command`, '', 'REG_SZ', `powershell.exe -Command "${vPs}"`);
280
+
281
+ // 📦 拷贝全部资源
282
+ const aKey = `${containerPath}\\shell\\JuiceEmail.CopyAll`;
283
+ regAdd(aKey, 'MUIVerb', 'REG_SZ', '📦 拷贝全部资源');
284
+ regAdd(aKey, 'Icon', 'REG_SZ', iconPath);
285
+ const aPs = `${cd}& '${node}' '${script}' init --all .; Write-Host ''; Read-Host 'Press Enter to close'`;
286
+ regAdd(`${aKey}\\command`, '', 'REG_SZ', `powershell.exe -Command "${aPs}"`);
287
+
288
+ // 📥 选择资源拷贝
289
+ const pKey = `${containerPath}\\shell\\JuiceEmail.CopyPick`;
290
+ regAdd(pKey, 'MUIVerb', 'REG_SZ', '📥 选择资源拷贝');
291
+ regAdd(pKey, 'Icon', 'REG_SZ', iconPath);
292
+ const pPs = `${cd}& '${node}' '${script}' init; Write-Host ''; Read-Host 'Press Enter to close'`;
293
+ regAdd(`${pKey}\\command`, '', 'REG_SZ', `powershell.exe -Command "${pPs}"`);
285
294
 
286
- for (const root of roots) {
287
- // 📥 从资源库拷贝到此处
288
- const initKey = `${root}\\JuiceEmail.Init`;
289
- regAdd(initKey, 'MUIVerb', 'REG_SZ', '📥 从资源库拷贝到此处');
290
- regAdd(initKey, 'Icon', 'REG_SZ', iconPath);
291
- regAdd(`${initKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'init'));
292
-
293
- // 📦 查看资源列表
294
- const viewKey = `${root}\\JuiceEmail.View`;
295
- regAdd(viewKey, 'MUIVerb', 'REG_SZ', '📦 查看资源列表');
296
- regAdd(viewKey, 'Icon', 'REG_SZ', iconPath);
297
- regAdd(`${viewKey}\\command`, '', 'REG_SZ', wrapWithPause(nodePath, scriptPath, 'view'));
298
-
299
- // 📋 浏览资源库
300
- const browseKey = `${root}\\JuiceEmail.Browse`;
301
- regAdd(browseKey, 'MUIVerb', 'REG_SZ', '📋 浏览资源库');
302
- regAdd(browseKey, 'Icon', 'REG_SZ', iconPath);
303
- regAdd(`${browseKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view -i'));
304
-
305
- // 📂 在此打开终端
306
295
  if (pwshPath) {
307
- const pwshKey = `${root}\\JuiceEmail.Pwsh`;
296
+ const pwshKey = `${containerPath}\\shell\\JuiceEmail.Pwsh`;
308
297
  regAdd(pwshKey, 'MUIVerb', 'REG_SZ', '📂 在此打开终端');
309
298
  regAdd(pwshKey, 'Icon', 'REG_SZ', pwshPath);
310
299
  regAdd(`${pwshKey}\\command`, '', 'REG_SZ',
311
- `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '%V'"`);
300
+ `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '${dirRef}'"`);
312
301
  }
313
302
  }
314
303
  }
@@ -323,7 +312,7 @@ async function registerContextMenu() {
323
312
 
324
313
  console.log(chalk.cyan('\n 注册 juice 右键菜单(当前用户,无需管理员权限)...\n'));
325
314
 
326
- // 清理所有旧版残留,避免新旧方案冲突
315
+ // 清理所有旧版残留
327
316
  cleanLegacyHklmParents();
328
317
  cleanLegacyCommandStore();
329
318
 
@@ -334,22 +323,27 @@ async function registerContextMenu() {
334
323
 
335
324
  let ok = true;
336
325
 
337
- // 先删除旧容器再重建,避免注册残留旧子命令(如旧版 HTML 容器的 WithConfig)
326
+ // 先删除旧容器再重建
338
327
  console.log(chalk.gray(' 清理旧版菜单...'));
339
328
  regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`);
340
329
  regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`);
341
330
  regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`);
342
331
  regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`);
332
+ // Clean up old individual Dir/Bg entries from v2.3.x
333
+ for (const root of [`${HKCU_SHELL}\\Directory\\shell`, `${HKCU_SHELL}\\Directory\\Background\\shell`]) {
334
+ regDelete(`${root}\\${PARENT_KEY_NAME}`);
335
+ for (const suffix of ['Init', 'View', 'Browse', 'Pwsh']) {
336
+ regDelete(`${root}\\JuiceEmail.${suffix}`);
337
+ }
338
+ }
343
339
 
344
- // HTML 容器:generate + snippet + pwsh
340
+ // HTML 容器
345
341
  console.log(chalk.gray(' 注册 .html 文件菜单...'));
346
- const htmlContainer = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`;
347
- registerSubCommands(htmlContainer, 'html', nodePath, scriptPath, iconPath, pwshPath);
342
+ registerSubCommands(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`, 'html', nodePath, scriptPath, iconPath, pwshPath);
348
343
 
349
- // YAML 容器:config + pwsh
344
+ // YAML 容器
350
345
  console.log(chalk.gray(' 注册 .yaml 文件菜单...'));
351
- const yamlContainer = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`;
352
- registerSubCommands(yamlContainer, 'yaml', nodePath, scriptPath, iconPath, pwshPath);
346
+ registerSubCommands(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`, 'yaml', nodePath, scriptPath, iconPath, pwshPath);
353
347
 
354
348
  // .html / .htm → 引用 HTML 容器
355
349
  for (const root of HTML_ROOTS) {
@@ -367,9 +361,22 @@ async function registerContextMenu() {
367
361
  regAdd(parentKey, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_YAML);
368
362
  }
369
363
 
370
- // Directory / Background → 独立容器
364
+ // Directory / Background → 各自独立容器
371
365
  console.log(chalk.gray(' 注册文件夹/空白处菜单...'));
372
- registerDirBgMenus(nodePath, scriptPath, iconPath, pwshPath);
366
+ registerSubCommands(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`, 'dir', nodePath, scriptPath, iconPath, pwshPath);
367
+ registerSubCommands(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`, 'bg', nodePath, scriptPath, iconPath, pwshPath);
368
+
369
+ // Directory → 引用 Dir 容器
370
+ const dirParent = `${HKCU_SHELL}\\Directory\\shell\\${PARENT_KEY_NAME}`;
371
+ ok = regAdd(dirParent, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
372
+ regAdd(dirParent, 'Icon', 'REG_SZ', iconPath);
373
+ regAdd(dirParent, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_DIR);
374
+
375
+ // Directory\Background → 引用 Bg 容器
376
+ const bgParent = `${HKCU_SHELL}\\Directory\\Background\\shell\\${PARENT_KEY_NAME}`;
377
+ ok = regAdd(bgParent, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML') && ok;
378
+ regAdd(bgParent, 'Icon', 'REG_SZ', iconPath);
379
+ regAdd(bgParent, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_BG);
373
380
 
374
381
  if (!ok) {
375
382
  console.log(chalk.yellow(' ⚠ 部分注册表项写入失败,右键菜单可能不完整。\n'));
@@ -382,20 +389,23 @@ async function registerContextMenu() {
382
389
  ` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
383
390
  ` ├── 📄 作为模板,生成邮件 HTML → juice -f(后台执行)\n` +
384
391
  ` ├── 🧩 作为片段,拼接邮件 HTML → juice -s(交互选择模板)\n` +
385
- ` ├── 📦 查看资源列表 → juice view\n` +
386
- ` ├── 📋 浏览资源库 → juice view -i\n` +
392
+ ` ├── 📋 查看可用资源 → juice view\n` +
393
+ ` ├── 📦 拷贝全部资源 → juice init --all\n` +
394
+ ` ├── 📥 选择资源拷贝 → juice init\n` +
387
395
  (pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
388
396
  `\n ${chalk.bold('.yaml / .yml')} 文件右键:\n` +
389
397
  ` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
390
398
  ` ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c(交互选择品牌/模板/片段)\n` +
391
- ` ├── 📦 查看资源列表 → juice view\n` +
392
- ` ├── 📋 浏览资源库 → juice view -i\n` +
399
+ ` ├── 📋 查看可用资源\n` +
400
+ ` ├── 📦 拷贝全部资源\n` +
401
+ ` ├── 📥 选择资源拷贝\n` +
393
402
  (pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
394
403
  `\n ${chalk.bold('文件夹 / 空白处')} 右键:\n` +
395
- ` 📥 从资源库拷贝到此处 → juice init\n` +
396
- ` 📦 查看资源列表 → juice view\n` +
397
- ` 📋 浏览资源库 → juice view -i\n` +
398
- (pwshPath ? ` 📂 在此打开终端\n` : '') +
404
+ ` ${chalk.bold('📧 juice 生成邮件 HTML')}\n` +
405
+ ` ├── 📋 查看可用资源\n` +
406
+ ` ├── 📦 拷贝全部资源\n` +
407
+ ` ├── 📥 选择资源拷贝\n` +
408
+ (pwshPath ? ` └── 📂 在此打开终端\n` : '') +
399
409
  '\n' +
400
410
  chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
401
411
  );
@@ -413,40 +423,32 @@ async function unregisterContextMenu() {
413
423
 
414
424
  let removed = 0;
415
425
 
416
- // 清理 HKCU 父菜单(文件类型)
426
+ // 清理文件类型父菜单
417
427
  const allRoots = [
418
428
  ...HTML_ROOTS,
419
429
  ...YAML_ROOTS,
430
+ `${HKCU_SHELL}\\Directory\\shell`,
431
+ `${HKCU_SHELL}\\Directory\\Background\\shell`,
420
432
  ];
421
433
  for (const root of allRoots) {
422
434
  if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
423
435
  }
424
436
 
425
- // 清理 Directory / Background 独立菜单项 + 旧版父键
426
- const dirBgRoots = [
427
- `${HKCU_SHELL}\\Directory\\shell`,
428
- `${HKCU_SHELL}\\Directory\\Background\\shell`,
429
- ];
430
- const dirBgKeys = ['JuiceEmail.Init', 'JuiceEmail.View', 'JuiceEmail.Browse', 'JuiceEmail.Pwsh',
431
- PARENT_KEY_NAME]; // PARENT_KEY_NAME = 'JuiceEmail' (old ExtendedSubCommandsKey parent)
432
- for (const root of dirBgRoots) {
433
- for (const key of dirBgKeys) {
434
- if (regDelete(`${root}\\${key}`)) removed++;
437
+ // 清理旧版 Dir/Bg 独立条目 (v2.3.x individual entries)
438
+ for (const root of [`${HKCU_SHELL}\\Directory\\shell`, `${HKCU_SHELL}\\Directory\\Background\\shell`]) {
439
+ for (const suffix of ['Init', 'View', 'Browse', 'Pwsh']) {
440
+ if (regDelete(`${root}\\JuiceEmail.${suffix}`)) removed++;
435
441
  }
436
442
  }
437
443
 
438
- // 清理所有子命令容器(含旧版 Dir/Bg 容器)
439
- if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`)) removed++;
440
- if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`)) removed++;
441
- if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`)) removed++;
442
- if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`)) removed++;
443
- if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`)) removed++;
444
- if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`)) removed++;
445
-
446
- // 清理旧版共享容器(v2.1.14 之前只有一个容器)
444
+ // 清理所有容器(含旧版残留)
445
+ for (const container of [SUB_CMDS_CONTAINER_HTML, SUB_CMDS_CONTAINER_YAML, SUB_CMDS_CONTAINER_DIR, SUB_CMDS_CONTAINER_BG]) {
446
+ if (regDelete(`${HKCU_SHELL}\\${container}`)) removed++;
447
+ }
448
+ // 旧版共享容器 (v2.1.14)
447
449
  regDelete(`${HKCU_SHELL}\\JuiceEmail.SubCommands`);
448
450
 
449
- // 清理旧版残留(HKLM 父菜单 + CommandStore)
451
+ // 旧版残留(HKLM 父菜单 + CommandStore)
450
452
  cleanLegacyHklmParents();
451
453
  cleanLegacyCommandStore();
452
454
 
package/src/init.js CHANGED
@@ -71,8 +71,7 @@ function deriveDefaultName(filePath) {
71
71
  async function directCopy(srcPath, cwd) {
72
72
  const resolved = path.resolve(srcPath);
73
73
  if (!fs.existsSync(resolved)) {
74
- console.error(chalk.red(`\n ✘ 文件不存在:${resolved}\n`));
75
- process.exit(1);
74
+ throw new Error(`文件不存在:${resolved}`);
76
75
  }
77
76
 
78
77
  const destName = await promptOutputName(
@@ -231,9 +230,110 @@ async function interactiveInit(edmDir, cwd) {
231
230
 
232
231
  // ─── Main Entry ──────────────────────────────────────────────────────────────
233
232
 
234
- async function runInitMode({ initPath, template, snippet, config }) {
233
+ function copyDir(src, dest) {
234
+ fs.mkdirSync(dest, { recursive: true });
235
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
236
+ const srcPath = path.join(src, entry.name);
237
+ const destPath = path.join(dest, entry.name);
238
+ if (entry.isDirectory()) {
239
+ copyDir(srcPath, destPath);
240
+ } else {
241
+ fs.copyFileSync(srcPath, destPath);
242
+ }
243
+ }
244
+ }
245
+
246
+ function dirSize(dir) {
247
+ let size = 0;
248
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
249
+ const p = path.join(dir, entry.name);
250
+ if (entry.isDirectory()) {
251
+ size += dirSize(p);
252
+ } else {
253
+ size += fs.statSync(p).size;
254
+ }
255
+ }
256
+ return size;
257
+ }
258
+
259
+ function countFiles(dir) {
260
+ let count = 0;
261
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
262
+ if (entry.isDirectory()) {
263
+ count += countFiles(path.join(dir, entry.name));
264
+ } else {
265
+ count++;
266
+ }
267
+ }
268
+ return count;
269
+ }
270
+
271
+ function findNextEdmVersion(targetDir) {
272
+ let v = 1;
273
+ while (true) {
274
+ const candidate = path.join(targetDir, 'edm-v' + v);
275
+ if (!fs.existsSync(candidate)) return candidate;
276
+ v++;
277
+ }
278
+ }
279
+
280
+ async function runInitMode({ initPath, template, snippet, config, all }) {
235
281
  const cwd = process.cwd();
236
282
 
283
+ // --all: copy entire EDM directory
284
+ if (all) {
285
+ let edmDir;
286
+ try {
287
+ edmDir = resolveEdmDir();
288
+ } catch (err) {
289
+ console.error(chalk.red(`\n ✘ ${err.message}\n`));
290
+ process.exit(1);
291
+ }
292
+ const target = all === true ? cwd : path.resolve(all);
293
+ const destDir = path.join(target, 'edm');
294
+
295
+ // Prevent deleting the source when target overlaps
296
+ if (path.resolve(edmDir) === path.resolve(destDir)) {
297
+ console.log(chalk.yellow('目标目录与 EDM 资源库相同,无需拷贝。\n'));
298
+ return;
299
+ }
300
+ console.log(chalk.cyan(`\n 拷贝 EDM 资源库...`));
301
+ console.log(chalk.gray(` 源:${edmDir}`));
302
+ console.log(chalk.gray(` 目标:${destDir}`));
303
+
304
+ if (fs.existsSync(destDir)) {
305
+ const { select } = await import('@inquirer/prompts');
306
+ const vName = findNextEdmVersion(target);
307
+ const action = await select({
308
+ message: `目录 ${destDir} 已存在:`,
309
+ choices: [
310
+ { name: '覆盖', value: 'overwrite' },
311
+ { name: `自动版本号(${path.basename(vName)})`, value: 'version' },
312
+ { name: '取消', value: 'cancel' },
313
+ ],
314
+ });
315
+ if (action === 'cancel') {
316
+ console.log(chalk.gray('已取消。\n'));
317
+ return;
318
+ }
319
+ if (action === 'version') {
320
+ copyDir(edmDir, vName);
321
+ const edmSize = dirSize(vName);
322
+ const fileCount = countFiles(vName);
323
+ console.log(chalk.green(`\n✔ 已拷贝 EDM 资源库 → ${path.basename(vName)}(${fileCount} 个文件,${fmtBytes(edmSize)})\n`));
324
+ return;
325
+ }
326
+ // overwrite: remove and replace
327
+ fs.rmSync(destDir, { recursive: true, force: true });
328
+ }
329
+
330
+ copyDir(edmDir, destDir);
331
+ const edmSize = dirSize(destDir);
332
+ const fileCount = countFiles(destDir);
333
+ console.log(chalk.green(`\n✔ 已拷贝 EDM 资源库(${fileCount} 个文件,${fmtBytes(edmSize)})\n`));
334
+ return;
335
+ }
336
+
237
337
  // Direct copy modes
238
338
  if (template) {
239
339
  await directCopy(template, cwd);
package/src/view.js CHANGED
@@ -12,6 +12,27 @@ function fmtBytes(b) {
12
12
  return b < 1024 ? `${b} B` : `${(b / 1024).toFixed(1)} KB`;
13
13
  }
14
14
 
15
+ /**
16
+ * Copy src to dest, auto-versioning (-v1, -v2, ...) if dest exists.
17
+ * Returns the actual destination path used.
18
+ */
19
+ function copyFileSafe(srcPath, destPath) {
20
+ if (!fs.existsSync(destPath)) {
21
+ fs.copyFileSync(srcPath, destPath);
22
+ return destPath;
23
+ }
24
+ const parsed = path.parse(destPath);
25
+ let v = 1;
26
+ while (true) {
27
+ const alt = path.join(parsed.dir, parsed.name + '-v' + v + parsed.ext);
28
+ if (!fs.existsSync(alt)) {
29
+ fs.copyFileSync(srcPath, alt);
30
+ return alt;
31
+ }
32
+ v++;
33
+ }
34
+ }
35
+
15
36
  // ─── Path Parser ──────────────────────────────────────────────────────────────
16
37
 
17
38
  /**
@@ -327,13 +348,17 @@ async function showCheckbox(title, choices) {
327
348
  * Trigger copy via the init module.
328
349
  */
329
350
  async function copyResource(type, resourcePath, cwd) {
330
- const { runInitMode } = require('./init');
331
- if (type === 'template') {
332
- await runInitMode({ template: resourcePath });
333
- } else if (type === 'snippet') {
334
- await runInitMode({ snippet: resourcePath });
335
- } else if (type === 'config') {
336
- await runInitMode({ config: resourcePath });
351
+ try {
352
+ const { runInitMode } = require('./init');
353
+ if (type === 'template') {
354
+ await runInitMode({ template: resourcePath });
355
+ } else if (type === 'snippet') {
356
+ await runInitMode({ snippet: resourcePath });
357
+ } else if (type === 'config') {
358
+ await runInitMode({ config: resourcePath });
359
+ }
360
+ } catch (err) {
361
+ console.error(chalk.red(` ✘ 拷贝失败:${err.message}`));
337
362
  }
338
363
  }
339
364
 
@@ -551,7 +576,7 @@ async function interactiveBrowse(edmDir, startParsed) {
551
576
  if (versions.length > 0) {
552
577
  const tplPath = versions[0].templatePath;
553
578
  copyItems.push({
554
- name: '📋 模板 HTML',
579
+ name: `📋 模板 HTML(${versions[0].meta.name || versions[0].name})`,
555
580
  value: 'template',
556
581
  description: path.basename(tplPath),
557
582
  checked: true,
@@ -588,7 +613,9 @@ async function interactiveBrowse(edmDir, startParsed) {
588
613
  });
589
614
 
590
615
  if (selected.length > 0) {
591
- const defaultBaseName = `${current.brand}-${current.series}-${current.variant}`;
616
+ // Include template version in default name for clarity
617
+ const versionName = versions.length > 0 ? versions[0].name : 'template';
618
+ const defaultBaseName = `${current.brand}-${versionName}-${current.series}-${current.variant}`;
592
619
  const { promptOutputName: pn } = require('./snippet');
593
620
  const outputBaseName = await pn(defaultBaseName, process.cwd());
594
621
  const cwd = process.cwd();
@@ -596,28 +623,22 @@ async function interactiveBrowse(edmDir, startParsed) {
596
623
 
597
624
  console.log(chalk.green('\n✔ 已拷贝:'));
598
625
  if (selected.includes('template') && versions.length > 0) {
599
- const tplPath = versions[0].templatePath;
600
- const dest = path.join(cwd, outputBaseName + path.extname(tplPath));
601
- fs.copyFileSync(tplPath, dest);
602
- console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
626
+ const src = versions[0].templatePath;
627
+ const dest = path.join(cwd, outputBaseName + path.extname(src));
628
+ const actual = copyFileSafe(src, dest);
629
+ console.log(` ${chalk.cyan('·')} ${cwdRel(actual)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(actual).size) + ')')}`);
603
630
  }
604
631
  if (selected.includes('snippet') && fs.existsSync(snipPath)) {
605
632
  const dest = path.join(cwd, outputBaseName + '-snippet' + path.extname(snipPath));
606
- fs.copyFileSync(snipPath, dest);
607
- console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(片段, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
633
+ const actual = copyFileSafe(snipPath, dest);
634
+ console.log(` ${chalk.cyan('·')} ${cwdRel(actual)} ${chalk.gray('(片段, ' + fmtBytes(fs.statSync(actual).size) + ')')}`);
608
635
  }
609
636
  if (selected.includes('config') && configs.length > 0) {
610
637
  const optimal = configs.find(c => c.isOptimal);
611
638
  const cfgPath = optimal ? optimal.path : configs[0].path;
612
639
  const dest = path.join(cwd, 'juice.yaml');
613
- if (fs.existsSync(dest)) {
614
- const alt = path.join(cwd, 'juice-v1.yaml');
615
- fs.copyFileSync(cfgPath, alt);
616
- console.log(` ${chalk.cyan('·')} ${cwdRel(alt)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(alt).size) + ')')}`);
617
- } else {
618
- fs.copyFileSync(cfgPath, dest);
619
- console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
620
- }
640
+ const actual = copyFileSafe(cfgPath, dest);
641
+ console.log(` ${chalk.cyan('·')} ${cwdRel(actual)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(actual).size) + ')')}`);
621
642
  }
622
643
  console.log();
623
644
  }