juice-email-cli 2.3.4 → 2.3.6
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 +26 -14
- package/bin/juice.js +6 -4
- package/package.json +1 -1
- package/src/context-menu.js +153 -174
- package/src/init.js +209 -122
- package/src/view.js +12 -0
package/README.md
CHANGED
|
@@ -351,34 +351,46 @@ edm/
|
|
|
351
351
|
|
|
352
352
|
## Windows 右键菜单
|
|
353
353
|
|
|
354
|
-
###
|
|
354
|
+
### .html / .htm 文件右键
|
|
355
355
|
|
|
356
356
|
```
|
|
357
357
|
📧 用 juice 生成邮件 HTML
|
|
358
|
-
├── 📄 作为模板,生成邮件 HTML
|
|
359
|
-
├── 🧩 作为片段,拼接邮件 HTML
|
|
360
|
-
├──
|
|
361
|
-
├──
|
|
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
|
-
###
|
|
366
|
+
### .yaml / .yml 文件右键
|
|
366
367
|
|
|
367
368
|
```
|
|
368
369
|
📧 用 juice 生成邮件 HTML
|
|
369
|
-
├── ⚙️ 作为配置,拼接邮件 HTML
|
|
370
|
-
├──
|
|
371
|
-
├──
|
|
370
|
+
├── ⚙️ 作为配置,拼接邮件 HTML → juice -c %1(交互选择品牌/模板/片段)
|
|
371
|
+
├── 📋 查看可用资源
|
|
372
|
+
├── 📦 拷贝全部资源
|
|
373
|
+
├── 📥 选择资源拷贝
|
|
372
374
|
└── 📂 打开 PowerShell
|
|
373
375
|
```
|
|
374
376
|
|
|
375
|
-
###
|
|
377
|
+
### 文件夹右键
|
|
376
378
|
|
|
377
379
|
```
|
|
378
|
-
📧 juice
|
|
379
|
-
├──
|
|
380
|
-
├── 📦
|
|
381
|
-
├──
|
|
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
|
|
73
|
-
├── 🧩 作为片段,拼接邮件 HTML → juice -s
|
|
74
|
-
├──
|
|
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
|
package/package.json
CHANGED
package/src/context-menu.js
CHANGED
|
@@ -6,20 +6,30 @@
|
|
|
6
6
|
* 使用 ExtendedSubCommandsKey 实现级联菜单(参考 WinRAR 和 Windows Defender 的实现)。
|
|
7
7
|
* 子命令存储在 Classes 根下的共享容器中,各文件类型父菜单通过 ExtendedSubCommandsKey 引用。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
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,122 +199,106 @@ 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
|
-
|
|
219
|
-
|
|
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
259
|
|
|
239
|
-
|
|
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
|
-
}
|
|
260
|
+
regResourceCmds(`Set-Location (Split-Path '%1'); `);
|
|
248
261
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
regCmd(DIR_VIEW, '📋 查看可用资源', `& '${node}' '${script}' view`);
|
|
290
|
-
|
|
291
|
-
// CopyAll: copy entire EDM to clicked directory
|
|
292
|
-
regCmd(DIR_COPYALL, '📦 拷贝全部资源', `& '${node}' '${script}' init --all '%V'`);
|
|
293
|
-
|
|
294
|
-
// CopyPick: interactive, cd to directory first so output lands there
|
|
295
|
-
regCmd(DIR_COPYPICK, '📥 选择资源拷贝', `Set-Location '%V'; & '${node}' '${script}' init`);
|
|
296
|
-
|
|
297
|
-
// Pwsh
|
|
298
|
-
if (pwshPath) {
|
|
299
|
-
const pwshKey = `${HKCU_SHELL}\\${DIR_PWSH}`;
|
|
300
|
-
regAdd(pwshKey, 'MUIVerb', 'REG_SZ', '📂 在此打开终端');
|
|
301
|
-
regAdd(pwshKey, 'Icon', 'REG_SZ', pwshPath);
|
|
302
|
-
regAdd(`${pwshKey}\\command`, '', 'REG_SZ',
|
|
303
|
-
`"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '%V'"`);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Build SubCommands string
|
|
307
|
-
const subCmds = [DIR_VIEW, DIR_COPYALL, DIR_COPYPICK];
|
|
308
|
-
if (pwshPath) subCmds.push(DIR_PWSH);
|
|
309
|
-
|
|
310
|
-
// Parent keys: Directory + Directory\Background
|
|
311
|
-
const roots = [
|
|
312
|
-
`${HKCU_SHELL}\\Directory\\shell`,
|
|
313
|
-
`${HKCU_SHELL}\\Directory\\Background\\shell`,
|
|
314
|
-
];
|
|
315
|
-
for (const root of roots) {
|
|
316
|
-
regAdd(`${root}\\${PARENT_KEY_NAME}`, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML');
|
|
317
|
-
regAdd(`${root}\\${PARENT_KEY_NAME}`, 'Icon', 'REG_SZ', iconPath);
|
|
318
|
-
regAdd(`${root}\\${PARENT_KEY_NAME}`, 'SubCommands', 'REG_SZ', subCmds.join(';'));
|
|
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}"`);
|
|
294
|
+
|
|
295
|
+
if (pwshPath) {
|
|
296
|
+
const pwshKey = `${containerPath}\\shell\\JuiceEmail.Pwsh`;
|
|
297
|
+
regAdd(pwshKey, 'MUIVerb', 'REG_SZ', '📂 在此打开终端');
|
|
298
|
+
regAdd(pwshKey, 'Icon', 'REG_SZ', pwshPath);
|
|
299
|
+
regAdd(`${pwshKey}\\command`, '', 'REG_SZ',
|
|
300
|
+
`"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '${dirRef}'"`);
|
|
301
|
+
}
|
|
319
302
|
}
|
|
320
303
|
}
|
|
321
304
|
|
|
@@ -329,7 +312,7 @@ async function registerContextMenu() {
|
|
|
329
312
|
|
|
330
313
|
console.log(chalk.cyan('\n 注册 juice 右键菜单(当前用户,无需管理员权限)...\n'));
|
|
331
314
|
|
|
332
|
-
//
|
|
315
|
+
// 清理所有旧版残留
|
|
333
316
|
cleanLegacyHklmParents();
|
|
334
317
|
cleanLegacyCommandStore();
|
|
335
318
|
|
|
@@ -340,22 +323,27 @@ async function registerContextMenu() {
|
|
|
340
323
|
|
|
341
324
|
let ok = true;
|
|
342
325
|
|
|
343
|
-
//
|
|
326
|
+
// 先删除旧容器再重建
|
|
344
327
|
console.log(chalk.gray(' 清理旧版菜单...'));
|
|
345
328
|
regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`);
|
|
346
329
|
regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`);
|
|
347
330
|
regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`);
|
|
348
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
|
+
}
|
|
349
339
|
|
|
350
|
-
// HTML
|
|
340
|
+
// HTML 容器
|
|
351
341
|
console.log(chalk.gray(' 注册 .html 文件菜单...'));
|
|
352
|
-
|
|
353
|
-
registerSubCommands(htmlContainer, 'html', nodePath, scriptPath, iconPath, pwshPath);
|
|
342
|
+
registerSubCommands(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`, 'html', nodePath, scriptPath, iconPath, pwshPath);
|
|
354
343
|
|
|
355
|
-
// YAML
|
|
344
|
+
// YAML 容器
|
|
356
345
|
console.log(chalk.gray(' 注册 .yaml 文件菜单...'));
|
|
357
|
-
|
|
358
|
-
registerSubCommands(yamlContainer, 'yaml', nodePath, scriptPath, iconPath, pwshPath);
|
|
346
|
+
registerSubCommands(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`, 'yaml', nodePath, scriptPath, iconPath, pwshPath);
|
|
359
347
|
|
|
360
348
|
// .html / .htm → 引用 HTML 容器
|
|
361
349
|
for (const root of HTML_ROOTS) {
|
|
@@ -373,21 +361,22 @@ async function registerContextMenu() {
|
|
|
373
361
|
regAdd(parentKey, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_YAML);
|
|
374
362
|
}
|
|
375
363
|
|
|
376
|
-
// Directory / Background →
|
|
364
|
+
// Directory / Background → 各自独立容器
|
|
377
365
|
console.log(chalk.gray(' 注册文件夹/空白处菜单...'));
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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);
|
|
391
380
|
|
|
392
381
|
if (!ok) {
|
|
393
382
|
console.log(chalk.yellow(' ⚠ 部分注册表项写入失败,右键菜单可能不完整。\n'));
|
|
@@ -400,20 +389,22 @@ async function registerContextMenu() {
|
|
|
400
389
|
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
401
390
|
` ├── 📄 作为模板,生成邮件 HTML → juice -f(后台执行)\n` +
|
|
402
391
|
` ├── 🧩 作为片段,拼接邮件 HTML → juice -s(交互选择模板)\n` +
|
|
403
|
-
` ├──
|
|
404
|
-
` ├──
|
|
392
|
+
` ├── 📋 查看可用资源 → juice view\n` +
|
|
393
|
+
` ├── 📦 拷贝全部资源 → juice init --all\n` +
|
|
394
|
+
` ├── 📥 选择资源拷贝 → juice init\n` +
|
|
405
395
|
(pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
|
|
406
396
|
`\n ${chalk.bold('.yaml / .yml')} 文件右键:\n` +
|
|
407
397
|
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
408
398
|
` ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c(交互选择品牌/模板/片段)\n` +
|
|
409
|
-
` ├──
|
|
410
|
-
` ├──
|
|
399
|
+
` ├── 📋 查看可用资源\n` +
|
|
400
|
+
` ├── 📦 拷贝全部资源\n` +
|
|
401
|
+
` ├── 📥 选择资源拷贝\n` +
|
|
411
402
|
(pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
|
|
412
403
|
`\n ${chalk.bold('文件夹 / 空白处')} 右键:\n` +
|
|
413
404
|
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
414
|
-
` ├── 📋
|
|
415
|
-
` ├── 📦
|
|
416
|
-
` ├── 📥
|
|
405
|
+
` ├── 📋 查看可用资源\n` +
|
|
406
|
+
` ├── 📦 拷贝全部资源\n` +
|
|
407
|
+
` ├── 📥 选择资源拷贝\n` +
|
|
417
408
|
(pwshPath ? ` └── 📂 在此打开终端\n` : '') +
|
|
418
409
|
'\n' +
|
|
419
410
|
chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
|
|
@@ -432,44 +423,32 @@ async function unregisterContextMenu() {
|
|
|
432
423
|
|
|
433
424
|
let removed = 0;
|
|
434
425
|
|
|
435
|
-
//
|
|
426
|
+
// 清理文件类型父菜单
|
|
436
427
|
const allRoots = [
|
|
437
428
|
...HTML_ROOTS,
|
|
438
429
|
...YAML_ROOTS,
|
|
430
|
+
`${HKCU_SHELL}\\Directory\\shell`,
|
|
431
|
+
`${HKCU_SHELL}\\Directory\\Background\\shell`,
|
|
439
432
|
];
|
|
440
433
|
for (const root of allRoots) {
|
|
441
434
|
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
442
435
|
}
|
|
443
436
|
|
|
444
|
-
//
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
];
|
|
449
|
-
// New parent key + old individual entries from v2.3.x
|
|
450
|
-
const dirBgKeys = [PARENT_KEY_NAME, 'JuiceEmail.Init', 'JuiceEmail.View', 'JuiceEmail.Browse', 'JuiceEmail.Pwsh'];
|
|
451
|
-
for (const root of dirBgRoots) {
|
|
452
|
-
for (const key of dirBgKeys) {
|
|
453
|
-
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++;
|
|
454
441
|
}
|
|
455
442
|
}
|
|
456
|
-
// CommandStore entries for Dir menus
|
|
457
|
-
for (const name of ['JuiceEmailDir.View', 'JuiceEmailDir.CopyAll', 'JuiceEmailDir.CopyPick', 'JuiceEmailDir.Pwsh']) {
|
|
458
|
-
if (regDelete(`${HKCU_SHELL}\\${name}`)) removed++;
|
|
459
|
-
}
|
|
460
443
|
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`)) removed++;
|
|
467
|
-
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`)) removed++;
|
|
468
|
-
|
|
469
|
-
// 清理旧版共享容器(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)
|
|
470
449
|
regDelete(`${HKCU_SHELL}\\JuiceEmail.SubCommands`);
|
|
471
450
|
|
|
472
|
-
//
|
|
451
|
+
// 旧版残留(HKLM 父菜单 + CommandStore)
|
|
473
452
|
cleanLegacyHklmParents();
|
|
474
453
|
cleanLegacyCommandStore();
|
|
475
454
|
|
package/src/init.js
CHANGED
|
@@ -6,8 +6,7 @@ const chalk = require('chalk');
|
|
|
6
6
|
const {
|
|
7
7
|
resolveEdmDir, loadMeta, findBrands, findTemplateVersions,
|
|
8
8
|
findSeriesDirs, findSnippetVariants, findConfigs,
|
|
9
|
-
|
|
10
|
-
promptOutputName, checkOutputConflicts, findNextVersion,
|
|
9
|
+
promptOutputName,
|
|
11
10
|
} = require('./snippet');
|
|
12
11
|
|
|
13
12
|
function fmtBytes(b) {
|
|
@@ -90,141 +89,203 @@ async function directCopy(srcPath, cwd) {
|
|
|
90
89
|
|
|
91
90
|
// ─── Interactive Init ────────────────────────────────────────────────────────
|
|
92
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Wrap @inquirer/prompts select with back/exit navigation.
|
|
94
|
+
* Returns the chosen value, 'back', or 'exit'.
|
|
95
|
+
*/
|
|
96
|
+
async function selectWithNav(choices, showBack) {
|
|
97
|
+
const { select } = await import('@inquirer/prompts');
|
|
98
|
+
const all = [];
|
|
99
|
+
if (showBack) all.push({ name: '.. 返回上级', value: 'back' });
|
|
100
|
+
all.push(...choices);
|
|
101
|
+
all.push({ name: '✕ 退出', value: 'exit' });
|
|
102
|
+
return select({ message: choices[0] ? undefined : '', choices: all, loop: false });
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
async function interactiveInit(edmDir, cwd) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
let step = 'brand';
|
|
107
|
+
let brand = null, version = null, series = null, variant = null;
|
|
108
|
+
|
|
109
|
+
while (true) {
|
|
110
|
+
if (step === 'brand') {
|
|
111
|
+
const brands = findBrands(edmDir);
|
|
112
|
+
const choices = brands.map(b => ({
|
|
113
|
+
name: formatName(b.meta, b.name) + (b.meta.description ? ' — ' + chalk.dim(b.meta.description) : ''),
|
|
114
|
+
value: b,
|
|
115
|
+
}));
|
|
116
|
+
const result = await selectWithNav(choices, false);
|
|
117
|
+
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
118
|
+
brand = result;
|
|
119
|
+
step = 'version';
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (step === 'version') {
|
|
124
|
+
const versions = findTemplateVersions(brand.path);
|
|
125
|
+
const choices = versions.map(v => ({
|
|
126
|
+
name: formatName(v.meta, v.name) + (v.meta.description ? ' — ' + chalk.dim(v.meta.description) : ''),
|
|
127
|
+
value: v,
|
|
128
|
+
}));
|
|
129
|
+
const result = await selectWithNav(choices, true);
|
|
130
|
+
if (result === 'back') { step = 'brand'; continue; }
|
|
131
|
+
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
132
|
+
version = result;
|
|
133
|
+
step = 'series';
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (step === 'series') {
|
|
138
|
+
const allSeries = findSeriesDirs(brand.path);
|
|
139
|
+
if (allSeries.length === 0) {
|
|
140
|
+
series = null;
|
|
141
|
+
step = 'copy';
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const choices = [
|
|
145
|
+
{ name: '[跳过] 仅使用模板', value: null },
|
|
146
|
+
...allSeries.map(s => ({
|
|
147
|
+
name: formatName(s.meta, s.name) + (s.meta.description ? ' — ' + chalk.dim(s.meta.description) : ''),
|
|
115
148
|
value: s,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
})),
|
|
150
|
+
];
|
|
151
|
+
const result = await selectWithNav(choices, true);
|
|
152
|
+
if (result === 'back') { step = 'version'; continue; }
|
|
153
|
+
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
154
|
+
series = result;
|
|
155
|
+
step = series ? 'variant' : 'copy';
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (step === 'variant') {
|
|
126
160
|
const variants = findSnippetVariants(series.path);
|
|
127
|
-
if (variants.length
|
|
128
|
-
variant =
|
|
161
|
+
if (variants.length === 0) {
|
|
162
|
+
variant = null;
|
|
163
|
+
step = 'copy';
|
|
164
|
+
continue;
|
|
129
165
|
}
|
|
166
|
+
const choices = variants.map(v => ({
|
|
167
|
+
name: formatName(v.meta, v.name) + (v.meta.description ? ' — ' + chalk.dim(v.meta.description) : ''),
|
|
168
|
+
value: v,
|
|
169
|
+
}));
|
|
170
|
+
const result = await selectWithNav(choices, true);
|
|
171
|
+
if (result === 'back') { step = 'series'; continue; }
|
|
172
|
+
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
173
|
+
variant = result;
|
|
174
|
+
step = 'copy';
|
|
175
|
+
continue;
|
|
130
176
|
}
|
|
131
|
-
}
|
|
132
177
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
name: '📋 模板 HTML',
|
|
137
|
-
value: 'template',
|
|
138
|
-
description: path.basename(version.templatePath),
|
|
139
|
-
checked: true,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
if (variant) {
|
|
143
|
-
const snipPath = path.join(variant.path, 'snippet.html');
|
|
144
|
-
if (fs.existsSync(snipPath)) {
|
|
178
|
+
if (step === 'copy') {
|
|
179
|
+
// Multi-select what to copy
|
|
180
|
+
const copyItems = [];
|
|
145
181
|
copyItems.push({
|
|
146
|
-
name:
|
|
147
|
-
value: '
|
|
148
|
-
description: path.basename(
|
|
182
|
+
name: `📋 模板 HTML(${version.meta.name || version.name})`,
|
|
183
|
+
value: 'template',
|
|
184
|
+
description: path.basename(version.templatePath),
|
|
149
185
|
checked: true,
|
|
150
186
|
});
|
|
151
|
-
}
|
|
152
187
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
if (variant) {
|
|
189
|
+
const snipPath = path.join(variant.path, 'snippet.html');
|
|
190
|
+
if (fs.existsSync(snipPath)) {
|
|
191
|
+
copyItems.push({
|
|
192
|
+
name: '🧩 片段 HTML',
|
|
193
|
+
value: 'snippet',
|
|
194
|
+
description: path.basename(snipPath),
|
|
195
|
+
checked: true,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const configs = findConfigs(variant.path);
|
|
199
|
+
if (configs.length > 0) {
|
|
200
|
+
copyItems.push({
|
|
201
|
+
name: '⚙️ 配置文件',
|
|
202
|
+
value: 'config',
|
|
203
|
+
description: configs.map(c => c.name).join(', '),
|
|
204
|
+
checked: true,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Show summary before multi-select
|
|
210
|
+
console.log(chalk.dim(`\n 品牌:${brand.meta.name || brand.name} → 模板:${version.meta.name || version.name}`));
|
|
211
|
+
if (series) console.log(chalk.dim(` 系列:${series.meta.name || series.name}${variant ? ' → 变体:' + (variant.meta.name || variant.name) : ''}`));
|
|
212
|
+
|
|
213
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
214
|
+
const navCopyItems = [
|
|
215
|
+
...copyItems,
|
|
216
|
+
{ name: '.. 返回上级', value: 'back' },
|
|
217
|
+
{ name: '✕ 退出', value: 'exit' },
|
|
218
|
+
];
|
|
219
|
+
const selected = await checkbox({
|
|
220
|
+
message: '选择要拷贝的内容:',
|
|
221
|
+
choices: navCopyItems,
|
|
160
222
|
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
223
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
224
|
+
// Filter out nav values
|
|
225
|
+
if (selected.includes('exit')) { console.log(chalk.gray('已退出。\n')); return; }
|
|
226
|
+
if (selected.includes('back')) {
|
|
227
|
+
step = series ? 'variant' : 'series';
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
169
230
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
231
|
+
const realSelected = selected.filter(s => s !== 'back' && s !== 'exit');
|
|
232
|
+
if (realSelected.length === 0) {
|
|
233
|
+
console.log(chalk.gray('未选择任何内容,已跳过。\n'));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
174
236
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
237
|
+
// Output name
|
|
238
|
+
const defaultBaseName = variant
|
|
239
|
+
? `${brand.name}-${version.name}-${series.name}-${variant.name}`
|
|
240
|
+
: `${brand.name}-${version.name}`;
|
|
241
|
+
const outputBaseName = await promptOutputName(defaultBaseName, cwd);
|
|
180
242
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
243
|
+
// Copy files
|
|
244
|
+
console.log(chalk.green('\n✔ 已拷贝:'));
|
|
245
|
+
const cwdRel = (p) => './' + path.relative(cwd, p);
|
|
184
246
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
247
|
+
if (realSelected.includes('template')) {
|
|
248
|
+
const destName = outputBaseName + path.extname(version.templatePath);
|
|
249
|
+
const dest = copyFileToCwd(version.templatePath, cwd, destName);
|
|
250
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
251
|
+
// Auto-copy icon
|
|
252
|
+
const icon = copyIcon(variant ? variant.path : null, version.path, brand.path, cwd);
|
|
253
|
+
if (icon) console.log(` ${chalk.cyan('·')} ${cwdRel(icon)} ${chalk.gray('(图标, ' + fmtBytes(fs.statSync(icon).size) + ')')}`);
|
|
254
|
+
}
|
|
190
255
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
256
|
+
if (realSelected.includes('snippet') && variant) {
|
|
257
|
+
const snipPath = path.join(variant.path, 'snippet.html');
|
|
258
|
+
const destName = outputBaseName + '-snippet' + path.extname(snipPath);
|
|
259
|
+
const dest = copyFileToCwd(snipPath, cwd, destName);
|
|
260
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(片段, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
261
|
+
}
|
|
197
262
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const optimal = configs.find(c => c.isOptimal);
|
|
206
|
-
cfgPath = optimal ? optimal.path : configs[0].path;
|
|
207
|
-
}
|
|
208
|
-
const dest = copyFileToCwd(cfgPath, cwd, 'juice.yaml');
|
|
209
|
-
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
210
|
-
}
|
|
263
|
+
if (realSelected.includes('config') && variant) {
|
|
264
|
+
const configs = findConfigs(variant.path);
|
|
265
|
+
const optimal = configs.find(c => c.isOptimal);
|
|
266
|
+
const cfgPath = optimal ? optimal.path : configs[0].path;
|
|
267
|
+
const dest = copyFileToCwd(cfgPath, cwd, 'juice.yaml');
|
|
268
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
269
|
+
}
|
|
211
270
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
271
|
+
console.log();
|
|
272
|
+
if (realSelected.includes('snippet') && realSelected.includes('template')) {
|
|
273
|
+
const snipFile = outputBaseName + '-snippet' + path.extname(path.join(variant.path, 'snippet.html'));
|
|
274
|
+
const tplFile = outputBaseName + path.extname(version.templatePath);
|
|
275
|
+
console.log(
|
|
276
|
+
' ' + chalk.dim('💡 下一步:') + '\n' +
|
|
277
|
+
' ' + chalk.cyan(`juice -s ${snipFile} -f ${tplFile}`) +
|
|
278
|
+
(realSelected.includes('config') ? chalk.cyan(' -c juice.yaml') : '') + '\n'
|
|
279
|
+
);
|
|
280
|
+
} else if (realSelected.includes('template')) {
|
|
281
|
+
const tplFile = outputBaseName + path.extname(version.templatePath);
|
|
282
|
+
console.log(
|
|
283
|
+
' ' + chalk.dim('💡 下一步:') + '\n' +
|
|
284
|
+
' ' + chalk.cyan(`juice -f ${tplFile}`) + '\n'
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
228
289
|
}
|
|
229
290
|
}
|
|
230
291
|
|
|
@@ -268,6 +329,30 @@ function countFiles(dir) {
|
|
|
268
329
|
return count;
|
|
269
330
|
}
|
|
270
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Find and copy favicon.ico alongside the template.
|
|
334
|
+
* Lookup: variant dir → template version dir → brand's first template → skip
|
|
335
|
+
*/
|
|
336
|
+
function copyIcon(variantDir, versionDir, brandDir, cwd) {
|
|
337
|
+
const candidates = [];
|
|
338
|
+
if (variantDir) candidates.push(path.join(variantDir, 'favicon.ico'));
|
|
339
|
+
if (versionDir) candidates.push(path.join(versionDir, 'favicon.ico'));
|
|
340
|
+
// First template version in brand
|
|
341
|
+
try {
|
|
342
|
+
const versions = findTemplateVersions(brandDir);
|
|
343
|
+
if (versions.length > 0) {
|
|
344
|
+
candidates.push(path.join(versions[0].path, 'favicon.ico'));
|
|
345
|
+
}
|
|
346
|
+
} catch (_) {}
|
|
347
|
+
for (const src of candidates) {
|
|
348
|
+
if (fs.existsSync(src)) {
|
|
349
|
+
const dest = copyFileToCwd(src, cwd, 'favicon.ico');
|
|
350
|
+
return dest;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
271
356
|
function findNextEdmVersion(targetDir) {
|
|
272
357
|
let v = 1;
|
|
273
358
|
while (true) {
|
|
@@ -308,12 +393,12 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
308
393
|
message: `目录 ${destDir} 已存在:`,
|
|
309
394
|
choices: [
|
|
310
395
|
{ name: '覆盖', value: 'overwrite' },
|
|
311
|
-
{ name:
|
|
312
|
-
{ name: '
|
|
396
|
+
{ name: `版本(${path.basename(vName)})`, value: 'version' },
|
|
397
|
+
{ name: '跳过', value: 'skip' },
|
|
313
398
|
],
|
|
314
399
|
});
|
|
315
|
-
if (action === '
|
|
316
|
-
console.log(chalk.gray('
|
|
400
|
+
if (action === 'skip') {
|
|
401
|
+
console.log(chalk.gray('已跳过。\n'));
|
|
317
402
|
return;
|
|
318
403
|
}
|
|
319
404
|
if (action === 'version') {
|
|
@@ -409,6 +494,8 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
409
494
|
const destName = outputBaseName + path.extname(tplPath);
|
|
410
495
|
const dest = copyFileToCwd(tplPath, cwd, destName);
|
|
411
496
|
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
497
|
+
const icon = copyIcon(parsed.variantData.path, version.path, brandDir, cwd);
|
|
498
|
+
if (icon) console.log(` ${chalk.cyan('·')} ${cwdRel(icon)} ${chalk.gray('(图标, ' + fmtBytes(fs.statSync(icon).size) + ')')}`);
|
|
412
499
|
}
|
|
413
500
|
if (selected.includes('snippet') && fs.existsSync(snipPath)) {
|
|
414
501
|
const destName = outputBaseName + '-snippet' + path.extname(snipPath);
|
package/src/view.js
CHANGED
|
@@ -627,6 +627,18 @@ async function interactiveBrowse(edmDir, startParsed) {
|
|
|
627
627
|
const dest = path.join(cwd, outputBaseName + path.extname(src));
|
|
628
628
|
const actual = copyFileSafe(src, dest);
|
|
629
629
|
console.log(` ${chalk.cyan('·')} ${cwdRel(actual)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(actual).size) + ')')}`);
|
|
630
|
+
// Auto-copy icon: variant dir → template version dir → first version in brand
|
|
631
|
+
const iconSrcs = [
|
|
632
|
+
path.join(current.variantData.path, 'favicon.ico'),
|
|
633
|
+
path.join(versions[0].path, 'favicon.ico'),
|
|
634
|
+
];
|
|
635
|
+
for (const isrc of iconSrcs) {
|
|
636
|
+
if (fs.existsSync(isrc)) {
|
|
637
|
+
const idest = copyFileSafe(isrc, path.join(cwd, 'favicon.ico'));
|
|
638
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(idest)} ${chalk.gray('(图标, ' + fmtBytes(fs.statSync(idest).size) + ')')}`);
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
630
642
|
}
|
|
631
643
|
if (selected.includes('snippet') && fs.existsSync(snipPath)) {
|
|
632
644
|
const dest = path.join(cwd, outputBaseName + '-snippet' + path.extname(snipPath));
|