juice-email-cli 2.3.2 → 2.3.4
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 +2 -0
- package/package.json +1 -1
- package/src/context-menu.js +102 -48
- package/src/init.js +103 -3
- package/src/view.js +108 -143
package/bin/juice.js
CHANGED
|
@@ -103,6 +103,7 @@ program
|
|
|
103
103
|
.option('-t, --template <file-path>', '仅拷贝模板 HTML 文件')
|
|
104
104
|
.option('-s, --snippet <file-path>', '仅拷贝片段 HTML 文件')
|
|
105
105
|
.option('-c, --config <file-path>', '仅拷贝配置 YAML 文件')
|
|
106
|
+
.option('--all [target]', '拷贝整个 EDM 资源目录到当前或指定目录')
|
|
106
107
|
.action(async (initPath, options) => {
|
|
107
108
|
const { runInitMode } = require('../src/init');
|
|
108
109
|
await runInitMode({
|
|
@@ -110,6 +111,7 @@ program
|
|
|
110
111
|
template: options.template || null,
|
|
111
112
|
snippet: options.snippet || null,
|
|
112
113
|
config: options.config || null,
|
|
114
|
+
all: options.all !== undefined ? (options.all || true) : null,
|
|
113
115
|
});
|
|
114
116
|
});
|
|
115
117
|
|
package/package.json
CHANGED
package/src/context-menu.js
CHANGED
|
@@ -228,7 +228,7 @@ function registerSubCommands(containerPath, kind, nodePath, scriptPath, iconPath
|
|
|
228
228
|
const viewKey = `${containerPath}\\shell\\${SUBCMDS.viewEdm}`;
|
|
229
229
|
regAdd(viewKey, 'MUIVerb', 'REG_SZ', '📦 查看资源列表');
|
|
230
230
|
regAdd(viewKey, 'Icon', 'REG_SZ', iconPath);
|
|
231
|
-
regAdd(`${viewKey}\\command`, '', 'REG_SZ',
|
|
231
|
+
regAdd(`${viewKey}\\command`, '', 'REG_SZ', wrapWithPause(nodePath, scriptPath, 'view'));
|
|
232
232
|
|
|
233
233
|
const viewIntKey = `${containerPath}\\shell\\${SUBCMDS.viewEdmInt}`;
|
|
234
234
|
regAdd(viewIntKey, 'MUIVerb', 'REG_SZ', '📋 浏览资源库');
|
|
@@ -246,52 +246,77 @@ function registerSubCommands(containerPath, kind, nodePath, scriptPath, iconPath
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
// ─── 非交互命令包装(始终暂停以便用户查看输出)─────────────────────────────────
|
|
250
|
+
|
|
251
|
+
function wrapWithPause(nodePath, scriptPath, cliArgs) {
|
|
252
|
+
const node = nodePath.replace(/'/g, "''");
|
|
253
|
+
const script = scriptPath.replace(/'/g, "''");
|
|
254
|
+
const ps = [
|
|
255
|
+
`& '${node}' '${script}' ${cliArgs}`,
|
|
256
|
+
`Write-Host ''`,
|
|
257
|
+
`Read-Host 'Press Enter to close'`,
|
|
258
|
+
].join('; ');
|
|
259
|
+
return `powershell.exe -Command "${ps}"`;
|
|
260
|
+
}
|
|
261
|
+
|
|
249
262
|
// ─── Directory / Background 菜单注册 ──────────────────────────────────────────
|
|
250
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Register Directory and Background right-click menus using SubCommands
|
|
266
|
+
* + CommandStore (same format as file-type menus for consistent UX).
|
|
267
|
+
*/
|
|
251
268
|
function registerDirBgMenus(nodePath, scriptPath, iconPath, pwshPath) {
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
const bgContainer = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`;
|
|
255
|
-
|
|
256
|
-
for (const containerPath of [dirContainer, bgContainer]) {
|
|
257
|
-
// 📥 从资源库拷贝到此处
|
|
258
|
-
const initKey = `${containerPath}\\shell\\${SUBCMDS.initEdm}`;
|
|
259
|
-
regAdd(initKey, 'MUIVerb', 'REG_SZ', '📥 从资源库拷贝到此处');
|
|
260
|
-
regAdd(initKey, 'Icon', 'REG_SZ', iconPath);
|
|
261
|
-
regAdd(`${initKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'init'));
|
|
262
|
-
|
|
263
|
-
// 📦 查看资源列表
|
|
264
|
-
const viewKey = `${containerPath}\\shell\\${SUBCMDS.viewEdm}`;
|
|
265
|
-
regAdd(viewKey, 'MUIVerb', 'REG_SZ', '📦 查看资源列表');
|
|
266
|
-
regAdd(viewKey, 'Icon', 'REG_SZ', iconPath);
|
|
267
|
-
regAdd(`${viewKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view'));
|
|
269
|
+
const node = nodePath.replace(/'/g, "''");
|
|
270
|
+
const script = scriptPath.replace(/'/g, "''");
|
|
268
271
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
// CommandStore subcommand names
|
|
273
|
+
const DIR_VIEW = 'JuiceEmailDir.View';
|
|
274
|
+
const DIR_COPYALL = 'JuiceEmailDir.CopyAll';
|
|
275
|
+
const DIR_COPYPICK= 'JuiceEmailDir.CopyPick';
|
|
276
|
+
const DIR_PWSH = 'JuiceEmailDir.Pwsh';
|
|
277
|
+
|
|
278
|
+
// Helper: register a CommandStore entry
|
|
279
|
+
function regCmd(name, label, psCmd) {
|
|
280
|
+
const cmdKey = `${HKCU_SHELL}\\${name}`;
|
|
281
|
+
regAdd(cmdKey, 'MUIVerb', 'REG_SZ', label);
|
|
282
|
+
regAdd(cmdKey, 'Icon', 'REG_SZ', iconPath);
|
|
283
|
+
// Always pause so user can see output
|
|
284
|
+
const fullPs = `${psCmd}; Write-Host ''; Read-Host 'Press Enter to close'`;
|
|
285
|
+
regAdd(`${cmdKey}\\command`, '', 'REG_SZ', `powershell.exe -Command "${fullPs}"`);
|
|
286
|
+
}
|
|
274
287
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
288
|
+
// View: non-interactive, no directory dependency
|
|
289
|
+
regCmd(DIR_VIEW, '📋 查看可用资源', `& '${node}' '${script}' view`);
|
|
290
|
+
|
|
291
|
+
// CopyAll: copy entire EDM to clicked directory
|
|
292
|
+
regCmd(DIR_COPYALL, '📦 拷贝全部资源', `& '${node}' '${script}' init --all '%V'`);
|
|
293
|
+
|
|
294
|
+
// CopyPick: interactive, cd to directory first so output lands there
|
|
295
|
+
regCmd(DIR_COPYPICK, '📥 选择资源拷贝', `Set-Location '%V'; & '${node}' '${script}' init`);
|
|
296
|
+
|
|
297
|
+
// Pwsh
|
|
298
|
+
if (pwshPath) {
|
|
299
|
+
const pwshKey = `${HKCU_SHELL}\\${DIR_PWSH}`;
|
|
300
|
+
regAdd(pwshKey, 'MUIVerb', 'REG_SZ', '📂 在此打开终端');
|
|
301
|
+
regAdd(pwshKey, 'Icon', 'REG_SZ', pwshPath);
|
|
302
|
+
regAdd(`${pwshKey}\\command`, '', 'REG_SZ',
|
|
303
|
+
`"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '%V'"`);
|
|
283
304
|
}
|
|
284
305
|
|
|
285
|
-
//
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
regAdd(dirParent, 'Icon', 'REG_SZ', iconPath);
|
|
289
|
-
regAdd(dirParent, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_DIR);
|
|
306
|
+
// Build SubCommands string
|
|
307
|
+
const subCmds = [DIR_VIEW, DIR_COPYALL, DIR_COPYPICK];
|
|
308
|
+
if (pwshPath) subCmds.push(DIR_PWSH);
|
|
290
309
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
310
|
+
// Parent keys: Directory + Directory\Background
|
|
311
|
+
const roots = [
|
|
312
|
+
`${HKCU_SHELL}\\Directory\\shell`,
|
|
313
|
+
`${HKCU_SHELL}\\Directory\\Background\\shell`,
|
|
314
|
+
];
|
|
315
|
+
for (const root of roots) {
|
|
316
|
+
regAdd(`${root}\\${PARENT_KEY_NAME}`, 'MUIVerb', 'REG_SZ', '📧 用 juice 生成邮件 HTML');
|
|
317
|
+
regAdd(`${root}\\${PARENT_KEY_NAME}`, 'Icon', 'REG_SZ', iconPath);
|
|
318
|
+
regAdd(`${root}\\${PARENT_KEY_NAME}`, 'SubCommands', 'REG_SZ', subCmds.join(';'));
|
|
319
|
+
}
|
|
295
320
|
}
|
|
296
321
|
|
|
297
322
|
// ─── 注册 ─────────────────────────────────────────────────────────────────────
|
|
@@ -348,8 +373,20 @@ async function registerContextMenu() {
|
|
|
348
373
|
regAdd(parentKey, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_YAML);
|
|
349
374
|
}
|
|
350
375
|
|
|
351
|
-
// Directory / Background →
|
|
376
|
+
// Directory / Background → 子菜单(SubCommands + CommandStore)
|
|
352
377
|
console.log(chalk.gray(' 注册文件夹/空白处菜单...'));
|
|
378
|
+
// Clean up old individual entries from v2.3.x before registering new format
|
|
379
|
+
for (const root of [`${HKCU_SHELL}\\Directory\\shell`, `${HKCU_SHELL}\\Directory\\Background\\shell`]) {
|
|
380
|
+
regDelete(`${root}\\${PARENT_KEY_NAME}`);
|
|
381
|
+
regDelete(`${root}\\JuiceEmail.Init`);
|
|
382
|
+
regDelete(`${root}\\JuiceEmail.View`);
|
|
383
|
+
regDelete(`${root}\\JuiceEmail.Browse`);
|
|
384
|
+
regDelete(`${root}\\JuiceEmail.Pwsh`);
|
|
385
|
+
}
|
|
386
|
+
// Clean up old CommandStore entries for Dir menu (from any prior version)
|
|
387
|
+
for (const name of ['JuiceEmailDir.View', 'JuiceEmailDir.CopyAll', 'JuiceEmailDir.CopyPick', 'JuiceEmailDir.Pwsh']) {
|
|
388
|
+
regDelete(`${HKCU_SHELL}\\${name}`);
|
|
389
|
+
}
|
|
353
390
|
registerDirBgMenus(nodePath, scriptPath, iconPath, pwshPath);
|
|
354
391
|
|
|
355
392
|
if (!ok) {
|
|
@@ -373,10 +410,10 @@ async function registerContextMenu() {
|
|
|
373
410
|
` ├── 📋 浏览资源库 → juice view -i\n` +
|
|
374
411
|
(pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
|
|
375
412
|
`\n ${chalk.bold('文件夹 / 空白处')} 右键:\n` +
|
|
376
|
-
` ${chalk.bold('📧 juice
|
|
377
|
-
` ├──
|
|
378
|
-
` ├── 📦
|
|
379
|
-
` ├──
|
|
413
|
+
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
414
|
+
` ├── 📋 查看可用资源 → juice view\n` +
|
|
415
|
+
` ├── 📦 拷贝全部资源 → juice init --all\n` +
|
|
416
|
+
` ├── 📥 选择资源拷贝 → juice init\n` +
|
|
380
417
|
(pwshPath ? ` └── 📂 在此打开终端\n` : '') +
|
|
381
418
|
'\n' +
|
|
382
419
|
chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
|
|
@@ -395,22 +432,39 @@ async function unregisterContextMenu() {
|
|
|
395
432
|
|
|
396
433
|
let removed = 0;
|
|
397
434
|
|
|
398
|
-
// 清理 HKCU
|
|
435
|
+
// 清理 HKCU 父菜单(文件类型)
|
|
399
436
|
const allRoots = [
|
|
400
437
|
...HTML_ROOTS,
|
|
401
438
|
...YAML_ROOTS,
|
|
402
|
-
`${HKCU_SHELL}\\Directory\\shell`,
|
|
403
|
-
`${HKCU_SHELL}\\Directory\\Background\\shell`,
|
|
404
439
|
];
|
|
405
440
|
for (const root of allRoots) {
|
|
406
441
|
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
407
442
|
}
|
|
408
443
|
|
|
409
|
-
//
|
|
444
|
+
// 清理 Directory / Background 父菜单 + 旧版独立条目
|
|
445
|
+
const dirBgRoots = [
|
|
446
|
+
`${HKCU_SHELL}\\Directory\\shell`,
|
|
447
|
+
`${HKCU_SHELL}\\Directory\\Background\\shell`,
|
|
448
|
+
];
|
|
449
|
+
// New parent key + old individual entries from v2.3.x
|
|
450
|
+
const dirBgKeys = [PARENT_KEY_NAME, 'JuiceEmail.Init', 'JuiceEmail.View', 'JuiceEmail.Browse', 'JuiceEmail.Pwsh'];
|
|
451
|
+
for (const root of dirBgRoots) {
|
|
452
|
+
for (const key of dirBgKeys) {
|
|
453
|
+
if (regDelete(`${root}\\${key}`)) removed++;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// CommandStore entries for Dir menus
|
|
457
|
+
for (const name of ['JuiceEmailDir.View', 'JuiceEmailDir.CopyAll', 'JuiceEmailDir.CopyPick', 'JuiceEmailDir.Pwsh']) {
|
|
458
|
+
if (regDelete(`${HKCU_SHELL}\\${name}`)) removed++;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// 清理所有子命令容器(含旧版 Dir/Bg 容器)
|
|
410
462
|
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`)) removed++;
|
|
411
463
|
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`)) removed++;
|
|
412
464
|
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`)) removed++;
|
|
413
465
|
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`)) removed++;
|
|
466
|
+
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`)) removed++;
|
|
467
|
+
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`)) removed++;
|
|
414
468
|
|
|
415
469
|
// 清理旧版共享容器(v2.1.14 之前只有一个容器)
|
|
416
470
|
regDelete(`${HKCU_SHELL}\\JuiceEmail.SubCommands`);
|
package/src/init.js
CHANGED
|
@@ -71,8 +71,7 @@ function deriveDefaultName(filePath) {
|
|
|
71
71
|
async function directCopy(srcPath, cwd) {
|
|
72
72
|
const resolved = path.resolve(srcPath);
|
|
73
73
|
if (!fs.existsSync(resolved)) {
|
|
74
|
-
|
|
75
|
-
process.exit(1);
|
|
74
|
+
throw new Error(`文件不存在:${resolved}`);
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
const destName = await promptOutputName(
|
|
@@ -231,9 +230,110 @@ async function interactiveInit(edmDir, cwd) {
|
|
|
231
230
|
|
|
232
231
|
// ─── Main Entry ──────────────────────────────────────────────────────────────
|
|
233
232
|
|
|
234
|
-
|
|
233
|
+
function copyDir(src, dest) {
|
|
234
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
235
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
236
|
+
const srcPath = path.join(src, entry.name);
|
|
237
|
+
const destPath = path.join(dest, entry.name);
|
|
238
|
+
if (entry.isDirectory()) {
|
|
239
|
+
copyDir(srcPath, destPath);
|
|
240
|
+
} else {
|
|
241
|
+
fs.copyFileSync(srcPath, destPath);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function dirSize(dir) {
|
|
247
|
+
let size = 0;
|
|
248
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
249
|
+
const p = path.join(dir, entry.name);
|
|
250
|
+
if (entry.isDirectory()) {
|
|
251
|
+
size += dirSize(p);
|
|
252
|
+
} else {
|
|
253
|
+
size += fs.statSync(p).size;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return size;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function countFiles(dir) {
|
|
260
|
+
let count = 0;
|
|
261
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
262
|
+
if (entry.isDirectory()) {
|
|
263
|
+
count += countFiles(path.join(dir, entry.name));
|
|
264
|
+
} else {
|
|
265
|
+
count++;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return count;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function findNextEdmVersion(targetDir) {
|
|
272
|
+
let v = 1;
|
|
273
|
+
while (true) {
|
|
274
|
+
const candidate = path.join(targetDir, 'edm-v' + v);
|
|
275
|
+
if (!fs.existsSync(candidate)) return candidate;
|
|
276
|
+
v++;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
235
281
|
const cwd = process.cwd();
|
|
236
282
|
|
|
283
|
+
// --all: copy entire EDM directory
|
|
284
|
+
if (all) {
|
|
285
|
+
let edmDir;
|
|
286
|
+
try {
|
|
287
|
+
edmDir = resolveEdmDir();
|
|
288
|
+
} catch (err) {
|
|
289
|
+
console.error(chalk.red(`\n ✘ ${err.message}\n`));
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
const target = all === true ? cwd : path.resolve(all);
|
|
293
|
+
const destDir = path.join(target, 'edm');
|
|
294
|
+
|
|
295
|
+
// Prevent deleting the source when target overlaps
|
|
296
|
+
if (path.resolve(edmDir) === path.resolve(destDir)) {
|
|
297
|
+
console.log(chalk.yellow('目标目录与 EDM 资源库相同,无需拷贝。\n'));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
console.log(chalk.cyan(`\n 拷贝 EDM 资源库...`));
|
|
301
|
+
console.log(chalk.gray(` 源:${edmDir}`));
|
|
302
|
+
console.log(chalk.gray(` 目标:${destDir}`));
|
|
303
|
+
|
|
304
|
+
if (fs.existsSync(destDir)) {
|
|
305
|
+
const { select } = await import('@inquirer/prompts');
|
|
306
|
+
const vName = findNextEdmVersion(target);
|
|
307
|
+
const action = await select({
|
|
308
|
+
message: `目录 ${destDir} 已存在:`,
|
|
309
|
+
choices: [
|
|
310
|
+
{ name: '覆盖', value: 'overwrite' },
|
|
311
|
+
{ name: `自动版本号(${path.basename(vName)})`, value: 'version' },
|
|
312
|
+
{ name: '取消', value: 'cancel' },
|
|
313
|
+
],
|
|
314
|
+
});
|
|
315
|
+
if (action === 'cancel') {
|
|
316
|
+
console.log(chalk.gray('已取消。\n'));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (action === 'version') {
|
|
320
|
+
copyDir(edmDir, vName);
|
|
321
|
+
const edmSize = dirSize(vName);
|
|
322
|
+
const fileCount = countFiles(vName);
|
|
323
|
+
console.log(chalk.green(`\n✔ 已拷贝 EDM 资源库 → ${path.basename(vName)}(${fileCount} 个文件,${fmtBytes(edmSize)})\n`));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// overwrite: remove and replace
|
|
327
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
copyDir(edmDir, destDir);
|
|
331
|
+
const edmSize = dirSize(destDir);
|
|
332
|
+
const fileCount = countFiles(destDir);
|
|
333
|
+
console.log(chalk.green(`\n✔ 已拷贝 EDM 资源库(${fileCount} 个文件,${fmtBytes(edmSize)})\n`));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
237
337
|
// Direct copy modes
|
|
238
338
|
if (template) {
|
|
239
339
|
await directCopy(template, cwd);
|
package/src/view.js
CHANGED
|
@@ -12,6 +12,27 @@ function fmtBytes(b) {
|
|
|
12
12
|
return b < 1024 ? `${b} B` : `${(b / 1024).toFixed(1)} KB`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Copy src to dest, auto-versioning (-v1, -v2, ...) if dest exists.
|
|
17
|
+
* Returns the actual destination path used.
|
|
18
|
+
*/
|
|
19
|
+
function copyFileSafe(srcPath, destPath) {
|
|
20
|
+
if (!fs.existsSync(destPath)) {
|
|
21
|
+
fs.copyFileSync(srcPath, destPath);
|
|
22
|
+
return destPath;
|
|
23
|
+
}
|
|
24
|
+
const parsed = path.parse(destPath);
|
|
25
|
+
let v = 1;
|
|
26
|
+
while (true) {
|
|
27
|
+
const alt = path.join(parsed.dir, parsed.name + '-v' + v + parsed.ext);
|
|
28
|
+
if (!fs.existsSync(alt)) {
|
|
29
|
+
fs.copyFileSync(srcPath, alt);
|
|
30
|
+
return alt;
|
|
31
|
+
}
|
|
32
|
+
v++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
// ─── Path Parser ──────────────────────────────────────────────────────────────
|
|
16
37
|
|
|
17
38
|
/**
|
|
@@ -327,13 +348,17 @@ async function showCheckbox(title, choices) {
|
|
|
327
348
|
* Trigger copy via the init module.
|
|
328
349
|
*/
|
|
329
350
|
async function copyResource(type, resourcePath, cwd) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
351
|
+
try {
|
|
352
|
+
const { runInitMode } = require('./init');
|
|
353
|
+
if (type === 'template') {
|
|
354
|
+
await runInitMode({ template: resourcePath });
|
|
355
|
+
} else if (type === 'snippet') {
|
|
356
|
+
await runInitMode({ snippet: resourcePath });
|
|
357
|
+
} else if (type === 'config') {
|
|
358
|
+
await runInitMode({ config: resourcePath });
|
|
359
|
+
}
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.error(chalk.red(` ✘ 拷贝失败:${err.message}`));
|
|
337
362
|
}
|
|
338
363
|
}
|
|
339
364
|
|
|
@@ -420,20 +445,6 @@ async function interactiveBrowse(edmDir, startParsed) {
|
|
|
420
445
|
description: allSeries.length + ' 个系列',
|
|
421
446
|
});
|
|
422
447
|
}
|
|
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
448
|
break;
|
|
438
449
|
}
|
|
439
450
|
|
|
@@ -511,27 +522,15 @@ async function interactiveBrowse(edmDir, startParsed) {
|
|
|
511
522
|
if (infoLines.length > 0) {
|
|
512
523
|
console.log(chalk.dim('\n ' + infoLines.join('\n ') + '\n'));
|
|
513
524
|
}
|
|
514
|
-
//
|
|
515
|
-
|
|
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
|
|
525
|
+
// Multi-select copy
|
|
526
|
+
const hasResources = fs.existsSync(snipPath) || configs.length > 0;
|
|
528
527
|
let versions;
|
|
529
528
|
const brandDir = path.join(edmDir, current.brand);
|
|
530
529
|
try { versions = findTemplateVersions(brandDir); } catch (_) { versions = []; }
|
|
531
|
-
if (versions.length > 0) {
|
|
530
|
+
if (hasResources || versions.length > 0) {
|
|
532
531
|
choices.push({
|
|
533
|
-
name: '📥
|
|
534
|
-
value: { action: 'copy-
|
|
532
|
+
name: '📥 拷贝到当前目录',
|
|
533
|
+
value: { action: 'copy-multi' },
|
|
535
534
|
});
|
|
536
535
|
}
|
|
537
536
|
break;
|
|
@@ -558,125 +557,91 @@ async function interactiveBrowse(edmDir, startParsed) {
|
|
|
558
557
|
|
|
559
558
|
// Copy actions
|
|
560
559
|
if (result.action === 'copy-template') {
|
|
561
|
-
|
|
562
|
-
if (
|
|
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) {
|
|
560
|
+
const v = current.versionData;
|
|
561
|
+
if (v && v.templatePath) {
|
|
594
562
|
console.log();
|
|
595
|
-
await copyResource('template',
|
|
563
|
+
await copyResource('template', v.templatePath, process.cwd());
|
|
596
564
|
console.log();
|
|
597
565
|
}
|
|
598
566
|
continue;
|
|
599
567
|
}
|
|
600
568
|
|
|
601
|
-
if (result.action === 'copy-
|
|
569
|
+
if (result.action === 'copy-multi') {
|
|
570
|
+
const brandDir = path.join(edmDir, current.brand);
|
|
571
|
+
const copyItems = [];
|
|
572
|
+
|
|
573
|
+
// Template
|
|
574
|
+
let versions;
|
|
575
|
+
try { versions = findTemplateVersions(brandDir); } catch (_) { versions = []; }
|
|
576
|
+
if (versions.length > 0) {
|
|
577
|
+
const tplPath = versions[0].templatePath;
|
|
578
|
+
copyItems.push({
|
|
579
|
+
name: `📋 模板 HTML(${versions[0].meta.name || versions[0].name})`,
|
|
580
|
+
value: 'template',
|
|
581
|
+
description: path.basename(tplPath),
|
|
582
|
+
checked: true,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Snippet
|
|
602
587
|
const snipPath = path.join(current.variantData.path, 'snippet.html');
|
|
603
588
|
if (fs.existsSync(snipPath)) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
589
|
+
copyItems.push({
|
|
590
|
+
name: '🧩 片段 HTML',
|
|
591
|
+
value: 'snippet',
|
|
592
|
+
description: path.basename(snipPath),
|
|
593
|
+
checked: true,
|
|
594
|
+
});
|
|
607
595
|
}
|
|
608
|
-
continue;
|
|
609
|
-
}
|
|
610
596
|
|
|
611
|
-
|
|
597
|
+
// Config
|
|
612
598
|
const configs = findConfigs(current.variantData.path);
|
|
613
|
-
if (configs.length
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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);
|
|
599
|
+
if (configs.length > 0) {
|
|
600
|
+
copyItems.push({
|
|
601
|
+
name: '⚙️ 配置文件',
|
|
602
|
+
value: 'config',
|
|
603
|
+
description: configs.map(c => c.name).join(', '),
|
|
604
|
+
checked: true,
|
|
605
|
+
});
|
|
647
606
|
}
|
|
648
|
-
if (!series) continue;
|
|
649
607
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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;
|
|
608
|
+
if (copyItems.length > 0) {
|
|
609
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
610
|
+
const selected = await checkbox({
|
|
611
|
+
message: '选择要拷贝的内容:',
|
|
612
|
+
choices: copyItems,
|
|
613
|
+
});
|
|
663
614
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
615
|
+
if (selected.length > 0) {
|
|
616
|
+
// Include template version in default name for clarity
|
|
617
|
+
const versionName = versions.length > 0 ? versions[0].name : 'template';
|
|
618
|
+
const defaultBaseName = `${current.brand}-${versionName}-${current.series}-${current.variant}`;
|
|
619
|
+
const { promptOutputName: pn } = require('./snippet');
|
|
620
|
+
const outputBaseName = await pn(defaultBaseName, process.cwd());
|
|
621
|
+
const cwd = process.cwd();
|
|
622
|
+
const cwdRel = (p) => './' + path.relative(cwd, p);
|
|
623
|
+
|
|
624
|
+
console.log(chalk.green('\n✔ 已拷贝:'));
|
|
625
|
+
if (selected.includes('template') && versions.length > 0) {
|
|
626
|
+
const src = versions[0].templatePath;
|
|
627
|
+
const dest = path.join(cwd, outputBaseName + path.extname(src));
|
|
628
|
+
const actual = copyFileSafe(src, dest);
|
|
629
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(actual)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(actual).size) + ')')}`);
|
|
630
|
+
}
|
|
631
|
+
if (selected.includes('snippet') && fs.existsSync(snipPath)) {
|
|
632
|
+
const dest = path.join(cwd, outputBaseName + '-snippet' + path.extname(snipPath));
|
|
633
|
+
const actual = copyFileSafe(snipPath, dest);
|
|
634
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(actual)} ${chalk.gray('(片段, ' + fmtBytes(fs.statSync(actual).size) + ')')}`);
|
|
635
|
+
}
|
|
636
|
+
if (selected.includes('config') && configs.length > 0) {
|
|
637
|
+
const optimal = configs.find(c => c.isOptimal);
|
|
638
|
+
const cfgPath = optimal ? optimal.path : configs[0].path;
|
|
639
|
+
const dest = path.join(cwd, 'juice.yaml');
|
|
640
|
+
const actual = copyFileSafe(cfgPath, dest);
|
|
641
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(actual)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(actual).size) + ')')}`);
|
|
642
|
+
}
|
|
643
|
+
console.log();
|
|
644
|
+
}
|
|
680
645
|
}
|
|
681
646
|
continue;
|
|
682
647
|
}
|