flu-cli 0.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/README.md +107 -0
- package/index.js +59 -0
- package/lib/createProject.js +187 -0
- package/lib/desktopPath.js +85 -0
- package/lib/flutterProjectCreator.js +66 -0
- package/lib/libCopier.js +304 -0
- package/lib/userInteraction.js +206 -0
- package/lib/utils.js +152 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Flutter CLI 工具
|
|
2
|
+
|
|
3
|
+
火之夜工作室 Flutter 项目创建工具,基于 Node.js 实现的命令行工具,用于快速创建和配置 Flutter 项目。
|
|
4
|
+
|
|
5
|
+
## 功能特点
|
|
6
|
+
|
|
7
|
+
- 交互式命令行界面,简化项目创建流程
|
|
8
|
+
- 支持创建应用(app)和模块(module)两种项目类型
|
|
9
|
+
- 提供基础版和增强版两种项目模板
|
|
10
|
+
- 自动配置项目包名和显示名称
|
|
11
|
+
- 支持从 Git 仓库获取最新模板代码
|
|
12
|
+
- 自动打开 IDE(支持 VS Code、Android Studio 等)
|
|
13
|
+
- 跨平台支持(Windows、macOS、Linux)
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
### 全局安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g flutter-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 本地安装
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install flutter-cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 使用方法
|
|
30
|
+
|
|
31
|
+
### 命令行使用
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# 如果全局安装
|
|
35
|
+
flutter-cli
|
|
36
|
+
|
|
37
|
+
# 或者使用 npx
|
|
38
|
+
npx flutter-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 创建新项目
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
flutter-cli create
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
按照提示完成以下步骤:
|
|
48
|
+
|
|
49
|
+
1. 选择项目类型(应用或模块)
|
|
50
|
+
2. 选择模板类型(基础版或增强版)
|
|
51
|
+
3. 输入项目名称、包名和显示名称
|
|
52
|
+
4. 选择项目存放路径
|
|
53
|
+
5. 选择要使用的 IDE
|
|
54
|
+
|
|
55
|
+
## 依赖项
|
|
56
|
+
|
|
57
|
+
- inquirer: 交互式命令行界面
|
|
58
|
+
- chalk: 终端彩色输出
|
|
59
|
+
- commander: 命令行参数解析
|
|
60
|
+
- ora: 终端加载动画
|
|
61
|
+
- cli-progress: 进度条显示
|
|
62
|
+
- fs-extra: 文件系统操作增强
|
|
63
|
+
|
|
64
|
+
## 开发
|
|
65
|
+
|
|
66
|
+
### 项目结构
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
flutter-cli/
|
|
70
|
+
├── index.js # 主入口文件
|
|
71
|
+
├── package.json # 项目配置
|
|
72
|
+
├── README.md # 项目说明
|
|
73
|
+
└── lib/ # 核心功能模块
|
|
74
|
+
├── createProject.js # 项目创建主流程
|
|
75
|
+
├── userInteraction.js # 用户交互功能
|
|
76
|
+
├── flutterProjectCreator.js # Flutter项目创建
|
|
77
|
+
├── libCopier.js # 项目模板复制
|
|
78
|
+
├── utils.js # 工具函数
|
|
79
|
+
└── desktopPath.js # 桌面路径获取
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 本地开发
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# 克隆仓库
|
|
86
|
+
git clone https://gitee.com/tengteng_fan/flutter-cli.git
|
|
87
|
+
cd flutter-cli
|
|
88
|
+
|
|
89
|
+
# 安装依赖
|
|
90
|
+
npm install
|
|
91
|
+
|
|
92
|
+
# 链接到全局
|
|
93
|
+
npm link
|
|
94
|
+
|
|
95
|
+
# 测试运行
|
|
96
|
+
flutter-cli
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 注意事项
|
|
100
|
+
|
|
101
|
+
- 确保系统已正确安装 Flutter SDK 并添加到环境变量
|
|
102
|
+
- 使用前可运行 `flutter doctor` 检查 Flutter 安装状态
|
|
103
|
+
- 项目创建过程中需要网络连接以从 Git 仓库获取模板
|
|
104
|
+
|
|
105
|
+
## 许可证
|
|
106
|
+
|
|
107
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 火之夜工作室 Flutter 项目创建工具
|
|
5
|
+
*
|
|
6
|
+
* 主入口文件,处理命令行参数并调用相应功能
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { program } = require('commander');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const { createProject } = require('./lib/createProject');
|
|
12
|
+
const packageJson = require('./package.json');
|
|
13
|
+
|
|
14
|
+
// 设置版本号和描述
|
|
15
|
+
program
|
|
16
|
+
.version(packageJson.version)
|
|
17
|
+
.description(chalk.cyan('火之夜工作室 Flutter 项目创建工具'));
|
|
18
|
+
|
|
19
|
+
// 创建项目命令
|
|
20
|
+
program
|
|
21
|
+
.command('create')
|
|
22
|
+
.description('创建一个新的Flutter项目')
|
|
23
|
+
.option('-t, --project-type <type>', '项目类型: app 或 module', 'app')
|
|
24
|
+
.option('-m, --template-type <type>', '模板类型: 0 (基础UI), 1 (State), 2 (GetX)', '2')
|
|
25
|
+
.option('-d, --demo <boolean>', '是否需要示例代码: true 或 false', true)
|
|
26
|
+
.option('-n, --project-name <name>', '项目名称 (小写字母、数字和下划线)')
|
|
27
|
+
.option('-p, --package-name <name>', '包名 (例如: com.example.project_name)')
|
|
28
|
+
.option('-o, --output-dir <path>', '项目输出目录')
|
|
29
|
+
.option('-i, --ide <type>', '使用的IDE: vscode, android_studio, none', 'none')
|
|
30
|
+
.option('-f, --flutter-sdk <path>', '自定义Flutter SDK路径')
|
|
31
|
+
.action((options) => {
|
|
32
|
+
console.log(chalk.bold.greenBright('欢迎使用 火之夜工作室 Flutter 项目创建工具\n'));
|
|
33
|
+
|
|
34
|
+
// 检查是否提供了命令行参数
|
|
35
|
+
const hasCommandLineArgs = options.projectName || options.packageName ||
|
|
36
|
+
options.projectType !== 'app' || options.templateType !== '2' ||
|
|
37
|
+
options.demo !== true || options.outputDir || options.ide !== 'none' || options.flutterSdk;
|
|
38
|
+
|
|
39
|
+
// 如果提供了命令行参数,则直接使用这些参数创建项目
|
|
40
|
+
// 否则使用交互式方式获取参数
|
|
41
|
+
createProject({
|
|
42
|
+
useCommandLineArgs: hasCommandLineArgs,
|
|
43
|
+
projectType: options.projectType,
|
|
44
|
+
templateType: options.templateType,
|
|
45
|
+
isNeedDemo: options.demo === 'false' ? false : Boolean(options.demo),
|
|
46
|
+
projectName: options.projectName,
|
|
47
|
+
packageName: options.packageName,
|
|
48
|
+
parentDir: options.outputDir,
|
|
49
|
+
ideChoice: options.ide
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 如果没有提供命令,默认执行创建项目
|
|
54
|
+
if (process.argv.length === 2) {
|
|
55
|
+
console.log(chalk.bold.greenBright('欢迎使用 火之夜工作室 Flutter 项目创建工具\n'));
|
|
56
|
+
createProject({});
|
|
57
|
+
} else {
|
|
58
|
+
program.parse(process.argv);
|
|
59
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 项目创建模块
|
|
3
|
+
*
|
|
4
|
+
* 负责协调整个项目创建流程
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const ora = require('ora');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs-extra');
|
|
12
|
+
const { getUserInteraction } = require('./userInteraction');
|
|
13
|
+
const { copyLibDirectory } = require('./libCopier');
|
|
14
|
+
const { runFlutterCreate } = require('./flutterProjectCreator');
|
|
15
|
+
const { printColored, updateReadme, openProjectInIde } = require('./utils');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 创建项目的主函数
|
|
19
|
+
*
|
|
20
|
+
* 协调整个项目创建流程,包括获取用户输入、创建项目、复制模板等
|
|
21
|
+
* @param {Object} options - 命令行参数选项
|
|
22
|
+
* @param {boolean} options.useCommandLineArgs - 是否使用命令行参数
|
|
23
|
+
* @param {string} options.projectType - 项目类型 ('app'或'module')
|
|
24
|
+
* @param {string} options.templateType - 模板类型 ('0', '1', '2')
|
|
25
|
+
* @param {boolean} options.isNeedDemo - 是否需要示例代码
|
|
26
|
+
* @param {string} options.projectName - 项目名称
|
|
27
|
+
* @param {string} options.packageName - 包名
|
|
28
|
+
* @param {string} options.parentDir - 项目存放路径
|
|
29
|
+
* @param {string} options.ideChoice - IDE选择
|
|
30
|
+
*/
|
|
31
|
+
async function createProject (options = {}) {
|
|
32
|
+
try {
|
|
33
|
+
// 获取项目类型 - 从命令行参数或交互式输入
|
|
34
|
+
const projectType = options.useCommandLineArgs ? options.projectType : await getUserInteraction.getProjectType();
|
|
35
|
+
|
|
36
|
+
// 获取模板类型 - 从命令行参数或交互式输入
|
|
37
|
+
const templateType = options.useCommandLineArgs ? options.templateType : await getUserInteraction.getTemplateType();
|
|
38
|
+
|
|
39
|
+
// 获取是否需要demo的确认 - 从命令行参数或交互式输入
|
|
40
|
+
const isNeedDemo = options.useCommandLineArgs ? options.isNeedDemo : await getUserInteraction.getIsNeedDemo();
|
|
41
|
+
|
|
42
|
+
// 获取当前脚本所在目录作为源目录
|
|
43
|
+
const sourceDir = path.dirname(require.main.filename);
|
|
44
|
+
|
|
45
|
+
// 获取项目信息 - 从命令行参数或交互式输入
|
|
46
|
+
let projectName, packageName, parentDir, targetDir, flutterSdkPath;
|
|
47
|
+
|
|
48
|
+
if (options.useCommandLineArgs && options.projectName && options.packageName && options.parentDir) {
|
|
49
|
+
// 使用命令行参数
|
|
50
|
+
projectName = options.projectName;
|
|
51
|
+
packageName = options.packageName;
|
|
52
|
+
parentDir = options.parentDir;
|
|
53
|
+
targetDir = path.join(parentDir, projectName);
|
|
54
|
+
flutterSdkPath = options.flutterSdk;
|
|
55
|
+
} else if (options.useCommandLineArgs) {
|
|
56
|
+
// 命令行参数不完整,使用交互式输入,但预填充已提供的参数
|
|
57
|
+
const defaultName = projectType === 'module' ? "hzy_example_module" : "hzy_example_project";
|
|
58
|
+
const projectInfo = await getUserInteraction.getProjectInfo(
|
|
59
|
+
projectType,
|
|
60
|
+
options.projectName || defaultName,
|
|
61
|
+
options.packageName || `com.example.${options.projectName || defaultName}`,
|
|
62
|
+
options.parentDir
|
|
63
|
+
);
|
|
64
|
+
projectName = projectInfo.projectName;
|
|
65
|
+
packageName = projectInfo.packageName;
|
|
66
|
+
parentDir = projectInfo.parentDir;
|
|
67
|
+
targetDir = projectInfo.targetDir;
|
|
68
|
+
flutterSdkPath = projectInfo.flutterSdkPath;
|
|
69
|
+
} else {
|
|
70
|
+
// 完全使用交互式输入
|
|
71
|
+
const projectInfo = await getUserInteraction.getProjectInfo(projectType);
|
|
72
|
+
projectName = projectInfo.projectName;
|
|
73
|
+
packageName = projectInfo.packageName;
|
|
74
|
+
parentDir = projectInfo.parentDir;
|
|
75
|
+
targetDir = projectInfo.targetDir;
|
|
76
|
+
flutterSdkPath = projectInfo.flutterSdkPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log('flutterSdkPath:', flutterSdkPath);
|
|
80
|
+
// 验证目标目录是否存在并具有写权限
|
|
81
|
+
if (!fs.existsSync(parentDir)) {
|
|
82
|
+
try {
|
|
83
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
84
|
+
} catch (e) {
|
|
85
|
+
printColored(`错误: 无法创建目录 ${parentDir}: ${e.message}`, 'red');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 检查目标目录是否具有写权限
|
|
91
|
+
try {
|
|
92
|
+
fs.accessSync(parentDir, fs.constants.W_OK);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
printColored(`错误: 目录 ${parentDir} 没有写权限`, 'red');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 检查目标目录是否已存在
|
|
99
|
+
if (fs.existsSync(targetDir)) {
|
|
100
|
+
const overwrite = await getUserInteraction.confirmOverwrite(targetDir);
|
|
101
|
+
if (!overwrite) {
|
|
102
|
+
printColored("操作已取消", 'yellow');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 删除已存在的目录
|
|
107
|
+
fs.removeSync(targetDir);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 使用flutter create创建新项目
|
|
111
|
+
const flutterCreateSpinner = ora('正在创建Flutter项目...').start();
|
|
112
|
+
const flutterCreateResult = await runFlutterCreate(targetDir, projectName, packageName, projectType, flutterSdkPath);
|
|
113
|
+
if (!flutterCreateResult) {
|
|
114
|
+
flutterCreateSpinner.fail('Flutter项目创建失败');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
flutterCreateSpinner.succeed('Flutter项目创建成功');
|
|
118
|
+
|
|
119
|
+
// 根据模板类型选择Git分支和是否需要demo分支
|
|
120
|
+
let templateBranch = 'master';
|
|
121
|
+
|
|
122
|
+
switch (templateType) {
|
|
123
|
+
case '0':
|
|
124
|
+
// 基础模版
|
|
125
|
+
templateBranch = 'master';
|
|
126
|
+
break;
|
|
127
|
+
case '1':
|
|
128
|
+
// State模板
|
|
129
|
+
templateBranch = 'depend';
|
|
130
|
+
break;
|
|
131
|
+
case '2':
|
|
132
|
+
// GetX模板
|
|
133
|
+
templateBranch = 'depend-get';
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (isNeedDemo && templateType != '0') {
|
|
140
|
+
templateBranch = templateBranch + '-demo';
|
|
141
|
+
}
|
|
142
|
+
// 复制项目文件
|
|
143
|
+
const copySpinner = ora('正在复制项目模板...').start();
|
|
144
|
+
const copyResult = await copyLibDirectory(sourceDir, targetDir, projectName, projectType, templateType, packageName, templateBranch);
|
|
145
|
+
if (!copyResult) {
|
|
146
|
+
copySpinner.fail('项目模板复制失败');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
copySpinner.succeed('项目模板复制成功');
|
|
150
|
+
|
|
151
|
+
// 更新README.md
|
|
152
|
+
updateReadme(targetDir, projectName,);
|
|
153
|
+
|
|
154
|
+
// 打印完成信息
|
|
155
|
+
printColored("\n新项目已创建成功!", 'green');
|
|
156
|
+
printColored(`项目路径: ${targetDir}`, 'green');
|
|
157
|
+
printColored("\n执行以下命令开始开发:\n", 'blue');
|
|
158
|
+
printColored(`cd ${targetDir}`, 'yellow');
|
|
159
|
+
printColored(`flutter pub get`, 'yellow');
|
|
160
|
+
printColored(`flutter run`, 'yellow');
|
|
161
|
+
|
|
162
|
+
// 获取用户选择的IDE类型 - 从命令行参数或交互式输入
|
|
163
|
+
const ideChoice = options.useCommandLineArgs ? options.ideChoice : await getUserInteraction.getIdeChoice();
|
|
164
|
+
if ((ideChoice == 'vscode' || ideChoice == 'android_studio') && flutterSdkPath === 'flutter') {
|
|
165
|
+
const pubGetSpinner = ora('正在获取Flutter依赖...').start();
|
|
166
|
+
try {
|
|
167
|
+
// 执行flutter pub get命令
|
|
168
|
+
execSync(`cd ${targetDir} && flutter pub get`, { stdio: 'inherit' });
|
|
169
|
+
pubGetSpinner.succeed('依赖获取成功');
|
|
170
|
+
} catch (e) {
|
|
171
|
+
pubGetSpinner.fail('依赖获取失败');
|
|
172
|
+
printColored(`错误信息: ${e.message}`, 'red');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 自动打开项目
|
|
176
|
+
openProjectInIde(targetDir, ideChoice);
|
|
177
|
+
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error.isTtyError) {
|
|
180
|
+
printColored("\n当前环境不支持交互式提示", 'red');
|
|
181
|
+
} else {
|
|
182
|
+
printColored(`\n错误: ${error.message}`, 'red');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { createProject };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 桌面路径获取模块
|
|
3
|
+
*
|
|
4
|
+
* 用于获取不同操作系统下的桌面路径
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 获取用户桌面路径,支持多操作系统
|
|
13
|
+
*
|
|
14
|
+
* @returns {Promise<string>} 桌面路径
|
|
15
|
+
*/
|
|
16
|
+
async function getDesktopPath () {
|
|
17
|
+
const platform = process.platform;
|
|
18
|
+
let desktopPath = '';
|
|
19
|
+
|
|
20
|
+
if (platform === 'darwin') { // macOS
|
|
21
|
+
desktopPath = path.join(os.homedir(), 'Desktop');
|
|
22
|
+
} else if (platform === 'win32') { // Windows
|
|
23
|
+
try {
|
|
24
|
+
// 尝试从环境变量获取桌面路径
|
|
25
|
+
const userProfile = process.env.USERPROFILE || os.homedir();
|
|
26
|
+
desktopPath = path.join(userProfile, 'Desktop');
|
|
27
|
+
|
|
28
|
+
// 如果路径不存在,尝试其他可能的位置
|
|
29
|
+
if (!fs.existsSync(desktopPath)) {
|
|
30
|
+
desktopPath = path.join(os.homedir(), 'Desktop');
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// 如果获取失败,使用默认路径
|
|
34
|
+
desktopPath = path.join(os.homedir(), 'Desktop');
|
|
35
|
+
}
|
|
36
|
+
} else if (platform === 'linux') { // Linux
|
|
37
|
+
// 首先尝试从环境变量获取
|
|
38
|
+
desktopPath = process.env.XDG_DESKTOP_DIR;
|
|
39
|
+
|
|
40
|
+
if (!desktopPath || !fs.existsSync(desktopPath)) {
|
|
41
|
+
// 尝试获取XDG_DESKTOP_DIR环境变量
|
|
42
|
+
const xdgConfig = path.join(os.homedir(), '.config/user-dirs.dirs');
|
|
43
|
+
|
|
44
|
+
if (fs.existsSync(xdgConfig)) {
|
|
45
|
+
try {
|
|
46
|
+
const content = fs.readFileSync(xdgConfig, 'utf8');
|
|
47
|
+
const lines = content.split('\n');
|
|
48
|
+
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (line.startsWith('XDG_DESKTOP_DIR=')) {
|
|
51
|
+
// 提取路径并处理引号和$HOME
|
|
52
|
+
let extractedPath = line.split('=')[1].trim().replace(/"/g, '');
|
|
53
|
+
extractedPath = extractedPath.replace('$HOME', os.homedir());
|
|
54
|
+
|
|
55
|
+
if (fs.existsSync(extractedPath)) {
|
|
56
|
+
desktopPath = extractedPath;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// 如果读取配置文件失败,使用默认路径
|
|
63
|
+
desktopPath = path.join(os.homedir(), 'Desktop');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 如果仍然没有找到,使用默认路径
|
|
68
|
+
if (!desktopPath || !fs.existsSync(desktopPath)) {
|
|
69
|
+
desktopPath = path.join(os.homedir(), 'Desktop');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
// 其他操作系统,使用默认路径
|
|
74
|
+
desktopPath = path.join(os.homedir(), 'Desktop');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 确保路径存在,如果不存在则使用用户主目录
|
|
78
|
+
if (!fs.existsSync(desktopPath)) {
|
|
79
|
+
desktopPath = os.homedir();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return desktopPath;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { getDesktopPath };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flutter项目创建模块
|
|
3
|
+
*
|
|
4
|
+
* 负责执行flutter create命令创建新项目
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { exec } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { printColored } = require('./utils');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const { console } = require('inspector');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 使用flutter create命令创建新项目
|
|
15
|
+
*
|
|
16
|
+
* @param {string} targetDir - 目标目录路径
|
|
17
|
+
* @param {string} projectName - 项目名称
|
|
18
|
+
* @param {string} packageName - 包名
|
|
19
|
+
* @param {string} projectType - 项目类型 ('app'-应用, 'module'-模块)
|
|
20
|
+
* @returns {Promise<boolean>} 是否创建成功
|
|
21
|
+
*/
|
|
22
|
+
async function runFlutterCreate (targetDir, projectName, packageName, projectType = 'app', flutterSdkPath = 'flutter') {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
printColored(`正在创建Flutter项目: ${flutterSdkPath}`, 'green');
|
|
25
|
+
try {
|
|
26
|
+
// 确保路径是绝对路径
|
|
27
|
+
targetDir = path.resolve(targetDir);
|
|
28
|
+
let flutterCmd = flutterSdkPath;
|
|
29
|
+
if (flutterSdkPath !== 'flutter') {
|
|
30
|
+
flutterCmd = path.join(flutterSdkPath, 'bin', 'flutter');
|
|
31
|
+
printColored(`使用自定义Flutter SDK路径: ${flutterCmd}`, 'yellow');
|
|
32
|
+
}
|
|
33
|
+
// 构建命令参数
|
|
34
|
+
const orgName = packageName.split('.').slice(0, -1).join('.');
|
|
35
|
+
let command = `${flutterCmd} create --org ${orgName} --project-name ${projectName}`;
|
|
36
|
+
// 根据项目类型添加参数
|
|
37
|
+
if (projectType === 'module') {
|
|
38
|
+
command += ' --template=module';
|
|
39
|
+
}
|
|
40
|
+
// 添加目标目录
|
|
41
|
+
command += ` "${targetDir}"`;
|
|
42
|
+
// 执行命令
|
|
43
|
+
exec(command, (error, stdout, stderr) => {
|
|
44
|
+
if (error) {
|
|
45
|
+
printColored(`创建Flutter项目失败: ${error.message}`, 'red');
|
|
46
|
+
printColored("请确保Flutter已正确安装并添加到系统PATH中", 'yellow');
|
|
47
|
+
printColored("可以尝试在命令行中运行'flutter doctor'检查Flutter安装状态", 'yellow');
|
|
48
|
+
resolve(false);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (stderr) {
|
|
53
|
+
printColored(`警告: ${stderr}`, 'yellow');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
printColored("Flutter项目创建成功!", 'green');
|
|
57
|
+
resolve(true);
|
|
58
|
+
});
|
|
59
|
+
} catch (e) {
|
|
60
|
+
printColored(`创建Flutter项目时发生未知错误: ${e.message}`, 'red');
|
|
61
|
+
resolve(false);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { runFlutterCreate };
|
package/lib/libCopier.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 项目模板复制模块
|
|
3
|
+
*
|
|
4
|
+
* 用于复制和处理 Flutter 项目的 lib 目录文件
|
|
5
|
+
* 支持从Git仓库获取代码
|
|
6
|
+
* 支持创建packages目录并克隆常用库
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs-extra');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { exec } = require('child_process');
|
|
13
|
+
const { printColored } = require('./utils');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 从Git仓库克隆代码到临时目录
|
|
17
|
+
*
|
|
18
|
+
* @param {string} gitUrl - Git仓库URL
|
|
19
|
+
* @param {string} branch - 分支名称
|
|
20
|
+
* @returns {Promise<string|null>} 临时目录路径或null
|
|
21
|
+
*/
|
|
22
|
+
async function cloneGitRepo (gitUrl, branch = 'master') {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
try {
|
|
25
|
+
// 创建临时目录
|
|
26
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flutter-cli-'));
|
|
27
|
+
printColored(`正在从Git仓库克隆代码到临时目录: ${tempDir}`, 'blue');
|
|
28
|
+
|
|
29
|
+
// 克隆指定分支的代码
|
|
30
|
+
const command = `git clone -b ${branch} --single-branch ${gitUrl} "${tempDir}"`;
|
|
31
|
+
|
|
32
|
+
exec(command, (error, stdout, stderr) => {
|
|
33
|
+
if (error) {
|
|
34
|
+
printColored(`Git仓库克隆失败: ${error.message}`, 'red');
|
|
35
|
+
if (fs.existsSync(tempDir)) {
|
|
36
|
+
fs.removeSync(tempDir);
|
|
37
|
+
}
|
|
38
|
+
resolve(null);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
printColored("Git仓库克隆成功!", 'green');
|
|
43
|
+
resolve(tempDir);
|
|
44
|
+
});
|
|
45
|
+
} catch (e) {
|
|
46
|
+
printColored(`创建临时目录失败: ${e.message}`, 'red');
|
|
47
|
+
resolve(null);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 复制并处理lib目录文件和pubspec.yaml
|
|
54
|
+
* 支持从本地目录或Git仓库复制
|
|
55
|
+
*
|
|
56
|
+
* @param {string} sourceDir - 源目录路径
|
|
57
|
+
* @param {string} targetDir - 目标目录路径
|
|
58
|
+
* @param {string} projectName - 项目名称
|
|
59
|
+
* @param {string} projectType - 项目类型
|
|
60
|
+
* @param {string} templateType - 模板类型
|
|
61
|
+
* @param {string} packageName - 包名
|
|
62
|
+
* @param {string} branch - Git分支
|
|
63
|
+
* @param {string} gitUrl - Git仓库URL
|
|
64
|
+
* @returns {Promise<boolean>} 是否复制成功
|
|
65
|
+
*/
|
|
66
|
+
async function copyLibDirectory (
|
|
67
|
+
sourceDir,
|
|
68
|
+
targetDir,
|
|
69
|
+
projectName,
|
|
70
|
+
projectType,
|
|
71
|
+
templateType,
|
|
72
|
+
packageName,
|
|
73
|
+
branch = 'master',
|
|
74
|
+
gitUrl = 'https://gitee.com/hot_night/hzy_original_project.git',
|
|
75
|
+
) {
|
|
76
|
+
try {
|
|
77
|
+
// 如果提供了gitUrl,从Git仓库获取代码
|
|
78
|
+
let tempDir = null;
|
|
79
|
+
if (gitUrl) {
|
|
80
|
+
tempDir = await cloneGitRepo(gitUrl, branch);
|
|
81
|
+
if (!tempDir) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
sourceDir = tempDir;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// 复制pubspec.yaml
|
|
89
|
+
// 源目录路径拼接pubspec.yaml文件名
|
|
90
|
+
const pubspecSource = path.join(sourceDir, 'pubspec.yaml');
|
|
91
|
+
// 目标目录路径拼接pubspec.yaml文件名
|
|
92
|
+
const pubspecTarget = path.join(targetDir, 'pubspec.yaml');
|
|
93
|
+
// 确保目标目录存在
|
|
94
|
+
if (fs.existsSync(pubspecSource)) {
|
|
95
|
+
printColored("正在复制并处理pubspec.yaml...", 'blue');
|
|
96
|
+
|
|
97
|
+
// 读取源文件内容
|
|
98
|
+
let sourceContent = fs.readFileSync(pubspecSource, 'utf8');
|
|
99
|
+
|
|
100
|
+
// 提取dependencies部分
|
|
101
|
+
let dependenciesContent = 'dependencies:\n';
|
|
102
|
+
const dependenciesMatch = sourceContent.match(/dependencies:[\s\S]*?(?=\n\s*dev_dependencies:)/);
|
|
103
|
+
if (dependenciesMatch) {
|
|
104
|
+
dependenciesContent = dependenciesMatch[0];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 读取目标文件内容
|
|
108
|
+
let targetContent = '';
|
|
109
|
+
if (fs.existsSync(pubspecTarget)) {
|
|
110
|
+
targetContent = fs.readFileSync(pubspecTarget, 'utf8');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 替换项目名称和描述
|
|
114
|
+
targetContent = targetContent.replace(
|
|
115
|
+
/description:\s*["'].*?["']/g,
|
|
116
|
+
'description: "火之夜工作室 Flutter 脚手架"'
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// 添加publish_to配置防止意外发布
|
|
120
|
+
if (!targetContent.includes('publish_to:')) {
|
|
121
|
+
targetContent = targetContent.replace(
|
|
122
|
+
`name: ${projectName}\n`,
|
|
123
|
+
`name: ${projectName}\npublish_to: none\n`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 替换dependencies部分
|
|
128
|
+
const dependenciesRegex = /dependencies:[\s\S]*?(?=\n\s*dev_dependencies:)/;
|
|
129
|
+
if (targetContent.match(dependenciesRegex)) {
|
|
130
|
+
targetContent = targetContent.replace(dependenciesRegex, dependenciesContent);
|
|
131
|
+
} else {
|
|
132
|
+
// 如果没有找到dependencies部分,添加到文件末尾
|
|
133
|
+
targetContent += '\n' + dependenciesContent;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 写入目标文件
|
|
137
|
+
fs.writeFileSync(pubspecTarget, targetContent, 'utf8');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 复制lib目录
|
|
141
|
+
const libSource = path.join(sourceDir, 'lib');
|
|
142
|
+
const libTarget = path.join(targetDir, 'lib');
|
|
143
|
+
|
|
144
|
+
if (fs.existsSync(libSource)) {
|
|
145
|
+
printColored("正在复制lib目录...", 'blue');
|
|
146
|
+
|
|
147
|
+
// 确保目标lib目录存在
|
|
148
|
+
fs.ensureDirSync(libTarget);
|
|
149
|
+
|
|
150
|
+
// 复制lib目录内容
|
|
151
|
+
fs.copySync(libSource, libTarget, {
|
|
152
|
+
overwrite: true,
|
|
153
|
+
filter: (src) => {
|
|
154
|
+
// 可以在这里添加过滤逻辑,例如排除某些文件
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 复制assets目录(如果存在)
|
|
163
|
+
const assetsSource = path.join(sourceDir, 'assets');
|
|
164
|
+
const assetsTarget = path.join(targetDir, 'assets');
|
|
165
|
+
|
|
166
|
+
if (fs.existsSync(assetsSource)) {
|
|
167
|
+
printColored("正在复制assets目录...", 'blue');
|
|
168
|
+
fs.copySync(assetsSource, assetsTarget, { overwrite: true });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 复制.vscode目录(如果存在)
|
|
172
|
+
const vscodeSource = path.join(sourceDir, '.vscode');
|
|
173
|
+
const vscodeTarget = path.join(targetDir, '.vscode');
|
|
174
|
+
|
|
175
|
+
if (fs.existsSync(vscodeSource)) {
|
|
176
|
+
printColored("正在复制.vscode目录...", 'blue');
|
|
177
|
+
fs.copySync(vscodeSource, vscodeTarget, { overwrite: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 待定: 暂时使用本地packages目录,里面的内容通过git clone 下载在package文件里.
|
|
183
|
+
* 后续会增加功能选项.根据用户选择使用本地packages目录,
|
|
184
|
+
* 还是直接在pubspec.yaml中添加依赖.
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
// 创建packages目录
|
|
188
|
+
const packagesDir = path.join(targetDir, 'packages');
|
|
189
|
+
|
|
190
|
+
// 如果packages目录存在,先删除
|
|
191
|
+
if (fs.existsSync(packagesDir)) {
|
|
192
|
+
printColored("删除已存在的packages目录...", 'blue');
|
|
193
|
+
fs.removeSync(packagesDir);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 重新创建packages目录
|
|
197
|
+
printColored("创建packages目录...", 'blue');
|
|
198
|
+
fs.ensureDirSync(packagesDir);
|
|
199
|
+
|
|
200
|
+
// 克隆常用库到packages目录
|
|
201
|
+
printColored("正在克隆常用库到packages目录...", 'blue');
|
|
202
|
+
|
|
203
|
+
// 定义克隆仓库的Promise函数
|
|
204
|
+
const cloneRepo = (repoUrl, targetDir, repoName) => {
|
|
205
|
+
return new Promise((resolve) => {
|
|
206
|
+
const command = `git clone ${repoUrl} "${targetDir}"`;
|
|
207
|
+
exec(command, (error, stdout, stderr) => {
|
|
208
|
+
if (error) {
|
|
209
|
+
printColored(`克隆${repoName}库失败: ${error.message}`, 'yellow');
|
|
210
|
+
resolve(false);
|
|
211
|
+
} else {
|
|
212
|
+
printColored(`克隆${repoName}库成功!`, 'green');
|
|
213
|
+
resolve(true);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// 并行克隆两个仓库
|
|
220
|
+
const toolRepoUrl = 'https://gitee.com/hot_night/hzy_normal_tool.git';
|
|
221
|
+
const networkRepoUrl = 'https://gitee.com/hot_night/hzy_normal_network.git';
|
|
222
|
+
let proArr = [];
|
|
223
|
+
if (templateType === '0') {
|
|
224
|
+
proArr = [
|
|
225
|
+
cloneRepo(toolRepoUrl, path.join(packagesDir, 'hzy_normal_tool'), 'hzy_normal_tool'),
|
|
226
|
+
];
|
|
227
|
+
} else {
|
|
228
|
+
proArr = [
|
|
229
|
+
cloneRepo(toolRepoUrl, path.join(packagesDir, 'hzy_normal_tool'), 'hzy_normal_tool'),
|
|
230
|
+
cloneRepo(networkRepoUrl, path.join(packagesDir, 'hzy_normal_network'), 'hzy_normal_network')
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
// 等待两个仓库克隆完成
|
|
234
|
+
await Promise.all(proArr);
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
printColored("packages目录克隆完成!", 'green');
|
|
239
|
+
|
|
240
|
+
// 复制test目录 (如果存在)
|
|
241
|
+
const testSource = path.join(sourceDir, 'test');
|
|
242
|
+
const testTarget = path.join(targetDir, 'test');
|
|
243
|
+
if (fs.existsSync(testSource)) {
|
|
244
|
+
printColored("正在复制test目录...", 'blue');
|
|
245
|
+
fs.copySync(testSource, testTarget, { overwrite: true });
|
|
246
|
+
}
|
|
247
|
+
// 全局替换包名和项目名称
|
|
248
|
+
processLibFiles(libTarget, packageName, projectName);
|
|
249
|
+
processLibFiles(testTarget, packageName, projectName);
|
|
250
|
+
printColored("项目模板复制完成!", 'green');
|
|
251
|
+
return true;
|
|
252
|
+
} finally {
|
|
253
|
+
// 清理临时目录
|
|
254
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
255
|
+
fs.removeSync(tempDir);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch (e) {
|
|
259
|
+
printColored(`复制项目模板时发生错误: ${e.message}`, 'red');
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 处理lib目录中的文件,替换包名等
|
|
266
|
+
*
|
|
267
|
+
* @param {string} libDir - lib目录路径
|
|
268
|
+
* @param {string} packageName - 包名
|
|
269
|
+
*/
|
|
270
|
+
function processLibFiles (targetDir, packageName, projectName) {
|
|
271
|
+
try {
|
|
272
|
+
// 递归处理目录中的所有文件
|
|
273
|
+
const processDir = (dir) => {
|
|
274
|
+
const files = fs.readdirSync(dir);
|
|
275
|
+
|
|
276
|
+
for (const file of files) {
|
|
277
|
+
const filePath = path.join(dir, file);
|
|
278
|
+
const stat = fs.statSync(filePath);
|
|
279
|
+
|
|
280
|
+
if (stat.isDirectory()) {
|
|
281
|
+
// 递归处理子目录
|
|
282
|
+
processDir(filePath);
|
|
283
|
+
} else if (stat.isFile() && (file.endsWith('.dart') || file.endsWith('.yaml'))) {
|
|
284
|
+
// 处理dart和yaml文件
|
|
285
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
286
|
+
|
|
287
|
+
// 替换packageName和projectName
|
|
288
|
+
content = content.replace(/hzy_original_project/g, projectName);
|
|
289
|
+
// 替换包名
|
|
290
|
+
content = content.replace(/com\.example\.hzy_original_project/g, packageName);
|
|
291
|
+
|
|
292
|
+
// 写回文件
|
|
293
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
processDir(targetDir);
|
|
299
|
+
} catch (e) {
|
|
300
|
+
printColored(`处理lib文件时发生错误: ${e.message}`, 'yellow');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = { copyLibDirectory };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户交互模块
|
|
3
|
+
*
|
|
4
|
+
* 包含所有与用户交互相关的函数
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const inquirer = require('inquirer');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { printColored, validatePackageName, validateProjectName } = require('./utils');
|
|
12
|
+
const { getDesktopPath } = require('./desktopPath');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 用户交互功能集合
|
|
16
|
+
*/
|
|
17
|
+
const getUserInteraction = {
|
|
18
|
+
/**
|
|
19
|
+
* 获取项目类型
|
|
20
|
+
*
|
|
21
|
+
* @returns {Promise<string>} 'app' 或 'module'
|
|
22
|
+
*/
|
|
23
|
+
getProjectType: async () => {
|
|
24
|
+
|
|
25
|
+
const { projectType } = await inquirer.prompt([
|
|
26
|
+
{
|
|
27
|
+
type: 'list',
|
|
28
|
+
name: 'projectType',
|
|
29
|
+
message: '请选择项目类型:',
|
|
30
|
+
choices: [
|
|
31
|
+
{ name: '应用 (app)', value: 'app' },
|
|
32
|
+
{ name: '模块 (module)', value: 'module' }
|
|
33
|
+
],
|
|
34
|
+
default: 'app'
|
|
35
|
+
}
|
|
36
|
+
]);
|
|
37
|
+
return projectType;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 获取模板类型
|
|
42
|
+
*
|
|
43
|
+
* @returns {Promise<string>} '1' 或 '2'
|
|
44
|
+
*/
|
|
45
|
+
getTemplateType: async () => {
|
|
46
|
+
|
|
47
|
+
const { templateType } = await inquirer.prompt([
|
|
48
|
+
{
|
|
49
|
+
type: 'list',
|
|
50
|
+
name: 'templateType',
|
|
51
|
+
message: '请选择项目模版:',
|
|
52
|
+
choices: [
|
|
53
|
+
{ name: 'min (基础UI)', value: '0' },
|
|
54
|
+
{ name: 'normal (State)', value: '1' },
|
|
55
|
+
{ name: 'pro (GetX)', value: '2' }
|
|
56
|
+
],
|
|
57
|
+
default: '2'
|
|
58
|
+
}
|
|
59
|
+
]);
|
|
60
|
+
return templateType;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
getIsNeedDemo: async () => {
|
|
64
|
+
const { isNeedDemo } = await inquirer.prompt([
|
|
65
|
+
{
|
|
66
|
+
type: 'confirm',
|
|
67
|
+
name: 'isNeedDemo',
|
|
68
|
+
message: '是否需要使用事例:',
|
|
69
|
+
default: true
|
|
70
|
+
}
|
|
71
|
+
])
|
|
72
|
+
return isNeedDemo;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 获取项目信息
|
|
77
|
+
*
|
|
78
|
+
* @param {string} projectType - 项目类型 ('app'或'module')
|
|
79
|
+
* @param {string} defaultProjectName - 默认项目名称
|
|
80
|
+
* @param {string} defaultPackageName - 默认包名
|
|
81
|
+
* @param {string} defaultParentDir - 默认父目录
|
|
82
|
+
* @returns {Promise<Object>} 项目信息对象
|
|
83
|
+
*/
|
|
84
|
+
getProjectInfo: async (projectType, defaultProjectName, defaultPackageName, defaultParentDir) => {
|
|
85
|
+
// 设置默认值
|
|
86
|
+
if (!defaultProjectName) {
|
|
87
|
+
defaultProjectName = projectType === 'module' ? "hzy_example_module" : "hzy_example_project";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 获取项目名称
|
|
91
|
+
const flutterPath = await getFlutterSdkPath();
|
|
92
|
+
|
|
93
|
+
const { projectName } = await inquirer.prompt([
|
|
94
|
+
{
|
|
95
|
+
type: 'input',
|
|
96
|
+
name: 'projectName',
|
|
97
|
+
message: '请输入新项目名称(小写字母、数字和下划线):',
|
|
98
|
+
default: defaultProjectName,
|
|
99
|
+
validate: validateProjectName
|
|
100
|
+
}
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
// 获取包名
|
|
104
|
+
const { packageName } = await inquirer.prompt([
|
|
105
|
+
{
|
|
106
|
+
type: 'input',
|
|
107
|
+
name: 'packageName',
|
|
108
|
+
message: '请输入包名:',
|
|
109
|
+
default: defaultPackageName || `com.example.${projectName}`,
|
|
110
|
+
validate: validatePackageName
|
|
111
|
+
}
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// 获取项目存放路径
|
|
116
|
+
let desktopPath;
|
|
117
|
+
if (!defaultParentDir) {
|
|
118
|
+
desktopPath = await getDesktopPath();
|
|
119
|
+
}
|
|
120
|
+
const { parentDir } = await inquirer.prompt([
|
|
121
|
+
{
|
|
122
|
+
type: 'input',
|
|
123
|
+
name: 'parentDir',
|
|
124
|
+
message: '请输入项目存放路径:',
|
|
125
|
+
default: defaultParentDir || desktopPath
|
|
126
|
+
}
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const targetDir = path.join(parentDir, projectName);
|
|
130
|
+
console.log('flutterPath:', flutterPath);
|
|
131
|
+
return {
|
|
132
|
+
projectName,
|
|
133
|
+
packageName,
|
|
134
|
+
parentDir,
|
|
135
|
+
targetDir,
|
|
136
|
+
flutterSdkPath: flutterPath
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 确认是否覆盖已存在的目录
|
|
142
|
+
*
|
|
143
|
+
* @param {string} targetDir - 目标目录路径
|
|
144
|
+
* @returns {Promise<boolean>} 是否覆盖
|
|
145
|
+
*/
|
|
146
|
+
confirmOverwrite: async (targetDir) => {
|
|
147
|
+
printColored(`\n警告: 目标目录 ${targetDir} 已存在!`, 'yellow');
|
|
148
|
+
|
|
149
|
+
const { overwrite } = await inquirer.prompt([
|
|
150
|
+
{
|
|
151
|
+
type: 'confirm',
|
|
152
|
+
name: 'overwrite',
|
|
153
|
+
message: '是否覆盖?',
|
|
154
|
+
default: false
|
|
155
|
+
}
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
if (overwrite) {
|
|
159
|
+
printColored("将覆盖目标目录", 'red');
|
|
160
|
+
} else {
|
|
161
|
+
printColored("操作已取消", 'green');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return overwrite;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 获取用户选择的IDE类型
|
|
169
|
+
*
|
|
170
|
+
* @returns {Promise<string>} IDE类型
|
|
171
|
+
*/
|
|
172
|
+
getIdeChoice: async () => {
|
|
173
|
+
const { ideChoice } = await inquirer.prompt([
|
|
174
|
+
{
|
|
175
|
+
type: 'list',
|
|
176
|
+
name: 'ideChoice',
|
|
177
|
+
message: '请选择要打开项目的IDE:',
|
|
178
|
+
choices: [
|
|
179
|
+
{ name: 'Visual Studio Code', value: 'vscode' },
|
|
180
|
+
{ name: 'Android Studio', value: 'android_studio' },
|
|
181
|
+
{ name: '打开文件夹', value: 'open_folder' },
|
|
182
|
+
{ name: '不打开', value: 'manual' }
|
|
183
|
+
],
|
|
184
|
+
default: 'vscode'
|
|
185
|
+
}
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
return ideChoice;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
async function getFlutterSdkPath () {
|
|
193
|
+
const { path: sdkPath } = await inquirer.prompt([{
|
|
194
|
+
type: 'input',
|
|
195
|
+
name: 'path',
|
|
196
|
+
|
|
197
|
+
message: '请输入Flutter SDK路径(留空使用系统默认):',
|
|
198
|
+
validate: input => !input || fs.existsSync(path.join(input, 'bin/flutter'))
|
|
199
|
+
}]);
|
|
200
|
+
return sdkPath || 'flutter';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = {
|
|
204
|
+
getFlutterSdkPath,
|
|
205
|
+
getUserInteraction
|
|
206
|
+
};
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具函数模块
|
|
3
|
+
*
|
|
4
|
+
* 包含各种实用工具函数
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { exec } = require('child_process');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 打印彩色文本
|
|
14
|
+
*
|
|
15
|
+
* @param {string} text - 要打印的文本
|
|
16
|
+
* @param {string} color - 颜色名称
|
|
17
|
+
*/
|
|
18
|
+
function printColored (text, color) {
|
|
19
|
+
const colorMap = {
|
|
20
|
+
red: chalk.red,
|
|
21
|
+
green: chalk.green,
|
|
22
|
+
yellow: chalk.yellow,
|
|
23
|
+
blue: chalk.blue,
|
|
24
|
+
purple: chalk.magenta,
|
|
25
|
+
cyan: chalk.cyan
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
console.log(chalk.bold(colorMap[color] ? colorMap[color](text) : text));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 验证项目名称是否符合Flutter项目命名规范
|
|
33
|
+
*
|
|
34
|
+
* @param {string} name - 项目名称
|
|
35
|
+
* @returns {boolean|string} 验证结果
|
|
36
|
+
*/
|
|
37
|
+
function validateProjectName (name) {
|
|
38
|
+
if (!name.match(/^[a-z][a-z0-9_]*$/)) {
|
|
39
|
+
return '错误: 项目名称必须以小写字母开头,只能包含小写字母、数字和下划线';
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 验证包名是否符合Java包名规范
|
|
46
|
+
*
|
|
47
|
+
* @param {string} name - 包名
|
|
48
|
+
* @returns {boolean|string} 验证结果
|
|
49
|
+
*/
|
|
50
|
+
function validatePackageName (name) {
|
|
51
|
+
if (!name.match(/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/)) {
|
|
52
|
+
return '错误: 包名必须符合Java包名规范,如com.example.myapp';
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 更新README.md文件
|
|
59
|
+
*
|
|
60
|
+
* @param {string} targetDir - 目标目录路径
|
|
61
|
+
* @param {string} projectName - 项目名称
|
|
62
|
+
|
|
63
|
+
*/
|
|
64
|
+
function updateReadme (targetDir, projectName) {
|
|
65
|
+
const currentDate = new Date().toISOString().split('T')[0];
|
|
66
|
+
const readmePath = path.join(targetDir, 'README.md');
|
|
67
|
+
|
|
68
|
+
const readmeContent = `#
|
|
69
|
+
|
|
70
|
+
基于火之夜工作室 Flutter 通用项目框架创建
|
|
71
|
+
|
|
72
|
+
## 项目信息
|
|
73
|
+
|
|
74
|
+
- 项目名称: ${projectName}
|
|
75
|
+
- 创建日期: ${currentDate}
|
|
76
|
+
|
|
77
|
+
## 快速开始
|
|
78
|
+
|
|
79
|
+
\`\`\`bash
|
|
80
|
+
# 安装依赖
|
|
81
|
+
flutter pub get
|
|
82
|
+
|
|
83
|
+
# 运行项目
|
|
84
|
+
flutter run
|
|
85
|
+
\`\`\`
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
fs.writeFileSync(readmePath, readmeContent, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 在IDE中自动打开项目
|
|
93
|
+
*
|
|
94
|
+
* @param {string} targetDir - 项目目录路径
|
|
95
|
+
* @param {string} ideType - IDE类型
|
|
96
|
+
*/
|
|
97
|
+
function openProjectInIde (targetDir, ideType = "vscode") {
|
|
98
|
+
const platform = process.platform;
|
|
99
|
+
let command = null;
|
|
100
|
+
|
|
101
|
+
// 确保路径是绝对路径
|
|
102
|
+
targetDir = path.resolve(targetDir);
|
|
103
|
+
|
|
104
|
+
switch (ideType) {
|
|
105
|
+
case 'vscode':
|
|
106
|
+
command = `code "${targetDir}"`;
|
|
107
|
+
break;
|
|
108
|
+
case 'android_studio':
|
|
109
|
+
if (platform === 'darwin') { // macOS
|
|
110
|
+
command = `open -a "Android Studio" "${targetDir}"`;
|
|
111
|
+
} else if (platform === 'win32') { // Windows
|
|
112
|
+
// 在Windows上查找Android Studio路径会比较复杂
|
|
113
|
+
// 这里简化处理,实际应用中可能需要更复杂的逻辑
|
|
114
|
+
command = `"C:\\Program Files\\Android\\Android Studio\\bin\\studio64.exe" "${targetDir}"`;
|
|
115
|
+
} else if (platform === 'linux') { // Linux
|
|
116
|
+
command = `studio "${targetDir}"`;
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
case 'open_folder':
|
|
120
|
+
if (platform === 'darwin') { // macOS
|
|
121
|
+
command = `open "${targetDir}"`;
|
|
122
|
+
} else if (platform === 'win32') { // Windows
|
|
123
|
+
command = `explorer "${targetDir}"`;
|
|
124
|
+
} else if (platform === 'linux') { // Linux
|
|
125
|
+
command = `xdg-open "${targetDir}"`;
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
case 'manual':
|
|
129
|
+
// 不执行任何命令
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (command) {
|
|
134
|
+
try {
|
|
135
|
+
exec(command, (error) => {
|
|
136
|
+
if (error) {
|
|
137
|
+
printColored(`无法打开IDE: ${error.message}`, 'yellow');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
} catch (e) {
|
|
141
|
+
printColored(`打开IDE时发生错误: ${e.message}`, 'yellow');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
printColored,
|
|
148
|
+
validateProjectName,
|
|
149
|
+
validatePackageName,
|
|
150
|
+
updateReadme,
|
|
151
|
+
openProjectInIde
|
|
152
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flu-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "火之夜工作室 Flutter 项目创建工具",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"flu-cli": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
|
+
"start": "node ./index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"flutter",
|
|
15
|
+
"cli",
|
|
16
|
+
"project",
|
|
17
|
+
"generator"
|
|
18
|
+
],
|
|
19
|
+
"author": "火之夜工作室",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^4.1.2",
|
|
23
|
+
"cli-progress": "^3.12.0",
|
|
24
|
+
"commander": "^11.1.0",
|
|
25
|
+
"fs-extra": "^11.2.0",
|
|
26
|
+
"inquirer": "^8.2.6",
|
|
27
|
+
"ora": "^5.4.1",
|
|
28
|
+
"path": "^0.12.7"
|
|
29
|
+
}
|
|
30
|
+
}
|