juice-email-cli 2.3.8 → 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 +81 -98
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,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,9 +358,7 @@ 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
|
-
// Always use package built-in EDM for --all (not CWD's local copy)
|
|
371
362
|
const pkgEdm = path.resolve(__dirname, '..', 'edm');
|
|
372
363
|
const edmDir = fs.existsSync(pkgEdm) ? pkgEdm : (() => {
|
|
373
364
|
try { return resolveEdmDir(); } catch (_) { return null; }
|
|
@@ -379,20 +370,17 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
379
370
|
const target = all === true ? cwd : path.resolve(all);
|
|
380
371
|
const destDir = path.join(target, 'edm');
|
|
381
372
|
|
|
382
|
-
// Prevent deleting the source when target overlaps
|
|
383
373
|
if (path.resolve(edmDir) === path.resolve(destDir)) {
|
|
384
374
|
console.log(chalk.yellow('目标目录与 EDM 资源库相同,无需拷贝。\n'));
|
|
385
375
|
return;
|
|
386
376
|
}
|
|
387
377
|
console.log(chalk.cyan(`\n 拷贝 EDM 资源库...`));
|
|
388
|
-
console.log(chalk.gray(` 源:${edmDir}`));
|
|
389
|
-
console.log(chalk.gray(` 目标:${destDir}`));
|
|
390
378
|
|
|
391
379
|
if (fs.existsSync(destDir)) {
|
|
392
380
|
const { select } = await import('@inquirer/prompts');
|
|
393
381
|
const vName = findNextEdmVersion(target);
|
|
394
382
|
const action = await select({
|
|
395
|
-
message: `目录
|
|
383
|
+
message: `目录 edm/ 已存在:`,
|
|
396
384
|
choices: [
|
|
397
385
|
{ name: '覆盖', value: 'overwrite' },
|
|
398
386
|
{ name: `版本(${path.basename(vName)})`, value: 'version' },
|
|
@@ -410,7 +398,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
410
398
|
console.log(chalk.green(`\n✔ 已拷贝 EDM 资源库 → ${path.basename(vName)}(${fileCount} 个文件,${fmtBytes(edmSize)})\n`));
|
|
411
399
|
return;
|
|
412
400
|
}
|
|
413
|
-
// overwrite: remove and replace
|
|
414
401
|
fs.rmSync(destDir, { recursive: true, force: true });
|
|
415
402
|
}
|
|
416
403
|
|
|
@@ -421,7 +408,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
421
408
|
return;
|
|
422
409
|
}
|
|
423
410
|
|
|
424
|
-
// Direct copy modes
|
|
425
411
|
if (template) {
|
|
426
412
|
await directCopy(template, cwd);
|
|
427
413
|
return;
|
|
@@ -435,7 +421,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
435
421
|
return;
|
|
436
422
|
}
|
|
437
423
|
|
|
438
|
-
// Path-based from EDM
|
|
439
424
|
if (initPath) {
|
|
440
425
|
const { parseViewPath } = require('./view');
|
|
441
426
|
let edmDir;
|
|
@@ -457,12 +442,11 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
457
442
|
if (parsed.type === 'template') {
|
|
458
443
|
await directCopy(parsed.versionData.templatePath, cwd);
|
|
459
444
|
} else if (parsed.type === 'variant') {
|
|
460
|
-
// Multi-select copy
|
|
461
445
|
const snipPath = path.join(parsed.variantData.path, 'snippet.html');
|
|
462
446
|
const configs = findConfigs(parsed.variantData.path);
|
|
463
447
|
const brandDir = path.join(edmDir, parsed.brand);
|
|
464
448
|
const versions = findTemplateVersions(brandDir);
|
|
465
|
-
const version = versions[0];
|
|
449
|
+
const version = versions[0];
|
|
466
450
|
const tplPath = version.templatePath;
|
|
467
451
|
|
|
468
452
|
const copyItems = [
|
|
@@ -521,7 +505,6 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
521
505
|
return;
|
|
522
506
|
}
|
|
523
507
|
|
|
524
|
-
// No arguments: interactive mode
|
|
525
508
|
let edmDir;
|
|
526
509
|
try {
|
|
527
510
|
edmDir = resolveEdmDir();
|