juice-email-cli 2.1.18 → 2.2.0

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.
@@ -0,0 +1,2 @@
1
+ name: "伊莱瑞特"
2
+ description: "Elabscience 品牌邮件模板"
@@ -0,0 +1,2 @@
1
+ name: "文献推广"
2
+ description: "最新研究文献推广,含产品引用表格"
@@ -0,0 +1,2 @@
1
+ name: "标准文献"
2
+ description: "含产品引用表格的标准文献推广布局"
@@ -0,0 +1,2 @@
1
+ name: "标准版"
2
+ description: "通用文献推广及产品推荐邮件模板"
@@ -0,0 +1,2 @@
1
+ name: "普诺赛"
2
+ description: "Procell 品牌邮件模板"
@@ -0,0 +1,2 @@
1
+ name: "标准版"
2
+ description: "通用产品推广邮件模板"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juice-email-cli",
3
- "version": "2.1.18",
3
+ "version": "2.2.0",
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": {
package/src/snippet.js CHANGED
@@ -35,36 +35,183 @@ function resolveEdmDir() {
35
35
  );
36
36
  }
37
37
 
38
+ // ─── 元数据读取 ─────────────────────────────────────────────────────────────────
39
+
40
+ /**
41
+ * 读取目录下的 _meta.yaml,返回 { name, description, series }。
42
+ * 文件不存在时回退为 { name: 目录名, description: "" }。
43
+ */
44
+ function loadMeta(dir) {
45
+ const metaPath = path.join(dir, '_meta.yaml');
46
+ if (fs.existsSync(metaPath)) {
47
+ try {
48
+ const raw = loadYaml(metaPath);
49
+ return {
50
+ name: raw.name || path.basename(dir),
51
+ description: raw.description || "",
52
+ series: raw.series || null,
53
+ };
54
+ } catch (_) {
55
+ // 解析失败时回退
56
+ }
57
+ }
58
+ return { name: path.basename(dir), description: "", series: null };
59
+ }
60
+
61
+ /**
62
+ * 根据版本 _meta.yaml 中的 series.allow / series.block 过滤系列列表。
63
+ * allow 和 block 互斥,同时存在时 allow 优先。
64
+ * 均不配置时返回全部系列。
65
+ */
66
+ function filterSeries(allSeries, versionMeta) {
67
+ if (!versionMeta || !versionMeta.series) return allSeries;
68
+
69
+ const { allow, block } = versionMeta.series;
70
+
71
+ if (allow && Array.isArray(allow)) {
72
+ return allSeries.filter((s) => allow.includes(s.name));
73
+ }
74
+
75
+ if (block && Array.isArray(block)) {
76
+ return allSeries.filter((s) => !block.includes(s.name));
77
+ }
78
+
79
+ return allSeries;
80
+ }
81
+
82
+ // ─── EDM 目录扫描 ───────────────────────────────────────────────────────────────
83
+
38
84
  function findBrands(edmDir) {
39
85
  const entries = fs.readdirSync(edmDir, { withFileTypes: true });
40
86
  const brands = entries
41
87
  .filter((e) => e.isDirectory())
42
- .map((e) => ({ name: e.name, path: path.join(edmDir, e.name) }));
88
+ .map((e) => {
89
+ const dir = path.join(edmDir, e.name);
90
+ const meta = loadMeta(dir);
91
+ return { name: e.name, path: dir, meta };
92
+ });
43
93
  if (brands.length === 0) {
44
94
  throw new Error(`EDM 目录下未找到任何品牌子目录:${edmDir}`);
45
95
  }
46
96
  return brands;
47
97
  }
48
98
 
49
- function findSnippetFolders(brandDir) {
50
- const entries = fs.readdirSync(brandDir, { withFileTypes: true });
51
- return entries
99
+ /**
100
+ * 扫描品牌下的模板版本:templates/<version>/
101
+ * 每个版本返回 { name, path, meta, templatePath }。
102
+ */
103
+ function findTemplateVersions(brandDir) {
104
+ const templatesDir = path.join(brandDir, 'templates');
105
+ if (!fs.existsSync(templatesDir)) {
106
+ throw new Error(`品牌目录下未找到 templates/:${brandDir}`);
107
+ }
108
+
109
+ const entries = fs.readdirSync(templatesDir, { withFileTypes: true });
110
+ const versions = [];
111
+ for (const e of entries) {
112
+ if (!e.isDirectory()) continue;
113
+ const dir = path.join(templatesDir, e.name);
114
+ const meta = loadMeta(dir);
115
+ const htmlFiles = findFiles(dir, /\.html?$/i);
116
+ if (htmlFiles.length === 0) continue;
117
+ versions.push({
118
+ name: e.name,
119
+ path: dir,
120
+ meta,
121
+ templatePath: htmlFiles[0].path,
122
+ });
123
+ }
124
+ if (versions.length === 0) {
125
+ throw new Error(`templates/ 下未找到任何含 .html 模板的版本子目录:${templatesDir}`);
126
+ }
127
+ return versions;
128
+ }
129
+
130
+ /**
131
+ * 扫描品牌下的片段系列:series/<series>/
132
+ * 每个系列返回 { name, path, meta }。
133
+ */
134
+ function findSeriesDirs(brandDir) {
135
+ const seriesDir = path.join(brandDir, 'series');
136
+ if (!fs.existsSync(seriesDir)) return [];
137
+
138
+ const entries = fs.readdirSync(seriesDir, { withFileTypes: true });
139
+ const list = entries
52
140
  .filter((e) => e.isDirectory())
53
- .map((e) => ({ name: e.name, path: path.join(brandDir, e.name) }));
141
+ .map((e) => {
142
+ const dir = path.join(seriesDir, e.name);
143
+ const meta = loadMeta(dir);
144
+ return { name: e.name, path: dir, meta };
145
+ });
146
+
147
+ if (list.length === 0) {
148
+ console.log(chalk.yellow(` ⚠ series/ 下暂无片段系列。`));
149
+ }
150
+ return list;
54
151
  }
55
152
 
56
- function findHtmlFiles(dir) {
153
+ /**
154
+ * 检测系列目录下的片段变体:
155
+ * - 有子目录且含 snippet.html → 多变体模式,返回变体列表
156
+ * - 无子目录、自身有 snippet.html → 单变体模式,自身作为唯一变体
157
+ * 每个变体返回 { name, path, meta, hasSnippet }。
158
+ */
159
+ function findSnippetVariants(seriesDir) {
160
+ const entries = fs.readdirSync(seriesDir, { withFileTypes: true });
161
+ const subDirs = entries.filter((e) => e.isDirectory());
162
+
163
+ if (subDirs.length > 0) {
164
+ // 潜在的多变体模式:检查是否有子目录包含 snippet.html
165
+ const variants = [];
166
+ for (const d of subDirs) {
167
+ const dir = path.join(seriesDir, d.name);
168
+ const snipPath = path.join(dir, 'snippet.html');
169
+ if (fs.existsSync(snipPath)) {
170
+ const meta = loadMeta(dir);
171
+ variants.push({ name: d.name, path: dir, meta, hasSnippet: true });
172
+ }
173
+ }
174
+ if (variants.length > 0) return variants;
175
+ }
176
+
177
+ // 单变体模式:系列目录自身就是变体
178
+ const snipPath = path.join(seriesDir, 'snippet.html');
179
+ if (fs.existsSync(snipPath)) {
180
+ const meta = loadMeta(seriesDir);
181
+ return [{ name: 'default', path: seriesDir, meta, hasSnippet: true }];
182
+ }
183
+
184
+ return [];
185
+ }
186
+
187
+ /**
188
+ * 列出变体目录下所有 YAML 配置,标记 juice.yaml 为最优配对。
189
+ * 返回 { name, path, isOptimal }。
190
+ */
191
+ function findConfigs(variantDir) {
192
+ const yamlFiles = findYamlFiles(variantDir);
193
+ return yamlFiles
194
+ .filter((f) => f.name !== '_meta.yaml' && f.name !== '_meta.yml')
195
+ .map((f) => ({
196
+ name: f.name,
197
+ path: f.path,
198
+ isOptimal: f.name === 'juice.yaml',
199
+ }));
200
+ }
201
+
202
+ function findFiles(dir, regex) {
57
203
  const entries = fs.readdirSync(dir, { withFileTypes: true });
58
204
  return entries
59
- .filter((e) => e.isFile() && /\.html?$/i.test(e.name))
205
+ .filter((e) => e.isFile() && regex.test(e.name))
60
206
  .map((e) => ({ name: e.name, path: path.join(dir, e.name) }));
61
207
  }
62
208
 
209
+ function findHtmlFiles(dir) {
210
+ return findFiles(dir, /\.html?$/i);
211
+ }
212
+
63
213
  function findYamlFiles(dir) {
64
- const entries = fs.readdirSync(dir, { withFileTypes: true });
65
- return entries
66
- .filter((e) => e.isFile() && /\.ya?ml$/i.test(e.name))
67
- .map((e) => ({ name: e.name, path: path.join(dir, e.name) }));
214
+ return findFiles(dir, /\.ya?ml$/i);
68
215
  }
69
216
 
70
217
  /**
@@ -307,79 +454,96 @@ async function assembleSnippet({ snippetPath, templatePath, config, cwd, outputB
307
454
 
308
455
  // ─── 交互式提示 ───────────────────────────────────────────────────────────────
309
456
 
457
+ function fmtChoice(meta, dirName) {
458
+ const display = meta.name || dirName;
459
+ const suffix = meta.name ? ` ${chalk.gray(`(${dirName})`)}` : "";
460
+ return { display: display + suffix, description: meta.description || "" };
461
+ }
462
+
310
463
  async function promptBrand(brands) {
311
464
  const { select } = await import('@inquirer/prompts');
465
+ const choices = brands.map((b) => ({
466
+ name: fmtChoice(b.meta, b.name).display,
467
+ value: b,
468
+ description: b.meta.description || undefined,
469
+ }));
470
+ return select({
471
+ message: '请选择品牌:',
472
+ choices,
473
+ });
474
+ }
475
+
476
+ async function promptTemplateVersion(versions) {
477
+ const { select } = await import('@inquirer/prompts');
478
+ const choices = versions.map((v) => ({
479
+ name: fmtChoice(v.meta, v.name).display,
480
+ value: v,
481
+ description: v.meta.description || undefined,
482
+ }));
312
483
  return select({
313
- message: '请选择品牌/系列:',
314
- choices: brands.map((b) => ({ name: b.name, value: b })),
484
+ message: '请选择模板版本:',
485
+ choices,
315
486
  });
316
487
  }
317
488
 
318
- async function promptSnippetFolder(folders) {
489
+ async function promptSeries(seriesList) {
319
490
  const { select } = await import('@inquirer/prompts');
320
- if (folders.length === 0) return null;
491
+ if (seriesList.length === 0) return null;
492
+
493
+ const choices = seriesList.map((s) => {
494
+ const variants = findSnippetVariants(s.path);
495
+ const extra = variants.length > 0 ? ` — 含 ${variants.length} 个片段变体` : "";
496
+ return {
497
+ name: fmtChoice(s.meta, s.name).display,
498
+ value: s,
499
+ description: (s.meta.description || "") + chalk.dim(extra),
500
+ };
501
+ });
502
+
321
503
  return select({
322
- message: '请选择片段文件夹:',
323
- choices: folders.map((f) => ({ name: f.name, value: f })),
504
+ message: '请选择片段系列:',
505
+ choices,
324
506
  });
325
507
  }
326
508
 
327
- async function promptSnippetFile(htmlFiles) {
509
+ async function promptSnippetVariant(variants) {
328
510
  const { select } = await import('@inquirer/prompts');
329
- if (htmlFiles.length === 0) return null;
330
- const defaultName = 'snippet.html';
331
- const defaultIdx = htmlFiles.findIndex((f) => f.name === defaultName);
511
+ if (variants.length === 0) return null;
512
+
513
+ const choices = variants.map((v) => ({
514
+ name: fmtChoice(v.meta, v.name).display,
515
+ value: v,
516
+ description: v.meta.description || undefined,
517
+ }));
518
+
519
+ const defaultIdx = variants.findIndex((v) => v.name === 'default');
332
520
 
333
521
  return select({
334
- message: '请选择片段 HTML 文件:',
335
- choices: htmlFiles.map((f, i) => ({
336
- name: f.name + (i === defaultIdx ? ' (默认)' : ''),
337
- value: f,
338
- })),
522
+ message: '请选择片段变体:',
523
+ choices,
339
524
  default: defaultIdx >= 0 ? defaultIdx : 0,
340
525
  });
341
526
  }
342
527
 
343
- /**
344
- * 交互模式的配置选择提示
345
- * 优先 CWD 下的配置文件,不存在则回退到片段目录下的,也可手动输入或跳过
346
- */
347
- async function promptConfigForInteractive(snippetDirYamlFiles) {
528
+ async function promptConfig(configs) {
348
529
  const { select, input } = await import('@inquirer/prompts');
349
530
 
350
- // 检查当前工作目录是否有配置文件
351
- const cwdCandidates = [
352
- { path: path.join(process.cwd(), 'juice.yaml'), name: 'juice.yaml' },
353
- { path: path.join(process.cwd(), 'juice.yml'), name: 'juice.yml' },
354
- ];
355
- const cwdConfig = cwdCandidates.find((c) => fs.existsSync(c.path));
356
-
357
531
  const choices = [];
358
532
  let defaultIdx = 0;
359
533
 
360
- if (cwdConfig) {
361
- choices.push({
362
- name: `[当前目录] ${cwdConfig.name} (优先)`,
363
- value: { type: 'file', path: cwdConfig.path, name: cwdConfig.name },
364
- });
365
- }
366
-
367
- // 片段目录下的配置文件
368
- const defaultName = 'juice.yaml';
369
- for (const f of snippetDirYamlFiles) {
370
- const isDefault = f.name === defaultName;
534
+ for (const c of configs) {
371
535
  choices.push({
372
- name: `[片段目录] ${f.name}${isDefault ? ' (默认)' : ''}`,
373
- value: { type: 'file', path: f.path, name: f.name },
536
+ name: c.isOptimal
537
+ ? `${chalk.green('')} ${c.name} ${chalk.green('(最优配对)')}`
538
+ : ` ${c.name}`,
539
+ value: { type: 'file', path: c.path, name: c.name },
374
540
  });
375
- if (isDefault && !cwdConfig) {
376
- defaultIdx = choices.length - 1;
377
- }
541
+ if (c.isOptimal) defaultIdx = choices.length - 1;
378
542
  }
379
543
 
380
544
  choices.push(
381
- { name: '[自定义] 输入路径...', value: { type: 'custom' } },
382
- { name: '[跳过] 不使用项目配置', value: { type: 'skip' } },
545
+ { name: ' [自定义] 输入其他路径...', value: { type: 'custom' } },
546
+ { name: ' [跳过] 不使用项目配置', value: { type: 'skip' } },
383
547
  );
384
548
 
385
549
  const result = await select({
@@ -396,24 +560,6 @@ async function promptConfigForInteractive(snippetDirYamlFiles) {
396
560
  return result;
397
561
  }
398
562
 
399
- async function promptTemplate(templateFiles) {
400
- const { select } = await import('@inquirer/prompts');
401
- if (templateFiles.length === 0) {
402
- throw new Error('品牌目录下未找到模板 HTML 文件。');
403
- }
404
- const templateRe = /-template\.html?$/i;
405
- const defaultIdx = templateFiles.findIndex((f) => templateRe.test(f.name));
406
-
407
- return select({
408
- message: '请选择模板 HTML 文件:',
409
- choices: templateFiles.map((f, i) => ({
410
- name: f.name + (i === defaultIdx ? ' (默认)' : ''),
411
- value: f,
412
- })),
413
- default: defaultIdx >= 0 ? defaultIdx : 0,
414
- });
415
- }
416
-
417
563
  // ─── 输出文件名处理 ───────────────────────────────────────────────────────────
418
564
 
419
565
  /**
@@ -519,10 +665,10 @@ async function promptConfirm(summary) {
519
665
  console.log('\n' + chalk.cyan('═══════════════════════════════════════════'));
520
666
  console.log(chalk.bold(' 片段组装汇总'));
521
667
  console.log(chalk.cyan('═══════════════════════════════════════════'));
522
- console.log(` 品牌: ${chalk.green(summary.brand)}`);
523
- console.log(` 模板 HTML: ${chalk.green(summary.templateFile)}`);
524
- console.log(` 片段文件夹: ${chalk.green(summary.snippetFolder)}`);
525
- console.log(` 片段 HTML: ${chalk.green(summary.snippetFile)}`);
668
+ console.log(` 品牌: ${chalk.green(summary.brandName)} ${chalk.gray(`(${summary.brand})`)}`);
669
+ console.log(` 模板版本: ${chalk.green(summary.versionName)} ${chalk.gray(`(${summary.version})`)}`);
670
+ console.log(` 片段系列: ${chalk.green(summary.seriesName)} ${chalk.gray(`(${summary.series})`)}`);
671
+ console.log(` 片段变体: ${chalk.green(summary.variantName)} ${chalk.gray(`(${summary.variant})`)}`);
526
672
  console.log(` 配置 YAML: ${chalk.green(summary.configFile)}`);
527
673
  console.log(chalk.gray('───────────────────────────────────────────'));
528
674
  console.log(` 输出目录: ${chalk.cyan(summary.outputDir)}`);
@@ -545,7 +691,7 @@ async function promptConfirm(summary) {
545
691
  /**
546
692
  * --snippet 模式:指定了片段 HTML
547
693
  * - 如果同时指定了 -f(模板),直接使用
548
- * - 如果未指定 -f,交互式选择:品牌 → 模板
694
+ * - 如果未指定 -f,交互式选择:品牌 → 模板版本
549
695
  * - 配置文件:自动检测片段目录下的 juice.yaml / juice.yml,-c 可覆盖
550
696
  * - 合并顺序:项目默认 → 用户目录 → 片段目录配置 → CLI -c
551
697
  */
@@ -565,13 +711,13 @@ async function runSnippetMode({ snippet, template, config: cliConfigPath, output
565
711
  process.exit(1);
566
712
  }
567
713
  } else {
568
- // 交互式选择:品牌 → 模板
714
+ // 交互式选择:品牌 → 模板版本
569
715
  const edmDir = resolveEdmDir();
570
716
  const brands = findBrands(edmDir);
571
717
  const brand = await promptBrand(brands);
572
- const templateFiles = findHtmlFiles(brand.path);
573
- const chosen = await promptTemplate(templateFiles);
574
- templatePath = chosen.path;
718
+ const versions = findTemplateVersions(brand.path);
719
+ const version = await promptTemplateVersion(versions);
720
+ templatePath = version.templatePath;
575
721
  }
576
722
 
577
723
  // 配置文件:非交互模式自动检测片段目录,交互模式提示用户选择
@@ -583,8 +729,8 @@ async function runSnippetMode({ snippet, template, config: cliConfigPath, output
583
729
  priorityConfigPath = findLocalConfig(snippetDir);
584
730
  } else {
585
731
  // 交互模式(只有 -s),提示选择配置
586
- const yamlFiles = findYamlFiles(snippetDir);
587
- const configChoice = await promptConfigForInteractive(yamlFiles);
732
+ const configs = findConfigs(snippetDir);
733
+ const configChoice = await promptConfig(configs);
588
734
  priorityConfigPath = (configChoice.type === 'file') ? configChoice.path : null;
589
735
  }
590
736
 
@@ -610,7 +756,6 @@ async function runSnippetMode({ snippet, template, config: cliConfigPath, output
610
756
  let outputBaseName;
611
757
 
612
758
  if (outputName) {
613
- // 命令行指定了 --name,检查冲突,有冲突则自动版本号
614
759
  const conflicts = checkOutputConflicts(defaultBaseName, process.cwd());
615
760
  if (conflicts.length > 0) {
616
761
  outputBaseName = findNextVersion(defaultBaseName, process.cwd());
@@ -621,7 +766,6 @@ async function runSnippetMode({ snippet, template, config: cliConfigPath, output
621
766
  outputBaseName = defaultBaseName;
622
767
  }
623
768
  } else if (template) {
624
- // 非交互模式(--snippet + -f 均指定),自动版本号
625
769
  const conflicts = checkOutputConflicts(defaultBaseName, process.cwd());
626
770
  if (conflicts.length > 0) {
627
771
  outputBaseName = findNextVersion(defaultBaseName, process.cwd());
@@ -632,7 +776,6 @@ async function runSnippetMode({ snippet, template, config: cliConfigPath, output
632
776
  outputBaseName = defaultBaseName;
633
777
  }
634
778
  } else {
635
- // 交互模式(--snippet 单独指定),提示用户输入
636
779
  outputBaseName = await promptOutputName(defaultBaseName, process.cwd());
637
780
  }
638
781
 
@@ -646,10 +789,8 @@ async function runSnippetMode({ snippet, template, config: cliConfigPath, output
646
789
  }
647
790
 
648
791
  /**
649
- * 交互模式:未指定 --snippet,未指定 -f
650
- * 流程:品牌 → 模板片段文件夹片段 HTML → 配置文件 → 汇总确认 → 执行
651
- * 配置文件:优先 CWD,不存在则用片段目录下 YAML,也可手动输入
652
- * 合并顺序:项目默认 → 用户目录 → 项目配置(CWD/片段/手动三选一)→ CLI -c
792
+ * 交互模式(无 --snippet,无 -f)— 方案 B 新结构
793
+ * 流程:品牌 → 模板版本片段系列片段变体 → 配置文件 → 输出名确认 → 执行
653
794
  */
654
795
  async function runInteractiveMode({ config: cliConfigPath }) {
655
796
  const edmDir = resolveEdmDir();
@@ -658,32 +799,43 @@ async function runInteractiveMode({ config: cliConfigPath }) {
658
799
  const brands = findBrands(edmDir);
659
800
  const brand = await promptBrand(brands);
660
801
 
661
- // 2. 选择模板
662
- const templateFiles = findHtmlFiles(brand.path);
663
- const templateChoice = await promptTemplate(templateFiles);
802
+ // 2. 选择模板版本
803
+ const versions = findTemplateVersions(brand.path);
804
+ const version = await promptTemplateVersion(versions);
664
805
 
665
- // 3. 选择片段文件夹
666
- const snippetFolders = findSnippetFolders(brand.path);
667
- if (snippetFolders.length === 0) {
668
- console.log(chalk.yellow(`\n ⚠ 品牌「${brand.name}」下暂无片段系列。`));
669
- await copyTemplateToCwd(templateChoice.path);
806
+ // 3. 选择片段系列(按版本 series.allow/block 过滤)
807
+ const allSeries = findSeriesDirs(brand.path);
808
+ if (allSeries.length === 0) {
809
+ console.log(chalk.yellow(`\n ⚠ 品牌「${brand.meta.name || brand.name}」下暂无片段系列。`));
810
+ await copyTemplateToCwd(version.templatePath);
670
811
  return;
671
812
  }
672
- const folder = await promptSnippetFolder(snippetFolders);
673
- if (!folder) return;
813
+ const filteredSeries = filterSeries(allSeries, version.meta);
814
+ if (filteredSeries.length === 0) {
815
+ console.log(chalk.yellow(`\n ⚠ 版本「${version.meta.name || version.name}」配置的系列过滤后无可用系列。`));
816
+ return;
817
+ }
818
+ const series = await promptSeries(filteredSeries);
819
+ if (!series) return;
674
820
 
675
- // 4. 选择片段 HTML
676
- const htmlFiles = findHtmlFiles(folder.path);
677
- if (htmlFiles.length === 0) {
678
- console.log(chalk.yellow(`\n ⚠ 片段系列「${folder.name}」下暂无 HTML 文件。`));
821
+ // 4. 选择片段变体
822
+ const variants = findSnippetVariants(series.path);
823
+ if (variants.length === 0) {
824
+ console.log(chalk.yellow(`\n ⚠ 系列「${series.meta.name || series.name}」下暂无 snippet.html。`));
825
+ return;
826
+ }
827
+ const variant = await promptSnippetVariant(variants);
828
+ if (!variant) return;
829
+
830
+ const snippetPath = path.join(variant.path, 'snippet.html');
831
+ if (!fs.existsSync(snippetPath)) {
832
+ console.log(chalk.red(`\n ✘ 片段文件不存在:${snippetPath}`));
679
833
  return;
680
834
  }
681
- const snippetFile = await promptSnippetFile(htmlFiles);
682
- if (!snippetFile) return;
683
835
 
684
- // 5. 选择配置文件(优先 CWD,不存在则用片段目录)
685
- const yamlFiles = findYamlFiles(folder.path);
686
- const configChoice = await promptConfigForInteractive(yamlFiles);
836
+ // 5. 选择配置文件
837
+ const configs = findConfigs(variant.path);
838
+ const configChoice = await promptConfig(configs, variant.path);
687
839
 
688
840
  let priorityConfigPath = null;
689
841
  let configFileName = '(跳过)';
@@ -693,17 +845,20 @@ async function runInteractiveMode({ config: cliConfigPath }) {
693
845
  }
694
846
 
695
847
  // 6. 确定输出文件名
696
- const defaultBaseName = path.parse(templateChoice.path).name;
848
+ const defaultBaseName = `${brand.name}-${version.name}-${series.name}-${variant.name}-template`;
697
849
  const outputBaseName = await promptOutputName(defaultBaseName, process.cwd());
698
850
 
699
851
  // 7. 汇总确认
700
852
  const confirmed = await promptConfirm({
853
+ brandName: brand.meta.name || brand.name,
701
854
  brand: brand.name,
702
- templateFile: templateChoice.name,
703
- snippetFolder: folder.name,
704
- snippetFile: snippetFile.name,
855
+ versionName: version.meta.name || version.name,
856
+ version: version.name,
857
+ seriesName: series.meta.name || series.name,
858
+ series: series.name,
859
+ variantName: variant.meta.name || variant.name,
860
+ variant: variant.name,
705
861
  configFile: configFileName,
706
- snippetPath: snippetFile.path,
707
862
  outputDir: process.cwd(),
708
863
  outputBaseName,
709
864
  });
@@ -718,8 +873,8 @@ async function runInteractiveMode({ config: cliConfigPath }) {
718
873
  config._layers = layers;
719
874
 
720
875
  await assembleSnippet({
721
- snippetPath: snippetFile.path,
722
- templatePath: templateChoice.path,
876
+ snippetPath: snippetPath,
877
+ templatePath: version.templatePath,
723
878
  config,
724
879
  cwd: process.cwd(),
725
880
  outputBaseName,