juice-email-cli 2.3.1 → 2.3.3

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
@@ -15,7 +15,9 @@
15
15
  - **双文件输出** —— 普通模式生成 `.output.html` + `.minified.html`
16
16
  - **四文件输出** —— 片段模式生成 `.raw.html` + `.html` + `.output.html` + `.minified.html`
17
17
  - **交互模式** —— 无参数运行,逐步选择品牌、模板、片段、配置
18
- - **Windows 右键菜单** —— 子菜单层级,一键注册/卸载
18
+ - **资源浏览** —— `juice view` 查看 EDM 资源树,`-i` 交互式逐层翻页
19
+ - **资源拷贝** —— `juice init` 将模板/片段/配置拷贝到当前目录
20
+ - **Windows 右键菜单** —— 文件/文件夹/空白处多场景支持,一键注册/卸载
19
21
 
20
22
  ---
21
23
 
@@ -97,6 +99,41 @@ juice
97
99
 
98
100
  流程:选择品牌 → 模板 → 片段文件夹 → 片段 HTML → 配置文件 → 输出文件名 → 确认执行。
99
101
 
102
+ ### 浏览资源(查看 EDM 模板库)
103
+
104
+ ```bash
105
+ # 查看完整资源树
106
+ juice view
107
+
108
+ # 查看指定品牌/系列/变体
109
+ juice view elabscience
110
+ juice view elabscience/literature
111
+
112
+ # 列出所有模板 / 系列 / 片段
113
+ juice view --templates
114
+ juice view --series
115
+ juice view --snippets
116
+
117
+ # 交互式浏览(可上下翻层级,叶节点可直接拷贝)
118
+ juice view -i
119
+ juice view -i elabscience
120
+ ```
121
+
122
+ ### 拷贝资源到当前目录
123
+
124
+ ```bash
125
+ # 交互式选择并多选拷贝
126
+ juice init
127
+
128
+ # 从指定 EDM 路径拷贝
129
+ juice init elabscience/templates/standard
130
+
131
+ # 仅拷贝指定文件
132
+ juice init --template edm/elabscience/templates/standard/template.html
133
+ juice init --snippet edm/elabscience/series/literature/default/snippet.html
134
+ juice init --config edm/elabscience/series/literature/default/juice.yaml
135
+ ```
136
+
100
137
  ### 参数说明
101
138
 
102
139
  | 参数 | 简写 | 说明 |
@@ -116,7 +153,9 @@ juice
116
153
  |------|------|------|----------|
117
154
  | ✓ | * | * | 片段模式(-s 指定片段,-f 可选指定模板) |
118
155
  | ✗ | ✓ | * | 普通模式(生成 .output.html + .minified.html) |
119
- | ✗ | ✗ | * | 交互式片段模式(逐步选择) |
156
+ | ✗ | ✗ | | 交互式片段模式(逐步选择) |
157
+ | — | — | — | `juice view [path]` 浏览资源 / `-i` 交互浏览 |
158
+ | — | — | — | `juice init [path]` 拷贝资源到当前目录 |
120
159
 
121
160
  ---
122
161
 
@@ -312,21 +351,36 @@ edm/
312
351
 
313
352
  ## Windows 右键菜单
314
353
 
315
- 注册后,在 `.html` / `.htm` / `.yaml` / `.yml` 文件上右键可看到统一子菜单(所有文件类型共享):
354
+ ### 文件右键(.html / .htm
316
355
 
317
356
  ```
318
357
  📧 用 juice 生成邮件 HTML
319
358
  ├── 📄 作为模板,生成邮件 HTML → juice -f %1(后台执行)
320
359
  ├── 🧩 作为片段,拼接邮件 HTML → juice -s %1(交互选择模板)
321
- ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c %1(交互选择品牌/模板/片段)
360
+ ├── 📦 查看资源列表 → juice view
361
+ ├── 📋 浏览资源库 → juice view -i
322
362
  └── 📂 打开 PowerShell ← 仅已安装 pwsh 时出现
323
363
  ```
324
364
 
325
- | 子命令 | 说明 |
326
- |--------|------|
327
- | 📄 作为模板,生成邮件 HTML | 将文件当模板处理,CSS 内联 + 变量替换 + 压缩 |
328
- | 🧩 作为片段,拼接邮件 HTML | 将文件当片段,打开终端选择模板后拼接输出 |
329
- | ⚙️ 作为配置,拼接邮件 HTML | 将文件当配置,打开终端选择品牌/模板/片段 |
365
+ ### 文件右键(.yaml / .yml)
366
+
367
+ ```
368
+ 📧 juice 生成邮件 HTML
369
+ ├── ⚙️ 作为配置,拼接邮件 HTML juice -c %1(交互选择品牌/模板/片段)
370
+ ├── 📦 查看资源列表 → juice view
371
+ ├── 📋 浏览资源库 → juice view -i
372
+ └── 📂 打开 PowerShell
373
+ ```
374
+
375
+ ### 文件夹 / 空白处右键
376
+
377
+ ```
378
+ 📧 juice 邮件工具
379
+ ├── 📥 从资源库拷贝到此处 → juice init
380
+ ├── 📦 查看资源列表 → juice view
381
+ ├── 📋 浏览资源库 → juice view -i
382
+ └── 📂 在此打开终端
383
+ ```
330
384
 
331
385
  ```bash
332
386
  juice --install # 注册(当前用户,无需管理员)
@@ -342,21 +396,31 @@ juice --uninstall # 卸载
342
396
  ```
343
397
  juice-cli/
344
398
  ├── bin/
345
- │ └── juice.js # CLI 入口
399
+ │ └── juice.js # CLI 入口(Commander.js)
346
400
  ├── src/
347
401
  │ ├── index.js # 核心逻辑(配置合并、模板处理、双输出)
348
402
  │ ├── snippet.js # 片段组装模式 + 交互式提示
403
+ │ ├── view.js # EDM 资源查看/浏览(juice view)
404
+ │ ├── init.js # EDM 资源拷贝初始化(juice init)
349
405
  │ └── context-menu.js # Windows 右键菜单注册
350
406
  ├── defaults/
351
407
  │ └── juice.yaml # CLI 内置默认配置
352
408
  ├── edm/ # EDM 模板库(npm 发布时包含)
353
409
  │ ├── elabscience/
354
- │ │ ├── elabscience-template.html
355
- │ │ └── literature/
356
- │ │ ├── snippet.html
357
- │ │ └── juice.yaml
410
+ │ │ ├── _meta.yaml
411
+ │ │ ├── templates/
412
+ │ │ │ └── standard/
413
+ │ │ └── template.html
414
+ │ │ └── series/
415
+ │ │ └── literature/
416
+ │ │ └── default/
417
+ │ │ ├── snippet.html
418
+ │ │ └── juice.yaml
358
419
  │ └── procell/
359
- └── procell-template.html
420
+ ├── _meta.yaml
421
+ │ └── templates/
422
+ │ └── standard/
423
+ │ └── template.html
360
424
  ├── icons/
361
425
  │ └── juice-icon.ico # 右键菜单图标
362
426
  ├── scripts/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juice-email-cli",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
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": {
@@ -228,7 +228,7 @@ function registerSubCommands(containerPath, kind, nodePath, scriptPath, iconPath
228
228
  const viewKey = `${containerPath}\\shell\\${SUBCMDS.viewEdm}`;
229
229
  regAdd(viewKey, 'MUIVerb', 'REG_SZ', '📦 查看资源列表');
230
230
  regAdd(viewKey, 'Icon', 'REG_SZ', iconPath);
231
- regAdd(`${viewKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view'));
231
+ regAdd(`${viewKey}\\command`, '', 'REG_SZ', wrapWithPause(nodePath, scriptPath, 'view'));
232
232
 
233
233
  const viewIntKey = `${containerPath}\\shell\\${SUBCMDS.viewEdmInt}`;
234
234
  regAdd(viewIntKey, 'MUIVerb', 'REG_SZ', '📋 浏览资源库');
@@ -246,52 +246,71 @@ function registerSubCommands(containerPath, kind, nodePath, scriptPath, iconPath
246
246
  }
247
247
  }
248
248
 
249
+ // ─── 非交互命令包装(始终暂停以便用户查看输出)─────────────────────────────────
250
+
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
+
249
262
  // ─── Directory / Background 菜单注册 ──────────────────────────────────────────
250
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
+ */
251
269
  function registerDirBgMenus(nodePath, scriptPath, iconPath, pwshPath) {
252
- // Register directory container subcommands
253
- const dirContainer = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`;
254
- const bgContainer = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`;
270
+ const roots = [
271
+ `${HKCU_SHELL}\\Directory\\shell`,
272
+ `${HKCU_SHELL}\\Directory\\Background\\shell`,
273
+ ];
255
274
 
256
- for (const containerPath of [dirContainer, bgContainer]) {
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
+ }
285
+
286
+ for (const root of roots) {
257
287
  // 📥 从资源库拷贝到此处
258
- const initKey = `${containerPath}\\shell\\${SUBCMDS.initEdm}`;
288
+ const initKey = `${root}\\JuiceEmail.Init`;
259
289
  regAdd(initKey, 'MUIVerb', 'REG_SZ', '📥 从资源库拷贝到此处');
260
290
  regAdd(initKey, 'Icon', 'REG_SZ', iconPath);
261
291
  regAdd(`${initKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'init'));
262
292
 
263
293
  // 📦 查看资源列表
264
- const viewKey = `${containerPath}\\shell\\${SUBCMDS.viewEdm}`;
294
+ const viewKey = `${root}\\JuiceEmail.View`;
265
295
  regAdd(viewKey, 'MUIVerb', 'REG_SZ', '📦 查看资源列表');
266
296
  regAdd(viewKey, 'Icon', 'REG_SZ', iconPath);
267
- regAdd(`${viewKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view'));
297
+ regAdd(`${viewKey}\\command`, '', 'REG_SZ', wrapWithPause(nodePath, scriptPath, 'view'));
268
298
 
269
299
  // 📋 浏览资源库
270
- const viewIntKey = `${containerPath}\\shell\\${SUBCMDS.viewEdmInt}`;
271
- regAdd(viewIntKey, 'MUIVerb', 'REG_SZ', '📋 浏览资源库');
272
- regAdd(viewIntKey, 'Icon', 'REG_SZ', iconPath);
273
- regAdd(`${viewIntKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view -i'));
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'));
274
304
 
275
305
  // 📂 在此打开终端
276
306
  if (pwshPath) {
277
- const pwshKey = `${containerPath}\\shell\\${SUBCMDS.pwsh}`;
307
+ const pwshKey = `${root}\\JuiceEmail.Pwsh`;
278
308
  regAdd(pwshKey, 'MUIVerb', 'REG_SZ', '📂 在此打开终端');
279
309
  regAdd(pwshKey, 'Icon', 'REG_SZ', pwshPath);
280
310
  regAdd(`${pwshKey}\\command`, '', 'REG_SZ',
281
- `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '%1'"`);
311
+ `"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '%V'"`);
282
312
  }
283
313
  }
284
-
285
- // Parent keys: Directory and Directory\Background
286
- const dirParent = `${HKCU_SHELL}\\Directory\\shell\\${PARENT_KEY_NAME}`;
287
- regAdd(dirParent, 'MUIVerb', 'REG_SZ', '📧 juice 邮件工具');
288
- regAdd(dirParent, 'Icon', 'REG_SZ', iconPath);
289
- regAdd(dirParent, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_DIR);
290
-
291
- const bgParent = `${HKCU_SHELL}\\Directory\\Background\\shell\\${PARENT_KEY_NAME}`;
292
- regAdd(bgParent, 'MUIVerb', 'REG_SZ', '📧 juice 邮件工具');
293
- regAdd(bgParent, 'Icon', 'REG_SZ', iconPath);
294
- regAdd(bgParent, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_BG);
295
314
  }
296
315
 
297
316
  // ─── 注册 ─────────────────────────────────────────────────────────────────────
@@ -373,11 +392,10 @@ async function registerContextMenu() {
373
392
  ` ├── 📋 浏览资源库 → juice view -i\n` +
374
393
  (pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
375
394
  `\n ${chalk.bold('文件夹 / 空白处')} 右键:\n` +
376
- ` ${chalk.bold('📧 juice 邮件工具')}\n` +
377
- ` ├── 📥 从资源库拷贝到此处 → juice init\n` +
378
- ` ├── 📦 查看资源列表 → juice view\n` +
379
- ` ├── 📋 浏览资源库 → juice view -i\n` +
380
- (pwshPath ? ` └── 📂 在此打开终端\n` : '') +
395
+ ` 📥 从资源库拷贝到此处 → juice init\n` +
396
+ ` 📦 查看资源列表 → juice view\n` +
397
+ ` 📋 浏览资源库 → juice view -i\n` +
398
+ (pwshPath ? ` 📂 在此打开终端\n` : '') +
381
399
  '\n' +
382
400
  chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
383
401
  );
@@ -395,22 +413,35 @@ async function unregisterContextMenu() {
395
413
 
396
414
  let removed = 0;
397
415
 
398
- // 清理 HKCU 父菜单(4 个文件类型 + Directory + Background)
416
+ // 清理 HKCU 父菜单(文件类型)
399
417
  const allRoots = [
400
418
  ...HTML_ROOTS,
401
419
  ...YAML_ROOTS,
402
- `${HKCU_SHELL}\\Directory\\shell`,
403
- `${HKCU_SHELL}\\Directory\\Background\\shell`,
404
420
  ];
405
421
  for (const root of allRoots) {
406
422
  if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
407
423
  }
408
424
 
409
- // 清理所有子命令容器
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++;
435
+ }
436
+ }
437
+
438
+ // 清理所有子命令容器(含旧版 Dir/Bg 容器)
410
439
  if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`)) removed++;
411
440
  if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`)) removed++;
412
441
  if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`)) removed++;
413
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++;
414
445
 
415
446
  // 清理旧版共享容器(v2.1.14 之前只有一个容器)
416
447
  regDelete(`${HKCU_SHELL}\\JuiceEmail.SubCommands`);
package/src/view.js CHANGED
@@ -420,20 +420,6 @@ async function interactiveBrowse(edmDir, startParsed) {
420
420
  description: allSeries.length + ' 个系列',
421
421
  });
422
422
  }
423
-
424
- // Copy options
425
- if (versions.length > 0) {
426
- choices.push({
427
- name: '📥 拷贝模板到当前目录',
428
- value: { action: 'copy-template' },
429
- });
430
- }
431
- if (allSeries.length > 0) {
432
- choices.push({
433
- name: '📥 拷贝配置到当前目录',
434
- value: { action: 'copy-config-brand' },
435
- });
436
- }
437
423
  break;
438
424
  }
439
425
 
@@ -511,27 +497,15 @@ async function interactiveBrowse(edmDir, startParsed) {
511
497
  if (infoLines.length > 0) {
512
498
  console.log(chalk.dim('\n ' + infoLines.join('\n ') + '\n'));
513
499
  }
514
- // Copy actions
515
- if (fs.existsSync(snipPath)) {
516
- choices.push({
517
- name: '📥 拷贝片段到当前目录',
518
- value: { action: 'copy-snippet' },
519
- });
520
- }
521
- if (configs.length > 0) {
522
- choices.push({
523
- name: '📥 拷贝配置到当前目录',
524
- value: { action: 'copy-config' },
525
- });
526
- }
527
- // Also allow copying the template
500
+ // Multi-select copy
501
+ const hasResources = fs.existsSync(snipPath) || configs.length > 0;
528
502
  let versions;
529
503
  const brandDir = path.join(edmDir, current.brand);
530
504
  try { versions = findTemplateVersions(brandDir); } catch (_) { versions = []; }
531
- if (versions.length > 0) {
505
+ if (hasResources || versions.length > 0) {
532
506
  choices.push({
533
- name: '📥 拷贝模板到当前目录',
534
- value: { action: 'copy-template' },
507
+ name: '📥 拷贝到当前目录',
508
+ value: { action: 'copy-multi' },
535
509
  });
536
510
  }
537
511
  break;
@@ -558,125 +532,95 @@ async function interactiveBrowse(edmDir, startParsed) {
558
532
 
559
533
  // Copy actions
560
534
  if (result.action === 'copy-template') {
561
- let tplPath;
562
- if (current.level === 'brand') {
563
- const brandDir = path.join(edmDir, current.brand);
564
- const versions = findTemplateVersions(brandDir);
565
- if (versions.length === 1) {
566
- tplPath = versions[0].templatePath;
567
- } else {
568
- // Let user pick which template version
569
- const tplChoices = versions.map(v => ({
570
- name: formatName(v.meta, v.name) + formatDesc(v.meta),
571
- value: v.templatePath,
572
- }));
573
- tplPath = await showMenu('选择要拷贝的模板版本', tplChoices);
574
- }
575
- } else if (current.templatePath) {
576
- tplPath = current.templatePath;
577
- } else if (current.versionData) {
578
- tplPath = current.versionData.templatePath;
579
- } else {
580
- const brandDir = path.join(edmDir, current.brand);
581
- const versions = findTemplateVersions(brandDir);
582
- if (versions.length === 0) continue;
583
- if (versions.length === 1) {
584
- tplPath = versions[0].templatePath;
585
- } else {
586
- const tplChoices = versions.map(v => ({
587
- name: formatName(v.meta, v.name) + formatDesc(v.meta),
588
- value: v.templatePath,
589
- }));
590
- tplPath = await showMenu('选择要拷贝的模板版本', tplChoices);
591
- }
592
- }
593
- if (tplPath) {
535
+ const v = current.versionData;
536
+ if (v && v.templatePath) {
594
537
  console.log();
595
- await copyResource('template', tplPath, process.cwd());
538
+ await copyResource('template', v.templatePath, process.cwd());
596
539
  console.log();
597
540
  }
598
541
  continue;
599
542
  }
600
543
 
601
- if (result.action === 'copy-snippet') {
544
+ if (result.action === 'copy-multi') {
545
+ const brandDir = path.join(edmDir, current.brand);
546
+ const copyItems = [];
547
+
548
+ // Template
549
+ let versions;
550
+ try { versions = findTemplateVersions(brandDir); } catch (_) { versions = []; }
551
+ if (versions.length > 0) {
552
+ const tplPath = versions[0].templatePath;
553
+ copyItems.push({
554
+ name: '📋 模板 HTML',
555
+ value: 'template',
556
+ description: path.basename(tplPath),
557
+ checked: true,
558
+ });
559
+ }
560
+
561
+ // Snippet
602
562
  const snipPath = path.join(current.variantData.path, 'snippet.html');
603
563
  if (fs.existsSync(snipPath)) {
604
- console.log();
605
- await copyResource('snippet', snipPath, process.cwd());
606
- console.log();
564
+ copyItems.push({
565
+ name: '🧩 片段 HTML',
566
+ value: 'snippet',
567
+ description: path.basename(snipPath),
568
+ checked: true,
569
+ });
607
570
  }
608
- continue;
609
- }
610
571
 
611
- if (result.action === 'copy-config') {
572
+ // Config
612
573
  const configs = findConfigs(current.variantData.path);
613
- if (configs.length === 1) {
614
- console.log();
615
- await copyResource('config', configs[0].path, process.cwd());
616
- console.log();
617
- } else if (configs.length > 1) {
618
- const cfgChoices = configs.map(c => ({
619
- name: c.isOptimal ? c.name + chalk.green(' (最优配对)') : c.name,
620
- value: c.path,
621
- }));
622
- const cfgPath = await showMenu('选择要拷贝的配置文件', cfgChoices);
623
- if (cfgPath) {
624
- console.log();
625
- await copyResource('config', cfgPath, process.cwd());
626
- console.log();
627
- }
628
- }
629
- continue;
630
- }
631
-
632
- if (result.action === 'copy-config-brand') {
633
- // From brand level: pick series → variant → config
634
- const brandDir = path.join(edmDir, current.brand);
635
- const allSeries = findSeriesDirs(brandDir);
636
- if (allSeries.length === 0) continue;
637
-
638
- let series;
639
- if (allSeries.length === 1) {
640
- series = allSeries[0];
641
- } else {
642
- const sChoices = allSeries.map(s => ({
643
- name: formatName(s.meta, s.name) + formatDesc(s.meta),
644
- value: s,
645
- }));
646
- series = await showMenu('选择系列', sChoices);
574
+ if (configs.length > 0) {
575
+ copyItems.push({
576
+ name: '⚙️ 配置文件',
577
+ value: 'config',
578
+ description: configs.map(c => c.name).join(', '),
579
+ checked: true,
580
+ });
647
581
  }
648
- if (!series) continue;
649
582
 
650
- const variants = findSnippetVariants(series.path);
651
- if (variants.length === 0) continue;
652
- let variant;
653
- if (variants.length === 1) {
654
- variant = variants[0];
655
- } else {
656
- const vChoices = variants.map(v => ({
657
- name: formatName(v.meta, v.name) + formatDesc(v.meta),
658
- value: v,
659
- }));
660
- variant = await showMenu('选择变体', vChoices);
661
- }
662
- if (!variant) continue;
583
+ if (copyItems.length > 0) {
584
+ const { checkbox } = await import('@inquirer/prompts');
585
+ const selected = await checkbox({
586
+ message: '选择要拷贝的内容:',
587
+ choices: copyItems,
588
+ });
663
589
 
664
- const configs = findConfigs(variant.path);
665
- if (configs.length === 0) continue;
666
- let cfgPath;
667
- if (configs.length === 1) {
668
- cfgPath = configs[0].path;
669
- } else {
670
- const cfgChoices = configs.map(c => ({
671
- name: c.isOptimal ? c.name + chalk.green(' (最优配对)') : c.name,
672
- value: c.path,
673
- }));
674
- cfgPath = await showMenu('选择配置文件', cfgChoices);
675
- }
676
- if (cfgPath) {
677
- console.log();
678
- await copyResource('config', cfgPath, process.cwd());
679
- console.log();
590
+ if (selected.length > 0) {
591
+ const defaultBaseName = `${current.brand}-${current.series}-${current.variant}`;
592
+ const { promptOutputName: pn } = require('./snippet');
593
+ const outputBaseName = await pn(defaultBaseName, process.cwd());
594
+ const cwd = process.cwd();
595
+ const cwdRel = (p) => './' + path.relative(cwd, p);
596
+
597
+ console.log(chalk.green('\n✔ 已拷贝:'));
598
+ 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) + ')')}`);
603
+ }
604
+ if (selected.includes('snippet') && fs.existsSync(snipPath)) {
605
+ 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) + ')')}`);
608
+ }
609
+ if (selected.includes('config') && configs.length > 0) {
610
+ const optimal = configs.find(c => c.isOptimal);
611
+ const cfgPath = optimal ? optimal.path : configs[0].path;
612
+ 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
+ }
621
+ }
622
+ console.log();
623
+ }
680
624
  }
681
625
  continue;
682
626
  }