juice-email-cli 2.2.1 → 2.3.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/bin/juice.js +40 -1
- package/package.json +1 -1
- package/src/context-menu.js +97 -11
- package/src/init.js +346 -0
- package/src/snippet.js +36 -1
- package/src/view.js +963 -0
package/src/view.js
ADDED
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const {
|
|
7
|
+
resolveEdmDir, loadMeta, findBrands, findTemplateVersions,
|
|
8
|
+
findSeriesDirs, findSnippetVariants, findConfigs,
|
|
9
|
+
} = require('./snippet');
|
|
10
|
+
|
|
11
|
+
function fmtBytes(b) {
|
|
12
|
+
return b < 1024 ? `${b} B` : `${(b / 1024).toFixed(1)} KB`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── Path Parser ──────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a view path like "elabscience/literature/default" or
|
|
19
|
+
* "elabscience/templates/standard" into structured parts.
|
|
20
|
+
*/
|
|
21
|
+
function parseViewPath(rawPath, edmDir) {
|
|
22
|
+
const segments = rawPath.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
23
|
+
if (segments.length === 0) {
|
|
24
|
+
throw new Error('路径不能为空。');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const brandName = segments[0];
|
|
28
|
+
const brandDir = path.join(edmDir, brandName);
|
|
29
|
+
if (!fs.existsSync(brandDir) || !fs.statSync(brandDir).isDirectory()) {
|
|
30
|
+
const brands = fs.readdirSync(edmDir, { withFileTypes: true })
|
|
31
|
+
.filter(e => e.isDirectory()).map(e => e.name);
|
|
32
|
+
throw new Error(
|
|
33
|
+
`品牌「${brandName}」不存在。可用品牌:${brands.join(', ') || '(无)'}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (segments.length === 1) {
|
|
38
|
+
return { type: 'brand', brand: brandName };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (segments[1] === 'templates') {
|
|
42
|
+
if (segments.length < 3) {
|
|
43
|
+
const versions = findTemplateVersions(brandDir);
|
|
44
|
+
throw new Error(
|
|
45
|
+
`请指定模板版本。可用:${versions.map(v => v.name).join(', ')}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const versionDir = path.join(brandDir, 'templates', segments[2]);
|
|
49
|
+
if (!fs.existsSync(versionDir)) {
|
|
50
|
+
const versions = findTemplateVersions(brandDir);
|
|
51
|
+
throw new Error(
|
|
52
|
+
`模板版本「${segments[2]}」不存在。可用:${versions.map(v => v.name).join(', ')}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const versions = findTemplateVersions(brandDir);
|
|
56
|
+
const version = versions.find(v => v.name === segments[2]);
|
|
57
|
+
if (!version) throw new Error(`模板版本「${segments[2]}」下无模板文件。`);
|
|
58
|
+
return { type: 'template', brand: brandName, version: segments[2], versionData: version };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// series path: brand/series[/variant]
|
|
62
|
+
const seriesName = segments[1];
|
|
63
|
+
const allSeries = findSeriesDirs(brandDir);
|
|
64
|
+
const series = allSeries.find(s => s.name === seriesName);
|
|
65
|
+
if (!series) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`系列「${seriesName}」不存在。可用:${allSeries.map(s => s.name).join(', ') || '(无)'}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (segments.length === 2) {
|
|
72
|
+
return { type: 'series', brand: brandName, series: seriesName, seriesData: series };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// variant
|
|
76
|
+
const variants = findSnippetVariants(series.path);
|
|
77
|
+
const variant = variants.find(v => v.name === segments[2]);
|
|
78
|
+
if (!variant) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`变体「${segments[2]}」不存在。可用:${variants.map(v => v.name).join(', ') || '(无)'}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
type: 'variant',
|
|
85
|
+
brand: brandName,
|
|
86
|
+
series: seriesName,
|
|
87
|
+
variant: segments[2],
|
|
88
|
+
seriesData: series,
|
|
89
|
+
variantData: variant,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Non-Interactive Tree Display ─────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function ind(n) { return ' '.repeat(n); }
|
|
96
|
+
|
|
97
|
+
function formatName(meta, dirName) {
|
|
98
|
+
const display = meta.name || dirName;
|
|
99
|
+
return display !== dirName
|
|
100
|
+
? chalk.bold.cyan(display) + ' ' + chalk.gray(`(${dirName})`)
|
|
101
|
+
: chalk.bold.cyan(dirName);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatDesc(meta) {
|
|
105
|
+
return meta.description ? ' ' + chalk.dim('— ' + meta.description) : '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function printBrandTree(edmDir, brand, depth) {
|
|
109
|
+
const d = depth || 0;
|
|
110
|
+
const meta = brand.meta;
|
|
111
|
+
const line = ind(d) + '📦 ' + formatName(meta, brand.name) + formatDesc(meta);
|
|
112
|
+
console.log(line);
|
|
113
|
+
|
|
114
|
+
// Templates
|
|
115
|
+
let versions;
|
|
116
|
+
try {
|
|
117
|
+
versions = findTemplateVersions(brand.path);
|
|
118
|
+
} catch (_) { versions = []; }
|
|
119
|
+
if (versions.length > 0) {
|
|
120
|
+
console.log(ind(d + 1) + '📋 模板');
|
|
121
|
+
for (const v of versions) {
|
|
122
|
+
console.log(ind(d + 2) + '└─ ' + formatName(v.meta, v.name) + formatDesc(v.meta));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Series
|
|
127
|
+
const allSeries = findSeriesDirs(brand.path);
|
|
128
|
+
if (allSeries.length > 0) {
|
|
129
|
+
console.log(ind(d + 1) + '📑 系列');
|
|
130
|
+
for (const s of allSeries) {
|
|
131
|
+
const variants = findSnippetVariants(s.path);
|
|
132
|
+
console.log(ind(d + 2) + '└─ ' + formatName(s.meta, s.name) + formatDesc(s.meta));
|
|
133
|
+
for (const v of variants) {
|
|
134
|
+
const files = [];
|
|
135
|
+
if (fs.existsSync(path.join(v.path, 'snippet.html'))) {
|
|
136
|
+
const stat = fs.statSync(path.join(v.path, 'snippet.html'));
|
|
137
|
+
files.push('📄 snippet.html (' + fmtBytes(stat.size) + ')');
|
|
138
|
+
}
|
|
139
|
+
const configs = findConfigs(v.path);
|
|
140
|
+
if (configs.length > 0) {
|
|
141
|
+
const optimal = configs.find(c => c.isOptimal) || configs[0];
|
|
142
|
+
files.push('⚙️ ' + optimal.name + (optimal.isOptimal ? ' (最优配对)' : ''));
|
|
143
|
+
}
|
|
144
|
+
console.log(ind(d + 3) + '└─ ' + formatName(v.meta, v.name) + formatDesc(v.meta));
|
|
145
|
+
for (const f of files) {
|
|
146
|
+
console.log(ind(d + 4) + '├─ ' + f);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
console.log(ind(d + 1) + chalk.gray('📑 系列 (无)'));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function printFullTree(edmDir) {
|
|
156
|
+
const brands = findBrands(edmDir);
|
|
157
|
+
console.log(chalk.bold('\n📧 EDM 资源总览\n'));
|
|
158
|
+
for (const b of brands) {
|
|
159
|
+
printBrandTree(edmDir, b, 0);
|
|
160
|
+
console.log();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function printSubTree(edmDir, parsed) {
|
|
165
|
+
const brandDir = path.join(edmDir, parsed.brand);
|
|
166
|
+
const brandMeta = loadMeta(brandDir);
|
|
167
|
+
|
|
168
|
+
switch (parsed.type) {
|
|
169
|
+
case 'brand': {
|
|
170
|
+
console.log(chalk.bold(`\n📧 ${brandMeta.name || parsed.brand}${formatDesc(brandMeta)}\n`));
|
|
171
|
+
const brand = { name: parsed.brand, path: brandDir, meta: brandMeta };
|
|
172
|
+
printBrandTree(edmDir, brand, 0);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case 'series': {
|
|
176
|
+
console.log(chalk.bold(`\n📧 ${brandMeta.name || parsed.brand} / ${parsed.seriesData.meta.name || parsed.series}\n`));
|
|
177
|
+
const variants = findSnippetVariants(parsed.seriesData.path);
|
|
178
|
+
console.log(ind(0) + '📑 ' + formatName(parsed.seriesData.meta, parsed.series) + formatDesc(parsed.seriesData.meta));
|
|
179
|
+
for (const v of variants) {
|
|
180
|
+
const files = [];
|
|
181
|
+
if (fs.existsSync(path.join(v.path, 'snippet.html'))) {
|
|
182
|
+
const stat = fs.statSync(path.join(v.path, 'snippet.html'));
|
|
183
|
+
files.push('📄 snippet.html (' + fmtBytes(stat.size) + ')');
|
|
184
|
+
}
|
|
185
|
+
const configs = findConfigs(v.path);
|
|
186
|
+
if (configs.length > 0) {
|
|
187
|
+
const optimal = configs.find(c => c.isOptimal) || configs[0];
|
|
188
|
+
files.push('⚙️ ' + optimal.name + (optimal.isOptimal ? ' (最优配对)' : ''));
|
|
189
|
+
}
|
|
190
|
+
console.log(ind(1) + '└─ ' + formatName(v.meta, v.name) + formatDesc(v.meta));
|
|
191
|
+
for (const f of files) {
|
|
192
|
+
console.log(ind(2) + '├─ ' + f);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
case 'variant': {
|
|
198
|
+
console.log(chalk.bold(`\n📧 ${brandMeta.name || parsed.brand} / ${parsed.seriesData.meta.name || parsed.series} / ${parsed.variantData.meta.name || parsed.variant}\n`));
|
|
199
|
+
const v = parsed.variantData;
|
|
200
|
+
console.log(ind(0) + '📌 ' + formatName(v.meta, parsed.variant) + formatDesc(v.meta));
|
|
201
|
+
if (fs.existsSync(path.join(v.path, 'snippet.html'))) {
|
|
202
|
+
const stat = fs.statSync(path.join(v.path, 'snippet.html'));
|
|
203
|
+
console.log(ind(1) + '├─ 📄 snippet.html (' + fmtBytes(stat.size) + ')');
|
|
204
|
+
}
|
|
205
|
+
const configs = findConfigs(v.path);
|
|
206
|
+
for (const c of configs) {
|
|
207
|
+
const marker = c.isOptimal ? chalk.green(' (最优配对)') : '';
|
|
208
|
+
console.log(ind(1) + '├─ ⚙️ ' + c.name + marker);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case 'template': {
|
|
213
|
+
console.log(chalk.bold(`\n📧 ${brandMeta.name || parsed.brand} / 模板\n`));
|
|
214
|
+
const v = parsed.versionData;
|
|
215
|
+
console.log(ind(0) + '📋 ' + formatName(v.meta, parsed.version) + formatDesc(v.meta));
|
|
216
|
+
const stat = fs.statSync(v.templatePath);
|
|
217
|
+
console.log(ind(1) + '└─ 📄 ' + path.basename(v.templatePath) + ' (' + fmtBytes(stat.size) + ')');
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
console.log();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function printFlatTemplates(edmDir) {
|
|
225
|
+
console.log(chalk.bold('\n📧 所有模板\n'));
|
|
226
|
+
const brands = findBrands(edmDir);
|
|
227
|
+
for (const b of brands) {
|
|
228
|
+
console.log('📦 ' + formatName(b.meta, b.name));
|
|
229
|
+
let versions;
|
|
230
|
+
try {
|
|
231
|
+
versions = findTemplateVersions(b.path);
|
|
232
|
+
} catch (_) { versions = []; }
|
|
233
|
+
if (versions.length === 0) {
|
|
234
|
+
console.log(ind(1) + chalk.gray('(无)'));
|
|
235
|
+
}
|
|
236
|
+
for (const v of versions) {
|
|
237
|
+
const stat = fs.statSync(v.templatePath);
|
|
238
|
+
console.log(ind(1) + '└─ 📋 ' + formatName(v.meta, v.name) + formatDesc(v.meta));
|
|
239
|
+
console.log(ind(2) + chalk.gray(path.basename(v.templatePath) + ' (' + fmtBytes(stat.size) + ')'));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
console.log();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function printFlatSeries(edmDir) {
|
|
246
|
+
console.log(chalk.bold('\n📧 所有系列\n'));
|
|
247
|
+
const brands = findBrands(edmDir);
|
|
248
|
+
for (const b of brands) {
|
|
249
|
+
console.log('📦 ' + formatName(b.meta, b.name));
|
|
250
|
+
const allSeries = findSeriesDirs(b.path);
|
|
251
|
+
if (allSeries.length === 0) {
|
|
252
|
+
console.log(ind(1) + chalk.gray('(无)'));
|
|
253
|
+
}
|
|
254
|
+
for (const s of allSeries) {
|
|
255
|
+
console.log(ind(1) + '└─ 📑 ' + formatName(s.meta, s.name) + formatDesc(s.meta));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
console.log();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function printFlatSnippets(edmDir) {
|
|
262
|
+
console.log(chalk.bold('\n📧 所有片段\n'));
|
|
263
|
+
const brands = findBrands(edmDir);
|
|
264
|
+
for (const b of brands) {
|
|
265
|
+
console.log('📦 ' + formatName(b.meta, b.name));
|
|
266
|
+
const allSeries = findSeriesDirs(b.path);
|
|
267
|
+
let count = 0;
|
|
268
|
+
for (const s of allSeries) {
|
|
269
|
+
const variants = findSnippetVariants(s.path);
|
|
270
|
+
for (const v of variants) {
|
|
271
|
+
count++;
|
|
272
|
+
const relPath = b.name + '/' + s.name + '/' + v.name;
|
|
273
|
+
const filePath = path.join(v.path, 'snippet.html');
|
|
274
|
+
let size = '';
|
|
275
|
+
if (fs.existsSync(filePath)) {
|
|
276
|
+
size = ' ' + chalk.gray('(' + fmtBytes(fs.statSync(filePath).size) + ')');
|
|
277
|
+
}
|
|
278
|
+
console.log(ind(1) + '└─ 🧩 ' + formatName(v.meta, v.name) + (' ' + chalk.dim('(' + relPath + ')') + size + formatDesc(v.meta)));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (count === 0) {
|
|
282
|
+
console.log(ind(1) + chalk.gray('(无)'));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
console.log();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ─── Interactive Browser ─────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Build initial state from a parsed path for interactive browsing.
|
|
292
|
+
*/
|
|
293
|
+
function parsedToStartNode(edmDir, parsed) {
|
|
294
|
+
const brandDir = path.join(edmDir, parsed.brand);
|
|
295
|
+
const brandMeta = loadMeta(brandDir);
|
|
296
|
+
|
|
297
|
+
switch (parsed.type) {
|
|
298
|
+
case 'brand':
|
|
299
|
+
return { level: 'brand', brand: parsed.brand, brandMeta };
|
|
300
|
+
case 'template':
|
|
301
|
+
return { level: 'template-detail', brand: parsed.brand, brandMeta, version: parsed.version, versionData: parsed.versionData };
|
|
302
|
+
case 'series':
|
|
303
|
+
return { level: 'series', brand: parsed.brand, brandMeta, series: parsed.series, seriesData: parsed.seriesData };
|
|
304
|
+
case 'variant':
|
|
305
|
+
return { level: 'variant', brand: parsed.brand, brandMeta, series: parsed.series, seriesData: parsed.seriesData, variant: parsed.variant, variantData: parsed.variantData };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function showMenu(title, choices) {
|
|
310
|
+
const { select } = await import('@inquirer/prompts');
|
|
311
|
+
return select({
|
|
312
|
+
message: title,
|
|
313
|
+
choices,
|
|
314
|
+
loop: false,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function showCheckbox(title, choices) {
|
|
319
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
320
|
+
return checkbox({
|
|
321
|
+
message: title,
|
|
322
|
+
choices,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Trigger copy via the init module.
|
|
328
|
+
*/
|
|
329
|
+
async function copyResource(type, resourcePath, cwd) {
|
|
330
|
+
const { runInitMode } = require('./init');
|
|
331
|
+
if (type === 'template') {
|
|
332
|
+
await runInitMode({ template: resourcePath });
|
|
333
|
+
} else if (type === 'snippet') {
|
|
334
|
+
await runInitMode({ snippet: resourcePath });
|
|
335
|
+
} else if (type === 'config') {
|
|
336
|
+
await runInitMode({ config: resourcePath });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function brandToChoice(b) {
|
|
341
|
+
return {
|
|
342
|
+
name: formatName(b.meta, b.name) + formatDesc(b.meta),
|
|
343
|
+
value: { action: 'navigate', node: { level: 'brand', brand: b.name, brandMeta: b.meta } },
|
|
344
|
+
description: b.meta.description || undefined,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function interactiveBrowse(edmDir, startParsed) {
|
|
349
|
+
let stack = [];
|
|
350
|
+
let current;
|
|
351
|
+
|
|
352
|
+
if (startParsed) {
|
|
353
|
+
// Build stack to the starting node
|
|
354
|
+
if (startParsed.type === 'brand') {
|
|
355
|
+
current = parsedToStartNode(edmDir, startParsed);
|
|
356
|
+
} else if (startParsed.type === 'template') {
|
|
357
|
+
// Stack: brand → template-detail
|
|
358
|
+
const brandNode = { level: 'brand', brand: startParsed.brand, brandMeta: loadMeta(path.join(edmDir, startParsed.brand)) };
|
|
359
|
+
stack = [brandNode];
|
|
360
|
+
current = parsedToStartNode(edmDir, startParsed);
|
|
361
|
+
} else if (startParsed.type === 'series') {
|
|
362
|
+
const brandNode = { level: 'brand', brand: startParsed.brand, brandMeta: loadMeta(path.join(edmDir, startParsed.brand)) };
|
|
363
|
+
stack = [brandNode];
|
|
364
|
+
current = parsedToStartNode(edmDir, startParsed);
|
|
365
|
+
} else if (startParsed.type === 'variant') {
|
|
366
|
+
const brandNode = { level: 'brand', brand: startParsed.brand, brandMeta: loadMeta(path.join(edmDir, startParsed.brand)) };
|
|
367
|
+
const seriesNode = { level: 'series', brand: startParsed.brand, brandMeta: brandNode.brandMeta, series: startParsed.series, seriesData: startParsed.seriesData };
|
|
368
|
+
stack = [brandNode, seriesNode];
|
|
369
|
+
current = parsedToStartNode(edmDir, startParsed);
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
current = { level: 'brands' };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
while (true) {
|
|
376
|
+
// Build choices for current level
|
|
377
|
+
let choices = [];
|
|
378
|
+
let title = '';
|
|
379
|
+
|
|
380
|
+
const hasParent = stack.length > 0;
|
|
381
|
+
const navChoices = [];
|
|
382
|
+
if (hasParent) {
|
|
383
|
+
navChoices.push({ name: '.. 返回上级', value: 'back' });
|
|
384
|
+
}
|
|
385
|
+
navChoices.push({ name: '✕ 退出', value: 'exit' });
|
|
386
|
+
|
|
387
|
+
switch (current.level) {
|
|
388
|
+
case 'brands': {
|
|
389
|
+
title = '选择品牌';
|
|
390
|
+
const brands = findBrands(edmDir);
|
|
391
|
+
for (const b of brands) {
|
|
392
|
+
choices.push(brandToChoice(b));
|
|
393
|
+
}
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
case 'brand': {
|
|
398
|
+
const brandDir = path.join(edmDir, current.brand);
|
|
399
|
+
title = current.brandMeta.name
|
|
400
|
+
? `${current.brandMeta.name} (${current.brand})`
|
|
401
|
+
: current.brand;
|
|
402
|
+
|
|
403
|
+
// Templates
|
|
404
|
+
let versions;
|
|
405
|
+
try { versions = findTemplateVersions(brandDir); } catch (_) { versions = []; }
|
|
406
|
+
if (versions.length > 0) {
|
|
407
|
+
choices.push({
|
|
408
|
+
name: '📋 查看模板',
|
|
409
|
+
value: { action: 'navigate', node: { level: 'templates', brand: current.brand, brandMeta: current.brandMeta } },
|
|
410
|
+
description: versions.length + ' 个版本',
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Series
|
|
415
|
+
const allSeries = findSeriesDirs(brandDir);
|
|
416
|
+
if (allSeries.length > 0) {
|
|
417
|
+
choices.push({
|
|
418
|
+
name: '📑 查看系列',
|
|
419
|
+
value: { action: 'navigate', node: { level: 'series-list', brand: current.brand, brandMeta: current.brandMeta } },
|
|
420
|
+
description: allSeries.length + ' 个系列',
|
|
421
|
+
});
|
|
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
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
case 'templates': {
|
|
441
|
+
const brandDir = path.join(edmDir, current.brand);
|
|
442
|
+
title = `${current.brandMeta.name || current.brand} / 模板`;
|
|
443
|
+
const versions = findTemplateVersions(brandDir);
|
|
444
|
+
for (const v of versions) {
|
|
445
|
+
choices.push({
|
|
446
|
+
name: formatName(v.meta, v.name) + formatDesc(v.meta),
|
|
447
|
+
value: { action: 'navigate', node: { level: 'template-detail', brand: current.brand, brandMeta: current.brandMeta, version: v.name, versionData: v } },
|
|
448
|
+
description: v.meta.description || undefined,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
case 'template-detail': {
|
|
455
|
+
const v = current.versionData;
|
|
456
|
+
title = `${v.meta.name || current.version} 模板`;
|
|
457
|
+
const stat = fs.statSync(v.templatePath);
|
|
458
|
+
// Show info as description, not in choices
|
|
459
|
+
console.log(chalk.dim(`\n ${path.basename(v.templatePath)} (${fmtBytes(stat.size)})\n`));
|
|
460
|
+
choices.push({
|
|
461
|
+
name: '📥 拷贝模板到当前目录',
|
|
462
|
+
value: { action: 'copy-template' },
|
|
463
|
+
});
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
case 'series-list': {
|
|
468
|
+
const brandDir = path.join(edmDir, current.brand);
|
|
469
|
+
title = `${current.brandMeta.name || current.brand} / 系列`;
|
|
470
|
+
const allSeries = findSeriesDirs(brandDir);
|
|
471
|
+
for (const s of allSeries) {
|
|
472
|
+
const variants = findSnippetVariants(s.path);
|
|
473
|
+
choices.push({
|
|
474
|
+
name: formatName(s.meta, s.name) + formatDesc(s.meta),
|
|
475
|
+
value: { action: 'navigate', node: { level: 'series', brand: current.brand, brandMeta: current.brandMeta, series: s.name, seriesData: s } },
|
|
476
|
+
description: (s.meta.description || '') + ` — ${variants.length} 个变体`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
case 'series': {
|
|
483
|
+
const s = current.seriesData;
|
|
484
|
+
title = `${current.brandMeta.name || current.brand} / ${s.meta.name || current.series}`;
|
|
485
|
+
const variants = findSnippetVariants(s.path);
|
|
486
|
+
for (const v of variants) {
|
|
487
|
+
choices.push({
|
|
488
|
+
name: formatName(v.meta, v.name) + formatDesc(v.meta),
|
|
489
|
+
value: { action: 'navigate', node: { level: 'variant', brand: current.brand, brandMeta: current.brandMeta, series: current.series, seriesData: s, variant: v.name, variantData: v } },
|
|
490
|
+
description: v.meta.description || undefined,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
case 'variant': {
|
|
497
|
+
const v = current.variantData;
|
|
498
|
+
title = `${v.meta.name || current.variant} 变体`;
|
|
499
|
+
// Show file info via console.log before the prompt
|
|
500
|
+
const snipPath = path.join(v.path, 'snippet.html');
|
|
501
|
+
let infoLines = [];
|
|
502
|
+
if (fs.existsSync(snipPath)) {
|
|
503
|
+
const stat = fs.statSync(snipPath);
|
|
504
|
+
infoLines.push('📄 snippet.html (' + fmtBytes(stat.size) + ')');
|
|
505
|
+
}
|
|
506
|
+
const configs = findConfigs(v.path);
|
|
507
|
+
for (const c of configs) {
|
|
508
|
+
const marker = c.isOptimal ? ' (最优配对)' : '';
|
|
509
|
+
infoLines.push('⚙️ ' + c.name + marker);
|
|
510
|
+
}
|
|
511
|
+
if (infoLines.length > 0) {
|
|
512
|
+
console.log(chalk.dim('\n ' + infoLines.join('\n ') + '\n'));
|
|
513
|
+
}
|
|
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
|
|
528
|
+
let versions;
|
|
529
|
+
const brandDir = path.join(edmDir, current.brand);
|
|
530
|
+
try { versions = findTemplateVersions(brandDir); } catch (_) { versions = []; }
|
|
531
|
+
if (versions.length > 0) {
|
|
532
|
+
choices.push({
|
|
533
|
+
name: '📥 拷贝模板到当前目录',
|
|
534
|
+
value: { action: 'copy-template' },
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (choices.length === 0) {
|
|
542
|
+
console.log(chalk.yellow(' 该层级无可用内容。'));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const allChoices = [...choices, ...(navChoices.length > 0 ? [new (await import('@inquirer/prompts')).Separator()] : []), ...navChoices];
|
|
546
|
+
const result = await showMenu(title, allChoices);
|
|
547
|
+
|
|
548
|
+
if (result === 'exit' || !result) break;
|
|
549
|
+
if (result === 'back') {
|
|
550
|
+
current = stack.pop();
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (result.action === 'navigate') {
|
|
554
|
+
stack.push(current);
|
|
555
|
+
current = result.node;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Copy actions
|
|
560
|
+
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) {
|
|
594
|
+
console.log();
|
|
595
|
+
await copyResource('template', tplPath, process.cwd());
|
|
596
|
+
console.log();
|
|
597
|
+
}
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (result.action === 'copy-snippet') {
|
|
602
|
+
const snipPath = path.join(current.variantData.path, 'snippet.html');
|
|
603
|
+
if (fs.existsSync(snipPath)) {
|
|
604
|
+
console.log();
|
|
605
|
+
await copyResource('snippet', snipPath, process.cwd());
|
|
606
|
+
console.log();
|
|
607
|
+
}
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (result.action === 'copy-config') {
|
|
612
|
+
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);
|
|
647
|
+
}
|
|
648
|
+
if (!series) continue;
|
|
649
|
+
|
|
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;
|
|
663
|
+
|
|
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();
|
|
680
|
+
}
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ─── Internal helpers that init.js needs ──────────────────────────────────────
|
|
687
|
+
// parseViewPath is used by init.js to resolve <brand>/templates/<version> etc.
|
|
688
|
+
|
|
689
|
+
// ─── Main Entry ──────────────────────────────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
async function runViewMode({ viewPath, interactive, scope }) {
|
|
692
|
+
let edmDir;
|
|
693
|
+
try {
|
|
694
|
+
edmDir = resolveEdmDir();
|
|
695
|
+
} catch (err) {
|
|
696
|
+
console.error(chalk.red(`\n ✘ ${err.message}\n`));
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (interactive) {
|
|
701
|
+
let startParsed = null;
|
|
702
|
+
if (scope === 'templates') {
|
|
703
|
+
// Start from a flat list: let user pick any template across all brands
|
|
704
|
+
const brands = findBrands(edmDir);
|
|
705
|
+
const allTemplates = [];
|
|
706
|
+
for (const b of brands) {
|
|
707
|
+
let versions;
|
|
708
|
+
try { versions = findTemplateVersions(b.path); } catch (_) { versions = []; }
|
|
709
|
+
for (const v of versions) {
|
|
710
|
+
allTemplates.push({ brand: b, version: v });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (allTemplates.length === 0) {
|
|
714
|
+
console.log(chalk.yellow('没有可用的模板。\n'));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const tplChoices = allTemplates.map(t => ({
|
|
718
|
+
name: t.brand.meta.name
|
|
719
|
+
? `${chalk.bold.cyan(t.brand.meta.name)} ${chalk.gray('(' + t.brand.name + ')')} / ${formatName(t.version.meta, t.version.name)}`
|
|
720
|
+
: formatName(t.brand.meta, t.brand.name) + ' / ' + formatName(t.version.meta, t.version.name),
|
|
721
|
+
value: t,
|
|
722
|
+
description: (t.version.meta.description || t.brand.meta.description || undefined),
|
|
723
|
+
}));
|
|
724
|
+
const picked = await showMenu('选择模板', tplChoices);
|
|
725
|
+
if (!picked) return;
|
|
726
|
+
// Show detail and allow copy
|
|
727
|
+
const stat = fs.statSync(picked.version.templatePath);
|
|
728
|
+
console.log(chalk.dim(`\n ${path.basename(picked.version.templatePath)} (${fmtBytes(stat.size)})\n`));
|
|
729
|
+
const { select } = await import('@inquirer/prompts');
|
|
730
|
+
const action = await select({
|
|
731
|
+
message: `${picked.version.meta.name || picked.version.name} 模板`,
|
|
732
|
+
choices: [
|
|
733
|
+
{ name: '📥 拷贝模板到当前目录', value: 'copy' },
|
|
734
|
+
{ name: '✕ 退出', value: 'exit' },
|
|
735
|
+
],
|
|
736
|
+
});
|
|
737
|
+
if (action === 'copy') {
|
|
738
|
+
console.log();
|
|
739
|
+
const { runInitMode } = require('./init');
|
|
740
|
+
await runInitMode({ template: picked.version.templatePath });
|
|
741
|
+
console.log();
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (scope === 'series') {
|
|
747
|
+
const brands = findBrands(edmDir);
|
|
748
|
+
const allSeries = [];
|
|
749
|
+
for (const b of brands) {
|
|
750
|
+
const sList = findSeriesDirs(b.path);
|
|
751
|
+
for (const s of sList) {
|
|
752
|
+
allSeries.push({ brand: b, series: s });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (allSeries.length === 0) {
|
|
756
|
+
console.log(chalk.yellow('没有可用的系列。\n'));
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const sChoices = allSeries.map(item => ({
|
|
760
|
+
name: item.brand.meta.name
|
|
761
|
+
? `${chalk.bold.cyan(item.brand.meta.name)} ${chalk.gray('(' + item.brand.name + ')')} / ${formatName(item.series.meta, item.series.name)}`
|
|
762
|
+
: formatName(item.brand.meta, item.brand.name) + ' / ' + formatName(item.series.meta, item.series.name),
|
|
763
|
+
value: item,
|
|
764
|
+
description: item.series.meta.description || undefined,
|
|
765
|
+
}));
|
|
766
|
+
const picked = await showMenu('选择系列', sChoices);
|
|
767
|
+
if (!picked) return;
|
|
768
|
+
// Show variants and copy actions inline
|
|
769
|
+
const variants = findSnippetVariants(picked.series.path);
|
|
770
|
+
if (variants.length === 0) {
|
|
771
|
+
console.log(chalk.yellow(' 该系列下无变体。\n'));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
let variant;
|
|
775
|
+
if (variants.length === 1) {
|
|
776
|
+
variant = variants[0];
|
|
777
|
+
} else {
|
|
778
|
+
const vChoices = variants.map(v => ({
|
|
779
|
+
name: formatName(v.meta, v.name) + formatDesc(v.meta),
|
|
780
|
+
value: v,
|
|
781
|
+
description: v.meta.description || undefined,
|
|
782
|
+
}));
|
|
783
|
+
variant = await showMenu(
|
|
784
|
+
`${picked.series.meta.name || picked.series.name} / 选择变体`,
|
|
785
|
+
vChoices
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
if (!variant) return;
|
|
789
|
+
|
|
790
|
+
// Show variant detail
|
|
791
|
+
const snipPath = path.join(variant.path, 'snippet.html');
|
|
792
|
+
if (fs.existsSync(snipPath)) {
|
|
793
|
+
const stat = fs.statSync(snipPath);
|
|
794
|
+
console.log(chalk.dim(`\n 📄 snippet.html (${fmtBytes(stat.size)})`));
|
|
795
|
+
}
|
|
796
|
+
const configs = findConfigs(variant.path);
|
|
797
|
+
for (const c of configs) {
|
|
798
|
+
console.log(chalk.dim(` ⚙️ ${c.name}${c.isOptimal ? ' (最优配对)' : ''}`));
|
|
799
|
+
}
|
|
800
|
+
console.log();
|
|
801
|
+
|
|
802
|
+
// Offer copy actions
|
|
803
|
+
const actions = [];
|
|
804
|
+
if (fs.existsSync(snipPath)) {
|
|
805
|
+
actions.push({ name: '📥 拷贝片段到当前目录', value: 'snippet' });
|
|
806
|
+
}
|
|
807
|
+
if (configs.length > 0) {
|
|
808
|
+
actions.push({ name: '📥 拷贝配置到当前目录', value: 'config' });
|
|
809
|
+
}
|
|
810
|
+
actions.push({ name: '✕ 退出', value: 'exit' });
|
|
811
|
+
|
|
812
|
+
const action = await showMenu(
|
|
813
|
+
`${variant.meta.name || variant.name} 变体`,
|
|
814
|
+
actions
|
|
815
|
+
);
|
|
816
|
+
if (action === 'exit') return;
|
|
817
|
+
const { runInitMode } = require('./init');
|
|
818
|
+
console.log();
|
|
819
|
+
if (action === 'snippet') {
|
|
820
|
+
await runInitMode({ snippet: snipPath });
|
|
821
|
+
} else if (action === 'config') {
|
|
822
|
+
let cfgPath;
|
|
823
|
+
if (configs.length === 1) {
|
|
824
|
+
cfgPath = configs[0].path;
|
|
825
|
+
} else {
|
|
826
|
+
const cfgChoices = configs.map(c => ({
|
|
827
|
+
name: c.isOptimal ? c.name + chalk.green(' (最优配对)') : c.name,
|
|
828
|
+
value: c.path,
|
|
829
|
+
}));
|
|
830
|
+
cfgPath = await showMenu('选择配置文件', cfgChoices);
|
|
831
|
+
}
|
|
832
|
+
if (cfgPath) await runInitMode({ config: cfgPath });
|
|
833
|
+
}
|
|
834
|
+
console.log();
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (scope === 'snippets') {
|
|
839
|
+
const brands = findBrands(edmDir);
|
|
840
|
+
const allSnippets = [];
|
|
841
|
+
for (const b of brands) {
|
|
842
|
+
const sList = findSeriesDirs(b.path);
|
|
843
|
+
for (const s of sList) {
|
|
844
|
+
const variants = findSnippetVariants(s.path);
|
|
845
|
+
for (const v of variants) {
|
|
846
|
+
const snipPath = path.join(v.path, 'snippet.html');
|
|
847
|
+
if (fs.existsSync(snipPath)) {
|
|
848
|
+
allSnippets.push({ brand: b, series: s, variant: v, snippetPath: snipPath });
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (allSnippets.length === 0) {
|
|
854
|
+
console.log(chalk.yellow('没有可用的片段。\n'));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const snipChoices = allSnippets.map(item => {
|
|
858
|
+
const relPath = `${item.brand.name}/${item.series.name}/${item.variant.name}`;
|
|
859
|
+
return {
|
|
860
|
+
name: item.brand.meta.name
|
|
861
|
+
? `${chalk.bold.cyan(item.brand.meta.name)} / ${formatName(item.variant.meta, item.variant.name)}`
|
|
862
|
+
: formatName(item.brand.meta, item.brand.name) + ' / ' + formatName(item.variant.meta, item.variant.name),
|
|
863
|
+
value: item,
|
|
864
|
+
description: chalk.dim(relPath) + (item.variant.meta.description ? ' — ' + item.variant.meta.description : ''),
|
|
865
|
+
};
|
|
866
|
+
});
|
|
867
|
+
const picked = await showMenu('选择片段', snipChoices);
|
|
868
|
+
if (!picked) return;
|
|
869
|
+
// Offer copy and browse actions
|
|
870
|
+
const snipActions = [
|
|
871
|
+
{ name: '📥 拷贝片段到当前目录', value: 'copy-snippet' },
|
|
872
|
+
];
|
|
873
|
+
const configs = findConfigs(picked.variant.path);
|
|
874
|
+
if (configs.length > 0) {
|
|
875
|
+
snipActions.push({ name: '📥 拷贝配置到当前目录', value: 'copy-config' });
|
|
876
|
+
}
|
|
877
|
+
snipActions.push({ name: '📋 查看所属系列', value: 'browse' });
|
|
878
|
+
snipActions.push({ name: '✕ 退出', value: 'exit' });
|
|
879
|
+
|
|
880
|
+
const action = await showMenu(
|
|
881
|
+
`${picked.variant.meta.name || picked.variant.name} 片段`,
|
|
882
|
+
snipActions
|
|
883
|
+
);
|
|
884
|
+
if (action === 'exit' || !action) return;
|
|
885
|
+
|
|
886
|
+
if (action === 'browse') {
|
|
887
|
+
// Enter interactive browse from this variant
|
|
888
|
+
startParsed = {
|
|
889
|
+
type: 'variant',
|
|
890
|
+
brand: picked.brand.name,
|
|
891
|
+
series: picked.series.name,
|
|
892
|
+
variant: picked.variant.name,
|
|
893
|
+
seriesData: picked.series,
|
|
894
|
+
variantData: picked.variant,
|
|
895
|
+
};
|
|
896
|
+
} else {
|
|
897
|
+
const { runInitMode } = require('./init');
|
|
898
|
+
console.log();
|
|
899
|
+
if (action === 'copy-snippet') {
|
|
900
|
+
await runInitMode({ snippet: picked.snippetPath });
|
|
901
|
+
} else if (action === 'copy-config') {
|
|
902
|
+
let cfgPath;
|
|
903
|
+
if (configs.length === 1) {
|
|
904
|
+
cfgPath = configs[0].path;
|
|
905
|
+
} else {
|
|
906
|
+
const cfgChoices = configs.map(c => ({
|
|
907
|
+
name: c.isOptimal ? c.name + chalk.green(' (最优配对)') : c.name,
|
|
908
|
+
value: c.path,
|
|
909
|
+
}));
|
|
910
|
+
cfgPath = await showMenu('选择配置文件', cfgChoices);
|
|
911
|
+
}
|
|
912
|
+
if (cfgPath) await runInitMode({ config: cfgPath });
|
|
913
|
+
}
|
|
914
|
+
console.log();
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (viewPath) {
|
|
920
|
+
try {
|
|
921
|
+
startParsed = parseViewPath(viewPath, edmDir);
|
|
922
|
+
} catch (err) {
|
|
923
|
+
console.error(chalk.red(`\n ✘ ${err.message}\n`));
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
await interactiveBrowse(edmDir, startParsed || null);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// ── Non-interactive mode ────────────────────────────────────────────────────
|
|
933
|
+
|
|
934
|
+
if (scope === 'templates') {
|
|
935
|
+
printFlatTemplates(edmDir);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (scope === 'series') {
|
|
939
|
+
printFlatSeries(edmDir);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (scope === 'snippets') {
|
|
943
|
+
printFlatSnippets(edmDir);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (viewPath) {
|
|
948
|
+
let parsed;
|
|
949
|
+
try {
|
|
950
|
+
parsed = parseViewPath(viewPath, edmDir);
|
|
951
|
+
} catch (err) {
|
|
952
|
+
console.error(chalk.red(`\n ✘ ${err.message}\n`));
|
|
953
|
+
process.exit(1);
|
|
954
|
+
}
|
|
955
|
+
printSubTree(edmDir, parsed);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// No args: full tree
|
|
960
|
+
printFullTree(edmDir);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
module.exports = { runViewMode, parseViewPath };
|