flu-cli 0.0.5 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLI.md +512 -0
- package/README.md +70 -260
- package/config/dev.config.js +56 -0
- package/config/templates.js +147 -0
- package/index.js +177 -81
- package/lib/commands/add.js +595 -0
- package/lib/commands/cache.js +99 -0
- package/lib/commands/completion.js +92 -0
- package/lib/commands/config.js +70 -0
- package/lib/commands/newClack.js +399 -0
- package/lib/commands/snippets.js +39 -0
- package/lib/commands/template.js +113 -0
- package/lib/commands/templates.js +84 -0
- package/lib/generators/model_generator.js +303 -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 +203 -0
- package/package.json +34 -7
- 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 +392 -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,92 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 生成 Shell 自动补全脚本
|
|
5
|
+
*/
|
|
6
|
+
export function completion () {
|
|
7
|
+
const script = `
|
|
8
|
+
###-begin-flu-cli-completion-###
|
|
9
|
+
#
|
|
10
|
+
# flu-cli command completion script
|
|
11
|
+
#
|
|
12
|
+
# Installation: flu-cli completion >> ~/.zshrc (or ~/.bashrc)
|
|
13
|
+
#
|
|
14
|
+
|
|
15
|
+
if type compdef &>/dev/null; then
|
|
16
|
+
_flu_cli_completion() {
|
|
17
|
+
local -a commands
|
|
18
|
+
commands=(
|
|
19
|
+
'new:创建新 Flutter 项目'
|
|
20
|
+
'n:创建新 Flutter 项目 (new 别名)'
|
|
21
|
+
'add:添加组件 (page, widget, etc.)'
|
|
22
|
+
'a:添加组件 (add 别名)'
|
|
23
|
+
'templates:管理模板'
|
|
24
|
+
't:管理模板 (templates 别名)'
|
|
25
|
+
'update-templates:更新模板缓存'
|
|
26
|
+
'u:更新模板缓存 (update-templates 别名)'
|
|
27
|
+
'cache:管理缓存'
|
|
28
|
+
'c:管理缓存 (cache 别名)'
|
|
29
|
+
'sync-snippets:同步标准代码片段'
|
|
30
|
+
'sync:同步标准代码片段 (sync-snippets 别名)'
|
|
31
|
+
'completion:生成自动补全脚本'
|
|
32
|
+
'comp:生成自动补全脚本 (completion 别名)'
|
|
33
|
+
'help:显示帮助信息'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
local -a add_types
|
|
37
|
+
add_types=(
|
|
38
|
+
'page:页面组件'
|
|
39
|
+
'widget:通用组件'
|
|
40
|
+
'component:业务组件'
|
|
41
|
+
'viewmodel:ViewModel'
|
|
42
|
+
'service:服务层 (Service)'
|
|
43
|
+
'model:数据模型 (Model)'
|
|
44
|
+
'module:完整功能模块'
|
|
45
|
+
'p:页面 (page 别名)'
|
|
46
|
+
'w:组件 (widget 别名)'
|
|
47
|
+
'c:业务组件 (component 别名)'
|
|
48
|
+
'v:ViewModel (vm 别名)'
|
|
49
|
+
's:服务 (service 别名)'
|
|
50
|
+
'm:模型 (model 别名)'
|
|
51
|
+
'mod:模块 (module 别名)'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_arguments -C \\
|
|
55
|
+
'1: :->command' \\
|
|
56
|
+
'*:: :->args'
|
|
57
|
+
|
|
58
|
+
case $state in
|
|
59
|
+
(command)
|
|
60
|
+
_describe -t commands 'flu-cli commands' commands
|
|
61
|
+
;;
|
|
62
|
+
(args)
|
|
63
|
+
case $line[1] in
|
|
64
|
+
(add|a)
|
|
65
|
+
_describe -t add_types 'component types' add_types
|
|
66
|
+
;;
|
|
67
|
+
(new|n)
|
|
68
|
+
_arguments \\
|
|
69
|
+
'--template[模板类型]:template:(lite modular clean)' \\
|
|
70
|
+
'--state[状态管理器]:state:(default provider getx riverpod)' \\
|
|
71
|
+
'--dir[目标目录]:dir:_files -/' \\
|
|
72
|
+
'--no-cache[禁用缓存]' \\
|
|
73
|
+
'--remote[使用远程模板]'
|
|
74
|
+
;;
|
|
75
|
+
*)
|
|
76
|
+
;;
|
|
77
|
+
esac
|
|
78
|
+
;;
|
|
79
|
+
esac
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
compdef _flu_cli_completion flu-cli
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
###-end-flu-cli-completion-###
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
console.log(script);
|
|
89
|
+
console.error(chalk.green('✅ 补全脚本已生成'));
|
|
90
|
+
console.error(chalk.gray('请将其添加到您的 shell 配置文件中 (例如 ~/.zshrc 或 ~/.bashrc)'));
|
|
91
|
+
console.error(chalk.gray('用法: flu-cli completion >> ~/.zshrc && source ~/.zshrc'));
|
|
92
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config 命令
|
|
3
|
+
* 职责:初始化和管理项目配置文件
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ProjectConfigManager, detectProjectTemplate } from '@flu-cli/core';
|
|
7
|
+
import { writeFileSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 初始化配置文件
|
|
13
|
+
*/
|
|
14
|
+
export function initConfig (options) {
|
|
15
|
+
try {
|
|
16
|
+
const projectDir = options.dir || process.cwd();
|
|
17
|
+
const configPath = join(projectDir, '.flu-cli.json');
|
|
18
|
+
|
|
19
|
+
if (existsSync(configPath) && !options.force) {
|
|
20
|
+
console.log(chalk.yellow('⚠️ 配置文件已存在 (.flu-cli.json)'));
|
|
21
|
+
console.log(chalk.gray('使用 --force 覆盖'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 检测当前项目模板,生成对应的默认配置
|
|
26
|
+
const template = detectProjectTemplate(projectDir) || 'custom';
|
|
27
|
+
let config = ProjectConfigManager.getDefaultConfigTemplate(template);
|
|
28
|
+
|
|
29
|
+
// Smart Init: 现实检查,防止默认配置依赖的 BasePage/BaseViewModel 在项目中不存在
|
|
30
|
+
try {
|
|
31
|
+
const pageConfig = config.generators && config.generators.page;
|
|
32
|
+
if (pageConfig && pageConfig.withBasePage) {
|
|
33
|
+
const possiblePaths = [
|
|
34
|
+
join(projectDir, 'lib/base/base_page.dart'),
|
|
35
|
+
join(projectDir, 'lib/core/base/base_page.dart'),
|
|
36
|
+
join(projectDir, 'lib/core/presentation/base/base_page.dart')
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
if (pageConfig.basePageImport && pageConfig.basePageImport.startsWith('lib/')) {
|
|
40
|
+
possiblePaths.unshift(join(projectDir, pageConfig.basePageImport));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const basePageExists = possiblePaths.some(p => existsSync(p));
|
|
44
|
+
|
|
45
|
+
if (!basePageExists) {
|
|
46
|
+
console.log(chalk.yellow('Smart Init: 未检测到 BasePage 文件,降级为原生模式 (Native Mode)'));
|
|
47
|
+
if (config.generators && config.generators.page) {
|
|
48
|
+
config.generators.page.withBasePage = false;
|
|
49
|
+
config.generators.page.withViewModel = false;
|
|
50
|
+
}
|
|
51
|
+
if (config.generators && config.generators.viewModel) {
|
|
52
|
+
config.generators.viewModel.withBaseViewModel = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.log(chalk.gray(`Smart Init 检查失败: ${e.message || e}`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
61
|
+
|
|
62
|
+
console.log(chalk.green(`✓ 配置文件已生成: ${configPath}`));
|
|
63
|
+
console.log(chalk.cyan(`ℹ️ 基于检测到的模板类型: ${template}`));
|
|
64
|
+
console.log(chalk.gray('你可以编辑 .flu-cli.json 来自定义生成规则'));
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(chalk.red(`初始化配置失败: ${error.message}`));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* new 命令(简化版)
|
|
3
|
+
* 职责:交互创建 Flutter 项目、选择模板与状态管理器,并触发生成器落地
|
|
4
|
+
* 说明:保留平台与包名,模板文件覆盖通用代码;支持内置/自定义模板与缓存开关
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as p from '@clack/prompts';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { getTemplate, isValidTemplate, getAllTemplates } from '../../config/templates.js';
|
|
13
|
+
import { selectTemplateWithEnquirer } from '../utils/templateSelectorEnquirer.js';
|
|
14
|
+
import { cloneOrUpdateTemplate } from '../templates/templateManager.js';
|
|
15
|
+
import { getAuthorName, saveAuthorName, saveDefaultTemplate } from '../utils/config.js';
|
|
16
|
+
import { ConfigManager } from '@flu-cli/core';
|
|
17
|
+
import { copyTemplate, replaceVariables, copyCustomTemplate, ensurePubspecName, mergePubspecFromTemplate, cleanupTemplateFiles } from '../templates/templateCopier.js';
|
|
18
|
+
import { runFlutterCreate } from '../utils/flutterHelper.js';
|
|
19
|
+
import { ProjectGenerator } from '../generators/project_generator.js';
|
|
20
|
+
import { syncSnippets } from './snippets.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 创建新项目
|
|
24
|
+
*/
|
|
25
|
+
export async function newProjectWithClack (projectName, options) {
|
|
26
|
+
p.intro(chalk.cyan.bold('🚀 创建 Flutter 项目'));
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const isInteractive = !projectName;
|
|
30
|
+
let finalProjectName = projectName;
|
|
31
|
+
let templateSelection = null;
|
|
32
|
+
let stateManager = options.state || 'default';
|
|
33
|
+
const nonInteractive = process.env.FLU_CLI_NON_INTERACTIVE === '1';
|
|
34
|
+
|
|
35
|
+
// ========== 交互式模式 ==========
|
|
36
|
+
if (isInteractive) {
|
|
37
|
+
// Step 1: 项目名称
|
|
38
|
+
finalProjectName = await p.text({
|
|
39
|
+
message: '请输入项目名称(直接回车使用: my_app)',
|
|
40
|
+
placeholder: 'my_app'
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (p.isCancel(finalProjectName)) {
|
|
44
|
+
p.cancel('操作已取消');
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!finalProjectName || finalProjectName === '') {
|
|
49
|
+
finalProjectName = 'my_app';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Step 2: 选择模板(实时预览,支持内置/自定义)
|
|
53
|
+
templateSelection = await selectTemplateWithEnquirer();
|
|
54
|
+
|
|
55
|
+
// Step 2.5: 选择状态管理器
|
|
56
|
+
const smChoice = await p.select({
|
|
57
|
+
message: '请选择状态管理器',
|
|
58
|
+
options: [
|
|
59
|
+
{ value: 'default', label: 'ChangeNotifier (默认)' },
|
|
60
|
+
{ value: 'provider', label: 'Provider' },
|
|
61
|
+
{ value: 'getx', label: 'GetX' },
|
|
62
|
+
{ value: 'riverpod', label: 'Riverpod' }
|
|
63
|
+
],
|
|
64
|
+
initialValue: 'default'
|
|
65
|
+
});
|
|
66
|
+
if (!p.isCancel(smChoice)) {
|
|
67
|
+
stateManager = smChoice;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Step 3: 项目路径(含冲突检测与删除确认)
|
|
71
|
+
const defaultPath = join(options.dir, finalProjectName);
|
|
72
|
+
let projectPath = await p.text({
|
|
73
|
+
message: `请输入项目路径(直接回车使用: ${defaultPath})`,
|
|
74
|
+
placeholder: defaultPath
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (p.isCancel(projectPath)) {
|
|
78
|
+
p.cancel('操作已取消');
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!projectPath || projectPath === '') {
|
|
83
|
+
projectPath = defaultPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Step 4: 包名
|
|
87
|
+
const defaultPackageName = `com.example.${finalProjectName}`;
|
|
88
|
+
let packageName = await p.text({
|
|
89
|
+
message: `请输入包名(直接回车使用: ${defaultPackageName})`,
|
|
90
|
+
placeholder: defaultPackageName
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (p.isCancel(packageName)) {
|
|
94
|
+
p.cancel('操作已取消');
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!packageName || packageName === '') {
|
|
99
|
+
packageName = defaultPackageName;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Step 5: 作者(使用缓存的名字)
|
|
103
|
+
const cachedAuthor = getAuthorName();
|
|
104
|
+
let author = await p.text({
|
|
105
|
+
message: `请输入作者名称(直接回车使用: ${cachedAuthor})`,
|
|
106
|
+
placeholder: cachedAuthor
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (p.isCancel(author)) {
|
|
110
|
+
p.cancel('操作已取消');
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!author || author === '') {
|
|
115
|
+
author = cachedAuthor;
|
|
116
|
+
} else {
|
|
117
|
+
// 保存新的作者名字
|
|
118
|
+
saveAuthorName(author);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Step 6: 检查目录是否存在(交互确认删除)
|
|
122
|
+
if (existsSync(projectPath)) {
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log(chalk.yellow(`⚠️ 目录已存在: ${projectPath}`));
|
|
125
|
+
|
|
126
|
+
const shouldDelete = await p.confirm({
|
|
127
|
+
message: '是否删除现有目录并重新创建?',
|
|
128
|
+
initialValue: true
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (p.isCancel(shouldDelete)) {
|
|
132
|
+
p.cancel('操作已取消');
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (shouldDelete) {
|
|
137
|
+
const { rmSync } = await import('fs');
|
|
138
|
+
try {
|
|
139
|
+
rmSync(projectPath, { recursive: true, force: true });
|
|
140
|
+
console.log(chalk.green(`✓ 已删除目录: ${projectPath}`));
|
|
141
|
+
} catch (error) {
|
|
142
|
+
p.cancel(`删除目录失败: ${error.message}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
p.cancel('操作已取消');
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Step 7: 显示信息并确认
|
|
152
|
+
const templateDisplay = templateSelection?.kind === 'builtin'
|
|
153
|
+
? getTemplate(templateSelection.name).displayName
|
|
154
|
+
: (ConfigManager.getInstance().getTemplate(templateSelection.id)?.name || '自定义模板');
|
|
155
|
+
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(chalk.cyan('📋 项目信息确认:'));
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log(` ${chalk.gray('项目名称:')} ${chalk.green(finalProjectName)}`);
|
|
160
|
+
console.log(` ${chalk.gray('项目模板:')} ${chalk.green(templateDisplay)}`);
|
|
161
|
+
console.log(` ${chalk.gray('项目路径:')} ${chalk.green(projectPath)}`);
|
|
162
|
+
console.log(` ${chalk.gray('包名:')} ${chalk.green(packageName)}`);
|
|
163
|
+
console.log(` ${chalk.gray('作者:')} ${chalk.green(author)}`);
|
|
164
|
+
console.log('');
|
|
165
|
+
|
|
166
|
+
const shouldContinue = await p.confirm({
|
|
167
|
+
message: '确认创建项目?'
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!shouldContinue || p.isCancel(shouldContinue)) {
|
|
171
|
+
p.cancel('操作已取消');
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Step 8: 创建项目
|
|
176
|
+
await createProject(finalProjectName, templateSelection, projectPath, { projectName: finalProjectName, packageName, author, stateManager }, options);
|
|
177
|
+
|
|
178
|
+
} else {
|
|
179
|
+
// ========== 命令行模式 ==========
|
|
180
|
+
if (!isValidProjectName(finalProjectName)) {
|
|
181
|
+
p.cancel('项目名称只能包含小写字母、数字和下划线');
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 如果通过参数传入模板名称,构造为内置模板选择
|
|
186
|
+
if (!templateSelection && options.template) {
|
|
187
|
+
templateSelection = { kind: 'builtin', name: options.template };
|
|
188
|
+
}
|
|
189
|
+
if (!templateSelection) {
|
|
190
|
+
templateSelection = await selectTemplateWithEnquirer();
|
|
191
|
+
}
|
|
192
|
+
if (templateSelection.kind === 'builtin' && !isValidTemplate(templateSelection.name)) {
|
|
193
|
+
p.cancel(`模板 "${templateSelection.name}" 不存在`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const projectDir = join(options.dir, finalProjectName);
|
|
198
|
+
|
|
199
|
+
// 检查目录是否存在(命令行模式也提供删除确认)
|
|
200
|
+
if (existsSync(projectDir)) {
|
|
201
|
+
if (nonInteractive) {
|
|
202
|
+
const { rmSync } = await import('fs');
|
|
203
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
204
|
+
} else {
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log(chalk.yellow(`⚠️ 目录已存在: ${projectDir}`));
|
|
207
|
+
const shouldDelete = await p.confirm({ message: '是否删除现有目录并重新创建?', initialValue: true });
|
|
208
|
+
if (p.isCancel(shouldDelete)) {
|
|
209
|
+
p.cancel('操作已取消');
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
if (shouldDelete) {
|
|
213
|
+
const { rmSync } = await import('fs');
|
|
214
|
+
try {
|
|
215
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
216
|
+
console.log(chalk.green(`✓ 已删除目录: ${projectDir}`));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
p.cancel(`删除目录失败: ${error.message}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
p.cancel('操作已取消');
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const defaultPackageName = `com.example.${finalProjectName}`;
|
|
229
|
+
let packageName = defaultPackageName;
|
|
230
|
+
if (!nonInteractive) {
|
|
231
|
+
packageName = await p.text({ message: `请输入包名(直接回车使用: ${defaultPackageName})`, placeholder: defaultPackageName });
|
|
232
|
+
if (p.isCancel(packageName)) {
|
|
233
|
+
p.cancel('操作已取消');
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
if (!packageName || packageName === '') {
|
|
237
|
+
packageName = defaultPackageName;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let author = 'Your Name';
|
|
242
|
+
if (!nonInteractive) {
|
|
243
|
+
author = await p.text({ message: '请输入作者名称(直接回车使用: Your Name)', placeholder: 'Your Name' });
|
|
244
|
+
if (p.isCancel(author)) {
|
|
245
|
+
p.cancel('操作已取消');
|
|
246
|
+
process.exit(0);
|
|
247
|
+
}
|
|
248
|
+
if (!author || author === '') {
|
|
249
|
+
author = 'Your Name';
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// CLI 模式支持 --state 传参
|
|
254
|
+
await createProject(finalProjectName, templateSelection, projectDir, { projectName: finalProjectName, packageName, author, stateManager }, options);
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
p.cancel(`创建失败: ${error.message}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 创建项目
|
|
264
|
+
*/
|
|
265
|
+
async function createProject (projectName, templateSelection, projectDir, projectInfo, options) {
|
|
266
|
+
const s = p.spinner();
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
// 1. 准备模板
|
|
270
|
+
s.start('正在准备模板...');
|
|
271
|
+
let templatePath;
|
|
272
|
+
if (templateSelection?.kind === 'builtin') {
|
|
273
|
+
const template = getTemplate(templateSelection.name);
|
|
274
|
+
// 根据运行环境决定是否使用本地模板
|
|
275
|
+
// 生产环境(production)默认走远程,其他环境默认走本地(除非显式使用 --remote)
|
|
276
|
+
const runMode = process.env.FLU_CLI_MODE || process.env.NODE_ENV || 'development';
|
|
277
|
+
const useLocal = !options.remote && runMode !== 'production' && (template.local || false);
|
|
278
|
+
const __here = dirname(fileURLToPath(import.meta.url));
|
|
279
|
+
const cliRoot = dirname(dirname(__here)); // flu-cli/flu-cli
|
|
280
|
+
const workspaceRoot = dirname(cliRoot);
|
|
281
|
+
const sourceLocalPath = join(workspaceRoot, `template-${templateSelection.name}`);
|
|
282
|
+
const forceUpdate = useLocal ? true : !options.cache;
|
|
283
|
+
templatePath = await cloneOrUpdateTemplate(
|
|
284
|
+
templateSelection.name,
|
|
285
|
+
template.repo,
|
|
286
|
+
template.branch,
|
|
287
|
+
forceUpdate,
|
|
288
|
+
useLocal,
|
|
289
|
+
sourceLocalPath
|
|
290
|
+
);
|
|
291
|
+
} else if (templateSelection?.kind === 'custom') {
|
|
292
|
+
const configManager = ConfigManager.getInstance();
|
|
293
|
+
const ct = configManager.getTemplate(templateSelection.id);
|
|
294
|
+
|
|
295
|
+
if (!ct) {
|
|
296
|
+
s.stop('模板准备失败');
|
|
297
|
+
throw new Error('未找到自定义模板');
|
|
298
|
+
}
|
|
299
|
+
if (ct.type === 'local') {
|
|
300
|
+
templatePath = await cloneOrUpdateTemplate(
|
|
301
|
+
ct.id,
|
|
302
|
+
null,
|
|
303
|
+
'main',
|
|
304
|
+
!options.cache,
|
|
305
|
+
true,
|
|
306
|
+
ct.path || ct.url // 兼容旧配置
|
|
307
|
+
);
|
|
308
|
+
} else {
|
|
309
|
+
templatePath = await cloneOrUpdateTemplate(
|
|
310
|
+
ct.id,
|
|
311
|
+
ct.url,
|
|
312
|
+
ct.branch || 'main',
|
|
313
|
+
!options.cache,
|
|
314
|
+
false
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
throw new Error('模板选择无效');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!templatePath) {
|
|
322
|
+
s.stop('模板准备失败');
|
|
323
|
+
throw new Error('模板准备失败');
|
|
324
|
+
}
|
|
325
|
+
s.stop('✓ 模板准备完成');
|
|
326
|
+
|
|
327
|
+
// 2. 创建 Flutter 项目
|
|
328
|
+
s.start('正在创建 Flutter 项目...');
|
|
329
|
+
const created = await runFlutterCreate(projectDir, projectInfo.projectName, projectInfo.packageName);
|
|
330
|
+
if (created) {
|
|
331
|
+
s.stop('✓ Flutter 项目创建成功');
|
|
332
|
+
} else {
|
|
333
|
+
s.stop('Flutter 项目创建失败');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 3. 复制模板文件(自定义模板仅覆盖通用代码,保留平台与包名)
|
|
337
|
+
s.start('正在复制模板文件...');
|
|
338
|
+
const isCustom = templateSelection?.kind === 'custom';
|
|
339
|
+
const copied = isCustom
|
|
340
|
+
? await copyCustomTemplate(templatePath, projectDir)
|
|
341
|
+
: await copyTemplate(templatePath, projectDir);
|
|
342
|
+
|
|
343
|
+
if (!copied) {
|
|
344
|
+
s.stop('模板文件复制失败');
|
|
345
|
+
throw new Error('模板文件复制失败');
|
|
346
|
+
}
|
|
347
|
+
s.stop('✓ 模板文件复制成功');
|
|
348
|
+
|
|
349
|
+
// 4. 配置项目
|
|
350
|
+
s.start('正在配置项目...');
|
|
351
|
+
const pg = new ProjectGenerator();
|
|
352
|
+
await pg.processTemplates(projectDir);
|
|
353
|
+
await replaceVariables(projectDir, projectInfo);
|
|
354
|
+
if (isCustom) {
|
|
355
|
+
mergePubspecFromTemplate(templatePath, projectDir, projectInfo.projectName);
|
|
356
|
+
} else {
|
|
357
|
+
ensurePubspecName(projectDir, projectInfo.projectName);
|
|
358
|
+
}
|
|
359
|
+
// 配置状态管理器
|
|
360
|
+
await pg.configureStateManager(projectDir, projectInfo.stateManager || 'default');
|
|
361
|
+
|
|
362
|
+
// 同步标准代码片段
|
|
363
|
+
s.start('正在同步代码片段...');
|
|
364
|
+
await syncSnippets(projectDir);
|
|
365
|
+
s.stop('✓ 代码片段同步完成');
|
|
366
|
+
|
|
367
|
+
cleanupTemplateFiles(projectDir);
|
|
368
|
+
s.stop('✓ 项目配置完成');
|
|
369
|
+
|
|
370
|
+
// 完成
|
|
371
|
+
p.outro(
|
|
372
|
+
chalk.green.bold('✨ 项目创建成功!\n\n') +
|
|
373
|
+
chalk.cyan('下一步:\n') +
|
|
374
|
+
chalk.yellow(` cd ${projectName}\n`) +
|
|
375
|
+
chalk.yellow(` flutter pub get\n`) +
|
|
376
|
+
chalk.yellow(` flutter run`)
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// 记忆默认模板
|
|
380
|
+
if (templateSelection?.kind === 'builtin') {
|
|
381
|
+
saveDefaultTemplate({ type: 'builtin', idOrName: templateSelection.name });
|
|
382
|
+
} else if (templateSelection?.kind === 'custom') {
|
|
383
|
+
saveDefaultTemplate({ type: 'custom', idOrName: templateSelection.id });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 正常退出
|
|
387
|
+
process.exit(0);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
s.stop('创建失败');
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* 验证项目名称
|
|
396
|
+
*/
|
|
397
|
+
function isValidProjectName (name) {
|
|
398
|
+
return /^[a-z][a-z0-9_]*$/.test(name);
|
|
399
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 同步代码片段到当前项目
|
|
10
|
+
*/
|
|
11
|
+
export async function syncSnippets (targetDir) {
|
|
12
|
+
const projectRoot = targetDir || process.cwd();
|
|
13
|
+
const vscodeDir = path.join(projectRoot, '.vscode');
|
|
14
|
+
const targetFile = path.join(vscodeDir, 'dart.code-snippets');
|
|
15
|
+
|
|
16
|
+
// 获取 centralized snippets 路径
|
|
17
|
+
// 假设结构: packages/v2/lib/commands/snippets.js -> packages/v2/templates/snippets/dart.code-snippets
|
|
18
|
+
const snippetsSource = path.join(__dirname, '../../templates/snippets/dart.code-snippets');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(snippetsSource)) {
|
|
21
|
+
console.log(chalk.red('❌ 错误: 找不到标准代码片段源文件'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// 确保 .vscode 目录存在
|
|
27
|
+
await fs.ensureDir(vscodeDir);
|
|
28
|
+
|
|
29
|
+
// 复制文件
|
|
30
|
+
await fs.copy(snippetsSource, targetFile, { overwrite: true });
|
|
31
|
+
|
|
32
|
+
console.log(chalk.green('✅ 代码片段同步成功!'));
|
|
33
|
+
console.log(chalk.gray(`已更新: ${targetFile}`));
|
|
34
|
+
console.log(chalk.cyan('提示: 重启 VS Code 或运行 "Developer: Reload Window" 以应用更改'));
|
|
35
|
+
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.log(chalk.red(`❌ 同步失败: ${error.message}`));
|
|
38
|
+
}
|
|
39
|
+
}
|