flu-cli 0.0.4 → 2.0.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/CLI.md +349 -0
- package/README.md +59 -155
- package/config/dev.config.js +56 -0
- package/config/templates.js +147 -0
- package/index.js +128 -81
- package/lib/commands/add.js +472 -0
- package/lib/commands/cache.js +99 -0
- package/lib/commands/completion.js +94 -0
- package/lib/commands/generate.js +26 -0
- package/lib/commands/newClack.js +396 -0
- package/lib/commands/snippets.js +39 -0
- package/lib/commands/templates.js +84 -0
- package/lib/generators/component_generator.js +93 -0
- package/lib/generators/model_generator.js +303 -0
- package/lib/generators/module_generator.js +141 -0
- package/lib/generators/page_generator.js +322 -0
- package/lib/generators/project_generator.js +96 -0
- package/lib/generators/service_generator.js +408 -0
- package/lib/generators/state_manager_generator.js +402 -0
- package/lib/generators/viewmodel_generator.js +115 -0
- package/lib/generators/widget_generator.js +104 -0
- package/lib/templates/templateCopier.js +296 -0
- package/lib/templates/templateManager.js +191 -0
- package/lib/utils/config.js +99 -0
- package/lib/utils/flutterHelper.js +85 -0
- package/lib/utils/index_updater.js +69 -0
- package/lib/utils/logger.js +57 -0
- package/lib/utils/project_detector.js +227 -0
- package/lib/utils/snippet_loader.js +32 -0
- package/lib/utils/string_helper.js +56 -0
- package/lib/utils/templateSelectorEnquirer.js +200 -0
- package/package.json +31 -6
- package/release.sh +107 -0
- package/scripts/e2e-state-tests.js +116 -0
- package/scripts/sync-base-to-templates.js +108 -0
- package/scripts/workspace-clone-all.sh +101 -0
- package/scripts/workspace-status-all.sh +112 -0
- package/templates/README.md +138 -0
- package/templates/base_files/base_list_page.dart.template +174 -0
- package/templates/base_files/base_list_viewmodel.dart.template +134 -0
- package/templates/base_files/base_page.dart.template +251 -0
- package/templates/base_files/base_viewmodel.dart.template +77 -0
- package/templates/base_files/theme/status_views_theme.dart.template +46 -0
- package/templates/snippets/dart.code-snippets +487 -0
- package/lib/createProject.js +0 -220
- package/lib/flutterProjectCreator.js +0 -80
- package/lib/libCopier.js +0 -368
- package/lib/userInteraction.js +0 -274
- package/lib/utils.js +0 -200
- package/publish.sh +0 -29
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.dart 更新工具
|
|
3
|
+
* 自动更新目录的 index.dart 导出文件
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { logger } from './logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 更新 index.dart 文件
|
|
12
|
+
* @param {string} dirPath - 目录路径
|
|
13
|
+
* @param {string} fileName - 要添加的文件名
|
|
14
|
+
*/
|
|
15
|
+
export function updateIndexFile (dirPath, fileName) {
|
|
16
|
+
try {
|
|
17
|
+
const indexPath = join(dirPath, 'index.dart');
|
|
18
|
+
|
|
19
|
+
// 如果 index.dart 不存在,不做处理
|
|
20
|
+
if (!existsSync(indexPath)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 读取现有内容
|
|
25
|
+
let content = readFileSync(indexPath, 'utf8');
|
|
26
|
+
|
|
27
|
+
// 检查是否已经导出
|
|
28
|
+
const exportStatement = `export '${fileName}';`;
|
|
29
|
+
if (content.includes(exportStatement)) {
|
|
30
|
+
return false; // 已经存在,不需要更新
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 找到注释行后添加导出语句
|
|
34
|
+
const lines = content.split('\n');
|
|
35
|
+
let insertIndex = -1;
|
|
36
|
+
|
|
37
|
+
// 找到第一个注释行之后的位置
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
if (lines[i].trim().startsWith('//')) {
|
|
40
|
+
insertIndex = i + 1;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 如果没找到注释,添加到末尾
|
|
46
|
+
if (insertIndex === -1) {
|
|
47
|
+
insertIndex = lines.length;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 跳过空行
|
|
51
|
+
while (insertIndex < lines.length && lines[insertIndex].trim() === '') {
|
|
52
|
+
insertIndex++;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 插入导出语句
|
|
56
|
+
lines.splice(insertIndex, 0, exportStatement);
|
|
57
|
+
|
|
58
|
+
// 写回文件
|
|
59
|
+
content = lines.join('\n');
|
|
60
|
+
writeFileSync(indexPath, content, 'utf8');
|
|
61
|
+
|
|
62
|
+
logger.info(`已更新 index.dart: ${exportStatement}`);
|
|
63
|
+
return true;
|
|
64
|
+
|
|
65
|
+
} catch (error) {
|
|
66
|
+
logger.warn(`更新 index.dart 失败: ${error.message}`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志工具
|
|
3
|
+
* 统一的日志输出格式
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
export const logger = {
|
|
9
|
+
/**
|
|
10
|
+
* 成功信息
|
|
11
|
+
*/
|
|
12
|
+
success (message) {
|
|
13
|
+
console.log(chalk.green('✅ ' + message));
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 错误信息
|
|
18
|
+
*/
|
|
19
|
+
error (message) {
|
|
20
|
+
console.log(chalk.red('❌ ' + message));
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 警告信息
|
|
25
|
+
*/
|
|
26
|
+
warn (message) {
|
|
27
|
+
console.log(chalk.yellow('⚠️ ' + message));
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 信息
|
|
32
|
+
*/
|
|
33
|
+
info (message) {
|
|
34
|
+
console.log(chalk.blue('ℹ️ ' + message));
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 标题
|
|
39
|
+
*/
|
|
40
|
+
title (message) {
|
|
41
|
+
console.log(chalk.bold.cyan('\n' + message + '\n'));
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 分隔线
|
|
46
|
+
*/
|
|
47
|
+
divider () {
|
|
48
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 空行
|
|
53
|
+
*/
|
|
54
|
+
newLine () {
|
|
55
|
+
console.log('');
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 项目类型检测工具
|
|
3
|
+
* 职责:基于目录结构识别 lite/modular/clean 模板,并提供生成路径解析
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 检测项目模板类型
|
|
11
|
+
* @param {string} projectDir - 项目目录
|
|
12
|
+
* @returns {'lite'|'modular'|'clean'|null} 模板类型
|
|
13
|
+
*/
|
|
14
|
+
export function detectProjectTemplate (projectDir = process.cwd()) {
|
|
15
|
+
try {
|
|
16
|
+
const libDir = join(projectDir, 'lib');
|
|
17
|
+
|
|
18
|
+
if (!existsSync(libDir)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 检测 Clean 架构特征
|
|
23
|
+
// Clean 架构有 features/xxx/data, features/xxx/domain, features/xxx/presentation
|
|
24
|
+
const featuresDir = join(libDir, 'features');
|
|
25
|
+
if (existsSync(featuresDir)) {
|
|
26
|
+
// 检查是否有 data/domain/presentation 三层结构
|
|
27
|
+
const coreDir = join(libDir, 'core');
|
|
28
|
+
const hasUsecases = existsSync(join(coreDir, 'usecases'));
|
|
29
|
+
const hasErrors = existsSync(join(coreDir, 'errors'));
|
|
30
|
+
|
|
31
|
+
if (hasUsecases && hasErrors) {
|
|
32
|
+
return 'clean';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 检测 Modular 架构特征
|
|
36
|
+
// Modular 架构有 core/base, core/router, shared
|
|
37
|
+
const hasBase = existsSync(join(coreDir, 'base'));
|
|
38
|
+
const hasRouter = existsSync(join(coreDir, 'router'));
|
|
39
|
+
const sharedDir = existsSync(join(libDir, 'shared'));
|
|
40
|
+
|
|
41
|
+
if (hasBase && hasRouter && sharedDir) {
|
|
42
|
+
return 'modular';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 检测 Lite 架构特征
|
|
47
|
+
// Lite 架构有 pages, viewmodels, widgets 等扁平目录
|
|
48
|
+
const pagesDir = join(libDir, 'pages');
|
|
49
|
+
const viewmodelsDir = join(libDir, 'viewmodels');
|
|
50
|
+
const widgetsDir = join(libDir, 'widgets');
|
|
51
|
+
|
|
52
|
+
if (existsSync(pagesDir) && existsSync(viewmodelsDir) && existsSync(widgetsDir)) {
|
|
53
|
+
return 'lite';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 获取页面生成路径
|
|
64
|
+
* @param {string} projectDir - 项目目录
|
|
65
|
+
* @param {string} moduleName - 模块名称
|
|
66
|
+
* @returns {string} 页面目录路径
|
|
67
|
+
*/
|
|
68
|
+
export function getPagePath (projectDir, moduleName) {
|
|
69
|
+
const template = detectProjectTemplate(projectDir);
|
|
70
|
+
|
|
71
|
+
switch (template) {
|
|
72
|
+
case 'lite':
|
|
73
|
+
return join(projectDir, 'lib', 'pages');
|
|
74
|
+
|
|
75
|
+
case 'modular':
|
|
76
|
+
return join(projectDir, 'lib', 'features', moduleName, 'pages');
|
|
77
|
+
|
|
78
|
+
case 'clean':
|
|
79
|
+
return join(projectDir, 'lib', 'features', moduleName, 'presentation', 'pages');
|
|
80
|
+
|
|
81
|
+
default:
|
|
82
|
+
return join(projectDir, 'lib', 'pages');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 获取 ViewModel 生成路径
|
|
88
|
+
* @param {string} projectDir - 项目目录
|
|
89
|
+
* @param {string} moduleName - 模块名称
|
|
90
|
+
* @returns {string} ViewModel 目录路径
|
|
91
|
+
*/
|
|
92
|
+
export function getViewModelPath (projectDir, moduleName) {
|
|
93
|
+
const template = detectProjectTemplate(projectDir);
|
|
94
|
+
|
|
95
|
+
switch (template) {
|
|
96
|
+
case 'lite':
|
|
97
|
+
return join(projectDir, 'lib', 'viewmodels');
|
|
98
|
+
|
|
99
|
+
case 'modular':
|
|
100
|
+
return join(projectDir, 'lib', 'features', moduleName, 'viewmodels');
|
|
101
|
+
|
|
102
|
+
case 'clean':
|
|
103
|
+
return join(projectDir, 'lib', 'features', moduleName, 'presentation', 'viewmodels');
|
|
104
|
+
|
|
105
|
+
default:
|
|
106
|
+
return join(projectDir, 'lib', 'viewmodels');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 获取 Widget 生成路径
|
|
112
|
+
* @param {string} projectDir - 项目目录
|
|
113
|
+
* @param {string} moduleName - 模块名称(可选)
|
|
114
|
+
* @returns {string} Widget 目录路径
|
|
115
|
+
*/
|
|
116
|
+
export function getWidgetPath (projectDir, moduleName = null) {
|
|
117
|
+
const template = detectProjectTemplate(projectDir);
|
|
118
|
+
|
|
119
|
+
switch (template) {
|
|
120
|
+
case 'lite':
|
|
121
|
+
return join(projectDir, 'lib', 'widgets');
|
|
122
|
+
|
|
123
|
+
case 'modular':
|
|
124
|
+
if (moduleName) {
|
|
125
|
+
return join(projectDir, 'lib', 'features', moduleName, 'widgets');
|
|
126
|
+
}
|
|
127
|
+
return join(projectDir, 'lib', 'shared', 'widgets');
|
|
128
|
+
|
|
129
|
+
case 'clean':
|
|
130
|
+
if (moduleName) {
|
|
131
|
+
return join(projectDir, 'lib', 'features', moduleName, 'presentation', 'widgets');
|
|
132
|
+
}
|
|
133
|
+
return join(projectDir, 'lib', 'shared', 'widgets');
|
|
134
|
+
|
|
135
|
+
default:
|
|
136
|
+
return join(projectDir, 'lib', 'widgets');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 获取 Service 生成路径
|
|
142
|
+
* @param {string} projectDir - 项目目录
|
|
143
|
+
* @param {string} moduleName - 模块名称(可选)
|
|
144
|
+
* @returns {string} Service 目录路径
|
|
145
|
+
*/
|
|
146
|
+
export function getServicePath (projectDir, moduleName = null) {
|
|
147
|
+
const template = detectProjectTemplate(projectDir);
|
|
148
|
+
|
|
149
|
+
switch (template) {
|
|
150
|
+
case 'lite':
|
|
151
|
+
return join(projectDir, 'lib', 'services');
|
|
152
|
+
|
|
153
|
+
case 'modular':
|
|
154
|
+
if (moduleName) {
|
|
155
|
+
return join(projectDir, 'lib', 'features', moduleName, 'services');
|
|
156
|
+
}
|
|
157
|
+
return join(projectDir, 'lib', 'shared', 'services');
|
|
158
|
+
|
|
159
|
+
case 'clean':
|
|
160
|
+
if (moduleName) {
|
|
161
|
+
return join(projectDir, 'lib', 'features', moduleName, 'data', 'datasources');
|
|
162
|
+
}
|
|
163
|
+
return join(projectDir, 'lib', 'core', 'network');
|
|
164
|
+
|
|
165
|
+
default:
|
|
166
|
+
return join(projectDir, 'lib', 'services');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 获取 Model 生成路径
|
|
172
|
+
* @param {string} projectDir - 项目目录
|
|
173
|
+
* @param {string} moduleName - 模块名称(可选)
|
|
174
|
+
* @returns {string} Model 目录路径
|
|
175
|
+
*/
|
|
176
|
+
export function getModelPath (projectDir, moduleName = null) {
|
|
177
|
+
const template = detectProjectTemplate(projectDir);
|
|
178
|
+
|
|
179
|
+
switch (template) {
|
|
180
|
+
case 'lite':
|
|
181
|
+
return join(projectDir, 'lib', 'models');
|
|
182
|
+
|
|
183
|
+
case 'modular':
|
|
184
|
+
if (moduleName) {
|
|
185
|
+
return join(projectDir, 'lib', 'features', moduleName, 'models');
|
|
186
|
+
}
|
|
187
|
+
return join(projectDir, 'lib', 'shared', 'models');
|
|
188
|
+
|
|
189
|
+
case 'clean':
|
|
190
|
+
if (moduleName) {
|
|
191
|
+
return join(projectDir, 'lib', 'features', moduleName, 'data', 'models');
|
|
192
|
+
}
|
|
193
|
+
return join(projectDir, 'lib', 'shared', 'models');
|
|
194
|
+
|
|
195
|
+
default:
|
|
196
|
+
return join(projectDir, 'lib', 'models');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 获取相对导入路径
|
|
202
|
+
* @param {string} fromPath - 源文件路径
|
|
203
|
+
* @param {string} toPath - 目标文件路径
|
|
204
|
+
* @returns {string} 相对路径
|
|
205
|
+
*/
|
|
206
|
+
export function getRelativeImportPath (fromPath, toPath) {
|
|
207
|
+
const fromParts = fromPath.split('/');
|
|
208
|
+
const toParts = toPath.split('/');
|
|
209
|
+
|
|
210
|
+
// 找到共同的父目录
|
|
211
|
+
let commonIndex = 0;
|
|
212
|
+
while (commonIndex < fromParts.length && commonIndex < toParts.length) {
|
|
213
|
+
if (fromParts[commonIndex] !== toParts[commonIndex]) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
commonIndex++;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 计算需要返回的层级
|
|
220
|
+
const upLevels = fromParts.length - commonIndex - 1;
|
|
221
|
+
const upPath = '../'.repeat(upLevels);
|
|
222
|
+
|
|
223
|
+
// 计算目标路径
|
|
224
|
+
const targetPath = toParts.slice(commonIndex).join('/');
|
|
225
|
+
|
|
226
|
+
return upPath + targetPath;
|
|
227
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import JSON5 from 'json5';
|
|
4
|
+
|
|
5
|
+
// 读取项目片段文件
|
|
6
|
+
export function loadProjectSnippets (projectDir) {
|
|
7
|
+
try {
|
|
8
|
+
const snippetsPath = join(projectDir, '.vscode', 'dart.code-snippets');
|
|
9
|
+
if (!existsSync(snippetsPath)) return {};
|
|
10
|
+
const raw = readFileSync(snippetsPath, 'utf8');
|
|
11
|
+
const json = JSON5.parse(raw);
|
|
12
|
+
return json || {};
|
|
13
|
+
} catch (_) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 渲染片段内容
|
|
19
|
+
export function renderSnippet (bodyLines, variables) {
|
|
20
|
+
const body = Array.isArray(bodyLines) ? bodyLines.join('\n') : String(bodyLines || '');
|
|
21
|
+
return body.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
22
|
+
return Object.prototype.hasOwnProperty.call(variables, key) ? String(variables[key]) : `{{${key}}}`;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 简便方法:按 key 获取片段并渲染
|
|
27
|
+
export function getSnippetContent (projectDir, key, variables) {
|
|
28
|
+
const map = loadProjectSnippets(projectDir);
|
|
29
|
+
const item = map[key];
|
|
30
|
+
if (!item || !item.body) return null;
|
|
31
|
+
return renderSnippet(item.body, variables);
|
|
32
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字符串工具函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 转换为 PascalCase
|
|
7
|
+
* home_page -> HomePage
|
|
8
|
+
*/
|
|
9
|
+
export function toPascalCase (str) {
|
|
10
|
+
return str
|
|
11
|
+
.split(/[-_\s]+/)
|
|
12
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
13
|
+
.join('');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 转换为 snake_case
|
|
18
|
+
* HomePage -> home_page
|
|
19
|
+
*/
|
|
20
|
+
export function toSnakeCase (str) {
|
|
21
|
+
return str
|
|
22
|
+
.replace(/([A-Z])/g, '_$1')
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/^_/, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 转换为 camelCase
|
|
29
|
+
* home_page -> homePage
|
|
30
|
+
*/
|
|
31
|
+
export function toCamelCase (str) {
|
|
32
|
+
const pascal = toPascalCase(str);
|
|
33
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 转换为 Title Case
|
|
38
|
+
* home_page -> Home Page
|
|
39
|
+
*/
|
|
40
|
+
export function toTitleCase (str) {
|
|
41
|
+
return str
|
|
42
|
+
.split(/[-_\s]+/)
|
|
43
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
44
|
+
.join(' ');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 转换为 kebab-case
|
|
49
|
+
* HomePage -> home-page
|
|
50
|
+
*/
|
|
51
|
+
export function toKebabCase (str) {
|
|
52
|
+
return str
|
|
53
|
+
.replace(/([A-Z])/g, '-$1')
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(/^-/, '');
|
|
56
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 使用 Enquirer 实现实时模板预览
|
|
3
|
+
* 当用户上下切换时,实时显示对应模板的结构
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import enquirer from 'enquirer';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { basename } from 'path';
|
|
10
|
+
import { getAllTemplates } from '../../config/templates.js';
|
|
11
|
+
import { getDefaultTemplate, getCustomTemplates, addOrUpdateCustomTemplate, saveDefaultTemplate } from '../utils/config.js';
|
|
12
|
+
|
|
13
|
+
const { Select } = enquirer;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 选择模板(带实时预览 - Enquirer 版本)
|
|
17
|
+
*/
|
|
18
|
+
export async function selectTemplateWithEnquirer () {
|
|
19
|
+
const templates = getAllTemplates();
|
|
20
|
+
const customTemplates = getCustomTemplates();
|
|
21
|
+
const defaultTpl = getDefaultTemplate();
|
|
22
|
+
|
|
23
|
+
// 创建自定义的 Select prompt
|
|
24
|
+
class TemplateSelect extends Select {
|
|
25
|
+
async render () {
|
|
26
|
+
const { submitted, size } = this.state;
|
|
27
|
+
const index = this.index;
|
|
28
|
+
// 选择项与模板索引可能不一致,安全获取
|
|
29
|
+
const choice = this.choices[index];
|
|
30
|
+
const isBuiltin = choice?.name?.startsWith('builtin:');
|
|
31
|
+
const builtinIndex = isBuiltin ? templates.findIndex(t => `builtin:${t.name.toLowerCase()}` === choice.name) : -1;
|
|
32
|
+
const template = isBuiltin && builtinIndex >= 0 ? templates[builtinIndex] : null;
|
|
33
|
+
|
|
34
|
+
let prompt = '';
|
|
35
|
+
let header = await this.header();
|
|
36
|
+
let prefix = await this.prefix();
|
|
37
|
+
let separator = await this.separator();
|
|
38
|
+
let message = await this.message();
|
|
39
|
+
|
|
40
|
+
if (this.options.promptLine !== false) {
|
|
41
|
+
prompt = [prefix, message, separator].filter(Boolean).join(' ');
|
|
42
|
+
this.state.prompt = prompt;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let output = prompt + '\n\n';
|
|
46
|
+
|
|
47
|
+
// 显示选项列表
|
|
48
|
+
for (let i = 0; i < this.choices.length; i++) {
|
|
49
|
+
const choice = this.choices[i];
|
|
50
|
+
const isSelected = i === index;
|
|
51
|
+
const prefix = isSelected ? chalk.cyan('❯') : ' ';
|
|
52
|
+
const style = isSelected ? chalk.cyan : chalk.gray;
|
|
53
|
+
output += `${prefix} ${style(choice.message)}\n`;
|
|
54
|
+
if (choice.hint) {
|
|
55
|
+
output += ` ${chalk.gray(choice.hint)}\n`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 显示当前模板的结构预览(仅内置模板)
|
|
60
|
+
output += '\n' + chalk.cyan('─'.repeat(70)) + '\n';
|
|
61
|
+
if (template) {
|
|
62
|
+
output += chalk.yellow('📁 项目结构预览:\n\n');
|
|
63
|
+
const structureLines = template.structure.trim().split('\n');
|
|
64
|
+
structureLines.forEach(line => {
|
|
65
|
+
output += chalk.gray(line) + '\n';
|
|
66
|
+
});
|
|
67
|
+
output += '\n';
|
|
68
|
+
output += chalk.cyan('✨ 特性: ') + chalk.gray(template.features.join(', ')) + '\n';
|
|
69
|
+
output += chalk.cyan('📊 适用: ') + chalk.gray(`${template.teamSize}, ${template.codeSize}`) + '\n';
|
|
70
|
+
} else {
|
|
71
|
+
output += chalk.yellow('自定义模板:不提供结构预览') + '\n';
|
|
72
|
+
}
|
|
73
|
+
output += chalk.cyan('─'.repeat(70));
|
|
74
|
+
|
|
75
|
+
this.clear(size);
|
|
76
|
+
this.write(output);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 构建选择项:内置模板 + 自定义模板 + 新增自定义入口
|
|
81
|
+
const builtinChoices = templates.map(t => ({
|
|
82
|
+
name: `builtin:${t.name.toLowerCase()}`,
|
|
83
|
+
message: `${t.displayName}`
|
|
84
|
+
}));
|
|
85
|
+
const customChoices = customTemplates.map(ct => ({
|
|
86
|
+
name: `custom:${ct.id}`,
|
|
87
|
+
message: `自定义:${ct.name}`,
|
|
88
|
+
hint: ct.type === 'git' ? ct.pathOrUrl : `本地: ${ct.pathOrUrl}`
|
|
89
|
+
}));
|
|
90
|
+
const addCustomChoice = { name: '__add_custom__', message: chalk.yellow('➕ 新增自定义模板(本地或 Git)') };
|
|
91
|
+
|
|
92
|
+
const allChoices = [...builtinChoices, ...customChoices, addCustomChoice];
|
|
93
|
+
|
|
94
|
+
// 计算默认选中索引
|
|
95
|
+
let initialIndex = 0;
|
|
96
|
+
if (defaultTpl && defaultTpl.type === 'builtin') {
|
|
97
|
+
const idx = allChoices.findIndex(c => c.name === `builtin:${defaultTpl.idOrName}`);
|
|
98
|
+
if (idx >= 0) initialIndex = idx;
|
|
99
|
+
} else if (defaultTpl && defaultTpl.type === 'custom') {
|
|
100
|
+
const idx = allChoices.findIndex(c => c.name === `custom:${defaultTpl.idOrName}`);
|
|
101
|
+
if (idx >= 0) initialIndex = idx;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const prompt = new TemplateSelect({
|
|
105
|
+
name: 'template',
|
|
106
|
+
message: '选择项目模板(使用 ↑↓ 键切换,实时查看结构)',
|
|
107
|
+
choices: allChoices,
|
|
108
|
+
initial: initialIndex
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const answer = await prompt.run();
|
|
113
|
+
|
|
114
|
+
// 清除屏幕上的大量输出,只保留简洁的确认信息
|
|
115
|
+
console.clear();
|
|
116
|
+
|
|
117
|
+
// 显示选中确认
|
|
118
|
+
if (answer === '__add_custom__') {
|
|
119
|
+
// 选择来源类型
|
|
120
|
+
const typePrompt = new enquirer.Select({
|
|
121
|
+
name: 'sourceType',
|
|
122
|
+
message: '选择自定义模板来源',
|
|
123
|
+
choices: [
|
|
124
|
+
{ name: 'local', message: '本地目录' },
|
|
125
|
+
{ name: 'git', message: 'Git 仓库' }
|
|
126
|
+
]
|
|
127
|
+
});
|
|
128
|
+
const sourceType = await typePrompt.run();
|
|
129
|
+
|
|
130
|
+
// 输入路径或 URL
|
|
131
|
+
const inputPrompt = new enquirer.Input({
|
|
132
|
+
name: 'pathOrUrl',
|
|
133
|
+
message: sourceType === 'git' ? '请输入 Git 仓库地址(如 https://... 或 git@...)' : '请输入本地模板目录绝对路径'
|
|
134
|
+
});
|
|
135
|
+
const pathOrUrl = await inputPrompt.run();
|
|
136
|
+
|
|
137
|
+
// 分支(Git)
|
|
138
|
+
let branch = 'main';
|
|
139
|
+
if (sourceType === 'git') {
|
|
140
|
+
const branchPrompt = new enquirer.Input({
|
|
141
|
+
name: 'branch',
|
|
142
|
+
message: '请输入分支名称(默认 main)',
|
|
143
|
+
initial: 'main'
|
|
144
|
+
});
|
|
145
|
+
branch = await branchPrompt.run();
|
|
146
|
+
if (!branch) branch = 'main';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 基本校验
|
|
150
|
+
if (sourceType === 'local' && !existsSync(pathOrUrl)) {
|
|
151
|
+
console.log(chalk.red('本地目录不存在:'), pathOrUrl);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 生成 id 与名称
|
|
156
|
+
const nameGuess = sourceType === 'git'
|
|
157
|
+
? (pathOrUrl.split('/').pop() || 'custom')
|
|
158
|
+
: basename(pathOrUrl);
|
|
159
|
+
const id = `${nameGuess.replace(/\W+/g, '-').toLowerCase()}-${Date.now()}`;
|
|
160
|
+
|
|
161
|
+
const saved = addOrUpdateCustomTemplate({
|
|
162
|
+
id,
|
|
163
|
+
name: nameGuess,
|
|
164
|
+
type: sourceType,
|
|
165
|
+
pathOrUrl,
|
|
166
|
+
branch
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
console.log(chalk.green('✓ 已保存自定义模板:'), chalk.cyan.bold(saved.name));
|
|
170
|
+
saveDefaultTemplate({ type: 'custom', idOrName: saved.id });
|
|
171
|
+
return { kind: 'custom', id: saved.id };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (answer.startsWith('builtin:')) {
|
|
175
|
+
const name = answer.split(':')[1];
|
|
176
|
+
const selectedTemplate = templates.find(t => t.name.toLowerCase() === name);
|
|
177
|
+
console.log(chalk.green('✓ 已选择:'), chalk.cyan.bold(selectedTemplate.displayName));
|
|
178
|
+
saveDefaultTemplate({ type: 'builtin', idOrName: name });
|
|
179
|
+
return { kind: 'builtin', name };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (answer.startsWith('custom:')) {
|
|
183
|
+
const id = answer.split(':')[1];
|
|
184
|
+
const ct = customTemplates.find(t => t.id === id);
|
|
185
|
+
console.log(chalk.green('✓ 已选择自定义:'), chalk.cyan.bold(ct?.name || id));
|
|
186
|
+
saveDefaultTemplate({ type: 'custom', idOrName: id });
|
|
187
|
+
return { kind: 'custom', id };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 兜底(保持兼容)
|
|
191
|
+
const name = answer;
|
|
192
|
+
saveDefaultTemplate({ type: 'builtin', idOrName: name });
|
|
193
|
+
const selectedTemplate = templates.find(t => t.name.toLowerCase() === name);
|
|
194
|
+
console.log(chalk.green('✓ 已选择:'), chalk.cyan.bold(selectedTemplate?.displayName || name));
|
|
195
|
+
return { kind: 'builtin', name };
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.log(chalk.yellow('\n操作已取消'));
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
}
|