juice-email-cli 2.2.2 → 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/bin/juice.js
CHANGED
|
@@ -75,7 +75,46 @@ program
|
|
|
75
75
|
└── 📂 打开 PowerShell (仅已安装 pwsh 时出现)
|
|
76
76
|
|
|
77
77
|
更多信息:https://gitee.com/siriussupreme/juice-cli
|
|
78
|
-
`)
|
|
78
|
+
`);
|
|
79
|
+
|
|
80
|
+
// ─── Subcommand: juice view ─────────────────────────────────────────────
|
|
81
|
+
program
|
|
82
|
+
.command('view [path]')
|
|
83
|
+
.description('查看 EDM 资源:品牌、模板、系列、片段变体')
|
|
84
|
+
.option('-i, --interactive', '交互式浏览,可上下翻层级')
|
|
85
|
+
.option('--templates', '列出所有模板')
|
|
86
|
+
.option('--series', '列出所有系列')
|
|
87
|
+
.option('--snippets', '列出所有片段')
|
|
88
|
+
.action(async (viewPath, options) => {
|
|
89
|
+
const { runViewMode } = require('../src/view');
|
|
90
|
+
await runViewMode({
|
|
91
|
+
viewPath: viewPath || null,
|
|
92
|
+
interactive: !!options.interactive,
|
|
93
|
+
scope: options.templates ? 'templates'
|
|
94
|
+
: (options.series ? 'series'
|
|
95
|
+
: (options.snippets ? 'snippets' : null)),
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ─── Subcommand: juice init ─────────────────────────────────────────────
|
|
100
|
+
program
|
|
101
|
+
.command('init [path]')
|
|
102
|
+
.description('从 EDM 资源拷贝模板/片段/配置到当前目录')
|
|
103
|
+
.option('-t, --template <file-path>', '仅拷贝模板 HTML 文件')
|
|
104
|
+
.option('-s, --snippet <file-path>', '仅拷贝片段 HTML 文件')
|
|
105
|
+
.option('-c, --config <file-path>', '仅拷贝配置 YAML 文件')
|
|
106
|
+
.action(async (initPath, options) => {
|
|
107
|
+
const { runInitMode } = require('../src/init');
|
|
108
|
+
await runInitMode({
|
|
109
|
+
initPath: initPath || null,
|
|
110
|
+
template: options.template || null,
|
|
111
|
+
snippet: options.snippet || null,
|
|
112
|
+
config: options.config || null,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ─── Default action (backward compatible) ───────────────────────────────
|
|
117
|
+
program
|
|
79
118
|
.action(async (options) => {
|
|
80
119
|
// 注册/卸载右键菜单
|
|
81
120
|
if (options.install) {
|
package/package.json
CHANGED
package/src/context-menu.js
CHANGED
|
@@ -90,10 +90,13 @@ const YAML_ROOTS = [
|
|
|
90
90
|
|
|
91
91
|
// 子命令名称常量
|
|
92
92
|
const SUBCMDS = {
|
|
93
|
-
generate:
|
|
94
|
-
snippet:
|
|
95
|
-
config:
|
|
96
|
-
|
|
93
|
+
generate: 'JuiceEmail.Generate',
|
|
94
|
+
snippet: 'JuiceEmail.Snippet',
|
|
95
|
+
config: 'JuiceEmail.WithConfig',
|
|
96
|
+
viewEdm: 'JuiceEmail.ViewEdm',
|
|
97
|
+
viewEdmInt:'JuiceEmail.ViewEdmInteractive',
|
|
98
|
+
initEdm: 'JuiceEmail.InitEdm',
|
|
99
|
+
pwsh: 'JuiceEmail.OpenPwsh',
|
|
97
100
|
};
|
|
98
101
|
|
|
99
102
|
const PARENT_KEY_NAME = 'JuiceEmail';
|
|
@@ -101,6 +104,8 @@ const PARENT_KEY_NAME = 'JuiceEmail';
|
|
|
101
104
|
// ExtendedSubCommandsKey 指向的容器路径(相对于 HKCU\Software\Classes)
|
|
102
105
|
const SUB_CMDS_CONTAINER_HTML = 'JuiceEmail.SubCommands';
|
|
103
106
|
const SUB_CMDS_CONTAINER_YAML = 'JuiceEmail.SubCommands.Yaml';
|
|
107
|
+
const SUB_CMDS_CONTAINER_DIR = 'JuiceEmail.SubCommands.Dir';
|
|
108
|
+
const SUB_CMDS_CONTAINER_BG = 'JuiceEmail.SubCommands.Bg';
|
|
104
109
|
|
|
105
110
|
// ─── 旧版残留清理 ─────────────────────────────────────────────────────────────
|
|
106
111
|
|
|
@@ -209,6 +214,19 @@ function registerSubCommands(containerPath, kind, nodePath, scriptPath, iconPath
|
|
|
209
214
|
regAdd(`${cfgKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, '-c %1'));
|
|
210
215
|
}
|
|
211
216
|
|
|
217
|
+
// Common items for file-type containers (html + yaml)
|
|
218
|
+
if (kind === 'html' || kind === 'yaml') {
|
|
219
|
+
const viewKey = `${containerPath}\\shell\\${SUBCMDS.viewEdm}`;
|
|
220
|
+
regAdd(viewKey, 'MUIVerb', 'REG_SZ', '📦 查看资源列表');
|
|
221
|
+
regAdd(viewKey, 'Icon', 'REG_SZ', iconPath);
|
|
222
|
+
regAdd(`${viewKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view'));
|
|
223
|
+
|
|
224
|
+
const viewIntKey = `${containerPath}\\shell\\${SUBCMDS.viewEdmInt}`;
|
|
225
|
+
regAdd(viewIntKey, 'MUIVerb', 'REG_SZ', '📋 浏览资源库');
|
|
226
|
+
regAdd(viewIntKey, 'Icon', 'REG_SZ', iconPath);
|
|
227
|
+
regAdd(`${viewIntKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view -i'));
|
|
228
|
+
}
|
|
229
|
+
|
|
212
230
|
// pwsh — 始终显示(可选)
|
|
213
231
|
if (pwshPath) {
|
|
214
232
|
const pwshKey = `${containerPath}\\shell\\${SUBCMDS.pwsh}`;
|
|
@@ -219,6 +237,54 @@ function registerSubCommands(containerPath, kind, nodePath, scriptPath, iconPath
|
|
|
219
237
|
}
|
|
220
238
|
}
|
|
221
239
|
|
|
240
|
+
// ─── Directory / Background 菜单注册 ──────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
function registerDirBgMenus(nodePath, scriptPath, iconPath, pwshPath) {
|
|
243
|
+
// Register directory container subcommands
|
|
244
|
+
const dirContainer = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`;
|
|
245
|
+
const bgContainer = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`;
|
|
246
|
+
|
|
247
|
+
for (const containerPath of [dirContainer, bgContainer]) {
|
|
248
|
+
// 📥 从资源库拷贝到此处
|
|
249
|
+
const initKey = `${containerPath}\\shell\\${SUBCMDS.initEdm}`;
|
|
250
|
+
regAdd(initKey, 'MUIVerb', 'REG_SZ', '📥 从资源库拷贝到此处');
|
|
251
|
+
regAdd(initKey, 'Icon', 'REG_SZ', iconPath);
|
|
252
|
+
regAdd(`${initKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'init'));
|
|
253
|
+
|
|
254
|
+
// 📦 查看资源列表
|
|
255
|
+
const viewKey = `${containerPath}\\shell\\${SUBCMDS.viewEdm}`;
|
|
256
|
+
regAdd(viewKey, 'MUIVerb', 'REG_SZ', '📦 查看资源列表');
|
|
257
|
+
regAdd(viewKey, 'Icon', 'REG_SZ', iconPath);
|
|
258
|
+
regAdd(`${viewKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view'));
|
|
259
|
+
|
|
260
|
+
// 📋 浏览资源库
|
|
261
|
+
const viewIntKey = `${containerPath}\\shell\\${SUBCMDS.viewEdmInt}`;
|
|
262
|
+
regAdd(viewIntKey, 'MUIVerb', 'REG_SZ', '📋 浏览资源库');
|
|
263
|
+
regAdd(viewIntKey, 'Icon', 'REG_SZ', iconPath);
|
|
264
|
+
regAdd(`${viewIntKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view -i'));
|
|
265
|
+
|
|
266
|
+
// 📂 在此打开终端
|
|
267
|
+
if (pwshPath) {
|
|
268
|
+
const pwshKey = `${containerPath}\\shell\\${SUBCMDS.pwsh}`;
|
|
269
|
+
regAdd(pwshKey, 'MUIVerb', 'REG_SZ', '📂 在此打开终端');
|
|
270
|
+
regAdd(pwshKey, 'Icon', 'REG_SZ', pwshPath);
|
|
271
|
+
regAdd(`${pwshKey}\\command`, '', 'REG_SZ',
|
|
272
|
+
`"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '%1'"`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Parent keys: Directory and Directory\Background
|
|
277
|
+
const dirParent = `${HKCU_SHELL}\\Directory\\shell\\${PARENT_KEY_NAME}`;
|
|
278
|
+
regAdd(dirParent, 'MUIVerb', 'REG_SZ', '📧 juice 邮件工具');
|
|
279
|
+
regAdd(dirParent, 'Icon', 'REG_SZ', iconPath);
|
|
280
|
+
regAdd(dirParent, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_DIR);
|
|
281
|
+
|
|
282
|
+
const bgParent = `${HKCU_SHELL}\\Directory\\Background\\shell\\${PARENT_KEY_NAME}`;
|
|
283
|
+
regAdd(bgParent, 'MUIVerb', 'REG_SZ', '📧 juice 邮件工具');
|
|
284
|
+
regAdd(bgParent, 'Icon', 'REG_SZ', iconPath);
|
|
285
|
+
regAdd(bgParent, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_BG);
|
|
286
|
+
}
|
|
287
|
+
|
|
222
288
|
// ─── 注册 ─────────────────────────────────────────────────────────────────────
|
|
223
289
|
|
|
224
290
|
async function registerContextMenu() {
|
|
@@ -243,6 +309,8 @@ async function registerContextMenu() {
|
|
|
243
309
|
// 先删除旧容器再重建,避免注册残留旧子命令(如旧版 HTML 容器的 WithConfig)
|
|
244
310
|
regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`);
|
|
245
311
|
regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`);
|
|
312
|
+
regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`);
|
|
313
|
+
regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`);
|
|
246
314
|
|
|
247
315
|
// HTML 容器:generate + snippet + pwsh
|
|
248
316
|
const htmlContainer = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`;
|
|
@@ -268,6 +336,9 @@ async function registerContextMenu() {
|
|
|
268
336
|
regAdd(parentKey, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_YAML);
|
|
269
337
|
}
|
|
270
338
|
|
|
339
|
+
// Directory / Background → 独立容器
|
|
340
|
+
registerDirBgMenus(nodePath, scriptPath, iconPath, pwshPath);
|
|
341
|
+
|
|
271
342
|
if (!ok) {
|
|
272
343
|
console.log(chalk.yellow(' ⚠ 部分注册表项写入失败,右键菜单可能不完整。\n'));
|
|
273
344
|
}
|
|
@@ -279,13 +350,21 @@ async function registerContextMenu() {
|
|
|
279
350
|
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
280
351
|
` ├── 📄 作为模板,生成邮件 HTML → juice -f(后台执行)\n` +
|
|
281
352
|
` ├── 🧩 作为片段,拼接邮件 HTML → juice -s(交互选择模板)\n` +
|
|
353
|
+
` ├── 📦 查看资源列表 → juice view\n` +
|
|
354
|
+
` ├── 📋 浏览资源库 → juice view -i\n` +
|
|
282
355
|
(pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
|
|
283
356
|
`\n ${chalk.bold('.yaml / .yml')} 文件右键:\n` +
|
|
284
357
|
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
358
|
+
` ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c(交互选择品牌/模板/片段)\n` +
|
|
359
|
+
` ├── 📦 查看资源列表 → juice view\n` +
|
|
360
|
+
` ├── 📋 浏览资源库 → juice view -i\n` +
|
|
361
|
+
(pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
|
|
362
|
+
`\n ${chalk.bold('文件夹 / 空白处')} 右键:\n` +
|
|
363
|
+
` ${chalk.bold('📧 juice 邮件工具')}\n` +
|
|
364
|
+
` ├── 📥 从资源库拷贝到此处 → juice init\n` +
|
|
365
|
+
` ├── 📦 查看资源列表 → juice view\n` +
|
|
366
|
+
` ├── 📋 浏览资源库 → juice view -i\n` +
|
|
367
|
+
(pwshPath ? ` └── 📂 在此打开终端\n` : '') +
|
|
289
368
|
'\n' +
|
|
290
369
|
chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
|
|
291
370
|
);
|
|
@@ -303,15 +382,22 @@ async function unregisterContextMenu() {
|
|
|
303
382
|
|
|
304
383
|
let removed = 0;
|
|
305
384
|
|
|
306
|
-
// 清理 HKCU 父菜单(4
|
|
307
|
-
const allRoots = [
|
|
385
|
+
// 清理 HKCU 父菜单(4 个文件类型 + Directory + Background)
|
|
386
|
+
const allRoots = [
|
|
387
|
+
...HTML_ROOTS,
|
|
388
|
+
...YAML_ROOTS,
|
|
389
|
+
`${HKCU_SHELL}\\Directory\\shell`,
|
|
390
|
+
`${HKCU_SHELL}\\Directory\\Background\\shell`,
|
|
391
|
+
];
|
|
308
392
|
for (const root of allRoots) {
|
|
309
393
|
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
310
394
|
}
|
|
311
395
|
|
|
312
|
-
//
|
|
396
|
+
// 清理所有子命令容器
|
|
313
397
|
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`)) removed++;
|
|
314
398
|
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`)) removed++;
|
|
399
|
+
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`)) removed++;
|
|
400
|
+
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`)) removed++;
|
|
315
401
|
|
|
316
402
|
// 清理旧版共享容器(v2.1.14 之前只有一个容器)
|
|
317
403
|
regDelete(`${HKCU_SHELL}\\JuiceEmail.SubCommands`);
|
package/src/init.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
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
|
+
promptBrand, promptTemplateVersion, promptSeries, promptSnippetVariant,
|
|
10
|
+
promptOutputName, checkOutputConflicts, findNextVersion,
|
|
11
|
+
} = require('./snippet');
|
|
12
|
+
|
|
13
|
+
function fmtBytes(b) {
|
|
14
|
+
return b < 1024 ? `${b} B` : `${(b / 1024).toFixed(1)} KB`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function formatName(meta, dirName) {
|
|
20
|
+
const display = meta.name || dirName;
|
|
21
|
+
return display !== dirName
|
|
22
|
+
? chalk.bold.cyan(display) + ' ' + chalk.gray(`(${dirName})`)
|
|
23
|
+
: chalk.bold.cyan(dirName);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Copy a single file to CWD with an optional custom name.
|
|
28
|
+
* Returns the destination path.
|
|
29
|
+
*/
|
|
30
|
+
function copyFileToCwd(srcPath, cwd, destName) {
|
|
31
|
+
const dest = path.join(cwd, destName || path.basename(srcPath));
|
|
32
|
+
if (fs.existsSync(dest)) {
|
|
33
|
+
// Find a non-conflicting name
|
|
34
|
+
const parsed = path.parse(dest);
|
|
35
|
+
let v = 1;
|
|
36
|
+
while (true) {
|
|
37
|
+
const candidate = path.join(cwd, parsed.name + '-v' + v + parsed.ext);
|
|
38
|
+
if (!fs.existsSync(candidate)) {
|
|
39
|
+
const altName = parsed.name + '-v' + v + parsed.ext;
|
|
40
|
+
const altPath = path.join(cwd, altName);
|
|
41
|
+
console.log(chalk.yellow(` ⚠ ${destName || path.basename(srcPath)} 已存在,使用:${altName}`));
|
|
42
|
+
fs.copyFileSync(srcPath, altPath);
|
|
43
|
+
return altPath;
|
|
44
|
+
}
|
|
45
|
+
v++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
fs.copyFileSync(srcPath, dest);
|
|
49
|
+
return dest;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Direct Copy (--template / --snippet / --config) ─────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Derive a human-friendly default name from an EDM file path.
|
|
56
|
+
* e.g. edm/elabscience/templates/standard/template.html → elabscience-standard
|
|
57
|
+
* edm/elabscience/series/literature/default/snippet.html → elabscience-literature-default-snippet
|
|
58
|
+
*/
|
|
59
|
+
function deriveDefaultName(filePath) {
|
|
60
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
61
|
+
// Try to match edm/<brand>/templates/<version>/...
|
|
62
|
+
const tplMatch = normalized.match(/edm\/([^/]+)\/templates\/([^/]+)/);
|
|
63
|
+
if (tplMatch) return `${tplMatch[1]}-${tplMatch[2]}`;
|
|
64
|
+
// Try to match edm/<brand>/series/<series>/<variant>/...
|
|
65
|
+
const snipMatch = normalized.match(/edm\/([^/]+)\/series\/([^/]+)\/([^/]+)/);
|
|
66
|
+
if (snipMatch) return `${snipMatch[1]}-${snipMatch[2]}-${snipMatch[3]}`;
|
|
67
|
+
// Fallback: source file basename
|
|
68
|
+
return path.parse(filePath).name;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function directCopy(srcPath, cwd) {
|
|
72
|
+
const resolved = path.resolve(srcPath);
|
|
73
|
+
if (!fs.existsSync(resolved)) {
|
|
74
|
+
console.error(chalk.red(`\n ✘ 文件不存在:${resolved}\n`));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const destName = await promptOutputName(
|
|
79
|
+
deriveDefaultName(resolved),
|
|
80
|
+
cwd
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const dest = path.join(cwd, destName + path.extname(resolved));
|
|
84
|
+
fs.copyFileSync(resolved, dest);
|
|
85
|
+
const stat = fs.statSync(dest);
|
|
86
|
+
|
|
87
|
+
console.log(chalk.green('\n✔ 已拷贝:'));
|
|
88
|
+
console.log(` ${chalk.cyan('·')} ./${path.relative(cwd, dest)} ${chalk.gray('(' + fmtBytes(stat.size) + ')')}`);
|
|
89
|
+
console.log();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Interactive Init ────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
async function interactiveInit(edmDir, cwd) {
|
|
95
|
+
const brands = findBrands(edmDir);
|
|
96
|
+
const brand = await promptBrand(brands);
|
|
97
|
+
const versions = findTemplateVersions(brand.path);
|
|
98
|
+
const version = await promptTemplateVersion(versions);
|
|
99
|
+
|
|
100
|
+
// Series selection
|
|
101
|
+
const allSeries = findSeriesDirs(brand.path);
|
|
102
|
+
let series = null;
|
|
103
|
+
let variant = null;
|
|
104
|
+
|
|
105
|
+
if (allSeries.length > 0) {
|
|
106
|
+
const { select } = await import('@inquirer/prompts');
|
|
107
|
+
const seriesChoices = [
|
|
108
|
+
{ name: '[跳过] 仅使用模板', value: null },
|
|
109
|
+
...allSeries.map(s => {
|
|
110
|
+
const meta = s.meta;
|
|
111
|
+
const display = meta.name
|
|
112
|
+
? formatName(meta, s.name)
|
|
113
|
+
: chalk.bold.cyan(s.name);
|
|
114
|
+
return {
|
|
115
|
+
name: display,
|
|
116
|
+
value: s,
|
|
117
|
+
description: meta.description || undefined,
|
|
118
|
+
};
|
|
119
|
+
}),
|
|
120
|
+
];
|
|
121
|
+
series = await select({
|
|
122
|
+
message: '选择片段系列(可选):',
|
|
123
|
+
choices: seriesChoices,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (series) {
|
|
127
|
+
const variants = findSnippetVariants(series.path);
|
|
128
|
+
if (variants.length > 0) {
|
|
129
|
+
variant = await promptSnippetVariant(variants);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Multi-select what to copy
|
|
135
|
+
const copyItems = [];
|
|
136
|
+
copyItems.push({
|
|
137
|
+
name: '📋 模板 HTML',
|
|
138
|
+
value: 'template',
|
|
139
|
+
description: path.basename(version.templatePath),
|
|
140
|
+
checked: true,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (variant) {
|
|
144
|
+
const snipPath = path.join(variant.path, 'snippet.html');
|
|
145
|
+
if (fs.existsSync(snipPath)) {
|
|
146
|
+
copyItems.push({
|
|
147
|
+
name: '🧩 片段 HTML',
|
|
148
|
+
value: 'snippet',
|
|
149
|
+
description: path.basename(snipPath),
|
|
150
|
+
checked: true,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const configs = findConfigs(variant.path);
|
|
155
|
+
if (configs.length > 0) {
|
|
156
|
+
copyItems.push({
|
|
157
|
+
name: '⚙️ 配置文件',
|
|
158
|
+
value: 'config',
|
|
159
|
+
description: configs.map(c => c.name).join(', '),
|
|
160
|
+
checked: true,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
166
|
+
const selected = await checkbox({
|
|
167
|
+
message: '选择要拷贝的内容:',
|
|
168
|
+
choices: copyItems,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (selected.length === 0) {
|
|
172
|
+
console.log(chalk.gray('未选择任何内容,已取消。\n'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Output name
|
|
177
|
+
const defaultBaseName = variant
|
|
178
|
+
? `${brand.name}-${version.name}-${series.name}-${variant.name}`
|
|
179
|
+
: `${brand.name}-${version.name}`;
|
|
180
|
+
const outputBaseName = await promptOutputName(defaultBaseName, cwd);
|
|
181
|
+
|
|
182
|
+
// Copy files
|
|
183
|
+
console.log(chalk.green('\n✔ 已拷贝:'));
|
|
184
|
+
const cwdRel = (p) => './' + path.relative(cwd, p);
|
|
185
|
+
|
|
186
|
+
if (selected.includes('template')) {
|
|
187
|
+
const destName = outputBaseName + path.extname(version.templatePath);
|
|
188
|
+
const dest = copyFileToCwd(version.templatePath, cwd, destName);
|
|
189
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (selected.includes('snippet') && variant) {
|
|
193
|
+
const snipPath = path.join(variant.path, 'snippet.html');
|
|
194
|
+
const destName = outputBaseName + '-snippet' + path.extname(snipPath);
|
|
195
|
+
const dest = copyFileToCwd(snipPath, cwd, destName);
|
|
196
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(片段, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (selected.includes('config') && variant) {
|
|
200
|
+
const configs = findConfigs(variant.path);
|
|
201
|
+
let cfgPath;
|
|
202
|
+
if (configs.length === 1) {
|
|
203
|
+
cfgPath = configs[0].path;
|
|
204
|
+
} else {
|
|
205
|
+
// Use optimal config
|
|
206
|
+
const optimal = configs.find(c => c.isOptimal);
|
|
207
|
+
cfgPath = optimal ? optimal.path : configs[0].path;
|
|
208
|
+
}
|
|
209
|
+
const dest = copyFileToCwd(cfgPath, cwd, 'juice.yaml');
|
|
210
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Summary
|
|
214
|
+
console.log();
|
|
215
|
+
if (selected.includes('snippet') && selected.includes('template')) {
|
|
216
|
+
const snipFile = outputBaseName + '-snippet' + path.extname(path.join(variant.path, 'snippet.html'));
|
|
217
|
+
const tplFile = outputBaseName + path.extname(version.templatePath);
|
|
218
|
+
console.log(
|
|
219
|
+
' ' + chalk.dim('💡 下一步:') + '\n' +
|
|
220
|
+
' ' + chalk.cyan(`juice -s ${snipFile} -f ${tplFile}`) +
|
|
221
|
+
(selected.includes('config') ? chalk.cyan(' -c juice.yaml') : '') + '\n'
|
|
222
|
+
);
|
|
223
|
+
} else if (selected.includes('template')) {
|
|
224
|
+
const tplFile = outputBaseName + path.extname(version.templatePath);
|
|
225
|
+
console.log(
|
|
226
|
+
' ' + chalk.dim('💡 下一步:') + '\n' +
|
|
227
|
+
' ' + chalk.cyan(`juice -f ${tplFile}`) + '\n'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Main Entry ──────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
async function runInitMode({ initPath, template, snippet, config }) {
|
|
235
|
+
const cwd = process.cwd();
|
|
236
|
+
|
|
237
|
+
// Direct copy modes
|
|
238
|
+
if (template) {
|
|
239
|
+
await directCopy(template, cwd);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (snippet) {
|
|
243
|
+
await directCopy(snippet, cwd);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (config) {
|
|
247
|
+
await directCopy(config, cwd);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Path-based from EDM
|
|
252
|
+
if (initPath) {
|
|
253
|
+
const { parseViewPath } = require('./view');
|
|
254
|
+
let edmDir;
|
|
255
|
+
try {
|
|
256
|
+
edmDir = resolveEdmDir();
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error(chalk.red(`\n ✘ ${err.message}\n`));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let parsed;
|
|
263
|
+
try {
|
|
264
|
+
parsed = parseViewPath(initPath, edmDir);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error(chalk.red(`\n ✘ ${err.message}\n`));
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (parsed.type === 'template') {
|
|
271
|
+
await directCopy(parsed.versionData.templatePath, cwd);
|
|
272
|
+
} else if (parsed.type === 'variant') {
|
|
273
|
+
// Multi-select copy
|
|
274
|
+
const snipPath = path.join(parsed.variantData.path, 'snippet.html');
|
|
275
|
+
const configs = findConfigs(parsed.variantData.path);
|
|
276
|
+
const brandDir = path.join(edmDir, parsed.brand);
|
|
277
|
+
const versions = findTemplateVersions(brandDir);
|
|
278
|
+
const version = versions[0]; // default to first version
|
|
279
|
+
const tplPath = version.templatePath;
|
|
280
|
+
|
|
281
|
+
const copyItems = [
|
|
282
|
+
{ name: '📋 模板 HTML', value: 'template', description: path.basename(tplPath), checked: true },
|
|
283
|
+
];
|
|
284
|
+
if (fs.existsSync(snipPath)) {
|
|
285
|
+
copyItems.push({ name: '🧩 片段 HTML', value: 'snippet', description: path.basename(snipPath), checked: true });
|
|
286
|
+
}
|
|
287
|
+
if (configs.length > 0) {
|
|
288
|
+
copyItems.push({ name: '⚙️ 配置文件', value: 'config', description: configs.map(c => c.name).join(', '), checked: true });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
292
|
+
const selected = await checkbox({
|
|
293
|
+
message: '选择要拷贝的内容:',
|
|
294
|
+
choices: copyItems,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (selected.length === 0) {
|
|
298
|
+
console.log(chalk.gray('未选择任何内容,已取消。\n'));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const defaultBaseName = `${parsed.brand}-${parsed.seriesData ? parsed.series : 'series'}-${parsed.variant}`;
|
|
303
|
+
const outputBaseName = await promptOutputName(defaultBaseName, cwd);
|
|
304
|
+
|
|
305
|
+
console.log(chalk.green('\n✔ 已拷贝:'));
|
|
306
|
+
const cwdRel = (p) => './' + path.relative(cwd, p);
|
|
307
|
+
|
|
308
|
+
if (selected.includes('template')) {
|
|
309
|
+
const destName = outputBaseName + path.extname(tplPath);
|
|
310
|
+
const dest = copyFileToCwd(tplPath, cwd, destName);
|
|
311
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
312
|
+
}
|
|
313
|
+
if (selected.includes('snippet') && fs.existsSync(snipPath)) {
|
|
314
|
+
const destName = outputBaseName + '-snippet' + path.extname(snipPath);
|
|
315
|
+
const dest = copyFileToCwd(snipPath, cwd, destName);
|
|
316
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(片段, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
317
|
+
}
|
|
318
|
+
if (selected.includes('config') && configs.length > 0) {
|
|
319
|
+
const optimal = configs.find(c => c.isOptimal);
|
|
320
|
+
const cfgPath = optimal ? optimal.path : configs[0].path;
|
|
321
|
+
const dest = copyFileToCwd(cfgPath, cwd, 'juice.yaml');
|
|
322
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
323
|
+
}
|
|
324
|
+
console.log();
|
|
325
|
+
} else {
|
|
326
|
+
console.error(chalk.red(
|
|
327
|
+
`\n ✘ 无法从「${initPath}」初始化。\n` +
|
|
328
|
+
` 请指定:<brand>/templates/<version> 或 <brand>/<series>/<variant>\n`
|
|
329
|
+
));
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// No arguments: interactive mode
|
|
336
|
+
let edmDir;
|
|
337
|
+
try {
|
|
338
|
+
edmDir = resolveEdmDir();
|
|
339
|
+
} catch (err) {
|
|
340
|
+
console.error(chalk.red(`\n ✘ ${err.message}\n`));
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
await interactiveInit(edmDir, cwd);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = { runInitMode };
|
package/src/snippet.js
CHANGED
|
@@ -881,4 +881,39 @@ async function runInteractiveMode({ config: cliConfigPath }) {
|
|
|
881
881
|
});
|
|
882
882
|
}
|
|
883
883
|
|
|
884
|
-
module.exports = {
|
|
884
|
+
module.exports = {
|
|
885
|
+
runSnippetMode,
|
|
886
|
+
runInteractiveMode,
|
|
887
|
+
// EDM resolution & scanning
|
|
888
|
+
resolveEdmDir,
|
|
889
|
+
loadMeta,
|
|
890
|
+
findBrands,
|
|
891
|
+
findTemplateVersions,
|
|
892
|
+
findSeriesDirs,
|
|
893
|
+
findSnippetVariants,
|
|
894
|
+
findConfigs,
|
|
895
|
+
findHtmlFiles,
|
|
896
|
+
findYamlFiles,
|
|
897
|
+
findLocalConfig,
|
|
898
|
+
getBrand,
|
|
899
|
+
filterSeries,
|
|
900
|
+
// config
|
|
901
|
+
buildSnippetConfig,
|
|
902
|
+
// content
|
|
903
|
+
insertIntoContent,
|
|
904
|
+
reindentHtml,
|
|
905
|
+
resolveSnippetOutputPaths,
|
|
906
|
+
assembleSnippet,
|
|
907
|
+
// prompts
|
|
908
|
+
promptBrand,
|
|
909
|
+
promptTemplateVersion,
|
|
910
|
+
promptSeries,
|
|
911
|
+
promptSnippetVariant,
|
|
912
|
+
promptConfig,
|
|
913
|
+
promptConfirm,
|
|
914
|
+
promptOutputName,
|
|
915
|
+
// output helpers
|
|
916
|
+
checkOutputConflicts,
|
|
917
|
+
findNextVersion,
|
|
918
|
+
copyTemplateToCwd,
|
|
919
|
+
};
|