flu-cli 2.0.2 → 2.0.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/CHANGELOG.md +36 -0
- package/CLI.md +1 -0
- package/README.md +139 -67
- package/config/templates.js +6 -127
- package/index.js +98 -50
- package/lib/commands/add.js +7 -7
- package/lib/commands/assets.js +183 -0
- package/lib/commands/config.js +41 -1
- package/lib/commands/newClack.js +85 -112
- package/lib/commands/snippets.js +50 -9
- package/lib/templates/templateCopier.js +14 -275
- package/lib/templates/templateManager.js +20 -163
- package/lib/utils/config.js +13 -51
- package/lib/utils/flutterHelper.js +7 -82
- package/lib/utils/i18n.js +7 -0
- package/lib/utils/templateSelectorEnquirer.js +70 -43
- package/locales/en-US.json +59 -0
- package/locales/zh-CN.json +59 -0
- package/package.json +2 -3
- package/release.sh +433 -11
- package/scripts/sync-base-to-templates.js +6 -6
- package/scripts/workspace-clone-all.sh +4 -4
- package/scripts/workspace-status-all.sh +3 -3
- package/lib/generators/project_generator.js +0 -96
- package/lib/generators/state_manager_generator.js +0 -402
- package/templates/README.md +0 -138
- package/templates/base_files/base_list_page.dart.template +0 -174
- package/templates/base_files/base_list_viewmodel.dart.template +0 -134
- package/templates/base_files/base_page.dart.template +0 -251
- package/templates/base_files/base_viewmodel.dart.template +0 -77
- package/templates/base_files/theme/status_views_theme.dart.template +0 -46
- package/templates/snippets/dart.code-snippets +0 -392
|
@@ -1,191 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 模板管理器
|
|
3
|
-
* 负责克隆、更新和管理模板缓存
|
|
2
|
+
* 模板管理器 - 桥接至 @flu-cli/core
|
|
4
3
|
*/
|
|
4
|
+
import { TemplateManager } from '@flu-cli/core';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
import { join, dirname } from 'path';
|
|
8
|
-
import { existsSync, mkdirSync } from 'fs';
|
|
9
|
-
import { homedir } from 'os';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
import fsExtra from 'fs-extra';
|
|
12
|
-
import { logger } from '../utils/logger.js';
|
|
13
|
-
import { getConfig } from '../../config/dev.config.js';
|
|
14
|
-
|
|
15
|
-
const { copySync, emptyDirSync } = fsExtra;
|
|
16
|
-
|
|
17
|
-
// 获取当前文件所在目录
|
|
18
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
-
const __dirname = dirname(__filename);
|
|
20
|
-
|
|
21
|
-
// 模板缓存目录
|
|
22
|
-
const CACHE_DIR = join(homedir(), '.flu-cli', 'templates');
|
|
23
|
-
|
|
24
|
-
// 本地模板目录(用于开发测试)
|
|
25
|
-
const LOCAL_TEMPLATES_DIR = join(__dirname, '..', '..', 'templates');
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 确保缓存目录存在
|
|
29
|
-
*/
|
|
30
|
-
function ensureCacheDir () {
|
|
31
|
-
if (!existsSync(CACHE_DIR)) {
|
|
32
|
-
mkdirSync(CACHE_DIR, { recursive: true });
|
|
33
|
-
}
|
|
34
|
-
}
|
|
6
|
+
const templateManager = TemplateManager.getInstance();
|
|
35
7
|
|
|
36
8
|
/**
|
|
37
9
|
* 获取模板缓存路径
|
|
38
10
|
*/
|
|
39
11
|
export function getTemplateCachePath (templateName) {
|
|
40
|
-
|
|
12
|
+
// 保持向前兼容,虽然 core 现在内部管理缓存路径
|
|
13
|
+
return templateManager.getTemplatePath(templateName);
|
|
41
14
|
}
|
|
42
15
|
|
|
43
16
|
/**
|
|
44
17
|
* 克隆或更新模板
|
|
45
|
-
* @param {string} templateName - 模板名称
|
|
46
|
-
* @param {string} repoUrl - Git 仓库地址(可选,本地模板不需要)
|
|
47
|
-
* @param {string} branch - 分支名称
|
|
48
|
-
* @param {boolean} forceUpdate - 是否强制更新
|
|
49
|
-
* @param {boolean} isLocal - 是否为本地模板
|
|
50
|
-
* @param {string} sourceLocalPath - 自定义本地模板路径
|
|
51
|
-
* @returns {Promise<string>} 模板路径
|
|
52
18
|
*/
|
|
53
19
|
export async function cloneOrUpdateTemplate (templateName, repoUrl, branch = 'main', forceUpdate = false, isLocal = false, sourceLocalPath = null) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const templatePath = getTemplateCachePath(templateName);
|
|
58
|
-
const localTemplatePath = sourceLocalPath || join(LOCAL_TEMPLATES_DIR, templateName);
|
|
59
|
-
|
|
60
|
-
// 优先级判断:
|
|
61
|
-
// 1. 如果明确指定 isLocal=false 且有 repoUrl,使用远程
|
|
62
|
-
// 2. 如果本地模板存在且 isLocal=true,使用本地
|
|
63
|
-
// 3. 如果本地模板不存在但有 repoUrl,使用远程
|
|
64
|
-
// 4. 否则报错
|
|
65
|
-
|
|
66
|
-
const shouldUseLocal = isLocal && existsSync(localTemplatePath);
|
|
67
|
-
const shouldUseRemote = repoUrl && (!isLocal || !existsSync(localTemplatePath));
|
|
68
|
-
|
|
69
|
-
// 使用本地模板
|
|
70
|
-
if (shouldUseLocal) {
|
|
71
|
-
logger.info(`使用本地模板: ${templateName}`);
|
|
72
|
-
|
|
73
|
-
// 如果缓存不存在或需要强制更新,复制本地模板到缓存
|
|
74
|
-
if (!existsSync(templatePath) || forceUpdate) {
|
|
75
|
-
if (existsSync(templatePath)) {
|
|
76
|
-
emptyDirSync(templatePath);
|
|
77
|
-
}
|
|
78
|
-
copySync(localTemplatePath, templatePath, { overwrite: true });
|
|
79
|
-
} else {
|
|
80
|
-
logger.info(`使用缓存的本地模板`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return templatePath;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 使用远程 Git 模板
|
|
87
|
-
if (shouldUseRemote) {
|
|
88
|
-
logger.info(`使用远程模板: ${templateName}`);
|
|
89
|
-
|
|
90
|
-
const git = simpleGit();
|
|
20
|
+
// 如果是本地模板,我们需要在 core 的配置中确保一致性,或者直接处理
|
|
21
|
+
// 实际上 v2 的 cloneOrUpdateTemplate 承担了“查找并准备”的职责
|
|
22
|
+
// 我们直接调用 core 的 prepareTemplate
|
|
91
23
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const templateGit = simpleGit(templatePath);
|
|
96
|
-
await templateGit.fetch();
|
|
97
|
-
await templateGit.reset(['--hard', `origin/${branch}`]);
|
|
98
|
-
await templateGit.clean('f', ['-d', '-x']);
|
|
99
|
-
} else {
|
|
100
|
-
logger.info(`使用缓存的远程模板`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return templatePath;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// 克隆新模板
|
|
107
|
-
logger.info(`从 Gitee 克隆模板 ${templateName}...`);
|
|
108
|
-
try {
|
|
109
|
-
await git.clone(repoUrl, templatePath, ['--branch', branch, '--single-branch', '--depth', '1']);
|
|
110
|
-
logger.success(`模板 ${templateName} 克隆成功`);
|
|
111
|
-
} catch (cloneError) {
|
|
112
|
-
logger.error(`克隆模板失败: ${cloneError.message}`);
|
|
113
|
-
logger.warn('可能的原因:');
|
|
114
|
-
logger.warn(' 1. 网络连接问题');
|
|
115
|
-
logger.warn(' 2. 仓库不存在或无访问权限');
|
|
116
|
-
logger.warn(' 3. Git 未安装或配置错误');
|
|
117
|
-
logger.info('提示:可以使用 --remote 选项尝试从远程下载');
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return templatePath;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 无法获取模板
|
|
125
|
-
logger.error(`无法获取模板 ${templateName}: 本地模板不存在且未提供远程仓库地址`);
|
|
126
|
-
return null;
|
|
127
|
-
|
|
128
|
-
} catch (error) {
|
|
129
|
-
logger.error(`模板操作失败: ${error.message}`);
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
24
|
+
// NOTE: v2 这里的参数逻辑与 core 的 TemplateManager 略有不同
|
|
25
|
+
// v2 会根据 templateName 去查找模板
|
|
26
|
+
return await templateManager.prepareTemplate(templateName, undefined, forceUpdate);
|
|
132
27
|
}
|
|
133
28
|
|
|
134
29
|
/**
|
|
135
30
|
* 检查模板是否有更新
|
|
136
31
|
*/
|
|
137
32
|
export async function checkTemplateUpdate (templateName) {
|
|
138
|
-
|
|
139
|
-
const templatePath = getTemplateCachePath(templateName);
|
|
140
|
-
|
|
141
|
-
if (!existsSync(templatePath)) {
|
|
142
|
-
return { hasUpdate: false, message: '模板未缓存' };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const git = simpleGit(templatePath);
|
|
146
|
-
|
|
147
|
-
// 获取本地和远程的最新提交
|
|
148
|
-
await git.fetch();
|
|
149
|
-
const status = await git.status();
|
|
150
|
-
|
|
151
|
-
if (status.behind > 0) {
|
|
152
|
-
return {
|
|
153
|
-
hasUpdate: true,
|
|
154
|
-
message: `有 ${status.behind} 个新提交`,
|
|
155
|
-
behind: status.behind
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return { hasUpdate: false, message: '已是最新版本' };
|
|
160
|
-
|
|
161
|
-
} catch (error) {
|
|
162
|
-
logger.error(`检查更新失败: ${error.message}`);
|
|
163
|
-
return { hasUpdate: false, message: '检查失败' };
|
|
164
|
-
}
|
|
33
|
+
return await templateManager.checkUpdate(templateName);
|
|
165
34
|
}
|
|
166
35
|
|
|
167
36
|
/**
|
|
168
37
|
* 获取模板信息
|
|
169
38
|
*/
|
|
170
39
|
export async function getTemplateInfo (templateName) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const log = await git.log({ maxCount: 1 });
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
path: templatePath,
|
|
183
|
-
lastCommit: log.latest,
|
|
184
|
-
exists: true
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
} catch (error) {
|
|
188
|
-
logger.error(`获取模板信息失败: ${error.message}`);
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
40
|
+
// v2 原本返回 path, lastCommit, exists
|
|
41
|
+
// core 的 TemplateManager 目前没提供完整的 getTemplateInfo,但我们可以简单封装
|
|
42
|
+
const path = templateManager.getTemplatePath(templateName);
|
|
43
|
+
const exists = !!path; // 简化处理
|
|
44
|
+
return {
|
|
45
|
+
path,
|
|
46
|
+
exists
|
|
47
|
+
};
|
|
191
48
|
}
|
package/lib/utils/config.js
CHANGED
|
@@ -1,99 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 配置管理 -
|
|
2
|
+
* 配置管理 - 桥接至 @flu-cli/core
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { join, dirname } from 'path';
|
|
7
|
-
import { homedir } from 'os';
|
|
5
|
+
import { ConfigManager } from '@flu-cli/core';
|
|
8
6
|
|
|
9
|
-
const
|
|
10
|
-
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
7
|
+
const configManager = ConfigManager.getInstance();
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
|
-
* 读取配置
|
|
10
|
+
* 读取配置 (由于 ConfigManager 内部管理,此方法仅用于向下兼容)
|
|
14
11
|
*/
|
|
15
12
|
export function getConfig () {
|
|
16
|
-
|
|
17
|
-
if (existsSync(CONFIG_FILE)) {
|
|
18
|
-
const data = readFileSync(CONFIG_FILE, 'utf8');
|
|
19
|
-
return JSON.parse(data);
|
|
20
|
-
}
|
|
21
|
-
} catch (error) {
|
|
22
|
-
// 忽略错误,返回默认配置
|
|
23
|
-
}
|
|
24
|
-
return {};
|
|
13
|
+
return configManager['config'] || {};
|
|
25
14
|
}
|
|
26
15
|
|
|
27
16
|
/**
|
|
28
17
|
* 保存配置
|
|
29
18
|
*/
|
|
30
19
|
export function saveConfig (config) {
|
|
31
|
-
|
|
32
|
-
// 确保目录存在
|
|
33
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
34
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
35
|
-
}
|
|
36
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
|
37
|
-
} catch (error) {
|
|
38
|
-
// 忽略错误
|
|
39
|
-
}
|
|
20
|
+
// ConfigManager 内部会自动保存,这里仅为兼容
|
|
40
21
|
}
|
|
41
22
|
|
|
42
23
|
/**
|
|
43
24
|
* 获取作者名字
|
|
44
25
|
*/
|
|
45
26
|
export function getAuthorName () {
|
|
46
|
-
|
|
47
|
-
return config.authorName || 'Your Name';
|
|
27
|
+
return configManager.getAuthorName();
|
|
48
28
|
}
|
|
49
29
|
|
|
50
30
|
/**
|
|
51
31
|
* 保存作者名字
|
|
52
32
|
*/
|
|
53
33
|
export function saveAuthorName (name) {
|
|
54
|
-
|
|
55
|
-
config.authorName = name;
|
|
56
|
-
saveConfig(config);
|
|
34
|
+
configManager.setAuthorName(name);
|
|
57
35
|
}
|
|
58
36
|
|
|
59
37
|
// 读取默认模板(builtin/custom)
|
|
60
38
|
export function getDefaultTemplate () {
|
|
61
|
-
|
|
62
|
-
return config.defaultTemplate || { type: 'builtin', idOrName: 'lite' };
|
|
39
|
+
return configManager.getDefaultTemplate();
|
|
63
40
|
}
|
|
64
41
|
|
|
65
42
|
// 保存默认模板(builtin/custom)
|
|
66
43
|
export function saveDefaultTemplate (defaultTemplate) {
|
|
67
|
-
|
|
68
|
-
config.defaultTemplate = defaultTemplate;
|
|
69
|
-
saveConfig(config);
|
|
44
|
+
configManager.setDefaultTemplate(defaultTemplate.type, defaultTemplate.idOrName);
|
|
70
45
|
}
|
|
71
46
|
|
|
72
47
|
// 获取自定义模板列表
|
|
73
48
|
export function getCustomTemplates () {
|
|
74
|
-
|
|
75
|
-
return Array.isArray(config.customTemplates) ? config.customTemplates : [];
|
|
49
|
+
return configManager.getTemplates();
|
|
76
50
|
}
|
|
77
51
|
|
|
78
52
|
// 根据 id 查找自定义模板
|
|
79
53
|
export function findCustomTemplateById (id) {
|
|
80
|
-
|
|
81
|
-
return list.find(t => t.id === id) || null;
|
|
54
|
+
return configManager.getTemplate(id) || null;
|
|
82
55
|
}
|
|
83
56
|
|
|
84
57
|
// 新增或更新自定义模板
|
|
85
58
|
export function addOrUpdateCustomTemplate (tpl) {
|
|
86
|
-
|
|
87
|
-
const list = Array.isArray(config.customTemplates) ? config.customTemplates : [];
|
|
88
|
-
const idx = list.findIndex(x => x.id === tpl.id);
|
|
89
|
-
if (idx >= 0) {
|
|
90
|
-
list[idx] = { ...list[idx], ...tpl };
|
|
91
|
-
} else {
|
|
92
|
-
list.push(tpl);
|
|
93
|
-
}
|
|
94
|
-
config.customTemplates = list;
|
|
95
|
-
// 顺带更新最后使用时间
|
|
96
|
-
tpl.lastUsedAt = Date.now();
|
|
97
|
-
saveConfig(config);
|
|
59
|
+
configManager.addTemplate(tpl);
|
|
98
60
|
return tpl;
|
|
99
61
|
}
|
|
@@ -1,85 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Flutter 辅助工具
|
|
3
|
-
* 职责:封装 flutter create/pub get/版本检测等命令调用
|
|
2
|
+
* Flutter 辅助工具 - 桥接至 @flu-cli/core
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 运行 flutter create 命令
|
|
14
|
-
* @param {string} projectDir - 项目目录
|
|
15
|
-
* @param {string} projectName - 项目名称
|
|
16
|
-
* @param {string} packageName - 包名
|
|
17
|
-
* @returns {Promise<boolean>} 是否成功
|
|
18
|
-
*/
|
|
19
|
-
export async function runFlutterCreate (projectDir, projectName, packageName) {
|
|
20
|
-
try {
|
|
21
|
-
const command = `flutter create --project-name ${projectName} --org ${packageName} ${projectDir}`;
|
|
22
|
-
|
|
23
|
-
const { stdout, stderr } = await execAsync(command);
|
|
24
|
-
|
|
25
|
-
if (stderr && !stderr.includes('Warning')) {
|
|
26
|
-
logger.warn(stderr);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return stdout.includes('All done!') || stdout.includes('Created project');
|
|
30
|
-
|
|
31
|
-
} catch (error) {
|
|
32
|
-
logger.error(`Flutter create 失败: ${error.message}`);
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* 运行 flutter pub get
|
|
39
|
-
* @param {string} projectDir - 项目目录
|
|
40
|
-
* @returns {Promise<boolean>} 是否成功
|
|
41
|
-
*/
|
|
42
|
-
export async function runFlutterPubGet (projectDir) {
|
|
43
|
-
try {
|
|
44
|
-
const { stdout, stderr } = await execAsync('flutter pub get', {
|
|
45
|
-
cwd: projectDir
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
if (stderr) {
|
|
49
|
-
logger.warn(stderr);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return true;
|
|
53
|
-
|
|
54
|
-
} catch (error) {
|
|
55
|
-
logger.error(`Flutter pub get 失败: ${error.message}`);
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 检查 Flutter 是否安装
|
|
62
|
-
* @returns {Promise<boolean>} 是否已安装
|
|
63
|
-
*/
|
|
64
|
-
export async function checkFlutterInstalled () {
|
|
65
|
-
try {
|
|
66
|
-
await execAsync('flutter --version');
|
|
67
|
-
return true;
|
|
68
|
-
} catch (error) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 获取 Flutter 版本
|
|
75
|
-
* @returns {Promise<string|null>} Flutter 版本
|
|
76
|
-
*/
|
|
77
|
-
export async function getFlutterVersion () {
|
|
78
|
-
try {
|
|
79
|
-
const { stdout } = await execAsync('flutter --version');
|
|
80
|
-
const match = stdout.match(/Flutter (\d+\.\d+\.\d+)/);
|
|
81
|
-
return match ? match[1] : null;
|
|
82
|
-
} catch (error) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
5
|
+
export {
|
|
6
|
+
runFlutterCreate,
|
|
7
|
+
runFlutterPubGet,
|
|
8
|
+
checkFlutterInstalled,
|
|
9
|
+
getFlutterVersion
|
|
10
|
+
} from '@flu-cli/core';
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 使用 Enquirer 实现实时模板预览
|
|
3
|
-
*
|
|
3
|
+
* 职责:负责 CLI 环境下的模板选择交互逻辑
|
|
4
|
+
* 该文件从 @flu-cli/core 迁移回来,以保持 core 的 Headless 纯净。
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import enquirer from 'enquirer';
|
|
7
8
|
import chalk from 'chalk';
|
|
8
9
|
import { existsSync } from 'fs';
|
|
9
10
|
import { basename } from 'path';
|
|
10
|
-
import {
|
|
11
|
-
import { ConfigManager } from '@flu-cli/core';
|
|
12
|
-
import { getDefaultTemplate, saveDefaultTemplate } from '../utils/config.js';
|
|
11
|
+
import { BUILTIN_TEMPLATES, ConfigManager } from '@flu-cli/core';
|
|
13
12
|
|
|
14
13
|
const { Select } = enquirer;
|
|
15
14
|
|
|
@@ -17,24 +16,27 @@ const { Select } = enquirer;
|
|
|
17
16
|
* 选择模板(带实时预览 - Enquirer 版本)
|
|
18
17
|
*/
|
|
19
18
|
export async function selectTemplateWithEnquirer () {
|
|
20
|
-
const
|
|
19
|
+
const builtinTemplates = Object.values(BUILTIN_TEMPLATES);
|
|
21
20
|
const configManager = ConfigManager.getInstance();
|
|
22
21
|
const customTemplates = configManager.getTemplates();
|
|
23
|
-
const defaultTpl = getDefaultTemplate();
|
|
22
|
+
const defaultTpl = configManager.getDefaultTemplate();
|
|
24
23
|
|
|
25
24
|
// 创建自定义的 Select prompt
|
|
26
25
|
class TemplateSelect extends Select {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
super(options);
|
|
28
|
+
}
|
|
29
|
+
|
|
27
30
|
async render () {
|
|
28
|
-
const {
|
|
31
|
+
const { size } = this.state;
|
|
29
32
|
const index = this.index;
|
|
30
33
|
// 选择项与模板索引可能不一致,安全获取
|
|
31
34
|
const choice = this.choices[index];
|
|
32
35
|
const isBuiltin = choice?.name?.startsWith('builtin:');
|
|
33
|
-
const
|
|
34
|
-
const template = isBuiltin
|
|
36
|
+
const builtinKey = isBuiltin ? choice.name.split(':')[1] : '';
|
|
37
|
+
const template = isBuiltin ? BUILTIN_TEMPLATES[builtinKey] : null;
|
|
35
38
|
|
|
36
39
|
let prompt = '';
|
|
37
|
-
let header = await this.header();
|
|
38
40
|
let prefix = await this.prefix();
|
|
39
41
|
let separator = await this.separator();
|
|
40
42
|
let message = await this.message();
|
|
@@ -50,9 +52,9 @@ export async function selectTemplateWithEnquirer () {
|
|
|
50
52
|
for (let i = 0; i < this.choices.length; i++) {
|
|
51
53
|
const choice = this.choices[i];
|
|
52
54
|
const isSelected = i === index;
|
|
53
|
-
const
|
|
55
|
+
const prefixStr = isSelected ? chalk.cyan('❯') : ' ';
|
|
54
56
|
const style = isSelected ? chalk.cyan : chalk.gray;
|
|
55
|
-
output += `${
|
|
57
|
+
output += `${prefixStr} ${style(choice.message)}\n`;
|
|
56
58
|
if (choice.hint) {
|
|
57
59
|
output += ` ${chalk.gray(choice.hint)}\n`;
|
|
58
60
|
}
|
|
@@ -63,7 +65,7 @@ export async function selectTemplateWithEnquirer () {
|
|
|
63
65
|
if (template) {
|
|
64
66
|
output += chalk.yellow('📁 项目结构预览:\n\n');
|
|
65
67
|
const structureLines = template.structure.trim().split('\n');
|
|
66
|
-
structureLines.forEach(line => {
|
|
68
|
+
structureLines.forEach((line) => {
|
|
67
69
|
output += chalk.gray(line) + '\n';
|
|
68
70
|
});
|
|
69
71
|
output += '\n';
|
|
@@ -80,38 +82,45 @@ export async function selectTemplateWithEnquirer () {
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
// 构建选择项:内置模板 + 自定义模板 + 新增自定义入口
|
|
83
|
-
const builtinChoices =
|
|
85
|
+
const builtinChoices = builtinTemplates.map(t => ({
|
|
84
86
|
name: `builtin:${t.name.toLowerCase()}`,
|
|
85
|
-
message: `${t.displayName}
|
|
87
|
+
message: `${t.displayName}`,
|
|
86
88
|
}));
|
|
87
89
|
const customChoices = customTemplates.map(ct => ({
|
|
88
90
|
name: `custom:${ct.id}`,
|
|
89
91
|
message: `自定义:${ct.name}`,
|
|
90
|
-
hint: ct.type === 'git' ? ct.url : `本地: ${ct.path}
|
|
92
|
+
hint: ct.type === 'git' ? ct.url : `本地: ${ct.path}`,
|
|
91
93
|
}));
|
|
92
|
-
const addCustomChoice = {
|
|
94
|
+
const addCustomChoice = {
|
|
95
|
+
name: '__add_custom__',
|
|
96
|
+
message: chalk.yellow('➕ 新增自定义模板(本地或 Git)'),
|
|
97
|
+
};
|
|
93
98
|
|
|
94
99
|
const allChoices = [...builtinChoices, ...customChoices, addCustomChoice];
|
|
95
100
|
|
|
96
101
|
// 计算默认选中索引
|
|
97
102
|
let initialIndex = 0;
|
|
98
103
|
if (defaultTpl && defaultTpl.type === 'builtin') {
|
|
99
|
-
const idx = allChoices.findIndex(
|
|
104
|
+
const idx = allChoices.findIndex(
|
|
105
|
+
c => c.name === `builtin:${defaultTpl.idOrName}`
|
|
106
|
+
);
|
|
100
107
|
if (idx >= 0) initialIndex = idx;
|
|
101
108
|
} else if (defaultTpl && defaultTpl.type === 'custom') {
|
|
102
|
-
const idx = allChoices.findIndex(
|
|
109
|
+
const idx = allChoices.findIndex(
|
|
110
|
+
c => c.name === `custom:${defaultTpl.idOrName}`
|
|
111
|
+
);
|
|
103
112
|
if (idx >= 0) initialIndex = idx;
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
const
|
|
115
|
+
const templatePrompt = new TemplateSelect({
|
|
107
116
|
name: 'template',
|
|
108
117
|
message: '选择项目模板(使用 ↑↓ 键切换,实时查看结构)',
|
|
109
118
|
choices: allChoices,
|
|
110
|
-
initial: initialIndex
|
|
119
|
+
initial: initialIndex,
|
|
111
120
|
});
|
|
112
121
|
|
|
113
122
|
try {
|
|
114
|
-
const answer = await
|
|
123
|
+
const answer = await templatePrompt.run();
|
|
115
124
|
|
|
116
125
|
// 清除屏幕上的大量输出,只保留简洁的确认信息
|
|
117
126
|
console.clear();
|
|
@@ -124,15 +133,18 @@ export async function selectTemplateWithEnquirer () {
|
|
|
124
133
|
message: '选择自定义模板来源',
|
|
125
134
|
choices: [
|
|
126
135
|
{ name: 'local', message: '本地目录' },
|
|
127
|
-
{ name: 'git', message: 'Git 仓库' }
|
|
128
|
-
]
|
|
136
|
+
{ name: 'git', message: 'Git 仓库' },
|
|
137
|
+
],
|
|
129
138
|
});
|
|
130
139
|
const sourceType = await typePrompt.run();
|
|
131
140
|
|
|
132
141
|
// 输入路径或 URL
|
|
133
142
|
const inputPrompt = new enquirer.Input({
|
|
134
143
|
name: 'pathOrUrl',
|
|
135
|
-
message:
|
|
144
|
+
message:
|
|
145
|
+
sourceType === 'git'
|
|
146
|
+
? '请输入 Git 仓库地址(如 https://... 或 git@...)'
|
|
147
|
+
: '请输入本地模板目录绝对路径',
|
|
136
148
|
});
|
|
137
149
|
const pathOrUrl = await inputPrompt.run();
|
|
138
150
|
|
|
@@ -142,7 +154,7 @@ export async function selectTemplateWithEnquirer () {
|
|
|
142
154
|
const branchPrompt = new enquirer.Input({
|
|
143
155
|
name: 'branch',
|
|
144
156
|
message: '请输入分支名称(默认 main)',
|
|
145
|
-
initial: 'main'
|
|
157
|
+
initial: 'main',
|
|
146
158
|
});
|
|
147
159
|
branch = await branchPrompt.run();
|
|
148
160
|
if (!branch) branch = 'main';
|
|
@@ -155,47 +167,62 @@ export async function selectTemplateWithEnquirer () {
|
|
|
155
167
|
}
|
|
156
168
|
|
|
157
169
|
// 生成 id 与名称
|
|
158
|
-
const nameGuess =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
170
|
+
const nameGuess =
|
|
171
|
+
sourceType === 'git'
|
|
172
|
+
? pathOrUrl.split('/').pop()?.replace('.git', '') || 'custom'
|
|
173
|
+
: basename(pathOrUrl);
|
|
174
|
+
const id = `${nameGuess
|
|
175
|
+
.replace(/\W+/g, '-')
|
|
176
|
+
.toLowerCase()}-${Date.now()}`;
|
|
162
177
|
|
|
163
178
|
const newTemplate = {
|
|
164
179
|
id,
|
|
165
180
|
name: nameGuess,
|
|
166
181
|
type: sourceType,
|
|
167
|
-
...(sourceType === 'git' ? { url: pathOrUrl, branch } : { path: pathOrUrl })
|
|
182
|
+
...(sourceType === 'git' ? { url: pathOrUrl, branch } : { path: pathOrUrl }),
|
|
168
183
|
};
|
|
169
184
|
|
|
170
185
|
configManager.addTemplate(newTemplate);
|
|
171
186
|
|
|
172
|
-
console.log(
|
|
173
|
-
|
|
187
|
+
console.log(
|
|
188
|
+
chalk.green('✓ 已保存自定义模板:'),
|
|
189
|
+
chalk.cyan.bold(newTemplate.name)
|
|
190
|
+
);
|
|
191
|
+
configManager.setDefaultTemplate('custom', newTemplate.id);
|
|
174
192
|
return { kind: 'custom', id: newTemplate.id };
|
|
175
193
|
}
|
|
176
194
|
|
|
177
195
|
if (answer.startsWith('builtin:')) {
|
|
178
196
|
const name = answer.split(':')[1];
|
|
179
|
-
const selectedTemplate =
|
|
180
|
-
console.log(
|
|
181
|
-
|
|
197
|
+
const selectedTemplate = BUILTIN_TEMPLATES[name];
|
|
198
|
+
console.log(
|
|
199
|
+
chalk.green('✓ 已选择:'),
|
|
200
|
+
chalk.cyan.bold(selectedTemplate.displayName)
|
|
201
|
+
);
|
|
202
|
+
configManager.setDefaultTemplate('builtin', name);
|
|
182
203
|
return { kind: 'builtin', name };
|
|
183
204
|
}
|
|
184
205
|
|
|
185
206
|
if (answer.startsWith('custom:')) {
|
|
186
207
|
const id = answer.split(':')[1];
|
|
187
208
|
const ct = customTemplates.find(t => t.id === id);
|
|
188
|
-
console.log(
|
|
189
|
-
|
|
209
|
+
console.log(
|
|
210
|
+
chalk.green('✓ 已选择自定义:'),
|
|
211
|
+
chalk.cyan.bold(ct?.name || id)
|
|
212
|
+
);
|
|
213
|
+
configManager.setDefaultTemplate('custom', id);
|
|
190
214
|
return { kind: 'custom', id };
|
|
191
215
|
}
|
|
192
216
|
|
|
193
217
|
// 兜底(保持兼容)
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
const selectedTemplate =
|
|
197
|
-
console.log(
|
|
198
|
-
|
|
218
|
+
const nameFallback = answer;
|
|
219
|
+
configManager.setDefaultTemplate('builtin', nameFallback);
|
|
220
|
+
const selectedTemplate = BUILTIN_TEMPLATES[nameFallback];
|
|
221
|
+
console.log(
|
|
222
|
+
chalk.green('✓ 已选择:'),
|
|
223
|
+
chalk.cyan.bold(selectedTemplate?.displayName || nameFallback)
|
|
224
|
+
);
|
|
225
|
+
return { kind: 'builtin', name: nameFallback };
|
|
199
226
|
} catch (error) {
|
|
200
227
|
console.log(chalk.yellow('\n操作已取消'));
|
|
201
228
|
process.exit(0);
|