juice-email-cli 2.2.2 → 2.3.1
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 +117 -18
- 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
|
@@ -56,19 +56,28 @@ function regAdd(key, valueName, type, data) {
|
|
|
56
56
|
if (type) args.push('/t', type);
|
|
57
57
|
if (data !== undefined) args.push('/d', data);
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
try {
|
|
60
|
+
const result = spawnSync('reg', args, { stdio: 'pipe', timeout: 10000 });
|
|
61
|
+
if (result.status !== 0) {
|
|
62
|
+
const msg = (result.stderr || '').toString().trim();
|
|
63
|
+
console.warn(chalk.yellow(` ⚠ reg add 失败\n 键:${key}\n 原因:${msg}`));
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.warn(chalk.yellow(` ⚠ reg add 超时:${key}`));
|
|
63
69
|
return false;
|
|
64
70
|
}
|
|
65
|
-
return true;
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
function regDelete(key) {
|
|
69
74
|
if (!isWindows) return false;
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
try {
|
|
76
|
+
const result = spawnSync('reg', ['delete', key, '/f'], { stdio: 'pipe', timeout: 10000 });
|
|
77
|
+
return result.status === 0;
|
|
78
|
+
} catch (_) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
// ─── 菜单结构常量 ─────────────────────────────────────────────────────────────
|
|
@@ -90,10 +99,13 @@ const YAML_ROOTS = [
|
|
|
90
99
|
|
|
91
100
|
// 子命令名称常量
|
|
92
101
|
const SUBCMDS = {
|
|
93
|
-
generate:
|
|
94
|
-
snippet:
|
|
95
|
-
config:
|
|
96
|
-
|
|
102
|
+
generate: 'JuiceEmail.Generate',
|
|
103
|
+
snippet: 'JuiceEmail.Snippet',
|
|
104
|
+
config: 'JuiceEmail.WithConfig',
|
|
105
|
+
viewEdm: 'JuiceEmail.ViewEdm',
|
|
106
|
+
viewEdmInt:'JuiceEmail.ViewEdmInteractive',
|
|
107
|
+
initEdm: 'JuiceEmail.InitEdm',
|
|
108
|
+
pwsh: 'JuiceEmail.OpenPwsh',
|
|
97
109
|
};
|
|
98
110
|
|
|
99
111
|
const PARENT_KEY_NAME = 'JuiceEmail';
|
|
@@ -101,6 +113,8 @@ const PARENT_KEY_NAME = 'JuiceEmail';
|
|
|
101
113
|
// ExtendedSubCommandsKey 指向的容器路径(相对于 HKCU\Software\Classes)
|
|
102
114
|
const SUB_CMDS_CONTAINER_HTML = 'JuiceEmail.SubCommands';
|
|
103
115
|
const SUB_CMDS_CONTAINER_YAML = 'JuiceEmail.SubCommands.Yaml';
|
|
116
|
+
const SUB_CMDS_CONTAINER_DIR = 'JuiceEmail.SubCommands.Dir';
|
|
117
|
+
const SUB_CMDS_CONTAINER_BG = 'JuiceEmail.SubCommands.Bg';
|
|
104
118
|
|
|
105
119
|
// ─── 旧版残留清理 ─────────────────────────────────────────────────────────────
|
|
106
120
|
|
|
@@ -209,6 +223,19 @@ function registerSubCommands(containerPath, kind, nodePath, scriptPath, iconPath
|
|
|
209
223
|
regAdd(`${cfgKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, '-c %1'));
|
|
210
224
|
}
|
|
211
225
|
|
|
226
|
+
// Common items for file-type containers (html + yaml)
|
|
227
|
+
if (kind === 'html' || kind === 'yaml') {
|
|
228
|
+
const viewKey = `${containerPath}\\shell\\${SUBCMDS.viewEdm}`;
|
|
229
|
+
regAdd(viewKey, 'MUIVerb', 'REG_SZ', '📦 查看资源列表');
|
|
230
|
+
regAdd(viewKey, 'Icon', 'REG_SZ', iconPath);
|
|
231
|
+
regAdd(`${viewKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view'));
|
|
232
|
+
|
|
233
|
+
const viewIntKey = `${containerPath}\\shell\\${SUBCMDS.viewEdmInt}`;
|
|
234
|
+
regAdd(viewIntKey, 'MUIVerb', 'REG_SZ', '📋 浏览资源库');
|
|
235
|
+
regAdd(viewIntKey, 'Icon', 'REG_SZ', iconPath);
|
|
236
|
+
regAdd(`${viewIntKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view -i'));
|
|
237
|
+
}
|
|
238
|
+
|
|
212
239
|
// pwsh — 始终显示(可选)
|
|
213
240
|
if (pwshPath) {
|
|
214
241
|
const pwshKey = `${containerPath}\\shell\\${SUBCMDS.pwsh}`;
|
|
@@ -219,6 +246,54 @@ function registerSubCommands(containerPath, kind, nodePath, scriptPath, iconPath
|
|
|
219
246
|
}
|
|
220
247
|
}
|
|
221
248
|
|
|
249
|
+
// ─── Directory / Background 菜单注册 ──────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
function registerDirBgMenus(nodePath, scriptPath, iconPath, pwshPath) {
|
|
252
|
+
// Register directory container subcommands
|
|
253
|
+
const dirContainer = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`;
|
|
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'));
|
|
268
|
+
|
|
269
|
+
// 📋 浏览资源库
|
|
270
|
+
const viewIntKey = `${containerPath}\\shell\\${SUBCMDS.viewEdmInt}`;
|
|
271
|
+
regAdd(viewIntKey, 'MUIVerb', 'REG_SZ', '📋 浏览资源库');
|
|
272
|
+
regAdd(viewIntKey, 'Icon', 'REG_SZ', iconPath);
|
|
273
|
+
regAdd(`${viewIntKey}\\command`, '', 'REG_SZ', wrapInteractive(nodePath, scriptPath, 'view -i'));
|
|
274
|
+
|
|
275
|
+
// 📂 在此打开终端
|
|
276
|
+
if (pwshPath) {
|
|
277
|
+
const pwshKey = `${containerPath}\\shell\\${SUBCMDS.pwsh}`;
|
|
278
|
+
regAdd(pwshKey, 'MUIVerb', 'REG_SZ', '📂 在此打开终端');
|
|
279
|
+
regAdd(pwshKey, 'Icon', 'REG_SZ', pwshPath);
|
|
280
|
+
regAdd(`${pwshKey}\\command`, '', 'REG_SZ',
|
|
281
|
+
`"${pwshPath}" -NoExit -Command "Set-Location -LiteralPath '%1'"`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Parent keys: Directory and Directory\Background
|
|
286
|
+
const dirParent = `${HKCU_SHELL}\\Directory\\shell\\${PARENT_KEY_NAME}`;
|
|
287
|
+
regAdd(dirParent, 'MUIVerb', 'REG_SZ', '📧 juice 邮件工具');
|
|
288
|
+
regAdd(dirParent, 'Icon', 'REG_SZ', iconPath);
|
|
289
|
+
regAdd(dirParent, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_DIR);
|
|
290
|
+
|
|
291
|
+
const bgParent = `${HKCU_SHELL}\\Directory\\Background\\shell\\${PARENT_KEY_NAME}`;
|
|
292
|
+
regAdd(bgParent, 'MUIVerb', 'REG_SZ', '📧 juice 邮件工具');
|
|
293
|
+
regAdd(bgParent, 'Icon', 'REG_SZ', iconPath);
|
|
294
|
+
regAdd(bgParent, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_BG);
|
|
295
|
+
}
|
|
296
|
+
|
|
222
297
|
// ─── 注册 ─────────────────────────────────────────────────────────────────────
|
|
223
298
|
|
|
224
299
|
async function registerContextMenu() {
|
|
@@ -241,14 +316,19 @@ async function registerContextMenu() {
|
|
|
241
316
|
let ok = true;
|
|
242
317
|
|
|
243
318
|
// 先删除旧容器再重建,避免注册残留旧子命令(如旧版 HTML 容器的 WithConfig)
|
|
319
|
+
console.log(chalk.gray(' 清理旧版菜单...'));
|
|
244
320
|
regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`);
|
|
245
321
|
regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`);
|
|
322
|
+
regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`);
|
|
323
|
+
regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`);
|
|
246
324
|
|
|
247
325
|
// HTML 容器:generate + snippet + pwsh
|
|
326
|
+
console.log(chalk.gray(' 注册 .html 文件菜单...'));
|
|
248
327
|
const htmlContainer = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`;
|
|
249
328
|
registerSubCommands(htmlContainer, 'html', nodePath, scriptPath, iconPath, pwshPath);
|
|
250
329
|
|
|
251
330
|
// YAML 容器:config + pwsh
|
|
331
|
+
console.log(chalk.gray(' 注册 .yaml 文件菜单...'));
|
|
252
332
|
const yamlContainer = `${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`;
|
|
253
333
|
registerSubCommands(yamlContainer, 'yaml', nodePath, scriptPath, iconPath, pwshPath);
|
|
254
334
|
|
|
@@ -268,6 +348,10 @@ async function registerContextMenu() {
|
|
|
268
348
|
regAdd(parentKey, 'ExtendedSubCommandsKey', 'REG_SZ', SUB_CMDS_CONTAINER_YAML);
|
|
269
349
|
}
|
|
270
350
|
|
|
351
|
+
// Directory / Background → 独立容器
|
|
352
|
+
console.log(chalk.gray(' 注册文件夹/空白处菜单...'));
|
|
353
|
+
registerDirBgMenus(nodePath, scriptPath, iconPath, pwshPath);
|
|
354
|
+
|
|
271
355
|
if (!ok) {
|
|
272
356
|
console.log(chalk.yellow(' ⚠ 部分注册表项写入失败,右键菜单可能不完整。\n'));
|
|
273
357
|
}
|
|
@@ -279,13 +363,21 @@ async function registerContextMenu() {
|
|
|
279
363
|
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
280
364
|
` ├── 📄 作为模板,生成邮件 HTML → juice -f(后台执行)\n` +
|
|
281
365
|
` ├── 🧩 作为片段,拼接邮件 HTML → juice -s(交互选择模板)\n` +
|
|
366
|
+
` ├── 📦 查看资源列表 → juice view\n` +
|
|
367
|
+
` ├── 📋 浏览资源库 → juice view -i\n` +
|
|
282
368
|
(pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
|
|
283
369
|
`\n ${chalk.bold('.yaml / .yml')} 文件右键:\n` +
|
|
284
370
|
` ${chalk.bold('📧 用 juice 生成邮件 HTML')}\n` +
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
371
|
+
` ├── ⚙️ 作为配置,拼接邮件 HTML → juice -c(交互选择品牌/模板/片段)\n` +
|
|
372
|
+
` ├── 📦 查看资源列表 → juice view\n` +
|
|
373
|
+
` ├── 📋 浏览资源库 → juice view -i\n` +
|
|
374
|
+
(pwshPath ? ` └── 📂 打开 PowerShell\n` : '') +
|
|
375
|
+
`\n ${chalk.bold('文件夹 / 空白处')} 右键:\n` +
|
|
376
|
+
` ${chalk.bold('📧 juice 邮件工具')}\n` +
|
|
377
|
+
` ├── 📥 从资源库拷贝到此处 → juice init\n` +
|
|
378
|
+
` ├── 📦 查看资源列表 → juice view\n` +
|
|
379
|
+
` ├── 📋 浏览资源库 → juice view -i\n` +
|
|
380
|
+
(pwshPath ? ` └── 📂 在此打开终端\n` : '') +
|
|
289
381
|
'\n' +
|
|
290
382
|
chalk.gray(' 注意:如菜单未出现,请重启文件资源管理器(explorer.exe)。\n')
|
|
291
383
|
);
|
|
@@ -303,15 +395,22 @@ async function unregisterContextMenu() {
|
|
|
303
395
|
|
|
304
396
|
let removed = 0;
|
|
305
397
|
|
|
306
|
-
// 清理 HKCU 父菜单(4
|
|
307
|
-
const allRoots = [
|
|
398
|
+
// 清理 HKCU 父菜单(4 个文件类型 + Directory + Background)
|
|
399
|
+
const allRoots = [
|
|
400
|
+
...HTML_ROOTS,
|
|
401
|
+
...YAML_ROOTS,
|
|
402
|
+
`${HKCU_SHELL}\\Directory\\shell`,
|
|
403
|
+
`${HKCU_SHELL}\\Directory\\Background\\shell`,
|
|
404
|
+
];
|
|
308
405
|
for (const root of allRoots) {
|
|
309
406
|
if (regDelete(`${root}\\${PARENT_KEY_NAME}`)) removed++;
|
|
310
407
|
}
|
|
311
408
|
|
|
312
|
-
//
|
|
409
|
+
// 清理所有子命令容器
|
|
313
410
|
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_HTML}`)) removed++;
|
|
314
411
|
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_YAML}`)) removed++;
|
|
412
|
+
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_DIR}`)) removed++;
|
|
413
|
+
if (regDelete(`${HKCU_SHELL}\\${SUB_CMDS_CONTAINER_BG}`)) removed++;
|
|
315
414
|
|
|
316
415
|
// 清理旧版共享容器(v2.1.14 之前只有一个容器)
|
|
317
416
|
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
|
+
};
|