flu-cli 2.0.5 → 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
@@ -11,115 +11,119 @@ import {
11
11
  generateService,
12
12
  generateModel,
13
13
  generateComponent,
14
- generateModule
15
- } from 'flu-cli-core';
14
+ generateModule,
15
+ } from 'flu-cli-core'
16
+ import { listGeneratorSelectableOptionsWithReport } from './config.js'
16
17
 
17
- const logger = new ConsoleLogger();
18
+ const logger = new ConsoleLogger()
18
19
 
19
20
  /**
20
21
  * 添加组件
21
22
  */
22
- export async function addComponent (type, name, options) {
23
+ export async function addComponent(type, name, options) {
24
+ const diagnostics = []
23
25
  // 处理 --list 参数
24
- if (options.list || (type === '--list')) {
25
- await printSupportedTypes();
26
- return;
26
+ if (options.list || type === '--list') {
27
+ await printSupportedTypes()
28
+ return { ok: true, diagnostics }
27
29
  }
28
30
 
29
31
  if (!type || !name) {
30
- logger.error('请指定类型和名称');
31
- await printSupportedTypes();
32
- return;
32
+ logger.error('请指定类型和名称')
33
+ await printSupportedTypes()
34
+ diagnostics.push('请指定类型和名称')
35
+ return { ok: false, diagnostics }
33
36
  }
34
37
 
35
- logger.title(`📝 添加 ${type}: ${name}`);
38
+ logger.title(`📝 添加 ${type}: ${name}`)
36
39
 
37
40
  try {
38
41
  // 处理别名
39
42
  const typeMap = {
40
- 'p': 'page',
41
- 'w': 'widget',
42
- 'c': 'component',
43
- 'v': 'viewmodel',
44
- 'vm': 'viewmodel',
45
- 's': 'service',
46
- 'm': 'model',
47
- 'mod': 'module'
48
- };
49
-
50
- const normalizedType = typeMap[type.toLowerCase()] || type.toLowerCase();
43
+ p: 'page',
44
+ w: 'widget',
45
+ c: 'component',
46
+ v: 'viewmodel',
47
+ vm: 'viewmodel',
48
+ s: 'service',
49
+ m: 'model',
50
+ mod: 'module',
51
+ }
52
+
53
+ const normalizedType = typeMap[type.toLowerCase()] || type.toLowerCase()
51
54
 
52
55
  // 自动推断 feature (如果未指定)
53
56
  if (!options.feature) {
54
- options.feature = inferFeatureFromCwd();
57
+ options.feature = inferFeatureFromCwd()
55
58
  }
56
59
 
57
60
  switch (normalizedType) {
58
61
  case 'page':
59
- await addPage(name, options);
60
- break;
62
+ return await addPage(name, options)
61
63
 
62
64
  case 'widget':
63
- await addWidget(name, options);
64
- break;
65
+ return await addWidget(name, options)
65
66
 
66
67
  case 'component':
67
- await addComponentItem(name, options);
68
- break;
68
+ return await addComponentItem(name, options)
69
69
 
70
70
  case 'viewmodel':
71
- await addViewModel(name, options);
72
- break;
71
+ return await addViewModel(name, options)
73
72
 
74
73
  case 'service':
75
- await addService(name, options);
76
- break;
74
+ return await addService(name, options)
77
75
 
78
76
  case 'model':
79
- await addModel(name, options);
80
- break;
77
+ return await addModel(name, options)
81
78
 
82
79
  case 'module':
83
- await addModule(name, options);
84
- break;
80
+ return await addModule(name, options)
85
81
 
86
82
  default:
87
- logger.error(`未知的类型: ${type}`);
88
- logger.info('支持的类型: page (p), widget (w), component (c), vm (v), service (s), model (m), module (mod)');
89
- return;
83
+ logger.error(`未知的类型: ${type}`)
84
+ logger.info(
85
+ '支持的类型: page (p), widget (w), component (c), vm (v), service (s), model (m), module (mod)',
86
+ )
87
+ diagnostics.push(`未知的类型: ${type}`)
88
+ return { ok: false, diagnostics }
90
89
  }
91
-
92
90
  } catch (error) {
93
- logger.error(`生成失败: ${error.message}`);
91
+ logger.error(`生成失败: ${error.message}`)
92
+ diagnostics.push(`生成失败: ${error.message}`)
93
+ return { ok: false, diagnostics }
94
94
  }
95
95
  }
96
96
 
97
97
  /**
98
98
  * 添加页面
99
99
  */
100
- async function addPage (name, options) {
100
+ async function addPage(name, options) {
101
101
  const {
102
102
  feature = null,
103
103
  stateful = false,
104
104
  stateless = false,
105
105
  noVm = false,
106
- listPage = false
107
- } = options;
106
+ listPage = false,
107
+ } = options
108
108
 
109
- logger.info(`生成页面: ${name}`);
109
+ logger.info(`生成页面: ${name}`)
110
110
  if (feature) {
111
- logger.info(`功能模块: ${feature}`);
111
+ logger.info(`功能模块: ${feature}`)
112
112
  }
113
113
 
114
114
  // 交互式选择页面类型 (如果未指定任何标志)
115
- let finalStateful = stateful;
116
- let finalStateless = stateless;
117
- let finalNoVm = noVm;
118
- let finalListPage = listPage;
119
-
120
- if (!stateful && !stateless && !listPage) {
115
+ let finalStateful = stateful
116
+ let finalStateless = stateless
117
+ let finalNoVm = noVm
118
+ let finalListPage = listPage
119
+ let selectedPageMixins = []
120
+ let selectedPageMixinImports = []
121
+ const shouldSkipInteractivePrompt =
122
+ process.env.FLU_CLI_NON_INTERACTIVE === '1' || !process.stdout.isTTY
123
+
124
+ if (!stateful && !stateless && !listPage && !shouldSkipInteractivePrompt) {
121
125
  try {
122
- const inquirer = (await import('inquirer')).default;
126
+ const inquirer = (await import('inquirer')).default
123
127
  const { pageType } = await inquirer.prompt([
124
128
  {
125
129
  type: 'list',
@@ -129,189 +133,240 @@ async function addPage (name, options) {
129
133
  { name: 'Stateful(BasePage) + ViewModel (推荐)', value: 'default' },
130
134
  { name: 'Stateful (no ViewModel)', value: 'stateful_no_vm' },
131
135
  { name: 'Stateless', value: 'stateless' },
132
- { name: 'List Page (列表页)', value: 'list_page' }
133
- ]
134
- }
135
- ]);
136
+ { name: 'List Page (列表页)', value: 'list_page' },
137
+ ],
138
+ },
139
+ ])
136
140
 
137
141
  if (pageType === 'stateful_no_vm') {
138
- finalStateful = true;
139
- finalNoVm = true;
142
+ finalStateful = true
143
+ finalNoVm = true
140
144
  } else if (pageType === 'stateless') {
141
- finalStateless = true;
142
- finalNoVm = true; // Stateless 通常不需要 VM 绑定
145
+ finalStateless = true
146
+ finalNoVm = true // Stateless 通常不需要 VM 绑定
143
147
  } else if (pageType === 'list_page') {
144
- finalListPage = true;
148
+ finalListPage = true
145
149
  }
146
150
  // default: stateful=false (use config default), stateless=false, noVm=false
147
151
  } catch (e) {
148
152
  if (process.stdout.isTTY) {
149
- logger.warn('交互式选择已取消,使用默认配置');
153
+ logger.warn('交互式选择已取消,使用默认配置')
150
154
  }
151
155
  }
156
+ } else if (!stateful && !stateless && !listPage && shouldSkipInteractivePrompt) {
157
+ logger.info('检测到非交互环境,页面类型使用默认推荐配置')
152
158
  }
153
159
 
154
160
  if (finalListPage) {
155
- logger.info(`类型: 列表页 (BaseListPage)`);
161
+ logger.info(`类型: 列表页 (BaseListPage)`)
156
162
  } else if (finalStateless) {
157
- logger.info(`强制类型: StatelessWidget`);
163
+ logger.info(`强制类型: StatelessWidget`)
158
164
  } else if (finalStateful) {
159
- logger.info(`类型: StatefulWidget`);
165
+ logger.info(`类型: StatefulWidget`)
160
166
  }
161
167
  if (finalNoVm) {
162
- logger.info(`不生成 ViewModel`);
168
+ logger.info(`不生成 ViewModel`)
163
169
  }
164
170
 
165
- const success = await generatePage(name, {
166
- feature,
167
- stateful: finalStateful,
168
- stateless: finalStateless,
169
- withViewModel: !finalNoVm,
170
- isListPage: finalListPage,
171
- outputDir: process.cwd()
172
- }, logger);
171
+ if (typeof options.mixins === 'string' && options.mixins.trim()) {
172
+ const selected = resolveRequestedMixins('page', options.mixins)
173
+ selectedPageMixins = selected.mixins
174
+ selectedPageMixinImports = selected.mixinImports
175
+ } else if (!shouldSkipInteractivePrompt) {
176
+ const selected = await promptGeneratorMixins('page')
177
+ selectedPageMixins = selected.mixins
178
+ selectedPageMixinImports = selected.mixinImports
179
+ }
180
+
181
+ const success = await generatePage(
182
+ name,
183
+ {
184
+ feature,
185
+ stateful: finalStateful,
186
+ stateless: finalStateless,
187
+ withViewModel: !finalNoVm,
188
+ isListPage: finalListPage,
189
+ overrideMixins: selectedPageMixins,
190
+ overrideMixinImports: selectedPageMixinImports,
191
+ outputDir: process.cwd(),
192
+ },
193
+ logger,
194
+ )
173
195
 
174
196
  if (success) {
175
- logger.newLine();
176
- logger.success('✅ 页面生成完成!');
177
- logger.newLine();
197
+ logger.newLine()
198
+ logger.success('✅ 页面生成完成!')
199
+ logger.newLine()
178
200
 
179
- logger.info('下一步:');
201
+ logger.info('下一步:')
180
202
  if (feature) {
181
- console.log(` 1. 在 lib/features/${feature}/pages/${name}_page.dart 中实现页面逻辑`);
203
+ console.log(` 1. 在 lib/features/${feature}/pages/${name}_page.dart 中实现页面逻辑`)
182
204
  if (!finalNoVm) {
183
- console.log(` 2. 在 lib/features/${feature}/viewmodels/${name}_viewmodel.dart 中实现业务逻辑`);
205
+ console.log(
206
+ ` 2. 在 lib/features/${feature}/viewmodels/${name}_viewmodel.dart 中实现业务逻辑`,
207
+ )
184
208
  }
185
209
  } else {
186
- console.log(` 1. 在 lib/pages/${name}_page.dart 中实现页面逻辑`);
210
+ console.log(` 1. 在 lib/pages/${name}_page.dart 中实现页面逻辑`)
187
211
  if (!finalNoVm) {
188
- console.log(` 2. 在 lib/viewmodels/${name}_viewmodel.dart 中实现业务逻辑`);
212
+ console.log(` 2. 在 lib/viewmodels/${name}_viewmodel.dart 中实现业务逻辑`)
189
213
  }
190
214
  }
191
- console.log(` ${finalNoVm ? 2 : 3}. 在路由中注册新页面`);
215
+ console.log(` ${finalNoVm ? 2 : 3}. 在路由中注册新页面`)
192
216
  }
217
+ return { ok: success, diagnostics: success ? [] : ['页面生成失败'] }
193
218
  }
194
219
 
195
220
  /**
196
221
  * 添加 Widget
197
222
  */
198
- async function addWidget (name, options) {
199
- const {
200
- feature = null,
201
- stateful = false
202
- } = options;
223
+ async function addWidget(name, options) {
224
+ const { feature = null, stateful = false } = options
203
225
 
204
- logger.info(`生成 Widget: ${name}`);
226
+ logger.info(`生成 Widget: ${name}`)
205
227
  if (feature) {
206
- logger.info(`功能模块: ${feature}`);
228
+ logger.info(`功能模块: ${feature}`)
207
229
  }
208
230
  if (stateful) {
209
- logger.info(`类型: StatefulWidget`);
231
+ logger.info(`类型: StatefulWidget`)
210
232
  }
211
233
 
212
- const success = await generateWidget(name, {
213
- feature,
214
- stateful,
215
- outputDir: process.cwd()
216
- }, logger);
234
+ const success = await generateWidget(
235
+ name,
236
+ {
237
+ feature,
238
+ stateful,
239
+ outputDir: process.cwd(),
240
+ },
241
+ logger,
242
+ )
217
243
 
218
244
  if (success) {
219
- logger.newLine();
220
- logger.success('✅ Widget 生成完成!');
245
+ logger.newLine()
246
+ logger.success('✅ Widget 生成完成!')
221
247
  }
248
+ return { ok: success, diagnostics: success ? [] : ['Widget 生成失败'] }
222
249
  }
223
250
 
224
251
  /**
225
252
  * 添加 Component
226
253
  */
227
- async function addComponentItem (name, options) {
228
- const {
229
- feature = null
230
- } = options;
254
+ async function addComponentItem(name, options) {
255
+ const { feature = null } = options
231
256
 
232
- logger.info(`生成 Component: ${name}`);
257
+ logger.info(`生成 Component: ${name}`)
233
258
  if (feature) {
234
- logger.info(`功能模块: ${feature}`);
259
+ logger.info(`功能模块: ${feature}`)
235
260
  }
236
261
 
237
- const success = await generateComponent(name, {
238
- feature,
239
- outputDir: options.outputDir || process.cwd()
240
- }, logger);
262
+ const success = await generateComponent(
263
+ name,
264
+ {
265
+ feature,
266
+ outputDir: options.outputDir || process.cwd(),
267
+ },
268
+ logger,
269
+ )
241
270
 
242
271
  if (success) {
243
- logger.newLine();
244
- logger.success('✅ Component 生成完成!');
272
+ logger.newLine()
273
+ logger.success('✅ Component 生成完成!')
245
274
  }
275
+ return { ok: success, diagnostics: success ? [] : ['Component 生成失败'] }
246
276
  }
247
277
 
248
278
  /**
249
279
  * 添加 ViewModel
250
280
  */
251
- async function addViewModel (name, options) {
252
- const {
253
- feature = null
254
- } = options;
281
+ async function addViewModel(name, options) {
282
+ const { feature = null } = options
283
+ const shouldSkipInteractivePrompt =
284
+ process.env.FLU_CLI_NON_INTERACTIVE === '1' || !process.stdout.isTTY
255
285
 
256
- logger.info(`生成 ViewModel: ${name}`);
286
+ logger.info(`生成 ViewModel: ${name}`)
257
287
  if (feature) {
258
- logger.info(`功能模块: ${feature}`);
288
+ logger.info(`功能模块: ${feature}`)
259
289
  }
260
290
 
261
- const success = await generateViewModel(name, {
262
- feature,
263
- outputDir: process.cwd()
264
- }, logger);
291
+ const selected =
292
+ typeof options.mixins === 'string' && options.mixins.trim()
293
+ ? resolveRequestedMixins('viewModel', options.mixins)
294
+ : shouldSkipInteractivePrompt
295
+ ? { mixins: [], mixinImports: [] }
296
+ : await promptGeneratorMixins('viewModel')
297
+
298
+ const success = await generateViewModel(
299
+ name,
300
+ {
301
+ feature,
302
+ overrideMixins: selected.mixins,
303
+ overrideMixinImports: selected.mixinImports,
304
+ outputDir: process.cwd(),
305
+ },
306
+ logger,
307
+ )
265
308
 
266
309
  if (success) {
267
- logger.newLine();
268
- logger.success('✅ ViewModel 生成完成!');
310
+ logger.newLine()
311
+ logger.success('✅ ViewModel 生成完成!')
269
312
  }
313
+ return { ok: success, diagnostics: success ? [] : ['ViewModel 生成失败'] }
270
314
  }
271
315
 
272
316
  /**
273
317
  * 添加 Service
274
318
  */
275
- async function addService (name, options) {
276
- const {
277
- feature = null
278
- } = options;
319
+ async function addService(name, options) {
320
+ const { feature = null } = options
321
+ const shouldSkipInteractivePrompt =
322
+ process.env.FLU_CLI_NON_INTERACTIVE === '1' || !process.stdout.isTTY
279
323
 
280
- logger.info(`生成 Service: ${name}`);
324
+ logger.info(`生成 Service: ${name}`)
281
325
  if (feature) {
282
- logger.info(`功能模块: ${feature}`);
326
+ logger.info(`功能模块: ${feature}`)
283
327
  }
284
328
 
285
- const success = await generateService(name, {
286
- feature,
287
- outputDir: process.cwd()
288
- }, logger);
329
+ const selected =
330
+ typeof options.mixins === 'string' && options.mixins.trim()
331
+ ? resolveRequestedMixins('service', options.mixins)
332
+ : shouldSkipInteractivePrompt
333
+ ? { mixins: [], mixinImports: [] }
334
+ : await promptGeneratorMixins('service')
335
+
336
+ const success = await generateService(
337
+ name,
338
+ {
339
+ feature,
340
+ overrideMixins: selected.mixins,
341
+ overrideMixinImports: selected.mixinImports,
342
+ outputDir: process.cwd(),
343
+ },
344
+ logger,
345
+ )
289
346
 
290
347
  if (success) {
291
- logger.newLine();
292
- logger.success('✅ Service 生成完成!');
348
+ logger.newLine()
349
+ logger.success('✅ Service 生成完成!')
293
350
  }
351
+ return { ok: success, diagnostics: success ? [] : ['Service 生成失败'] }
294
352
  }
295
353
 
296
354
  /**
297
355
  * 添加 Model
298
356
  */
299
- async function addModel (name, options) {
300
- const {
301
- feature = null,
302
- json = null
303
- } = options;
357
+ async function addModel(name, options) {
358
+ const { feature = null, json = null } = options
304
359
 
305
- logger.info(`生成 Model: ${name}`);
360
+ logger.info(`生成 Model: ${name}`)
306
361
  if (feature) {
307
- logger.info(`功能模块: ${feature}`);
362
+ logger.info(`功能模块: ${feature}`)
308
363
  }
309
364
 
310
365
  // 交互式获取 JSON 数据
311
- let jsonData = null;
366
+ let jsonData = null
312
367
  if (!json) {
313
368
  try {
314
- const inquirer = (await import('inquirer')).default;
369
+ const inquirer = (await import('inquirer')).default
315
370
  const { source } = await inquirer.prompt([
316
371
  {
317
372
  type: 'list',
@@ -321,10 +376,10 @@ async function addModel (name, options) {
321
376
  { name: '使用默认模板', value: 'default' },
322
377
  { name: '输入 JSON (粘贴)', value: 'input' },
323
378
  { name: '使用编辑器输入 (推荐)', value: 'editor' },
324
- { name: '选择 JSON 文件', value: 'file' }
325
- ]
326
- }
327
- ]);
379
+ { name: '选择 JSON 文件', value: 'file' },
380
+ ],
381
+ },
382
+ ])
328
383
 
329
384
  if (source === 'input') {
330
385
  const { content } = await inquirer.prompt([
@@ -334,15 +389,15 @@ async function addModel (name, options) {
334
389
  message: '请输入 JSON 内容:',
335
390
  validate: (input) => {
336
391
  try {
337
- JSON.parse(input);
338
- return true;
392
+ JSON.parse(input)
393
+ return true
339
394
  } catch (e) {
340
- return `JSON 格式错误: ${e.message}`;
395
+ return `JSON 格式错误: ${e.message}`
341
396
  }
342
- }
343
- }
344
- ]);
345
- jsonData = JSON.parse(content);
397
+ },
398
+ },
399
+ ])
400
+ jsonData = JSON.parse(content)
346
401
  } else if (source === 'editor') {
347
402
  const { content } = await inquirer.prompt([
348
403
  {
@@ -352,15 +407,15 @@ async function addModel (name, options) {
352
407
  default: '{\n "example": "value"\n}',
353
408
  validate: (input) => {
354
409
  try {
355
- JSON.parse(input);
356
- return true;
410
+ JSON.parse(input)
411
+ return true
357
412
  } catch (e) {
358
- return `JSON 格式错误: ${e.message}`;
413
+ return `JSON 格式错误: ${e.message}`
359
414
  }
360
- }
361
- }
362
- ]);
363
- jsonData = JSON.parse(content);
415
+ },
416
+ },
417
+ ])
418
+ jsonData = JSON.parse(content)
364
419
  } else if (source === 'file') {
365
420
  const { filePath } = await inquirer.prompt([
366
421
  {
@@ -368,67 +423,74 @@ async function addModel (name, options) {
368
423
  name: 'filePath',
369
424
  message: '请输入 JSON 文件路径:',
370
425
  validate: (input) => {
371
- if (!input) return '路径不能为空';
372
- return true;
373
- }
374
- }
375
- ]);
426
+ if (!input) return '路径不能为空'
427
+ return true
428
+ },
429
+ },
430
+ ])
376
431
  // 这里只是更新 options.json,后续逻辑会处理
377
- options.json = filePath;
432
+ options.json = filePath
378
433
  }
379
434
  } catch (e) {
380
435
  // 如果交互失败(非 TTY),则忽略,使用默认行为
381
436
  if (process.stdout.isTTY) {
382
- logger.warn('交互式输入已取消,使用默认模板');
437
+ logger.warn('交互式输入已取消,使用默认模板')
383
438
  }
384
439
  }
385
440
  } else {
386
- logger.info(`从 JSON 文件生成: ${json}`);
441
+ logger.info(`从 JSON 文件生成: ${json}`)
387
442
  }
388
443
 
389
- const success = await generateModel(name, {
390
- feature,
391
- jsonFile: options.json || json,
392
- jsonData,
393
- outputDir: process.cwd()
394
- }, logger);
444
+ const success = await generateModel(
445
+ name,
446
+ {
447
+ feature,
448
+ jsonFile: options.json || json,
449
+ jsonData,
450
+ outputDir: process.cwd(),
451
+ },
452
+ logger,
453
+ )
395
454
 
396
455
  if (success) {
397
- logger.newLine();
398
- logger.success('✅ Model 生成完成!');
456
+ logger.newLine()
457
+ logger.success('✅ Model 生成完成!')
399
458
  }
459
+ return { ok: success, diagnostics: success ? [] : ['Model 生成失败'] }
400
460
  }
401
461
 
402
462
  /**
403
463
  * 添加模块
404
464
  */
405
- async function addModule (name, options) {
406
- logger.info(`生成模块: ${name}`);
465
+ async function addModule(name, options) {
466
+ logger.info(`生成模块: ${name}`)
407
467
 
408
468
  const success = await generateModule(name, {
409
- outputDir: options.outputDir || process.cwd()
410
- });
469
+ outputDir: options.outputDir || process.cwd(),
470
+ })
411
471
 
412
472
  if (!success) {
413
- logger.error('模块创建失败');
473
+ logger.error('模块创建失败')
474
+ return { ok: false, diagnostics: ['模块创建失败'] }
414
475
  }
476
+ return { ok: true, diagnostics: [] }
415
477
  }
416
478
 
417
479
  /**
418
480
  * 打印支持的类型列表
419
481
  */
420
- async function printSupportedTypes () {
421
- const chalk = (await import('chalk')).default;
482
+ async function printSupportedTypes() {
483
+ const chalk = (await import('chalk')).default
422
484
 
423
- console.log(chalk.bold.cyan('\n📦 flu-cli add - 代码生成工具\n'));
424
- console.log(chalk.gray('用于快速生成 Flutter 项目中的各种代码组件\n'));
485
+ console.log(chalk.bold.cyan('\n📦 flu-cli add - 代码生成工具\n'))
486
+ console.log(chalk.gray('用于快速生成 Flutter 项目中的各种代码组件\n'))
425
487
 
426
488
  // 基本用法
427
- console.log(chalk.bold.yellow('📖 基本用法:'));
428
- console.log(chalk.white(' flu-cli add <type> <name> [options]\n'));
489
+ console.log(chalk.bold.yellow('📖 基本用法:'))
490
+ console.log(chalk.white(' flu-cli add <type> <name> [options]\n'))
429
491
 
430
492
  // 支持的类型
431
- console.log(chalk.bold.yellow('🎯 支持的组件类型:\n'));
493
+ console.log(chalk.bold.yellow('🎯 支持的组件类型:\n'))
432
494
 
433
495
  const types = [
434
496
  {
@@ -441,66 +503,48 @@ async function printSupportedTypes () {
441
503
  '--stateful 创建 StatefulWidget',
442
504
  '--stateless 强制创建 StatelessWidget',
443
505
  '--list-page 创建列表页 (BaseListPage)',
444
- '--no-vm 不生成 ViewModel'
506
+ '--no-vm 不生成 ViewModel',
445
507
  ],
446
508
  examples: [
447
509
  'flu-cli add page home',
448
510
  'flu-cli add page login --feature auth',
449
- 'flu-cli add page profile --stateful --no-vm'
450
- ]
511
+ 'flu-cli add page profile --stateful --no-vm',
512
+ ],
451
513
  },
452
514
  {
453
515
  name: 'widget',
454
516
  emoji: '🧩',
455
517
  desc: '通用组件',
456
518
  detail: '生成可复用的 Widget 组件',
457
- options: [
458
- '-f, --feature <name> 指定功能模块',
459
- '--stateful 创建 StatefulWidget'
460
- ],
461
- examples: [
462
- 'flu-cli add widget custom_button',
463
- 'flu-cli add widget user_avatar --stateful'
464
- ]
519
+ options: ['-f, --feature <name> 指定功能模块', '--stateful 创建 StatefulWidget'],
520
+ examples: ['flu-cli add widget custom_button', 'flu-cli add widget user_avatar --stateful'],
465
521
  },
466
522
  {
467
523
  name: 'component',
468
524
  emoji: '🔧',
469
525
  desc: '业务组件',
470
526
  detail: '生成特定业务场景的组件',
471
- options: [
472
- '-f, --feature <name> 指定功能模块'
473
- ],
527
+ options: ['-f, --feature <name> 指定功能模块'],
474
528
  examples: [
475
529
  'flu-cli add component product_card',
476
- 'flu-cli add component order_item --feature shop'
477
- ]
530
+ 'flu-cli add component order_item --feature shop',
531
+ ],
478
532
  },
479
533
  {
480
534
  name: 'vm / viewmodel',
481
535
  emoji: '🎮',
482
536
  desc: '视图模型',
483
537
  detail: '生成独立的 ViewModel 文件',
484
- options: [
485
- '-f, --feature <name> 指定功能模块'
486
- ],
487
- examples: [
488
- 'flu-cli add vm settings',
489
- 'flu-cli add viewmodel user --feature auth'
490
- ]
538
+ options: ['-f, --feature <name> 指定功能模块'],
539
+ examples: ['flu-cli add vm settings', 'flu-cli add viewmodel user --feature auth'],
491
540
  },
492
541
  {
493
542
  name: 'service',
494
543
  emoji: '⚙️',
495
544
  desc: '服务层',
496
545
  detail: '生成基础 Service 文件',
497
- options: [
498
- '-f, --feature <name> 指定功能模块'
499
- ],
500
- examples: [
501
- 'flu-cli add service user',
502
- 'flu-cli add service auth --feature user'
503
- ]
546
+ options: ['-f, --feature <name> 指定功能模块'],
547
+ examples: ['flu-cli add service user', 'flu-cli add service auth --feature user'],
504
548
  },
505
549
  {
506
550
  name: 'model',
@@ -509,12 +553,9 @@ async function printSupportedTypes () {
509
553
  detail: '生成数据模型类 (支持交互式输入 JSON)',
510
554
  options: [
511
555
  '-f, --feature <name> 指定功能模块',
512
- '--json <file> 从 JSON 文件生成 (可选)'
556
+ '--json <file> 从 JSON 文件生成 (可选)',
513
557
  ],
514
- examples: [
515
- 'flu-cli add model user',
516
- 'flu-cli add model product --json ./data/product.json'
517
- ]
558
+ examples: ['flu-cli add model user', 'flu-cli add model product --json ./data/product.json'],
518
559
  },
519
560
  {
520
561
  name: 'module',
@@ -522,74 +563,132 @@ async function printSupportedTypes () {
522
563
  desc: '完整模块',
523
564
  detail: '生成包含多个文件的完整功能模块',
524
565
  options: [],
525
- examples: [
526
- 'flu-cli add module shop',
527
- 'flu-cli add module user_profile'
528
- ]
529
- }
530
- ];
566
+ examples: ['flu-cli add module shop', 'flu-cli add module user_profile'],
567
+ },
568
+ ]
531
569
 
532
570
  types.forEach((t, index) => {
533
571
  // 类型名称和描述
534
- console.log(chalk.bold.green(` ${t.emoji} ${t.name}`));
535
- console.log(chalk.gray(` ${t.detail}`));
572
+ console.log(chalk.bold.green(` ${t.emoji} ${t.name}`))
573
+ console.log(chalk.gray(` ${t.detail}`))
536
574
 
537
575
  // 可用选项
538
576
  if (t.options.length > 0) {
539
- console.log(chalk.cyan(' 选项:'));
540
- t.options.forEach(opt => {
541
- console.log(chalk.gray(` ${opt}`));
542
- });
577
+ console.log(chalk.cyan(' 选项:'))
578
+ t.options.forEach((opt) => {
579
+ console.log(chalk.gray(` ${opt}`))
580
+ })
543
581
  }
544
582
 
545
583
  // 使用示例
546
584
  if (t.examples.length > 0) {
547
- console.log(chalk.cyan(' 示例:'));
548
- t.examples.forEach(ex => {
549
- console.log(chalk.white(` ${ex}`));
550
- });
585
+ console.log(chalk.cyan(' 示例:'))
586
+ t.examples.forEach((ex) => {
587
+ console.log(chalk.white(` ${ex}`))
588
+ })
551
589
  }
552
590
 
553
591
  // 添加分隔线(最后一项除外)
554
592
  if (index < types.length - 1) {
555
- console.log('');
593
+ console.log('')
556
594
  }
557
- });
595
+ })
558
596
 
559
597
  // 通用选项
560
- console.log(chalk.bold.yellow('\n⚡ 通用选项:\n'));
561
- console.log(chalk.white(' --list 查看此帮助信息'));
562
- console.log(chalk.white(' -f, --feature <name> 指定功能模块(modular/clean 架构)\n'));
598
+ console.log(chalk.bold.yellow('\n⚡ 通用选项:\n'))
599
+ console.log(chalk.white(' --list 查看此帮助信息'))
600
+ console.log(chalk.white(' -f, --feature <name> 指定功能模块(modular/clean 架构)\n'))
563
601
 
564
602
  // 快速开始
565
- console.log(chalk.bold.yellow('🚀 快速开始:\n'));
566
- console.log(chalk.white(' # 查看所有支持的类型'));
567
- console.log(chalk.gray(' flu-cli add --list\n'));
568
- console.log(chalk.white(' # 创建一个简单页面'));
569
- console.log(chalk.gray(' flu-cli add page home\n'));
570
- console.log(chalk.white(' # 创建带功能模块的页面'));
571
- console.log(chalk.gray(' flu-cli add page login --feature auth\n'));
572
- console.log(chalk.white(' # 创建一个 Service'));
573
- console.log(chalk.gray(' flu-cli add service user\n'));
603
+ console.log(chalk.bold.yellow('🚀 快速开始:\n'))
604
+ console.log(chalk.white(' # 查看所有支持的类型'))
605
+ console.log(chalk.gray(' flu-cli add --list\n'))
606
+ console.log(chalk.white(' # 创建一个简单页面'))
607
+ console.log(chalk.gray(' flu-cli add page home\n'))
608
+ console.log(chalk.white(' # 创建带功能模块的页面'))
609
+ console.log(chalk.gray(' flu-cli add page login --feature auth\n'))
610
+ console.log(chalk.white(' # 创建一个 Service'))
611
+ console.log(chalk.gray(' flu-cli add service user\n'))
574
612
 
575
613
  // 提示信息
576
- console.log(chalk.bold.cyan('💡 提示:\n'));
577
- console.log(chalk.gray(' • 支持简写: p(page), w(widget), c(component), v(vm), s(service), m(model)'));
578
- console.log(chalk.gray(' • 使用 -f --feature 可将组件生成到指定功能模块下'));
579
- console.log(chalk.gray(' • page 类型默认会同时生成 ViewModel,使用 --no-vm 可禁用'));
580
- console.log(chalk.gray(' • 更多帮助请运行: flu-cli add --help\n'));
614
+ console.log(chalk.bold.cyan('💡 提示:\n'))
615
+ console.log(
616
+ chalk.gray(' • 支持简写: p(page), w(widget), c(component), v(vm), s(service), m(model)'),
617
+ )
618
+ console.log(chalk.gray(' • 使用 -f --feature 可将组件生成到指定功能模块下'))
619
+ console.log(chalk.gray(' • page 类型默认会同时生成 ViewModel,使用 --no-vm 可禁用'))
620
+ console.log(chalk.gray(' • 更多帮助请运行: flu-cli add --help\n'))
581
621
  }
582
622
 
583
623
  /**
584
624
  * 从当前工作目录推断 feature 名称
585
625
  * 规则: 如果路径包含 /features/<name>/...,则返回 <name>
586
626
  */
587
- function inferFeatureFromCwd () {
588
- const cwd = process.cwd();
589
- const match = cwd.match(/features[\\/]([^\\/]+)/);
627
+ function inferFeatureFromCwd() {
628
+ const cwd = process.cwd()
629
+ const match = cwd.match(/features[\\/]([^\\/]+)/)
590
630
  if (match && match[1]) {
591
- return match[1];
631
+ return match[1]
592
632
  }
593
- return null;
633
+ return null
594
634
  }
595
635
 
636
+ async function promptGeneratorMixins(target) {
637
+ const mixinOptions = getAvailableMixins(target)
638
+
639
+ if (mixinOptions.length === 0) {
640
+ return { mixins: [], mixinImports: [] }
641
+ }
642
+
643
+ try {
644
+ const inquirer = (await import('inquirer')).default
645
+ const { selectedMixins } = await inquirer.prompt([
646
+ {
647
+ type: 'checkbox',
648
+ name: 'selectedMixins',
649
+ message: `选择要附加到${getGeneratorTargetLabel(target)}的 Mixin(可多选):`,
650
+ choices: mixinOptions.map((item) => ({
651
+ name: item.name,
652
+ value: item.name,
653
+ short: item.name,
654
+ })),
655
+ },
656
+ ])
657
+
658
+ const selected = mixinOptions.filter((item) => selectedMixins.includes(item.name))
659
+ return {
660
+ mixins: selected.map((item) => item.name),
661
+ mixinImports: selected.map((item) => item.import),
662
+ }
663
+ } catch {
664
+ return { mixins: [], mixinImports: [] }
665
+ }
666
+ }
667
+
668
+ function getGeneratorTargetLabel(target) {
669
+ if (target === 'page') return '页面'
670
+ if (target === 'viewModel') return 'ViewModel'
671
+ return 'Service'
672
+ }
673
+
674
+ function getAvailableMixins(target) {
675
+ const reportResult = listGeneratorSelectableOptionsWithReport({
676
+ dir: process.cwd(),
677
+ target,
678
+ })
679
+ return reportResult?.report?.mixins || []
680
+ }
681
+
682
+ function resolveRequestedMixins(target, rawValue) {
683
+ const available = getAvailableMixins(target)
684
+ const requestedNames = String(rawValue)
685
+ .split(',')
686
+ .map((item) => item.trim())
687
+ .filter(Boolean)
688
+
689
+ const selected = available.filter((item) => requestedNames.includes(item.name))
690
+ return {
691
+ mixins: selected.map((item) => item.name),
692
+ mixinImports: selected.map((item) => item.import),
693
+ }
694
+ }