juice-email-cli 2.3.7 → 2.3.9
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/package.json +1 -1
- package/src/init.js +93 -108
package/package.json
CHANGED
package/src/init.js
CHANGED
|
@@ -29,7 +29,6 @@ function formatName(meta, dirName) {
|
|
|
29
29
|
function copyFileToCwd(srcPath, cwd, destName) {
|
|
30
30
|
const dest = path.join(cwd, destName || path.basename(srcPath));
|
|
31
31
|
if (fs.existsSync(dest)) {
|
|
32
|
-
// Find a non-conflicting name
|
|
33
32
|
const parsed = path.parse(dest);
|
|
34
33
|
let v = 1;
|
|
35
34
|
while (true) {
|
|
@@ -50,20 +49,12 @@ function copyFileToCwd(srcPath, cwd, destName) {
|
|
|
50
49
|
|
|
51
50
|
// ─── Direct Copy (--template / --snippet / --config) ─────────────────────────
|
|
52
51
|
|
|
53
|
-
/**
|
|
54
|
-
* Derive a human-friendly default name from an EDM file path.
|
|
55
|
-
* e.g. edm/elabscience/templates/standard/template.html → elabscience-standard
|
|
56
|
-
* edm/elabscience/series/literature/default/snippet.html → elabscience-literature-default-snippet
|
|
57
|
-
*/
|
|
58
52
|
function deriveDefaultName(filePath) {
|
|
59
53
|
const normalized = filePath.replace(/\\/g, '/');
|
|
60
|
-
// Try to match edm/<brand>/templates/<version>/...
|
|
61
54
|
const tplMatch = normalized.match(/edm\/([^/]+)\/templates\/([^/]+)/);
|
|
62
55
|
if (tplMatch) return `${tplMatch[1]}-${tplMatch[2]}`;
|
|
63
|
-
// Try to match edm/<brand>/series/<series>/<variant>/...
|
|
64
56
|
const snipMatch = normalized.match(/edm\/([^/]+)\/series\/([^/]+)\/([^/]+)/);
|
|
65
57
|
if (snipMatch) return `${snipMatch[1]}-${snipMatch[2]}-${snipMatch[3]}`;
|
|
66
|
-
// Fallback: source file basename
|
|
67
58
|
return path.parse(filePath).name;
|
|
68
59
|
}
|
|
69
60
|
|
|
@@ -89,17 +80,13 @@ async function directCopy(srcPath, cwd) {
|
|
|
89
80
|
|
|
90
81
|
// ─── Interactive Init ────────────────────────────────────────────────────────
|
|
91
82
|
|
|
92
|
-
|
|
93
|
-
* Wrap @inquirer/prompts select with back/exit navigation.
|
|
94
|
-
* Returns the chosen value, 'back', or 'exit'.
|
|
95
|
-
*/
|
|
96
|
-
async function selectWithNav(choices, showBack) {
|
|
83
|
+
async function selectWithNav(message, choices, showBack) {
|
|
97
84
|
const { select } = await import('@inquirer/prompts');
|
|
98
85
|
const all = [];
|
|
99
86
|
if (showBack) all.push({ name: '.. 返回上级', value: 'back' });
|
|
100
87
|
all.push(...choices);
|
|
101
88
|
all.push({ name: '✕ 退出', value: 'exit' });
|
|
102
|
-
return select({ message
|
|
89
|
+
return select({ message, choices: all, loop: false });
|
|
103
90
|
}
|
|
104
91
|
|
|
105
92
|
async function interactiveInit(edmDir, cwd) {
|
|
@@ -113,7 +100,7 @@ async function interactiveInit(edmDir, cwd) {
|
|
|
113
100
|
name: formatName(b.meta, b.name) + (b.meta.description ? ' — ' + chalk.dim(b.meta.description) : ''),
|
|
114
101
|
value: b,
|
|
115
102
|
}));
|
|
116
|
-
const result = await selectWithNav(choices, false);
|
|
103
|
+
const result = await selectWithNav('选择品牌:', choices, false);
|
|
117
104
|
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
118
105
|
brand = result;
|
|
119
106
|
step = 'version';
|
|
@@ -126,7 +113,7 @@ async function interactiveInit(edmDir, cwd) {
|
|
|
126
113
|
name: formatName(v.meta, v.name) + (v.meta.description ? ' — ' + chalk.dim(v.meta.description) : ''),
|
|
127
114
|
value: v,
|
|
128
115
|
}));
|
|
129
|
-
const result = await selectWithNav(choices, true);
|
|
116
|
+
const result = await selectWithNav('选择模板版本:', choices, true);
|
|
130
117
|
if (result === 'back') { step = 'brand'; continue; }
|
|
131
118
|
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
132
119
|
version = result;
|
|
@@ -148,7 +135,7 @@ async function interactiveInit(edmDir, cwd) {
|
|
|
148
135
|
value: s,
|
|
149
136
|
})),
|
|
150
137
|
];
|
|
151
|
-
const result = await selectWithNav(choices, true);
|
|
138
|
+
const result = await selectWithNav('选择片段系列:', choices, true);
|
|
152
139
|
if (result === 'back') { step = 'version'; continue; }
|
|
153
140
|
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
154
141
|
series = result;
|
|
@@ -167,7 +154,7 @@ async function interactiveInit(edmDir, cwd) {
|
|
|
167
154
|
name: formatName(v.meta, v.name) + (v.meta.description ? ' — ' + chalk.dim(v.meta.description) : ''),
|
|
168
155
|
value: v,
|
|
169
156
|
}));
|
|
170
|
-
const result = await selectWithNav(choices, true);
|
|
157
|
+
const result = await selectWithNav('选择片段变体:', choices, true);
|
|
171
158
|
if (result === 'back') { step = 'series'; continue; }
|
|
172
159
|
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
173
160
|
variant = result;
|
|
@@ -176,7 +163,6 @@ async function interactiveInit(edmDir, cwd) {
|
|
|
176
163
|
}
|
|
177
164
|
|
|
178
165
|
if (step === 'copy') {
|
|
179
|
-
// Multi-select what to copy
|
|
180
166
|
const copyItems = [];
|
|
181
167
|
copyItems.push({
|
|
182
168
|
name: `📋 模板 HTML(${version.meta.name || version.name})`,
|
|
@@ -206,85 +192,97 @@ async function interactiveInit(edmDir, cwd) {
|
|
|
206
192
|
}
|
|
207
193
|
}
|
|
208
194
|
|
|
209
|
-
// Show summary before multi-select
|
|
210
195
|
console.log(chalk.dim(`\n 品牌:${brand.meta.name || brand.name} → 模板:${version.meta.name || version.name}`));
|
|
211
196
|
if (series) console.log(chalk.dim(` 系列:${series.meta.name || series.name}${variant ? ' → 变体:' + (variant.meta.name || variant.name) : ''}`));
|
|
212
197
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
198
|
+
// Loop: select → checkbox → confirm, allow back/modify at any point
|
|
199
|
+
let selected = copyItems.filter(c => c.checked).map(c => c.value);
|
|
200
|
+
while (true) {
|
|
201
|
+
const picked = selected
|
|
202
|
+
.map(v => copyItems.find(c => c.value === v))
|
|
203
|
+
.filter(Boolean)
|
|
204
|
+
.map(c => c.name.replace(/^[^\s]+\s/, ''));
|
|
205
|
+
const summary = picked.length > 0 ? picked.join(' + ') : '(未选择)';
|
|
206
|
+
const mainChoices = [];
|
|
207
|
+
if (picked.length > 0) {
|
|
208
|
+
mainChoices.push({ name: `✅ 确认拷贝(${summary})`, value: 'confirm' });
|
|
209
|
+
}
|
|
210
|
+
mainChoices.push({ name: `🔄 修改选择(${summary})`, value: 'edit' });
|
|
223
211
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
212
|
+
const action = await selectWithNav(
|
|
213
|
+
'拷贝操作:',
|
|
214
|
+
mainChoices,
|
|
215
|
+
true
|
|
216
|
+
);
|
|
217
|
+
if (action === 'back') { step = series ? 'variant' : 'series'; break; }
|
|
218
|
+
if (action === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
219
|
+
if (action === 'edit') {
|
|
220
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
221
|
+
selected = await checkbox({
|
|
222
|
+
message: '选择要拷贝的内容:',
|
|
223
|
+
choices: copyItems,
|
|
224
|
+
});
|
|
225
|
+
if (selected.length === 0) { console.log(chalk.dim(' (已清空选择)\n')); }
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
230
228
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
229
|
+
if (selected.length === 0) {
|
|
230
|
+
console.log(chalk.gray('未选择任何内容。\n'));
|
|
231
|
+
step = series ? 'variant' : 'series';
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
236
234
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const outputBaseName = await promptOutputName(defaultBaseName, cwd);
|
|
235
|
+
const defaultBaseName = variant
|
|
236
|
+
? `${brand.name}-${version.name}-${series.name}-${variant.name}`
|
|
237
|
+
: `${brand.name}-${version.name}`;
|
|
238
|
+
const outputBaseName = await promptOutputName(defaultBaseName, cwd);
|
|
242
239
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const cwdRel = (p) => './' + path.relative(cwd, p);
|
|
240
|
+
console.log(chalk.green('\n✔ 已拷贝:'));
|
|
241
|
+
const cwdRel = (p) => './' + path.relative(cwd, p);
|
|
246
242
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
243
|
+
if (selected.includes('template')) {
|
|
244
|
+
const destName = outputBaseName + path.extname(version.templatePath);
|
|
245
|
+
const dest = copyFileToCwd(version.templatePath, cwd, destName);
|
|
246
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
247
|
+
const icon = copyIcon(variant ? variant.path : null, version.path, brand.path, cwd);
|
|
248
|
+
if (icon) console.log(` ${chalk.cyan('·')} ${cwdRel(icon)} ${chalk.gray('(图标, ' + fmtBytes(fs.statSync(icon).size) + ')')}`);
|
|
249
|
+
}
|
|
255
250
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
251
|
+
if (selected.includes('snippet') && variant) {
|
|
252
|
+
const snipPath = path.join(variant.path, 'snippet.html');
|
|
253
|
+
const destName = outputBaseName + '-snippet' + path.extname(snipPath);
|
|
254
|
+
const dest = copyFileToCwd(snipPath, cwd, destName);
|
|
255
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(片段, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
256
|
+
}
|
|
262
257
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
258
|
+
if (selected.includes('config') && variant) {
|
|
259
|
+
const configs = findConfigs(variant.path);
|
|
260
|
+
const optimal = configs.find(c => c.isOptimal);
|
|
261
|
+
const cfgPath = optimal ? optimal.path : configs[0].path;
|
|
262
|
+
const dest = copyFileToCwd(cfgPath, cwd, 'juice.yaml');
|
|
263
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
264
|
+
}
|
|
270
265
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
266
|
+
console.log();
|
|
267
|
+
if (selected.includes('snippet') && selected.includes('template')) {
|
|
268
|
+
const snipFile = outputBaseName + '-snippet' + path.extname(path.join(variant.path, 'snippet.html'));
|
|
269
|
+
const tplFile = outputBaseName + path.extname(version.templatePath);
|
|
270
|
+
console.log(
|
|
271
|
+
' ' + chalk.dim('💡 下一步:') + '\n' +
|
|
272
|
+
' ' + chalk.cyan(`juice -s ${snipFile} -f ${tplFile}`) +
|
|
273
|
+
(selected.includes('config') ? chalk.cyan(' -c juice.yaml') : '') + '\n'
|
|
274
|
+
);
|
|
275
|
+
} else if (selected.includes('template')) {
|
|
276
|
+
const tplFile = outputBaseName + path.extname(version.templatePath);
|
|
277
|
+
console.log(
|
|
278
|
+
' ' + chalk.dim('💡 下一步:') + '\n' +
|
|
279
|
+
' ' + chalk.cyan(`juice -f ${tplFile}`) + '\n'
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
286
283
|
}
|
|
287
|
-
return;
|
|
284
|
+
if (step === 'copy') return; // confirmed and copied
|
|
285
|
+
continue; // went back
|
|
288
286
|
}
|
|
289
287
|
}
|
|
290
288
|
}
|
|
@@ -329,15 +327,10 @@ function countFiles(dir) {
|
|
|
329
327
|
return count;
|
|
330
328
|
}
|
|
331
329
|
|
|
332
|
-
/**
|
|
333
|
-
* Find and copy favicon.ico alongside the template.
|
|
334
|
-
* Lookup: variant dir → template version dir → brand's first template → skip
|
|
335
|
-
*/
|
|
336
330
|
function copyIcon(variantDir, versionDir, brandDir, cwd) {
|
|
337
331
|
const candidates = [];
|
|
338
332
|
if (variantDir) candidates.push(path.join(variantDir, 'favicon.ico'));
|
|
339
333
|
if (versionDir) candidates.push(path.join(versionDir, 'favicon.ico'));
|
|
340
|
-
// First template version in brand
|
|
341
334
|
try {
|
|
342
335
|
const versions = findTemplateVersions(brandDir);
|
|
343
336
|
if (versions.length > 0) {
|
|
@@ -365,32 +358,29 @@ function findNextEdmVersion(targetDir) {
|
|
|
365
358
|
async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
366
359
|
const cwd = process.cwd();
|
|
367
360
|
|
|
368
|
-
// --all: copy entire EDM directory
|
|
369
361
|
if (all) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
362
|
+
const pkgEdm = path.resolve(__dirname, '..', 'edm');
|
|
363
|
+
const edmDir = fs.existsSync(pkgEdm) ? pkgEdm : (() => {
|
|
364
|
+
try { return resolveEdmDir(); } catch (_) { return null; }
|
|
365
|
+
})();
|
|
366
|
+
if (!edmDir) {
|
|
367
|
+
console.error(chalk.red('\n ✘ EDM 资源库不存在,无法拷贝。\n'));
|
|
375
368
|
process.exit(1);
|
|
376
369
|
}
|
|
377
370
|
const target = all === true ? cwd : path.resolve(all);
|
|
378
371
|
const destDir = path.join(target, 'edm');
|
|
379
372
|
|
|
380
|
-
// Prevent deleting the source when target overlaps
|
|
381
373
|
if (path.resolve(edmDir) === path.resolve(destDir)) {
|
|
382
374
|
console.log(chalk.yellow('目标目录与 EDM 资源库相同,无需拷贝。\n'));
|
|
383
375
|
return;
|
|
384
376
|
}
|
|
385
377
|
console.log(chalk.cyan(`\n 拷贝 EDM 资源库...`));
|
|
386
|
-
console.log(chalk.gray(` 源:${edmDir}`));
|
|
387
|
-
console.log(chalk.gray(` 目标:${destDir}`));
|
|
388
378
|
|
|
389
379
|
if (fs.existsSync(destDir)) {
|
|
390
380
|
const { select } = await import('@inquirer/prompts');
|
|
391
381
|
const vName = findNextEdmVersion(target);
|
|
392
382
|
const action = await select({
|
|
393
|
-
message: `目录
|
|
383
|
+
message: `目录 edm/ 已存在:`,
|
|
394
384
|
choices: [
|
|
395
385
|
{ name: '覆盖', value: 'overwrite' },
|
|
396
386
|
{ name: `版本(${path.basename(vName)})`, value: 'version' },
|
|
@@ -408,7 +398,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
408
398
|
console.log(chalk.green(`\n✔ 已拷贝 EDM 资源库 → ${path.basename(vName)}(${fileCount} 个文件,${fmtBytes(edmSize)})\n`));
|
|
409
399
|
return;
|
|
410
400
|
}
|
|
411
|
-
// overwrite: remove and replace
|
|
412
401
|
fs.rmSync(destDir, { recursive: true, force: true });
|
|
413
402
|
}
|
|
414
403
|
|
|
@@ -419,7 +408,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
419
408
|
return;
|
|
420
409
|
}
|
|
421
410
|
|
|
422
|
-
// Direct copy modes
|
|
423
411
|
if (template) {
|
|
424
412
|
await directCopy(template, cwd);
|
|
425
413
|
return;
|
|
@@ -433,7 +421,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
433
421
|
return;
|
|
434
422
|
}
|
|
435
423
|
|
|
436
|
-
// Path-based from EDM
|
|
437
424
|
if (initPath) {
|
|
438
425
|
const { parseViewPath } = require('./view');
|
|
439
426
|
let edmDir;
|
|
@@ -455,12 +442,11 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
455
442
|
if (parsed.type === 'template') {
|
|
456
443
|
await directCopy(parsed.versionData.templatePath, cwd);
|
|
457
444
|
} else if (parsed.type === 'variant') {
|
|
458
|
-
// Multi-select copy
|
|
459
445
|
const snipPath = path.join(parsed.variantData.path, 'snippet.html');
|
|
460
446
|
const configs = findConfigs(parsed.variantData.path);
|
|
461
447
|
const brandDir = path.join(edmDir, parsed.brand);
|
|
462
448
|
const versions = findTemplateVersions(brandDir);
|
|
463
|
-
const version = versions[0];
|
|
449
|
+
const version = versions[0];
|
|
464
450
|
const tplPath = version.templatePath;
|
|
465
451
|
|
|
466
452
|
const copyItems = [
|
|
@@ -519,7 +505,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
519
505
|
return;
|
|
520
506
|
}
|
|
521
507
|
|
|
522
|
-
// No arguments: interactive mode
|
|
523
508
|
let edmDir;
|
|
524
509
|
try {
|
|
525
510
|
edmDir = resolveEdmDir();
|