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,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模板复制器
|
|
3
|
+
* 职责:复制内置/自定义模板至目标目录,并执行变量替换与文件筛选
|
|
4
|
+
* 特性:保留平台工程与包名、支持 .template 文件落地、清理模板特有文件
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fsExtra from 'fs-extra';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
10
|
+
import { logger } from '../utils/logger.js';
|
|
11
|
+
|
|
12
|
+
const { copySync, removeSync } = fsExtra;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 复制模板到目标目录
|
|
16
|
+
* @param {string} templatePath - 模板路径
|
|
17
|
+
* @param {string} targetPath - 目标路径
|
|
18
|
+
* @returns {Promise<boolean>} 是否成功
|
|
19
|
+
*/
|
|
20
|
+
export async function copyTemplate (templatePath, targetPath) {
|
|
21
|
+
try {
|
|
22
|
+
// 检查模板路径是否存在
|
|
23
|
+
if (!existsSync(templatePath)) {
|
|
24
|
+
logger.error(`模板路径不存在: ${templatePath}`);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 需要排除的文件和目录
|
|
29
|
+
const excludes = [
|
|
30
|
+
'.git',
|
|
31
|
+
'.github',
|
|
32
|
+
'node_modules',
|
|
33
|
+
'.flu-cli.yaml',
|
|
34
|
+
'README.template.md',
|
|
35
|
+
'.DS_Store',
|
|
36
|
+
'pubspec.yaml' // 排除本地调试用的 pubspec.yaml
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// 复制文件,排除特定文件
|
|
40
|
+
copySync(templatePath, targetPath, {
|
|
41
|
+
filter: (src) => {
|
|
42
|
+
const basename = src.split('/').pop();
|
|
43
|
+
return !excludes.includes(basename);
|
|
44
|
+
},
|
|
45
|
+
overwrite: true
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 处理 pubspec.yaml.template -> pubspec.yaml
|
|
49
|
+
const templatePubspec = join(templatePath, 'pubspec.yaml.template');
|
|
50
|
+
const targetPubspec = join(targetPath, 'pubspec.yaml');
|
|
51
|
+
if (existsSync(templatePubspec)) {
|
|
52
|
+
// 复制 .template 文件并覆盖 Flutter 生成的 pubspec.yaml
|
|
53
|
+
copySync(templatePubspec, targetPubspec, { overwrite: true });
|
|
54
|
+
logger.info('已使用模板的 pubspec.yaml.template');
|
|
55
|
+
} else {
|
|
56
|
+
logger.warn('未找到 pubspec.yaml.template,使用 Flutter 默认配置');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.error(`复制模板失败: ${error.message}`);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 仅复制自定义模板中的通用代码与配置,保留目标工程的平台与包名
|
|
68
|
+
export async function copyCustomTemplate (templatePath, targetPath) {
|
|
69
|
+
try {
|
|
70
|
+
if (!existsSync(templatePath)) {
|
|
71
|
+
logger.error(`模板路径不存在: ${templatePath}`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const includeDirs = ['lib', 'assets', '.vscode'];
|
|
76
|
+
const includeFiles = ['analysis_options.yaml', 'README.md', 'pubspec.yaml'];
|
|
77
|
+
|
|
78
|
+
for (const dir of includeDirs) {
|
|
79
|
+
const srcDir = join(templatePath, dir);
|
|
80
|
+
const dstDir = join(targetPath, dir);
|
|
81
|
+
if (existsSync(srcDir)) {
|
|
82
|
+
copySync(srcDir, dstDir, { overwrite: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const file of includeFiles) {
|
|
87
|
+
const srcFile = join(templatePath, file);
|
|
88
|
+
const dstFile = join(targetPath, file);
|
|
89
|
+
if (existsSync(srcFile)) {
|
|
90
|
+
copySync(srcFile, dstFile, { overwrite: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
logger.error(`复制自定义模板失败: ${error.message}`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 使用模板的 pubspec.yaml 合并覆盖目标项目(仅修改 name 保持项目名)
|
|
103
|
+
*/
|
|
104
|
+
export function mergePubspecFromTemplate (templatePath, projectDir, projectName) {
|
|
105
|
+
try {
|
|
106
|
+
const tplPub = join(templatePath, 'pubspec.yaml');
|
|
107
|
+
const dstPub = join(projectDir, 'pubspec.yaml');
|
|
108
|
+
if (!existsSync(tplPub) || !existsSync(dstPub)) return;
|
|
109
|
+
let content = readFileSync(tplPub, 'utf8');
|
|
110
|
+
if (/^name:\s+/m.test(content)) {
|
|
111
|
+
content = content.replace(/^name:\s+.*/m, `name: ${projectName}`);
|
|
112
|
+
} else {
|
|
113
|
+
content = `name: ${projectName}\n` + content;
|
|
114
|
+
}
|
|
115
|
+
writeFileSync(dstPub, content, 'utf8');
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// 忽略错误
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 替换文件中的变量
|
|
123
|
+
* @param {string} projectDir - 项目目录
|
|
124
|
+
* @param {object} variables - 变量对象
|
|
125
|
+
*/
|
|
126
|
+
export async function replaceVariables (projectDir, variables) {
|
|
127
|
+
try {
|
|
128
|
+
// 需要替换变量的文件模式
|
|
129
|
+
const patterns = [
|
|
130
|
+
'**/*.dart',
|
|
131
|
+
'pubspec.yaml',
|
|
132
|
+
'README.md',
|
|
133
|
+
'android/app/build.gradle',
|
|
134
|
+
'ios/Runner/Info.plist'
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
// 变量映射
|
|
138
|
+
const replacements = {
|
|
139
|
+
'{{projectName}}': variables.projectName,
|
|
140
|
+
'{{project_name}}': variables.projectName,
|
|
141
|
+
'{{package_name}}': variables.packageName,
|
|
142
|
+
'{{author}}': variables.author || 'Your Name',
|
|
143
|
+
'{{year}}': new Date().getFullYear().toString()
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// 递归替换目录中的文件
|
|
147
|
+
replaceInDirectory(projectDir, replacements);
|
|
148
|
+
|
|
149
|
+
// 修复 test 文件中的导入路径
|
|
150
|
+
fixTestFile(projectDir, variables.projectName);
|
|
151
|
+
|
|
152
|
+
return true;
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.error(`替换变量失败: ${error.message}`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 递归替换目录中的文件内容
|
|
162
|
+
*/
|
|
163
|
+
function replaceInDirectory (dir, replacements) {
|
|
164
|
+
const files = readdirSync(dir);
|
|
165
|
+
|
|
166
|
+
files.forEach(file => {
|
|
167
|
+
const filePath = join(dir, file);
|
|
168
|
+
const stat = statSync(filePath);
|
|
169
|
+
|
|
170
|
+
if (stat.isDirectory()) {
|
|
171
|
+
// 跳过特定目录
|
|
172
|
+
if (['.git', 'node_modules', '.dart_tool', 'build'].includes(file)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
replaceInDirectory(filePath, replacements);
|
|
176
|
+
} else if (stat.isFile()) {
|
|
177
|
+
// 只处理文本文件
|
|
178
|
+
if (isTextFile(filePath)) {
|
|
179
|
+
replaceInFile(filePath, replacements);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 替换单个文件中的内容
|
|
187
|
+
*/
|
|
188
|
+
function replaceInFile (filePath, replacements) {
|
|
189
|
+
try {
|
|
190
|
+
let content = readFileSync(filePath, 'utf8');
|
|
191
|
+
let modified = false;
|
|
192
|
+
|
|
193
|
+
// 执行所有替换
|
|
194
|
+
Object.entries(replacements).forEach(([pattern, value]) => {
|
|
195
|
+
if (content.includes(pattern)) {
|
|
196
|
+
content = content.replace(new RegExp(pattern, 'g'), value);
|
|
197
|
+
modified = true;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 如果有修改,写回文件
|
|
202
|
+
if (modified) {
|
|
203
|
+
writeFileSync(filePath, content, 'utf8');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// 忽略二进制文件等错误
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 判断是否为文本文件
|
|
213
|
+
*/
|
|
214
|
+
function isTextFile (filePath) {
|
|
215
|
+
const textExtensions = [
|
|
216
|
+
'.dart', '.yaml', '.yml', '.json', '.md', '.txt',
|
|
217
|
+
'.gradle', '.xml', '.plist', '.swift', '.kt', '.java',
|
|
218
|
+
'.js', '.ts', '.html', '.css', '.sh'
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
return textExtensions.some(ext => filePath.endsWith(ext));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 修复 test 文件中的导入路径
|
|
226
|
+
*/
|
|
227
|
+
function fixTestFile (projectDir, projectName) {
|
|
228
|
+
const testFile = join(projectDir, 'test', 'widget_test.dart');
|
|
229
|
+
|
|
230
|
+
if (existsSync(testFile)) {
|
|
231
|
+
try {
|
|
232
|
+
let content = readFileSync(testFile, 'utf8');
|
|
233
|
+
|
|
234
|
+
// 替换导入路径:从 main.dart 改为 app.dart
|
|
235
|
+
content = content.replace(
|
|
236
|
+
`import 'package:${projectName}/main.dart';`,
|
|
237
|
+
`import 'package:${projectName}/app.dart';`
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// 如果测试内容还是默认的计数器测试,可以注释掉或删除
|
|
241
|
+
if (content.includes('Counter increments smoke test')) {
|
|
242
|
+
content = `import 'package:flutter_test/flutter_test.dart';
|
|
243
|
+
import 'package:${projectName}/app.dart';
|
|
244
|
+
|
|
245
|
+
void main() {
|
|
246
|
+
testWidgets('App smoke test', (WidgetTester tester) async {
|
|
247
|
+
await tester.pumpWidget(const App());
|
|
248
|
+
expect(find.byType(App), findsOneWidget);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
writeFileSync(testFile, content, 'utf8');
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// 忽略错误
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 强制校正 pubspec.yaml 的 name 字段为项目名
|
|
263
|
+
*/
|
|
264
|
+
export function ensurePubspecName (projectDir, projectName) {
|
|
265
|
+
try {
|
|
266
|
+
const pubspec = join(projectDir, 'pubspec.yaml');
|
|
267
|
+
if (!existsSync(pubspec)) return;
|
|
268
|
+
let content = readFileSync(pubspec, 'utf8');
|
|
269
|
+
if (/^name:\s+/m.test(content)) {
|
|
270
|
+
content = content.replace(/^name:\s+.*/m, `name: ${projectName}`);
|
|
271
|
+
} else {
|
|
272
|
+
content = `name: ${projectName}\n` + content;
|
|
273
|
+
}
|
|
274
|
+
writeFileSync(pubspec, content, 'utf8');
|
|
275
|
+
} catch (error) {
|
|
276
|
+
// 忽略错误
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 清理模板特有文件
|
|
282
|
+
*/
|
|
283
|
+
export function cleanupTemplateFiles (projectDir) {
|
|
284
|
+
const filesToRemove = [
|
|
285
|
+
'.flu-cli.yaml',
|
|
286
|
+
'README.template.md',
|
|
287
|
+
'.git'
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
filesToRemove.forEach(file => {
|
|
291
|
+
const filePath = join(projectDir, file);
|
|
292
|
+
if (existsSync(filePath)) {
|
|
293
|
+
removeSync(filePath);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模板管理器
|
|
3
|
+
* 负责克隆、更新和管理模板缓存
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import simpleGit from 'simple-git';
|
|
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
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 获取模板缓存路径
|
|
38
|
+
*/
|
|
39
|
+
export function getTemplateCachePath (templateName) {
|
|
40
|
+
return join(CACHE_DIR, templateName);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 克隆或更新模板
|
|
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
|
+
*/
|
|
53
|
+
export async function cloneOrUpdateTemplate (templateName, repoUrl, branch = 'main', forceUpdate = false, isLocal = false, sourceLocalPath = null) {
|
|
54
|
+
try {
|
|
55
|
+
ensureCacheDir();
|
|
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();
|
|
91
|
+
|
|
92
|
+
// 如果模板已存在
|
|
93
|
+
if (existsSync(templatePath)) {
|
|
94
|
+
if (forceUpdate) {
|
|
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
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 检查模板是否有更新
|
|
136
|
+
*/
|
|
137
|
+
export async function checkTemplateUpdate (templateName) {
|
|
138
|
+
try {
|
|
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
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 获取模板信息
|
|
169
|
+
*/
|
|
170
|
+
export async function getTemplateInfo (templateName) {
|
|
171
|
+
try {
|
|
172
|
+
const templatePath = getTemplateCachePath(templateName);
|
|
173
|
+
|
|
174
|
+
if (!existsSync(templatePath)) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const git = simpleGit(templatePath);
|
|
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
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置管理 - 缓存用户设置
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
|
|
9
|
+
const CONFIG_DIR = join(homedir(), '.flu-cli');
|
|
10
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 读取配置
|
|
14
|
+
*/
|
|
15
|
+
export function getConfig () {
|
|
16
|
+
try {
|
|
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 {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 保存配置
|
|
29
|
+
*/
|
|
30
|
+
export function saveConfig (config) {
|
|
31
|
+
try {
|
|
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
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 获取作者名字
|
|
44
|
+
*/
|
|
45
|
+
export function getAuthorName () {
|
|
46
|
+
const config = getConfig();
|
|
47
|
+
return config.authorName || 'Your Name';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 保存作者名字
|
|
52
|
+
*/
|
|
53
|
+
export function saveAuthorName (name) {
|
|
54
|
+
const config = getConfig();
|
|
55
|
+
config.authorName = name;
|
|
56
|
+
saveConfig(config);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 读取默认模板(builtin/custom)
|
|
60
|
+
export function getDefaultTemplate () {
|
|
61
|
+
const config = getConfig();
|
|
62
|
+
return config.defaultTemplate || { type: 'builtin', idOrName: 'lite' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 保存默认模板(builtin/custom)
|
|
66
|
+
export function saveDefaultTemplate (defaultTemplate) {
|
|
67
|
+
const config = getConfig();
|
|
68
|
+
config.defaultTemplate = defaultTemplate;
|
|
69
|
+
saveConfig(config);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 获取自定义模板列表
|
|
73
|
+
export function getCustomTemplates () {
|
|
74
|
+
const config = getConfig();
|
|
75
|
+
return Array.isArray(config.customTemplates) ? config.customTemplates : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 根据 id 查找自定义模板
|
|
79
|
+
export function findCustomTemplateById (id) {
|
|
80
|
+
const list = getCustomTemplates();
|
|
81
|
+
return list.find(t => t.id === id) || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 新增或更新自定义模板
|
|
85
|
+
export function addOrUpdateCustomTemplate (tpl) {
|
|
86
|
+
const config = getConfig();
|
|
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);
|
|
98
|
+
return tpl;
|
|
99
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flutter 辅助工具
|
|
3
|
+
* 职责:封装 flutter create/pub get/版本检测等命令调用
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { logger } from './logger.js';
|
|
9
|
+
|
|
10
|
+
const execAsync = promisify(exec);
|
|
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
|
+
}
|