juice-email-cli 2.3.5 → 2.3.7
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 +29 -6
- package/package.json +1 -1
- package/src/init.js +209 -122
- package/src/view.js +12 -0
package/bin/juice.js
CHANGED
|
@@ -5,6 +5,29 @@ const { program } = require('commander');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const pkg = require('../package.json');
|
|
7
7
|
const { run } = require('../src/index');
|
|
8
|
+
let ExitPromptError;
|
|
9
|
+
try {
|
|
10
|
+
({ ExitPromptError } = require('@inquirer/core'));
|
|
11
|
+
} catch (_) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wrap an async action handler to catch Ctrl+C and other errors gracefully.
|
|
15
|
+
*/
|
|
16
|
+
function safeAction(fn) {
|
|
17
|
+
return async (...args) => {
|
|
18
|
+
try {
|
|
19
|
+
await fn(...args);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (ExitPromptError && err instanceof ExitPromptError) {
|
|
22
|
+
// Ctrl+C — exit silently
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
console.error('\n' + require('chalk').red(` ✘ ${err.message}`));
|
|
26
|
+
if (process.env.DEBUG) console.error(err);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
8
31
|
|
|
9
32
|
program
|
|
10
33
|
.name('juice')
|
|
@@ -87,7 +110,7 @@ program
|
|
|
87
110
|
.option('--templates', '列出所有模板')
|
|
88
111
|
.option('--series', '列出所有系列')
|
|
89
112
|
.option('--snippets', '列出所有片段')
|
|
90
|
-
.action(async (viewPath, options) => {
|
|
113
|
+
.action(safeAction(async (viewPath, options) => {
|
|
91
114
|
const { runViewMode } = require('../src/view');
|
|
92
115
|
await runViewMode({
|
|
93
116
|
viewPath: viewPath || null,
|
|
@@ -96,7 +119,7 @@ program
|
|
|
96
119
|
: (options.series ? 'series'
|
|
97
120
|
: (options.snippets ? 'snippets' : null)),
|
|
98
121
|
});
|
|
99
|
-
});
|
|
122
|
+
}));
|
|
100
123
|
|
|
101
124
|
// ─── Subcommand: juice init ─────────────────────────────────────────────
|
|
102
125
|
program
|
|
@@ -106,7 +129,7 @@ program
|
|
|
106
129
|
.option('-s, --snippet <file-path>', '仅拷贝片段 HTML 文件')
|
|
107
130
|
.option('-c, --config <file-path>', '仅拷贝配置 YAML 文件')
|
|
108
131
|
.option('--all [target]', '拷贝整个 EDM 资源目录到当前或指定目录')
|
|
109
|
-
.action(async (initPath, options) => {
|
|
132
|
+
.action(safeAction(async (initPath, options) => {
|
|
110
133
|
const { runInitMode } = require('../src/init');
|
|
111
134
|
await runInitMode({
|
|
112
135
|
initPath: initPath || null,
|
|
@@ -115,11 +138,11 @@ program
|
|
|
115
138
|
config: options.config || null,
|
|
116
139
|
all: options.all !== undefined ? (options.all || true) : null,
|
|
117
140
|
});
|
|
118
|
-
});
|
|
141
|
+
}));
|
|
119
142
|
|
|
120
143
|
// ─── Default action (backward compatible) ───────────────────────────────
|
|
121
144
|
program
|
|
122
|
-
.action(async (options) => {
|
|
145
|
+
.action(safeAction(async (options) => {
|
|
123
146
|
// 注册/卸载右键菜单
|
|
124
147
|
if (options.install) {
|
|
125
148
|
const { registerContextMenu } = require('../src/context-menu');
|
|
@@ -161,7 +184,7 @@ program
|
|
|
161
184
|
// 全交互模式:无 --snippet,无 -f
|
|
162
185
|
const { runInteractiveMode } = require('../src/snippet');
|
|
163
186
|
await runInteractiveMode({ config: options.config });
|
|
164
|
-
});
|
|
187
|
+
}));
|
|
165
188
|
|
|
166
189
|
program.allowUnknownOption(false);
|
|
167
190
|
program.parse(process.argv);
|
package/package.json
CHANGED
package/src/init.js
CHANGED
|
@@ -6,8 +6,7 @@ const chalk = require('chalk');
|
|
|
6
6
|
const {
|
|
7
7
|
resolveEdmDir, loadMeta, findBrands, findTemplateVersions,
|
|
8
8
|
findSeriesDirs, findSnippetVariants, findConfigs,
|
|
9
|
-
|
|
10
|
-
promptOutputName, checkOutputConflicts, findNextVersion,
|
|
9
|
+
promptOutputName,
|
|
11
10
|
} = require('./snippet');
|
|
12
11
|
|
|
13
12
|
function fmtBytes(b) {
|
|
@@ -90,141 +89,203 @@ async function directCopy(srcPath, cwd) {
|
|
|
90
89
|
|
|
91
90
|
// ─── Interactive Init ────────────────────────────────────────────────────────
|
|
92
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Wrap @inquirer/prompts select with back/exit navigation.
|
|
94
|
+
* Returns the chosen value, 'back', or 'exit'.
|
|
95
|
+
*/
|
|
96
|
+
async function selectWithNav(choices, showBack) {
|
|
97
|
+
const { select } = await import('@inquirer/prompts');
|
|
98
|
+
const all = [];
|
|
99
|
+
if (showBack) all.push({ name: '.. 返回上级', value: 'back' });
|
|
100
|
+
all.push(...choices);
|
|
101
|
+
all.push({ name: '✕ 退出', value: 'exit' });
|
|
102
|
+
return select({ message: choices[0] ? undefined : '', choices: all, loop: false });
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
async function interactiveInit(edmDir, cwd) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
let step = 'brand';
|
|
107
|
+
let brand = null, version = null, series = null, variant = null;
|
|
108
|
+
|
|
109
|
+
while (true) {
|
|
110
|
+
if (step === 'brand') {
|
|
111
|
+
const brands = findBrands(edmDir);
|
|
112
|
+
const choices = brands.map(b => ({
|
|
113
|
+
name: formatName(b.meta, b.name) + (b.meta.description ? ' — ' + chalk.dim(b.meta.description) : ''),
|
|
114
|
+
value: b,
|
|
115
|
+
}));
|
|
116
|
+
const result = await selectWithNav(choices, false);
|
|
117
|
+
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
118
|
+
brand = result;
|
|
119
|
+
step = 'version';
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (step === 'version') {
|
|
124
|
+
const versions = findTemplateVersions(brand.path);
|
|
125
|
+
const choices = versions.map(v => ({
|
|
126
|
+
name: formatName(v.meta, v.name) + (v.meta.description ? ' — ' + chalk.dim(v.meta.description) : ''),
|
|
127
|
+
value: v,
|
|
128
|
+
}));
|
|
129
|
+
const result = await selectWithNav(choices, true);
|
|
130
|
+
if (result === 'back') { step = 'brand'; continue; }
|
|
131
|
+
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
132
|
+
version = result;
|
|
133
|
+
step = 'series';
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (step === 'series') {
|
|
138
|
+
const allSeries = findSeriesDirs(brand.path);
|
|
139
|
+
if (allSeries.length === 0) {
|
|
140
|
+
series = null;
|
|
141
|
+
step = 'copy';
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const choices = [
|
|
145
|
+
{ name: '[跳过] 仅使用模板', value: null },
|
|
146
|
+
...allSeries.map(s => ({
|
|
147
|
+
name: formatName(s.meta, s.name) + (s.meta.description ? ' — ' + chalk.dim(s.meta.description) : ''),
|
|
115
148
|
value: s,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
})),
|
|
150
|
+
];
|
|
151
|
+
const result = await selectWithNav(choices, true);
|
|
152
|
+
if (result === 'back') { step = 'version'; continue; }
|
|
153
|
+
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
154
|
+
series = result;
|
|
155
|
+
step = series ? 'variant' : 'copy';
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (step === 'variant') {
|
|
126
160
|
const variants = findSnippetVariants(series.path);
|
|
127
|
-
if (variants.length
|
|
128
|
-
variant =
|
|
161
|
+
if (variants.length === 0) {
|
|
162
|
+
variant = null;
|
|
163
|
+
step = 'copy';
|
|
164
|
+
continue;
|
|
129
165
|
}
|
|
166
|
+
const choices = variants.map(v => ({
|
|
167
|
+
name: formatName(v.meta, v.name) + (v.meta.description ? ' — ' + chalk.dim(v.meta.description) : ''),
|
|
168
|
+
value: v,
|
|
169
|
+
}));
|
|
170
|
+
const result = await selectWithNav(choices, true);
|
|
171
|
+
if (result === 'back') { step = 'series'; continue; }
|
|
172
|
+
if (result === 'exit') { console.log(chalk.gray('已退出。\n')); return; }
|
|
173
|
+
variant = result;
|
|
174
|
+
step = 'copy';
|
|
175
|
+
continue;
|
|
130
176
|
}
|
|
131
|
-
}
|
|
132
177
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
name: '📋 模板 HTML',
|
|
137
|
-
value: 'template',
|
|
138
|
-
description: path.basename(version.templatePath),
|
|
139
|
-
checked: true,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
if (variant) {
|
|
143
|
-
const snipPath = path.join(variant.path, 'snippet.html');
|
|
144
|
-
if (fs.existsSync(snipPath)) {
|
|
178
|
+
if (step === 'copy') {
|
|
179
|
+
// Multi-select what to copy
|
|
180
|
+
const copyItems = [];
|
|
145
181
|
copyItems.push({
|
|
146
|
-
name:
|
|
147
|
-
value: '
|
|
148
|
-
description: path.basename(
|
|
182
|
+
name: `📋 模板 HTML(${version.meta.name || version.name})`,
|
|
183
|
+
value: 'template',
|
|
184
|
+
description: path.basename(version.templatePath),
|
|
149
185
|
checked: true,
|
|
150
186
|
});
|
|
151
|
-
}
|
|
152
187
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
if (variant) {
|
|
189
|
+
const snipPath = path.join(variant.path, 'snippet.html');
|
|
190
|
+
if (fs.existsSync(snipPath)) {
|
|
191
|
+
copyItems.push({
|
|
192
|
+
name: '🧩 片段 HTML',
|
|
193
|
+
value: 'snippet',
|
|
194
|
+
description: path.basename(snipPath),
|
|
195
|
+
checked: true,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const configs = findConfigs(variant.path);
|
|
199
|
+
if (configs.length > 0) {
|
|
200
|
+
copyItems.push({
|
|
201
|
+
name: '⚙️ 配置文件',
|
|
202
|
+
value: 'config',
|
|
203
|
+
description: configs.map(c => c.name).join(', '),
|
|
204
|
+
checked: true,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Show summary before multi-select
|
|
210
|
+
console.log(chalk.dim(`\n 品牌:${brand.meta.name || brand.name} → 模板:${version.meta.name || version.name}`));
|
|
211
|
+
if (series) console.log(chalk.dim(` 系列:${series.meta.name || series.name}${variant ? ' → 变体:' + (variant.meta.name || variant.name) : ''}`));
|
|
212
|
+
|
|
213
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
214
|
+
const navCopyItems = [
|
|
215
|
+
...copyItems,
|
|
216
|
+
{ name: '.. 返回上级', value: 'back' },
|
|
217
|
+
{ name: '✕ 退出', value: 'exit' },
|
|
218
|
+
];
|
|
219
|
+
const selected = await checkbox({
|
|
220
|
+
message: '选择要拷贝的内容:',
|
|
221
|
+
choices: navCopyItems,
|
|
160
222
|
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
223
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
224
|
+
// Filter out nav values
|
|
225
|
+
if (selected.includes('exit')) { console.log(chalk.gray('已退出。\n')); return; }
|
|
226
|
+
if (selected.includes('back')) {
|
|
227
|
+
step = series ? 'variant' : 'series';
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
169
230
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
231
|
+
const realSelected = selected.filter(s => s !== 'back' && s !== 'exit');
|
|
232
|
+
if (realSelected.length === 0) {
|
|
233
|
+
console.log(chalk.gray('未选择任何内容,已跳过。\n'));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
174
236
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
237
|
+
// Output name
|
|
238
|
+
const defaultBaseName = variant
|
|
239
|
+
? `${brand.name}-${version.name}-${series.name}-${variant.name}`
|
|
240
|
+
: `${brand.name}-${version.name}`;
|
|
241
|
+
const outputBaseName = await promptOutputName(defaultBaseName, cwd);
|
|
180
242
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
243
|
+
// Copy files
|
|
244
|
+
console.log(chalk.green('\n✔ 已拷贝:'));
|
|
245
|
+
const cwdRel = (p) => './' + path.relative(cwd, p);
|
|
184
246
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
247
|
+
if (realSelected.includes('template')) {
|
|
248
|
+
const destName = outputBaseName + path.extname(version.templatePath);
|
|
249
|
+
const dest = copyFileToCwd(version.templatePath, cwd, destName);
|
|
250
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
251
|
+
// Auto-copy icon
|
|
252
|
+
const icon = copyIcon(variant ? variant.path : null, version.path, brand.path, cwd);
|
|
253
|
+
if (icon) console.log(` ${chalk.cyan('·')} ${cwdRel(icon)} ${chalk.gray('(图标, ' + fmtBytes(fs.statSync(icon).size) + ')')}`);
|
|
254
|
+
}
|
|
190
255
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
256
|
+
if (realSelected.includes('snippet') && variant) {
|
|
257
|
+
const snipPath = path.join(variant.path, 'snippet.html');
|
|
258
|
+
const destName = outputBaseName + '-snippet' + path.extname(snipPath);
|
|
259
|
+
const dest = copyFileToCwd(snipPath, cwd, destName);
|
|
260
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(片段, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
261
|
+
}
|
|
197
262
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const optimal = configs.find(c => c.isOptimal);
|
|
206
|
-
cfgPath = optimal ? optimal.path : configs[0].path;
|
|
207
|
-
}
|
|
208
|
-
const dest = copyFileToCwd(cfgPath, cwd, 'juice.yaml');
|
|
209
|
-
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
210
|
-
}
|
|
263
|
+
if (realSelected.includes('config') && variant) {
|
|
264
|
+
const configs = findConfigs(variant.path);
|
|
265
|
+
const optimal = configs.find(c => c.isOptimal);
|
|
266
|
+
const cfgPath = optimal ? optimal.path : configs[0].path;
|
|
267
|
+
const dest = copyFileToCwd(cfgPath, cwd, 'juice.yaml');
|
|
268
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(配置, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
269
|
+
}
|
|
211
270
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
271
|
+
console.log();
|
|
272
|
+
if (realSelected.includes('snippet') && realSelected.includes('template')) {
|
|
273
|
+
const snipFile = outputBaseName + '-snippet' + path.extname(path.join(variant.path, 'snippet.html'));
|
|
274
|
+
const tplFile = outputBaseName + path.extname(version.templatePath);
|
|
275
|
+
console.log(
|
|
276
|
+
' ' + chalk.dim('💡 下一步:') + '\n' +
|
|
277
|
+
' ' + chalk.cyan(`juice -s ${snipFile} -f ${tplFile}`) +
|
|
278
|
+
(realSelected.includes('config') ? chalk.cyan(' -c juice.yaml') : '') + '\n'
|
|
279
|
+
);
|
|
280
|
+
} else if (realSelected.includes('template')) {
|
|
281
|
+
const tplFile = outputBaseName + path.extname(version.templatePath);
|
|
282
|
+
console.log(
|
|
283
|
+
' ' + chalk.dim('💡 下一步:') + '\n' +
|
|
284
|
+
' ' + chalk.cyan(`juice -f ${tplFile}`) + '\n'
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
228
289
|
}
|
|
229
290
|
}
|
|
230
291
|
|
|
@@ -268,6 +329,30 @@ function countFiles(dir) {
|
|
|
268
329
|
return count;
|
|
269
330
|
}
|
|
270
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Find and copy favicon.ico alongside the template.
|
|
334
|
+
* Lookup: variant dir → template version dir → brand's first template → skip
|
|
335
|
+
*/
|
|
336
|
+
function copyIcon(variantDir, versionDir, brandDir, cwd) {
|
|
337
|
+
const candidates = [];
|
|
338
|
+
if (variantDir) candidates.push(path.join(variantDir, 'favicon.ico'));
|
|
339
|
+
if (versionDir) candidates.push(path.join(versionDir, 'favicon.ico'));
|
|
340
|
+
// First template version in brand
|
|
341
|
+
try {
|
|
342
|
+
const versions = findTemplateVersions(brandDir);
|
|
343
|
+
if (versions.length > 0) {
|
|
344
|
+
candidates.push(path.join(versions[0].path, 'favicon.ico'));
|
|
345
|
+
}
|
|
346
|
+
} catch (_) {}
|
|
347
|
+
for (const src of candidates) {
|
|
348
|
+
if (fs.existsSync(src)) {
|
|
349
|
+
const dest = copyFileToCwd(src, cwd, 'favicon.ico');
|
|
350
|
+
return dest;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
271
356
|
function findNextEdmVersion(targetDir) {
|
|
272
357
|
let v = 1;
|
|
273
358
|
while (true) {
|
|
@@ -308,12 +393,12 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
308
393
|
message: `目录 ${destDir} 已存在:`,
|
|
309
394
|
choices: [
|
|
310
395
|
{ name: '覆盖', value: 'overwrite' },
|
|
311
|
-
{ name:
|
|
312
|
-
{ name: '
|
|
396
|
+
{ name: `版本(${path.basename(vName)})`, value: 'version' },
|
|
397
|
+
{ name: '跳过', value: 'skip' },
|
|
313
398
|
],
|
|
314
399
|
});
|
|
315
|
-
if (action === '
|
|
316
|
-
console.log(chalk.gray('
|
|
400
|
+
if (action === 'skip') {
|
|
401
|
+
console.log(chalk.gray('已跳过。\n'));
|
|
317
402
|
return;
|
|
318
403
|
}
|
|
319
404
|
if (action === 'version') {
|
|
@@ -409,6 +494,8 @@ async function runInitMode({ initPath, template, snippet, config, all }) {
|
|
|
409
494
|
const destName = outputBaseName + path.extname(tplPath);
|
|
410
495
|
const dest = copyFileToCwd(tplPath, cwd, destName);
|
|
411
496
|
console.log(` ${chalk.cyan('·')} ${cwdRel(dest)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(dest).size) + ')')}`);
|
|
497
|
+
const icon = copyIcon(parsed.variantData.path, version.path, brandDir, cwd);
|
|
498
|
+
if (icon) console.log(` ${chalk.cyan('·')} ${cwdRel(icon)} ${chalk.gray('(图标, ' + fmtBytes(fs.statSync(icon).size) + ')')}`);
|
|
412
499
|
}
|
|
413
500
|
if (selected.includes('snippet') && fs.existsSync(snipPath)) {
|
|
414
501
|
const destName = outputBaseName + '-snippet' + path.extname(snipPath);
|
package/src/view.js
CHANGED
|
@@ -627,6 +627,18 @@ async function interactiveBrowse(edmDir, startParsed) {
|
|
|
627
627
|
const dest = path.join(cwd, outputBaseName + path.extname(src));
|
|
628
628
|
const actual = copyFileSafe(src, dest);
|
|
629
629
|
console.log(` ${chalk.cyan('·')} ${cwdRel(actual)} ${chalk.gray('(模板, ' + fmtBytes(fs.statSync(actual).size) + ')')}`);
|
|
630
|
+
// Auto-copy icon: variant dir → template version dir → first version in brand
|
|
631
|
+
const iconSrcs = [
|
|
632
|
+
path.join(current.variantData.path, 'favicon.ico'),
|
|
633
|
+
path.join(versions[0].path, 'favicon.ico'),
|
|
634
|
+
];
|
|
635
|
+
for (const isrc of iconSrcs) {
|
|
636
|
+
if (fs.existsSync(isrc)) {
|
|
637
|
+
const idest = copyFileSafe(isrc, path.join(cwd, 'favicon.ico'));
|
|
638
|
+
console.log(` ${chalk.cyan('·')} ${cwdRel(idest)} ${chalk.gray('(图标, ' + fmtBytes(fs.statSync(idest).size) + ')')}`);
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
630
642
|
}
|
|
631
643
|
if (selected.includes('snippet') && fs.existsSync(snipPath)) {
|
|
632
644
|
const dest = path.join(cwd, outputBaseName + '-snippet' + path.extname(snipPath));
|