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.
- package/edm/elabscience/_meta.yaml +2 -0
- package/edm/elabscience/series/literature/_meta.yaml +2 -0
- package/edm/elabscience/series/literature/default/_meta.yaml +2 -0
- package/edm/elabscience/templates/standard/_meta.yaml +2 -0
- package/edm/procell/_meta.yaml +2 -0
- package/edm/procell/templates/standard/_meta.yaml +2 -0
- package/package.json +1 -1
- package/src/snippet.js +273 -118
- /package/edm/elabscience/{literature → series/literature/default}/juice.yaml +0 -0
- /package/edm/elabscience/{literature → series/literature/default}/snippet.html +0 -0
- /package/edm/elabscience/{favicon.ico → templates/standard/favicon.ico} +0 -0
- /package/edm/elabscience/{elabscience-template.html → templates/standard/template.html} +0 -0
- /package/edm/procell/{favicon.ico → templates/standard/favicon.ico} +0 -0
- /package/edm/procell/{procell-template.html → templates/standard/template.html} +0 -0
package/package.json
CHANGED
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) =>
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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) =>
|
|
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
|
-
|
|
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() &&
|
|
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
|
-
|
|
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
|
|
484
|
+
message: '请选择模板版本:',
|
|
485
|
+
choices,
|
|
315
486
|
});
|
|
316
487
|
}
|
|
317
488
|
|
|
318
|
-
async function
|
|
489
|
+
async function promptSeries(seriesList) {
|
|
319
490
|
const { select } = await import('@inquirer/prompts');
|
|
320
|
-
if (
|
|
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
|
|
504
|
+
message: '请选择片段系列:',
|
|
505
|
+
choices,
|
|
324
506
|
});
|
|
325
507
|
}
|
|
326
508
|
|
|
327
|
-
async function
|
|
509
|
+
async function promptSnippetVariant(variants) {
|
|
328
510
|
const { select } = await import('@inquirer/prompts');
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
const
|
|
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: '
|
|
335
|
-
choices
|
|
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
|
-
|
|
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:
|
|
373
|
-
|
|
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 (
|
|
376
|
-
defaultIdx = choices.length - 1;
|
|
377
|
-
}
|
|
541
|
+
if (c.isOptimal) defaultIdx = choices.length - 1;
|
|
378
542
|
}
|
|
379
543
|
|
|
380
544
|
choices.push(
|
|
381
|
-
{ name: '[自定义]
|
|
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(`
|
|
524
|
-
console.log(`
|
|
525
|
-
console.log(`
|
|
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
|
|
573
|
-
const
|
|
574
|
-
templatePath =
|
|
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
|
|
587
|
-
const configChoice = await
|
|
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
|
-
*
|
|
650
|
-
* 流程:品牌 →
|
|
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
|
|
663
|
-
const
|
|
802
|
+
// 2. 选择模板版本
|
|
803
|
+
const versions = findTemplateVersions(brand.path);
|
|
804
|
+
const version = await promptTemplateVersion(versions);
|
|
664
805
|
|
|
665
|
-
// 3.
|
|
666
|
-
const
|
|
667
|
-
if (
|
|
668
|
-
console.log(chalk.yellow(`\n ⚠ 品牌「${brand.name}」下暂无片段系列。`));
|
|
669
|
-
await copyTemplateToCwd(
|
|
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
|
|
673
|
-
if (
|
|
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.
|
|
676
|
-
const
|
|
677
|
-
if (
|
|
678
|
-
console.log(chalk.yellow(`\n ⚠
|
|
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.
|
|
685
|
-
const
|
|
686
|
-
const configChoice = await
|
|
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 =
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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:
|
|
722
|
-
templatePath:
|
|
876
|
+
snippetPath: snippetPath,
|
|
877
|
+
templatePath: version.templatePath,
|
|
723
878
|
config,
|
|
724
879
|
cwd: process.cwd(),
|
|
725
880
|
outputBaseName,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|