juice-email-cli 2.3.8 → 2.3.10
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 +91 -99
- package/src/view.js +2 -1
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,10 +80,6 @@ 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
83
|
async function selectWithNav(message, choices, showBack) {
|
|
97
84
|
const { select } = await import('@inquirer/prompts');
|
|
98
85
|
const all = [];
|
|
@@ -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,105 @@ 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: confirm / shortcuts / customize, allow back 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
|
+
// Quick shortcut: snippet + config only (skip template)
|
|
211
|
+
if (variant && copyItems.some(c => c.value === 'snippet') && copyItems.some(c => c.value === 'config')) {
|
|
212
|
+
mainChoices.push({ name: '🧩 仅片段 + 配置', value: 'snippet-config' });
|
|
213
|
+
}
|
|
214
|
+
mainChoices.push({ name: `🔄 自定义选择(${summary})`, value: 'edit' });
|
|
223
215
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
216
|
+
const action = await selectWithNav(
|
|
217
|
+
'拷贝操作:',
|
|
218
|
+
mainChoices,
|
|
219
|
+
true
|
|
220
|
+
);
|
|
221
|
+
if (action === 'back') { step = series ? 'variant' : 'series'; break; }
|
|
222
|
+
if (action === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
223
|
+
if (action === 'edit') {
|
|
224
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
225
|
+
selected = await checkbox({
|
|
226
|
+
message: '选择要拷贝的内容:',
|
|
227
|
+
choices: copyItems,
|
|
228
|
+
});
|
|
229
|
+
if (selected.length === 0) { console.log(chalk.dim(' (已清空选择)\n')); }
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (action === 'snippet-config') {
|
|
233
|
+
selected = ['snippet', 'config'];
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
230
236
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
237
|
+
if (selected.length === 0) {
|
|
238
|
+
console.log(chalk.gray('未选择任何内容。\n'));
|
|
239
|
+
step = series ? 'variant' : 'series';
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
236
242
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const outputBaseName = await promptOutputName(defaultBaseName, cwd);
|
|
243
|
+
const defaultBaseName = variant
|
|
244
|
+
? `${brand.name}-${version.name}-${series.name}-${variant.name}`
|
|
245
|
+
: `${brand.name}-${version.name}`;
|
|
246
|
+
const outputBaseName = await promptOutputName(defaultBaseName, cwd);
|
|
242
247
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const cwdRel = (p) => './' + path.relative(cwd, p);
|
|
248
|
+
console.log(chalk.green('\n✔ 已拷贝:'));
|
|
249
|
+
const cwdRel = (p) => './' + path.relative(cwd, p);
|
|
246
250
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
251
|
+
if (selected.includes('template')) {
|
|
252
|
+
const destName = outputBaseName + path.extname(version.templatePath);
|
|
253
|
+
const dest = copyFileToCwd(version.templatePath, cwd, destName);
|
|
254
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
255
|
+
const icon = copyIcon(variant ? variant.path : null, version.path, brand.path, cwd);
|
|
256
|
+
if (icon) console.log(` ${chalk.cyan('·')} ${cwdRel(icon)} ${chalk.gray('(图标, ' + fmtBytes(fs.statSync(icon).size) + ')')}`);
|
|
257
|
+
}
|
|
255
258
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
if (selected.includes('snippet') && variant) {
|
|
260
|
+
const snipPath = path.join(variant.path, 'snippet.html');
|
|
261
|
+
const destName = outputBaseName + '-snippet' + path.extname(snipPath);
|
|
262
|
+
const dest = copyFileToCwd(snipPath, cwd, destName);
|
|
263
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(片段, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
264
|
+
}
|
|
262
265
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
266
|
+
if (selected.includes('config') && variant) {
|
|
267
|
+
const configs = findConfigs(variant.path);
|
|
268
|
+
const optimal = configs.find(c => c.isOptimal);
|
|
269
|
+
const cfgPath = optimal ? optimal.path : configs[0].path;
|
|
270
|
+
const dest = copyFileToCwd(cfgPath, cwd, 'juice.yaml');
|
|
271
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
272
|
+
}
|
|
270
273
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
274
|
+
console.log();
|
|
275
|
+
if (selected.includes('snippet') && selected.includes('template')) {
|
|
276
|
+
const snipFile = outputBaseName + '-snippet' + path.extname(path.join(variant.path, 'snippet.html'));
|
|
277
|
+
const tplFile = outputBaseName + path.extname(version.templatePath);
|
|
278
|
+
console.log(
|
|
279
|
+
' ' + chalk.dim('💡 下一步:') + '\n' +
|
|
280
|
+
' ' + chalk.cyan(`juice -s ${snipFile} -f ${tplFile}`) +
|
|
281
|
+
(selected.includes('config') ? chalk.cyan(' -c juice.yaml') : '') + '\n'
|
|
282
|
+
);
|
|
283
|
+
} else if (selected.includes('template')) {
|
|
284
|
+
const tplFile = outputBaseName + path.extname(version.templatePath);
|
|
285
|
+
console.log(
|
|
286
|
+
' ' + chalk.dim('💡 下一步:') + '\n' +
|
|
287
|
+
' ' + chalk.cyan(`juice -f ${tplFile}`) + '\n'
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
286
291
|
}
|
|
287
|
-
return;
|
|
292
|
+
if (step === 'copy') return; // confirmed and copied
|
|
293
|
+
continue; // went back
|
|
288
294
|
}
|
|
289
295
|
}
|
|
290
296
|
}
|
|
@@ -329,15 +335,10 @@ function countFiles(dir) {
|
|
|
329
335
|
return count;
|
|
330
336
|
}
|
|
331
337
|
|
|
332
|
-
/**
|
|
333
|
-
* Find and copy favicon.ico alongside the template.
|
|
334
|
-
* Lookup: variant dir → template version dir → brand's first template → skip
|
|
335
|
-
*/
|
|
336
338
|
function copyIcon(variantDir, versionDir, brandDir, cwd) {
|
|
337
339
|
const candidates = [];
|
|
338
340
|
if (variantDir) candidates.push(path.join(variantDir, 'favicon.ico'));
|
|
339
341
|
if (versionDir) candidates.push(path.join(versionDir, 'favicon.ico'));
|
|
340
|
-
// First template version in brand
|
|
341
342
|
try {
|
|
342
343
|
const versions = findTemplateVersions(brandDir);
|
|
343
344
|
if (versions.length > 0) {
|
|
@@ -346,7 +347,8 @@ function copyIcon(variantDir, versionDir, brandDir, cwd) {
|
|
|
346
347
|
} catch (_) {}
|
|
347
348
|
for (const src of candidates) {
|
|
348
349
|
if (fs.existsSync(src)) {
|
|
349
|
-
const dest =
|
|
350
|
+
const dest = path.join(cwd, 'favicon.ico');
|
|
351
|
+
fs.copyFileSync(src, dest);
|
|
350
352
|
return dest;
|
|
351
353
|
}
|
|
352
354
|
}
|
|
@@ -365,9 +367,7 @@ function findNextEdmVersion(targetDir) {
|
|
|
365
367
|
async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
366
368
|
const cwd = process.cwd();
|
|
367
369
|
|
|
368
|
-
// --all: copy entire EDM directory
|
|
369
370
|
if (all) {
|
|
370
|
-
// Always use package built-in EDM for --all (not CWD's local copy)
|
|
371
371
|
const pkgEdm = path.resolve(__dirname, '..', 'edm');
|
|
372
372
|
const edmDir = fs.existsSync(pkgEdm) ? pkgEdm : (() => {
|
|
373
373
|
try { return resolveEdmDir(); } catch (_) { return null; }
|
|
@@ -379,20 +379,17 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
379
379
|
const target = all === true ? cwd : path.resolve(all);
|
|
380
380
|
const destDir = path.join(target, 'edm');
|
|
381
381
|
|
|
382
|
-
// Prevent deleting the source when target overlaps
|
|
383
382
|
if (path.resolve(edmDir) === path.resolve(destDir)) {
|
|
384
383
|
console.log(chalk.yellow('目标目录与 EDM 资源库相同,无需拷贝。\n'));
|
|
385
384
|
return;
|
|
386
385
|
}
|
|
387
386
|
console.log(chalk.cyan(`\n 拷贝 EDM 资源库...`));
|
|
388
|
-
console.log(chalk.gray(` 源:${edmDir}`));
|
|
389
|
-
console.log(chalk.gray(` 目标:${destDir}`));
|
|
390
387
|
|
|
391
388
|
if (fs.existsSync(destDir)) {
|
|
392
389
|
const { select } = await import('@inquirer/prompts');
|
|
393
390
|
const vName = findNextEdmVersion(target);
|
|
394
391
|
const action = await select({
|
|
395
|
-
message: `目录
|
|
392
|
+
message: `目录 edm/ 已存在:`,
|
|
396
393
|
choices: [
|
|
397
394
|
{ name: '覆盖', value: 'overwrite' },
|
|
398
395
|
{ name: `版本(${path.basename(vName)})`, value: 'version' },
|
|
@@ -410,7 +407,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
410
407
|
console.log(chalk.green(`\n✔ 已拷贝 EDM 资源库 → ${path.basename(vName)}(${fileCount} 个文件,${fmtBytes(edmSize)})\n`));
|
|
411
408
|
return;
|
|
412
409
|
}
|
|
413
|
-
// overwrite: remove and replace
|
|
414
410
|
fs.rmSync(destDir, { recursive: true, force: true });
|
|
415
411
|
}
|
|
416
412
|
|
|
@@ -421,7 +417,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
421
417
|
return;
|
|
422
418
|
}
|
|
423
419
|
|
|
424
|
-
// Direct copy modes
|
|
425
420
|
if (template) {
|
|
426
421
|
await directCopy(template, cwd);
|
|
427
422
|
return;
|
|
@@ -435,7 +430,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
435
430
|
return;
|
|
436
431
|
}
|
|
437
432
|
|
|
438
|
-
// Path-based from EDM
|
|
439
433
|
if (initPath) {
|
|
440
434
|
const { parseViewPath } = require('./view');
|
|
441
435
|
let edmDir;
|
|
@@ -457,12 +451,11 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
457
451
|
if (parsed.type === 'template') {
|
|
458
452
|
await directCopy(parsed.versionData.templatePath, cwd);
|
|
459
453
|
} else if (parsed.type === 'variant') {
|
|
460
|
-
// Multi-select copy
|
|
461
454
|
const snipPath = path.join(parsed.variantData.path, 'snippet.html');
|
|
462
455
|
const configs = findConfigs(parsed.variantData.path);
|
|
463
456
|
const brandDir = path.join(edmDir, parsed.brand);
|
|
464
457
|
const versions = findTemplateVersions(brandDir);
|
|
465
|
-
const version = versions[0];
|
|
458
|
+
const version = versions[0];
|
|
466
459
|
const tplPath = version.templatePath;
|
|
467
460
|
|
|
468
461
|
const copyItems = [
|
|
@@ -521,7 +514,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
521
514
|
return;
|
|
522
515
|
}
|
|
523
516
|
|
|
524
|
-
// No arguments: interactive mode
|
|
525
517
|
let edmDir;
|
|
526
518
|
try {
|
|
527
519
|
edmDir = resolveEdmDir();
|
package/src/view.js
CHANGED
|
@@ -634,7 +634,8 @@ async function interactiveBrowse(edmDir, startParsed) {
|
|
|
634
634
|
];
|
|
635
635
|
for (const isrc of iconSrcs) {
|
|
636
636
|
if (fs.existsSync(isrc)) {
|
|
637
|
-
const idest =
|
|
637
|
+
const idest = path.join(cwd, 'favicon.ico');
|
|
638
|
+
fs.copyFileSync(isrc, idest);
|
|
638
639
|
console.log(` ${chalk.cyan('·')} ${cwdRel(idest)} ${chalk.gray('(图标, ' + fmtBytes(fs.statSync(idest).size) + ')')}`);
|
|
639
640
|
break;
|
|
640
641
|
}
|