flu-cli 2.0.6 → 2.1.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +17 -4
  3. package/config/dev.config.js +11 -11
  4. package/config/templates.js +10 -10
  5. package/index.js +554 -102
  6. package/lib/commands/add.js +365 -266
  7. package/lib/commands/assets.js +77 -78
  8. package/lib/commands/cache.js +29 -52
  9. package/lib/commands/completion.js +13 -11
  10. package/lib/commands/config.js +150 -44
  11. package/lib/commands/init-ai-base.js +89 -0
  12. package/lib/commands/newClack.js +269 -178
  13. package/lib/commands/snippets.js +58 -43
  14. package/lib/commands/template.js +98 -58
  15. package/lib/commands/templates.js +101 -57
  16. package/lib/commands/upload.js +313 -0
  17. package/lib/commands/vnext-options.js +206 -0
  18. package/lib/generators/model_generator.js +91 -88
  19. package/lib/generators/page_generator.js +100 -93
  20. package/lib/generators/service_generator.js +44 -39
  21. package/lib/generators/viewmodel_generator.js +25 -29
  22. package/lib/generators/widget_generator.js +30 -35
  23. package/lib/templates/templateCopier.js +14 -15
  24. package/lib/templates/templateManager.js +22 -21
  25. package/lib/utils/config.js +37 -20
  26. package/lib/utils/flutterHelper.js +2 -2
  27. package/lib/utils/i18n.js +3 -3
  28. package/lib/utils/index_updater.js +22 -23
  29. package/lib/utils/json-output.js +59 -0
  30. package/lib/utils/logger.js +17 -17
  31. package/lib/utils/project_detector.js +66 -66
  32. package/lib/utils/snippet_loader.js +21 -19
  33. package/lib/utils/string_helper.js +13 -13
  34. package/lib/utils/templateSelectorEnquirer.js +94 -108
  35. package/locales/en-US.json +1 -1
  36. package/locales/zh-CN.json +2 -2
  37. package/package.json +60 -57
  38. package/scripts/smoke-vnext-generate.mjs +1934 -0
  39. package/scripts/smoke-vnext-params.mjs +92 -0
  40. package/CLI.md +0 -513
  41. package/release.sh +0 -529
  42. package/scripts/e2e-state-tests.js +0 -116
  43. package/scripts/sync-base-to-templates.js +0 -108
  44. package/scripts/workspace-clone-all.sh +0 -101
  45. package/scripts/workspace-status-all.sh +0 -112
@@ -4,369 +4,460 @@
4
4
  * 说明:保留平台与包名,模板文件覆盖通用代码;支持内置/自定义模板与缓存开关
5
5
  */
6
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, ProjectGenerator } 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 { syncSnippets } from './snippets.js';
20
- import { configAssets } from './assets.js';
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, ProjectGenerator } from 'flu-cli-core'
17
+ import {
18
+ copyTemplate,
19
+ replaceVariables,
20
+ copyCustomTemplate,
21
+ ensurePubspecName,
22
+ mergePubspecFromTemplate,
23
+ cleanupTemplateFiles,
24
+ } from '../templates/templateCopier.js'
25
+ import { runFlutterCreate } from '../utils/flutterHelper.js'
26
+ import { syncSnippets } from './snippets.js'
27
+ import { configAssets } from './assets.js'
28
+ import { buildVNextProjectOptions } from './vnext-options.js'
21
29
 
22
30
  /**
23
31
  * 创建新项目
24
32
  */
25
- export async function newProjectWithClack (projectName, options) {
26
- p.intro(chalk.cyan.bold('🚀 创建 Flutter 项目'));
33
+ export async function newProjectWithClack(projectName, options) {
34
+ const jsonMode = options?.json === true
35
+ if (jsonMode) {
36
+ process.env.FLU_CLI_NON_INTERACTIVE = '1'
37
+ process.env.FLU_CLI_JSON_OUTPUT = '1'
38
+ if (!projectName) throw new Error('json 模式下必须提供 project-name')
39
+ if (!options.template) throw new Error('json 模式下必须提供 --template <type>')
40
+ } else {
41
+ p.intro(chalk.cyan.bold('🚀 创建 Flutter 项目'))
42
+ }
27
43
 
28
44
  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';
45
+ const isInteractive = !projectName
46
+ let finalProjectName = projectName
47
+ let templateSelection = null
48
+ let stateManager = options.state || 'default'
49
+ const nonInteractive = process.env.FLU_CLI_NON_INTERACTIVE === '1'
34
50
 
35
51
  // ========== 交互式模式 ==========
36
52
  if (isInteractive) {
37
53
  // Step 1: 项目名称
38
54
  finalProjectName = await p.text({
39
55
  message: '请输入项目名称(直接回车使用: my_app)',
40
- placeholder: 'my_app'
41
- });
56
+ placeholder: 'my_app',
57
+ })
42
58
 
43
59
  if (p.isCancel(finalProjectName)) {
44
- p.cancel('操作已取消');
45
- process.exit(0);
60
+ p.cancel('操作已取消')
61
+ process.exit(0)
46
62
  }
47
63
 
48
64
  if (!finalProjectName || finalProjectName === '') {
49
- finalProjectName = 'my_app';
65
+ finalProjectName = 'my_app'
50
66
  }
51
67
 
52
68
  // Step 2: 选择模板(实时预览,支持内置/自定义)
53
- templateSelection = await selectTemplateWithEnquirer();
69
+ templateSelection = await selectTemplateWithEnquirer()
54
70
 
55
71
  // Step 2.5: 选择状态管理器 (Native 模板跳过)
56
- let includeNetworkLayer = true; // 默认包含网络层
72
+ let includeNetworkLayer = true // 默认包含网络层
57
73
  if (templateSelection.kind === 'builtin' && templateSelection.name === 'native') {
58
- stateManager = 'default';
59
- includeNetworkLayer = false; // Native 模板不需要网络层
74
+ stateManager = 'default'
75
+ includeNetworkLayer = false // Native 模板不需要网络层
60
76
  } else {
61
77
  const smChoice = await p.select({
62
78
  message: '请选择状态管理器',
63
79
  options: [
64
80
  { value: 'default', label: 'ChangeNotifier (默认)' },
65
81
  { value: 'provider', label: 'Provider' },
66
- { value: 'getx', label: 'GetX' }
82
+ { value: 'getx', label: 'GetX' },
67
83
  // { value: 'riverpod', label: 'Riverpod' }
68
84
  ],
69
- initialValue: 'default'
70
- });
85
+ initialValue: 'default',
86
+ })
71
87
  if (p.isCancel(smChoice)) {
72
- p.cancel('操作已取消');
73
- process.exit(0);
88
+ p.cancel('操作已取消')
89
+ process.exit(0)
74
90
  }
75
- stateManager = smChoice;
91
+ stateManager = smChoice
76
92
 
77
93
  // Step 2.6: 选择是否包含网络层
78
94
  const nlChoice = await p.select({
79
95
  message: '是否包含网络层?',
80
96
  options: [
81
- { value: true, label: '包含网络层 (推荐)', hint: '包含 Dio 网络工具、AppConfig、StorageUtil' },
82
- { value: false, label: '不包含网络层', hint: '纯净项目,适合工具类或离线应用' }
97
+ {
98
+ value: true,
99
+ label: '包含网络层 (推荐)',
100
+ hint: '包含 Dio 网络工具、AppConfig、StorageUtil',
101
+ },
102
+ { value: false, label: '不包含网络层', hint: '纯净项目,适合工具类或离线应用' },
83
103
  ],
84
- initialValue: true
85
- });
104
+ initialValue: true,
105
+ })
86
106
  if (p.isCancel(nlChoice)) {
87
- p.cancel('操作已取消');
88
- process.exit(0);
107
+ p.cancel('操作已取消')
108
+ process.exit(0)
89
109
  }
90
- includeNetworkLayer = nlChoice;
110
+ includeNetworkLayer = nlChoice
91
111
  }
92
112
 
93
113
  // Step 3: 项目路径(含冲突检测与删除确认)
94
- const defaultPath = join(options.dir, finalProjectName);
114
+ const defaultPath = join(options.dir, finalProjectName)
95
115
  let projectPath = await p.text({
96
116
  message: `请输入项目路径(直接回车使用: ${defaultPath})`,
97
- placeholder: defaultPath
98
- });
117
+ placeholder: defaultPath,
118
+ })
99
119
 
100
120
  if (p.isCancel(projectPath)) {
101
- p.cancel('操作已取消');
102
- process.exit(0);
121
+ p.cancel('操作已取消')
122
+ process.exit(0)
103
123
  }
104
124
 
105
125
  if (!projectPath || projectPath === '') {
106
- projectPath = defaultPath;
126
+ projectPath = defaultPath
107
127
  }
108
128
 
109
129
  // Step 4: 包名
110
- const defaultPackageName = `com.example.${finalProjectName}`;
130
+ const defaultPackageName = `com.example.${finalProjectName}`
111
131
  let packageName = await p.text({
112
132
  message: `请输入包名(直接回车使用: ${defaultPackageName})`,
113
- placeholder: defaultPackageName
114
- });
133
+ placeholder: defaultPackageName,
134
+ })
115
135
 
116
136
  if (p.isCancel(packageName)) {
117
- p.cancel('操作已取消');
118
- process.exit(0);
137
+ p.cancel('操作已取消')
138
+ process.exit(0)
119
139
  }
120
140
 
121
141
  if (!packageName || packageName === '') {
122
- packageName = defaultPackageName;
142
+ packageName = defaultPackageName
123
143
  }
124
144
 
125
145
  // Step 5: 作者(使用缓存的名字)
126
- const cachedAuthor = getAuthorName();
146
+ const cachedAuthor = getAuthorName()
127
147
  let author = await p.text({
128
148
  message: `请输入作者名称(直接回车使用: ${cachedAuthor})`,
129
- placeholder: cachedAuthor
130
- });
149
+ placeholder: cachedAuthor,
150
+ })
131
151
 
132
152
  if (p.isCancel(author)) {
133
- p.cancel('操作已取消');
134
- process.exit(0);
153
+ p.cancel('操作已取消')
154
+ process.exit(0)
135
155
  }
136
156
 
137
157
  if (!author || author === '') {
138
- author = cachedAuthor;
158
+ author = cachedAuthor
139
159
  } else {
140
160
  // 保存新的作者名字
141
- saveAuthorName(author);
161
+ saveAuthorName(author)
142
162
  }
143
163
 
144
164
  // Step 6: 检查目录是否存在(交互确认删除)
145
165
  if (existsSync(projectPath)) {
146
- console.log('');
147
- console.log(chalk.yellow(`⚠️ 目录已存在: ${projectPath}`));
166
+ console.log('')
167
+ console.log(chalk.yellow(`⚠️ 目录已存在: ${projectPath}`))
148
168
 
149
169
  const shouldDelete = await p.confirm({
150
170
  message: '是否删除现有目录并重新创建?',
151
- initialValue: true
152
- });
171
+ initialValue: true,
172
+ })
153
173
 
154
174
  if (p.isCancel(shouldDelete)) {
155
- p.cancel('操作已取消');
156
- process.exit(0);
175
+ p.cancel('操作已取消')
176
+ process.exit(0)
157
177
  }
158
178
 
159
179
  if (shouldDelete) {
160
- const { rmSync } = await import('fs');
180
+ const { rmSync } = await import('fs')
161
181
  try {
162
- rmSync(projectPath, { recursive: true, force: true });
163
- console.log(chalk.green(`✓ 已删除目录: ${projectPath}`));
182
+ rmSync(projectPath, { recursive: true, force: true })
183
+ console.log(chalk.green(`✓ 已删除目录: ${projectPath}`))
164
184
  } catch (error) {
165
- p.cancel(`删除目录失败: ${error.message}`);
166
- process.exit(1);
185
+ p.cancel(`删除目录失败: ${error.message}`)
186
+ process.exit(1)
167
187
  }
168
188
  } else {
169
- p.cancel('操作已取消');
170
- process.exit(0);
189
+ p.cancel('操作已取消')
190
+ process.exit(0)
171
191
  }
172
192
  }
173
193
 
174
194
  // Step 7: 显示信息并确认
175
- const templateDisplay = templateSelection?.kind === 'builtin'
176
- ? getTemplate(templateSelection.name).displayName
177
- : (ConfigManager.getInstance().getTemplate(templateSelection.id)?.name || '自定义模板');
178
-
179
- console.log('');
180
- console.log(chalk.cyan('📋 项目信息确认:'));
181
- console.log('');
182
- console.log(` ${chalk.gray('项目名称:')} ${chalk.green(finalProjectName)}`);
183
- console.log(` ${chalk.gray('项目模板:')} ${chalk.green(templateDisplay)}`);
184
- console.log(` ${chalk.gray('网络层:')} ${chalk.green(includeNetworkLayer ? '包含' : '不包含')}`);
185
- console.log(` ${chalk.gray('项目路径:')} ${chalk.green(projectPath)}`);
186
- console.log(` ${chalk.gray('包名:')} ${chalk.green(packageName)}`);
187
- console.log(` ${chalk.gray('作者:')} ${chalk.green(author)}`);
188
- console.log('');
195
+ const templateDisplay =
196
+ templateSelection?.kind === 'builtin'
197
+ ? getTemplate(templateSelection.name).displayName
198
+ : ConfigManager.getInstance().getTemplate(templateSelection.id)?.name || '自定义模板'
199
+
200
+ console.log('')
201
+ console.log(chalk.cyan('📋 项目信息确认:'))
202
+ console.log('')
203
+ console.log(` ${chalk.gray('项目名称:')} ${chalk.green(finalProjectName)}`)
204
+ console.log(` ${chalk.gray('项目模板:')} ${chalk.green(templateDisplay)}`)
205
+ console.log(
206
+ ` ${chalk.gray('网络层:')} ${chalk.green(includeNetworkLayer ? '包含' : '不包含')}`,
207
+ )
208
+ console.log(` ${chalk.gray('项目路径:')} ${chalk.green(projectPath)}`)
209
+ console.log(` ${chalk.gray('包名:')} ${chalk.green(packageName)}`)
210
+ console.log(` ${chalk.gray('作者:')} ${chalk.green(author)}`)
211
+ console.log('')
189
212
 
190
213
  const shouldContinue = await p.confirm({
191
- message: '确认创建项目?'
192
- });
214
+ message: '确认创建项目?',
215
+ })
193
216
 
194
217
  if (!shouldContinue || p.isCancel(shouldContinue)) {
195
- p.cancel('操作已取消');
196
- process.exit(0);
218
+ p.cancel('操作已取消')
219
+ process.exit(0)
197
220
  }
198
221
 
199
222
  // Step 8: 创建项目
200
- await createProject(finalProjectName, templateSelection, projectPath, { projectName: finalProjectName, packageName, author, stateManager, includeNetworkLayer }, options);
201
-
223
+ await createProject(
224
+ finalProjectName,
225
+ templateSelection,
226
+ projectPath,
227
+ { projectName: finalProjectName, packageName, author, stateManager, includeNetworkLayer },
228
+ options,
229
+ )
202
230
  } else {
203
231
  // ========== 命令行模式 ==========
204
232
  if (!isValidProjectName(finalProjectName)) {
205
- p.cancel('项目名称只能包含小写字母、数字和下划线');
206
- process.exit(1);
233
+ const msg = '项目名称只能包含小写字母、数字和下划线'
234
+ if (jsonMode) throw new Error(msg)
235
+ p.cancel(msg)
236
+ process.exit(1)
207
237
  }
208
238
 
209
- // 如果通过参数传入模板名称,构造为内置模板选择
239
+ // 如果通过参数传入模板名称,优先识别内置模板,其次识别已保存的自定义模板 ID
210
240
  if (!templateSelection && options.template) {
211
- templateSelection = { kind: 'builtin', name: options.template };
241
+ const customTemplate = ConfigManager.getInstance().getTemplate(options.template)
242
+ templateSelection = customTemplate
243
+ ? { kind: 'custom', id: customTemplate.id }
244
+ : { kind: 'builtin', name: options.template }
212
245
  }
213
246
  if (!templateSelection) {
214
- templateSelection = await selectTemplateWithEnquirer();
247
+ templateSelection = await selectTemplateWithEnquirer()
215
248
  }
216
249
  if (templateSelection.kind === 'builtin' && !isValidTemplate(templateSelection.name)) {
217
- p.cancel(`模板 "${templateSelection.name}" 不存在`);
218
- process.exit(1);
250
+ const msg = `模板 "${templateSelection.name}" 不存在`
251
+ if (jsonMode) throw new Error(msg)
252
+ p.cancel(msg)
253
+ process.exit(1)
219
254
  }
220
255
 
221
- const projectDir = join(options.dir, finalProjectName);
256
+ const projectDir = join(options.dir, finalProjectName)
222
257
 
223
258
  // 检查目录是否存在(命令行模式也提供删除确认)
224
259
  if (existsSync(projectDir)) {
225
260
  if (nonInteractive) {
226
- const { rmSync } = await import('fs');
227
- rmSync(projectDir, { recursive: true, force: true });
261
+ const { rmSync } = await import('fs')
262
+ rmSync(projectDir, { recursive: true, force: true })
228
263
  } else {
229
- console.log('');
230
- console.log(chalk.yellow(`⚠️ 目录已存在: ${projectDir}`));
231
- const shouldDelete = await p.confirm({ message: '是否删除现有目录并重新创建?', initialValue: true });
264
+ console.log('')
265
+ console.log(chalk.yellow(`⚠️ 目录已存在: ${projectDir}`))
266
+ const shouldDelete = await p.confirm({
267
+ message: '是否删除现有目录并重新创建?',
268
+ initialValue: true,
269
+ })
232
270
  if (p.isCancel(shouldDelete)) {
233
- p.cancel('操作已取消');
234
- process.exit(0);
271
+ p.cancel('操作已取消')
272
+ process.exit(0)
235
273
  }
236
274
  if (shouldDelete) {
237
- const { rmSync } = await import('fs');
275
+ const { rmSync } = await import('fs')
238
276
  try {
239
- rmSync(projectDir, { recursive: true, force: true });
240
- console.log(chalk.green(`✓ 已删除目录: ${projectDir}`));
277
+ rmSync(projectDir, { recursive: true, force: true })
278
+ console.log(chalk.green(`✓ 已删除目录: ${projectDir}`))
241
279
  } catch (error) {
242
- p.cancel(`删除目录失败: ${error.message}`);
243
- process.exit(1);
280
+ p.cancel(`删除目录失败: ${error.message}`)
281
+ process.exit(1)
244
282
  }
245
283
  } else {
246
- p.cancel('操作已取消');
247
- process.exit(0);
284
+ p.cancel('操作已取消')
285
+ process.exit(0)
248
286
  }
249
287
  }
250
288
  }
251
289
 
252
- const defaultPackageName = `com.example.${finalProjectName}`;
253
- let packageName = options.package || defaultPackageName;
290
+ const defaultPackageName = `com.example.${finalProjectName}`
291
+ let packageName = options.package || defaultPackageName
254
292
  if (!options.package && !nonInteractive) {
255
- packageName = await p.text({ message: `请输入包名(直接回车使用: ${defaultPackageName})`, placeholder: defaultPackageName });
293
+ packageName = await p.text({
294
+ message: `请输入包名(直接回车使用: ${defaultPackageName})`,
295
+ placeholder: defaultPackageName,
296
+ })
256
297
  if (p.isCancel(packageName)) {
257
- p.cancel('操作已取消');
258
- process.exit(0);
298
+ p.cancel('操作已取消')
299
+ process.exit(0)
259
300
  }
260
301
  if (!packageName || packageName === '') {
261
- packageName = defaultPackageName;
302
+ packageName = defaultPackageName
262
303
  }
263
304
  }
264
305
 
265
- let author = options.author || 'Your Name';
306
+ let author = options.author || 'Your Name'
266
307
  if (!options.author && !nonInteractive) {
267
- author = await p.text({ message: '请输入作者名称(直接回车使用: Your Name)', placeholder: 'Your Name' });
308
+ author = await p.text({
309
+ message: '请输入作者名称(直接回车使用: Your Name)',
310
+ placeholder: 'Your Name',
311
+ })
268
312
  if (p.isCancel(author)) {
269
- p.cancel('操作已取消');
270
- process.exit(0);
313
+ p.cancel('操作已取消')
314
+ process.exit(0)
271
315
  }
272
316
  if (!author || author === '') {
273
- author = 'Your Name';
317
+ author = 'Your Name'
274
318
  }
275
319
  }
276
320
 
277
- // CLI 模式支持 --state 传参
278
- const includeNetworkLayer = options.network !== false; // 默认包含,除非明确指定 --no-network
279
- await createProject(finalProjectName, templateSelection, projectDir, { projectName: finalProjectName, packageName, author, stateManager, includeNetworkLayer }, options);
321
+ // CLI 模式支持 --state 传参;Native 默认保持纯 Flutter,显式 --network 才注入 Core 能力。
322
+ const isNativeTemplate =
323
+ templateSelection.kind === 'builtin' && templateSelection.name === 'native'
324
+ const includeNetworkLayer =
325
+ options.network !== undefined ? options.network !== false : !isNativeTemplate
326
+ const vNextOptions = buildVNextProjectOptions(options)
327
+ return await createProject(
328
+ finalProjectName,
329
+ templateSelection,
330
+ projectDir,
331
+ {
332
+ projectName: finalProjectName,
333
+ packageName,
334
+ author,
335
+ stateManager,
336
+ includeNetworkLayer,
337
+ ...vNextOptions,
338
+ },
339
+ options,
340
+ )
280
341
  }
281
342
  } catch (error) {
282
- p.cancel(`创建失败: ${error.message}`);
283
- process.exit(1);
343
+ if (jsonMode) throw error
344
+ p.cancel(`创建失败: ${error.message}`)
345
+ process.exit(1)
284
346
  }
285
347
  }
286
348
 
287
349
  /**
288
350
  * 创建项目
289
351
  */
290
- async function createProject (projectName, templateSelection, projectDir, projectInfo, options) {
291
- const s = p.spinner();
352
+ async function createProject(projectName, templateSelection, projectDir, projectInfo, options) {
353
+ const s = p.spinner()
354
+ const jsonMode = process.env.FLU_CLI_JSON_OUTPUT === '1'
292
355
 
293
356
  try {
294
357
  // 1. 设置生成选项
295
- const templateId = templateSelection.kind === 'builtin'
296
- ? templateSelection.name
297
- : templateSelection.id;
358
+ const templateId =
359
+ templateSelection.kind === 'builtin' ? templateSelection.name : templateSelection.id
360
+ const shouldCreateProjectConfig =
361
+ templateSelection.kind === 'builtin' && ['lite', 'modular', 'clean'].includes(templateId)
298
362
 
299
363
  const generatorOptions = {
300
364
  templateType: templateId,
301
365
  stateManager: projectInfo.stateManager || 'default',
302
366
  packageName: projectInfo.packageName,
303
367
  outputDir: dirname(projectDir),
304
- createFlutterProject: true, // v2 总是需要 flutter create
368
+ createFlutterProject: true, // 新建项目默认需要 flutter create
305
369
  forceUpdate: !options.cache,
306
370
  author: projectInfo.author,
307
371
  flutterTemplate: options.flutterTemplate || 'app',
308
- includeNetworkLayer: projectInfo.includeNetworkLayer !== false // 默认包含网络层
309
- };
372
+ includeNetworkLayer: projectInfo.includeNetworkLayer !== false, // 默认包含网络层
373
+ examples: projectInfo.examples || [],
374
+ composition: projectInfo.composition,
375
+ architectureMode: projectInfo.architectureMode,
376
+ enableMixinOptions: projectInfo.enableMixinOptions,
377
+ platforms: projectInfo.platforms,
378
+ flutterSdk: projectInfo.flutterSdk,
379
+ capabilities: projectInfo.capabilities,
380
+ helpers: projectInfo.helpers,
381
+ createProjectConfig: shouldCreateProjectConfig,
382
+ }
310
383
 
311
384
  // 2. 调用核心生成器
312
- s.start('正在生成项目 (Core Engine)...');
313
- const pg = new ProjectGenerator();
314
- const success = await pg.generate(projectName, generatorOptions);
385
+ s.start('正在生成项目 (Core Engine)...')
386
+ const pg = new ProjectGenerator()
387
+ const success = await pg.generate(projectName, generatorOptions)
315
388
 
316
389
  if (!success) {
317
- s.stop('项目生成失败');
318
- throw new Error('核心生成器返回失败');
390
+ s.stop('项目生成失败')
391
+ throw new Error('核心生成器返回失败')
319
392
  }
320
- s.stop('✓ 项目生成成功');
393
+ s.stop('✓ 项目生成成功')
321
394
 
322
395
  // 3. CLI 特色功能:同步标准代码片段 (Native 模板跳过)
323
- const isNative = templateId === 'native' || templateId === 'none';
396
+ const isNative = templateId === 'native' || templateId === 'none'
324
397
  if (!isNative) {
325
- s.start('正在同步代码片段...');
326
- await syncSnippets(projectDir);
327
- s.stop('✓ 代码片段同步完成');
398
+ s.start('正在同步代码片段...')
399
+ await syncSnippets(projectDir)
400
+ s.stop('✓ 代码片段同步完成')
328
401
  }
329
402
 
330
403
  // 4. 清理模板残余(ProjectGenerator 可能已经做了一部分,这里确保万一)
331
- cleanupTemplateFiles(projectDir);
332
-
333
- // 完成
334
- p.outro(
335
- chalk.green.bold('✨ 项目创建成功!\n\n') +
336
- chalk.cyan('下一步:\n') +
337
- chalk.yellow(` cd ${projectName}\n`) +
338
- chalk.yellow(` flutter pub get\n`) +
339
- chalk.yellow(` flutter run`)
340
- );
341
-
342
- // 5. 提示资源配置
343
- const shouldConfigAssets = await p.confirm({
344
- message: '项目已创建。是否现在就配置 App 图标和启动图?',
345
- initialValue: false
346
- });
347
-
348
- if (shouldConfigAssets) {
349
- await configAssets({ dir: projectDir });
404
+ cleanupTemplateFiles(projectDir)
405
+
406
+ if (!jsonMode) {
407
+ p.outro(
408
+ chalk.green.bold('✨ 项目创建成功!\n\n') +
409
+ chalk.cyan('下一步:\n') +
410
+ chalk.yellow(` cd ${projectName}\n`) +
411
+ chalk.yellow(` flutter pub get\n`) +
412
+ chalk.yellow(` flutter run`),
413
+ )
414
+ }
415
+
416
+ // 5. 提示资源配置(非交互模式下跳过,避免 CI / 脚本挂起)
417
+ const nonInteractiveExit = process.env.FLU_CLI_NON_INTERACTIVE === '1'
418
+ if (!nonInteractiveExit) {
419
+ const shouldConfigAssets = await p.confirm({
420
+ message: '项目已创建。是否现在就配置 App 图标和启动图?',
421
+ initialValue: false,
422
+ })
423
+
424
+ if (shouldConfigAssets) {
425
+ await configAssets({ dir: projectDir })
426
+ }
350
427
  }
351
428
 
352
429
  // 6. 记忆默认模板
353
430
  if (templateSelection?.kind === 'builtin') {
354
- saveDefaultTemplate({ type: 'builtin', idOrName: templateSelection.name });
431
+ saveDefaultTemplate({ type: 'builtin', idOrName: templateSelection.name })
355
432
  } else if (templateSelection?.kind === 'custom') {
356
- saveDefaultTemplate({ type: 'custom', idOrName: templateSelection.id });
433
+ saveDefaultTemplate({ type: 'custom', idOrName: templateSelection.id })
357
434
  }
358
435
 
359
- // 正常退出
360
- process.exit(0);
436
+ if (!jsonMode) process.exit(0)
437
+ return {
438
+ ok: true,
439
+ projectName,
440
+ projectDir,
441
+ templateId,
442
+ stateManager: generatorOptions.stateManager,
443
+ packageName: generatorOptions.packageName,
444
+ includeNetworkLayer: generatorOptions.includeNetworkLayer,
445
+ examples: generatorOptions.examples,
446
+ platforms: generatorOptions.platforms,
447
+ flutterSdk: generatorOptions.flutterSdk,
448
+ capabilities: generatorOptions.capabilities,
449
+ helpers: generatorOptions.helpers,
450
+ snippetsSynced: !isNative,
451
+ }
361
452
  } catch (error) {
362
- s.stop('创建失败');
363
- throw error;
453
+ s.stop('创建失败')
454
+ throw error
364
455
  }
365
456
  }
366
457
 
367
458
  /**
368
459
  * 验证项目名称
369
460
  */
370
- function isValidProjectName (name) {
371
- return /^[a-z][a-z0-9_]*$/.test(name);
461
+ function isValidProjectName(name) {
462
+ return /^[a-z][a-z0-9_]*$/.test(name)
372
463
  }