flu-cli 2.0.0 → 2.0.2

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/index.js CHANGED
@@ -16,7 +16,7 @@ import { newProjectWithClack } from './lib/commands/newClack.js';
16
16
  import { addComponent } from './lib/commands/add.js';
17
17
  import { listTemplates, showTemplateDetail } from './lib/commands/templates.js';
18
18
  import { updateTemplates, cleanCache } from './lib/commands/cache.js';
19
- import { generateStateManager } from './lib/commands/generate.js';
19
+ // generate-sm 命令已移除
20
20
 
21
21
  // 获取 package.json
22
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -68,7 +68,6 @@ program
68
68
  .option('--stateless', '强制创建 StatelessWidget(即使有 ViewModel,不推荐)')
69
69
  .option('--list-page', '创建列表页 (BaseListPage)')
70
70
  .option('--no-vm', '不生成 ViewModel(仅 page 类型)')
71
- .option('--type <type>', 'Service 类型: api, storage, auth(仅 service 类型)', 'api')
72
71
  .option('--json <file>', '从 JSON 文件生成(仅 model 类型)')
73
72
  .option('--list', '查看支持的类型列表')
74
73
  .action((type, name, options) => {
@@ -88,15 +87,7 @@ program
88
87
  }
89
88
  });
90
89
 
91
- program
92
- .command('generate-sm <type>')
93
- .alias('g')
94
- .description('为现有项目生成状态管理适配与配置')
95
- .option('-d, --dir <path>', '项目目录', process.cwd())
96
- .option('-m, --module <name>', '模块名称(modular/clean 支持分模块适配器布局)')
97
- .action((type, options) => {
98
- generateStateManager(type, options);
99
- });
90
+ // generate-sm 命令已移除
100
91
 
101
92
  // ========== 缓存管理命令 ==========
102
93
  program
@@ -133,6 +124,43 @@ program
133
124
  syncSnippets();
134
125
  });
135
126
 
127
+ // ========== 模板管理命令 (New) ==========
128
+ import { listAllTemplates, addTemplate, removeTemplate } from './lib/commands/template.js';
129
+
130
+ const templateCmd = program.command('template');
131
+
132
+ templateCmd
133
+ .description('管理自定义项目模板')
134
+ .action(() => {
135
+ listAllTemplates();
136
+ });
137
+
138
+ templateCmd
139
+ .command('list')
140
+ .description('列出所有模板')
141
+ .action(() => {
142
+ listAllTemplates();
143
+ });
144
+
145
+ templateCmd
146
+ .command('add <id> <source>')
147
+ .description('添加自定义模板 (id: 唯一标识, source: Git URL 或本地路径)')
148
+ .option('--local', '添加本地模板 (source 为路径)')
149
+ .option('-n, --name <name>', '显示名称')
150
+ .option('-b, --branch <branch>', 'Git 分支 (默认 main)')
151
+ .option('-d, --description <text>', '描述信息')
152
+ .option('-f, --force', '覆盖已存在的模板')
153
+ .action((id, source, options) => {
154
+ addTemplate(id, source, options);
155
+ });
156
+
157
+ templateCmd
158
+ .command('remove <id>')
159
+ .description('删除自定义模板')
160
+ .action((id) => {
161
+ removeTemplate(id);
162
+ });
163
+
136
164
  // ========== 自动补全 ==========
137
165
  import { completion } from './lib/commands/completion.js';
138
166
 
@@ -145,11 +173,32 @@ program
145
173
  });
146
174
 
147
175
 
176
+ // ========== 配置管理命令 ==========
177
+ import { initConfig } from './lib/commands/config.js';
178
+
179
+ const configCmd = program.command('config');
180
+
181
+ configCmd
182
+ .description('管理项目生成配置 (.flu-cli.json)')
183
+ .action(() => {
184
+ configCmd.help();
185
+ });
186
+
187
+ configCmd
188
+ .command('init')
189
+ .description('初始化配置文件 (基于当前项目结构)')
190
+ .option('-d, --dir <path>', '项目目录', process.cwd())
191
+ .option('-f, --force', '覆盖已存在的配置文件')
192
+ .action((options) => {
193
+ initConfig(options);
194
+ });
195
+
196
+
148
197
  // 解析命令行参数
149
198
  program.parse(process.argv);
150
199
 
151
200
  // 如果没有提供命令,显示帮助信息
152
201
  if (process.argv.length === 2) {
153
202
  console.log(chalk.bold.cyan('\n🚀 欢迎使用 flu-cli v2.0\n'));
154
- program.help();
203
+ program.help({ error: false });
155
204
  }
@@ -3,14 +3,18 @@
3
3
  * 生成页面、Widget、Component、ViewModel 等
4
4
  */
5
5
 
6
- import { logger } from '../utils/logger.js';
7
- import { generatePage } from '../generators/page_generator.js';
8
- import { generateWidget } from '../generators/widget_generator.js';
9
- import { generateComponent } from '../generators/component_generator.js';
10
- import { generateViewModel } from '../generators/viewmodel_generator.js';
11
- import { generateService } from '../generators/service_generator.js';
12
- import { generateModel } from '../generators/model_generator.js';
13
- import { generateModule } from '../generators/module_generator.js';
6
+ import {
7
+ ConsoleLogger,
8
+ generatePage,
9
+ generateWidget,
10
+ generateViewModel,
11
+ generateService,
12
+ generateModel,
13
+ generateComponent,
14
+ generateModule
15
+ } from '@flu-cli/core';
16
+
17
+ const logger = new ConsoleLogger();
14
18
 
15
19
  /**
16
20
  * 添加组件
@@ -45,6 +49,11 @@ export async function addComponent (type, name, options) {
45
49
 
46
50
  const normalizedType = typeMap[type.toLowerCase()] || type.toLowerCase();
47
51
 
52
+ // 自动推断 feature (如果未指定)
53
+ if (!options.feature) {
54
+ options.feature = inferFeatureFromCwd();
55
+ }
56
+
48
57
  switch (normalizedType) {
49
58
  case 'page':
50
59
  await addPage(name, options);
@@ -102,25 +111,65 @@ async function addPage (name, options) {
102
111
  logger.info(`功能模块: ${feature}`);
103
112
  }
104
113
 
105
- if (listPage) {
114
+ // 交互式选择页面类型 (如果未指定任何标志)
115
+ let finalStateful = stateful;
116
+ let finalStateless = stateless;
117
+ let finalNoVm = noVm;
118
+ let finalListPage = listPage;
119
+
120
+ if (!stateful && !stateless && !listPage) {
121
+ try {
122
+ const inquirer = (await import('inquirer')).default;
123
+ const { pageType } = await inquirer.prompt([
124
+ {
125
+ type: 'list',
126
+ name: 'pageType',
127
+ message: '请选择页面类型:',
128
+ choices: [
129
+ { name: 'Stateful(BasePage) + ViewModel (推荐)', value: 'default' },
130
+ { name: 'Stateful (no ViewModel)', value: 'stateful_no_vm' },
131
+ { name: 'Stateless', value: 'stateless' },
132
+ { name: 'List Page (列表页)', value: 'list_page' }
133
+ ]
134
+ }
135
+ ]);
136
+
137
+ if (pageType === 'stateful_no_vm') {
138
+ finalStateful = true;
139
+ finalNoVm = true;
140
+ } else if (pageType === 'stateless') {
141
+ finalStateless = true;
142
+ finalNoVm = true; // Stateless 通常不需要 VM 绑定
143
+ } else if (pageType === 'list_page') {
144
+ finalListPage = true;
145
+ }
146
+ // default: stateful=false (use config default), stateless=false, noVm=false
147
+ } catch (e) {
148
+ if (process.stdout.isTTY) {
149
+ logger.warn('交互式选择已取消,使用默认配置');
150
+ }
151
+ }
152
+ }
153
+
154
+ if (finalListPage) {
106
155
  logger.info(`类型: 列表页 (BaseListPage)`);
107
- } else if (stateless) {
156
+ } else if (finalStateless) {
108
157
  logger.info(`强制类型: StatelessWidget`);
109
- } else if (stateful) {
158
+ } else if (finalStateful) {
110
159
  logger.info(`类型: StatefulWidget`);
111
160
  }
112
- if (noVm) {
161
+ if (finalNoVm) {
113
162
  logger.info(`不生成 ViewModel`);
114
163
  }
115
164
 
116
165
  const success = generatePage(name, {
117
166
  feature,
118
- stateful,
119
- stateless,
120
- withViewModel: !noVm,
121
- isListPage: listPage,
167
+ stateful: finalStateful,
168
+ stateless: finalStateless,
169
+ withViewModel: !finalNoVm,
170
+ isListPage: finalListPage,
122
171
  outputDir: process.cwd()
123
- });
172
+ }, logger);
124
173
 
125
174
  if (success) {
126
175
  logger.newLine();
@@ -130,16 +179,16 @@ async function addPage (name, options) {
130
179
  logger.info('下一步:');
131
180
  if (feature) {
132
181
  console.log(` 1. 在 lib/features/${feature}/pages/${name}_page.dart 中实现页面逻辑`);
133
- if (!noVm) {
182
+ if (!finalNoVm) {
134
183
  console.log(` 2. 在 lib/features/${feature}/viewmodels/${name}_viewmodel.dart 中实现业务逻辑`);
135
184
  }
136
185
  } else {
137
186
  console.log(` 1. 在 lib/pages/${name}_page.dart 中实现页面逻辑`);
138
- if (!noVm) {
187
+ if (!finalNoVm) {
139
188
  console.log(` 2. 在 lib/viewmodels/${name}_viewmodel.dart 中实现业务逻辑`);
140
189
  }
141
190
  }
142
- console.log(` ${noVm ? 2 : 3}. 在路由中注册新页面`);
191
+ console.log(` ${finalNoVm ? 2 : 3}. 在路由中注册新页面`);
143
192
  }
144
193
  }
145
194
 
@@ -164,7 +213,7 @@ async function addWidget (name, options) {
164
213
  feature,
165
214
  stateful,
166
215
  outputDir: process.cwd()
167
- });
216
+ }, logger);
168
217
 
169
218
  if (success) {
170
219
  logger.newLine();
@@ -187,8 +236,8 @@ async function addComponentItem (name, options) {
187
236
 
188
237
  const success = generateComponent(name, {
189
238
  feature,
190
- outputDir: process.cwd()
191
- });
239
+ outputDir: options.outputDir || process.cwd()
240
+ }, logger);
192
241
 
193
242
  if (success) {
194
243
  logger.newLine();
@@ -212,7 +261,7 @@ async function addViewModel (name, options) {
212
261
  const success = generateViewModel(name, {
213
262
  feature,
214
263
  outputDir: process.cwd()
215
- });
264
+ }, logger);
216
265
 
217
266
  if (success) {
218
267
  logger.newLine();
@@ -225,36 +274,22 @@ async function addViewModel (name, options) {
225
274
  */
226
275
  async function addService (name, options) {
227
276
  const {
228
- feature = null,
229
- type = 'api'
277
+ feature = null
230
278
  } = options;
231
279
 
232
280
  logger.info(`生成 Service: ${name}`);
233
281
  if (feature) {
234
282
  logger.info(`功能模块: ${feature}`);
235
283
  }
236
- logger.info(`类型: ${type}`);
237
284
 
238
285
  const success = generateService(name, {
239
286
  feature,
240
- type,
241
287
  outputDir: process.cwd()
242
- });
288
+ }, logger);
243
289
 
244
290
  if (success) {
245
291
  logger.newLine();
246
292
  logger.success('✅ Service 生成完成!');
247
- logger.newLine();
248
-
249
- if (type === 'api') {
250
- logger.info('提示: 记得在 pubspec.yaml 中添加 http 依赖');
251
- console.log(' dependencies:');
252
- console.log(' http: ^1.1.0');
253
- } else if (type === 'storage' || type === 'auth') {
254
- logger.info('提示: 记得在 pubspec.yaml 中添加 shared_preferences 依赖');
255
- console.log(' dependencies:');
256
- console.log(' shared_preferences: ^2.2.0');
257
- }
258
293
  }
259
294
  }
260
295
 
@@ -271,15 +306,92 @@ async function addModel (name, options) {
271
306
  if (feature) {
272
307
  logger.info(`功能模块: ${feature}`);
273
308
  }
274
- if (json) {
309
+
310
+ // 交互式获取 JSON 数据
311
+ let jsonData = null;
312
+ if (!json) {
313
+ try {
314
+ const inquirer = (await import('inquirer')).default;
315
+ const { source } = await inquirer.prompt([
316
+ {
317
+ type: 'list',
318
+ name: 'source',
319
+ message: '请选择 JSON 数据源:',
320
+ choices: [
321
+ { name: '使用默认模板', value: 'default' },
322
+ { name: '输入 JSON (粘贴)', value: 'input' },
323
+ { name: '使用编辑器输入 (推荐)', value: 'editor' },
324
+ { name: '选择 JSON 文件', value: 'file' }
325
+ ]
326
+ }
327
+ ]);
328
+
329
+ if (source === 'input') {
330
+ const { content } = await inquirer.prompt([
331
+ {
332
+ type: 'input',
333
+ name: 'content',
334
+ message: '请输入 JSON 内容:',
335
+ validate: (input) => {
336
+ try {
337
+ JSON.parse(input);
338
+ return true;
339
+ } catch (e) {
340
+ return `JSON 格式错误: ${e.message}`;
341
+ }
342
+ }
343
+ }
344
+ ]);
345
+ jsonData = JSON.parse(content);
346
+ } else if (source === 'editor') {
347
+ const { content } = await inquirer.prompt([
348
+ {
349
+ type: 'editor',
350
+ name: 'content',
351
+ message: '请在编辑器中输入 JSON 内容,保存并关闭以继续',
352
+ default: '{\n "example": "value"\n}',
353
+ validate: (input) => {
354
+ try {
355
+ JSON.parse(input);
356
+ return true;
357
+ } catch (e) {
358
+ return `JSON 格式错误: ${e.message}`;
359
+ }
360
+ }
361
+ }
362
+ ]);
363
+ jsonData = JSON.parse(content);
364
+ } else if (source === 'file') {
365
+ const { filePath } = await inquirer.prompt([
366
+ {
367
+ type: 'input',
368
+ name: 'filePath',
369
+ message: '请输入 JSON 文件路径:',
370
+ validate: (input) => {
371
+ if (!input) return '路径不能为空';
372
+ return true;
373
+ }
374
+ }
375
+ ]);
376
+ // 这里只是更新 options.json,后续逻辑会处理
377
+ options.json = filePath;
378
+ }
379
+ } catch (e) {
380
+ // 如果交互失败(非 TTY),则忽略,使用默认行为
381
+ if (process.stdout.isTTY) {
382
+ logger.warn('交互式输入已取消,使用默认模板');
383
+ }
384
+ }
385
+ } else {
275
386
  logger.info(`从 JSON 文件生成: ${json}`);
276
387
  }
277
388
 
278
389
  const success = generateModel(name, {
279
390
  feature,
280
- jsonFile: json,
391
+ jsonFile: options.json || json,
392
+ jsonData,
281
393
  outputDir: process.cwd()
282
- });
394
+ }, logger);
283
395
 
284
396
  if (success) {
285
397
  logger.newLine();
@@ -294,7 +406,7 @@ async function addModule (name, options) {
294
406
  logger.info(`生成模块: ${name}`);
295
407
 
296
408
  const success = generateModule(name, {
297
- outputDir: process.cwd()
409
+ outputDir: options.outputDir || process.cwd()
298
410
  });
299
411
 
300
412
  if (!success) {
@@ -381,25 +493,23 @@ async function printSupportedTypes () {
381
493
  name: 'service',
382
494
  emoji: '⚙️',
383
495
  desc: '服务层',
384
- detail: '生成 API、存储、认证等服务',
496
+ detail: '生成基础 Service 文件',
385
497
  options: [
386
- '-f, --feature <name> 指定功能模块',
387
- '--type <type> 服务类型: api, storage, auth (默认: api)'
498
+ '-f, --feature <name> 指定功能模块'
388
499
  ],
389
500
  examples: [
390
- 'flu-cli add service user --type api',
391
- 'flu-cli add service cache --type storage',
392
- 'flu-cli add service auth --type auth --feature user'
501
+ 'flu-cli add service user',
502
+ 'flu-cli add service auth --feature user'
393
503
  ]
394
504
  },
395
505
  {
396
506
  name: 'model',
397
507
  emoji: '📊',
398
508
  desc: '数据模型',
399
- detail: '生成数据模型类',
509
+ detail: '生成数据模型类 (支持交互式输入 JSON)',
400
510
  options: [
401
511
  '-f, --feature <name> 指定功能模块',
402
- '--json <file> 从 JSON 文件生成'
512
+ '--json <file> 从 JSON 文件生成 (可选)'
403
513
  ],
404
514
  examples: [
405
515
  'flu-cli add model user',
@@ -459,8 +569,8 @@ async function printSupportedTypes () {
459
569
  console.log(chalk.gray(' flu-cli add page home\n'));
460
570
  console.log(chalk.white(' # 创建带功能模块的页面'));
461
571
  console.log(chalk.gray(' flu-cli add page login --feature auth\n'));
462
- console.log(chalk.white(' # 创建一个 API 服务'));
463
- console.log(chalk.gray(' flu-cli add service user --type api\n'));
572
+ console.log(chalk.white(' # 创建一个 Service'));
573
+ console.log(chalk.gray(' flu-cli add service user\n'));
464
574
 
465
575
  // 提示信息
466
576
  console.log(chalk.bold.cyan('💡 提示:\n'));
@@ -470,3 +580,16 @@ async function printSupportedTypes () {
470
580
  console.log(chalk.gray(' • 更多帮助请运行: flu-cli add --help\n'));
471
581
  }
472
582
 
583
+ /**
584
+ * 从当前工作目录推断 feature 名称
585
+ * 规则: 如果路径包含 /features/<name>/...,则返回 <name>
586
+ */
587
+ function inferFeatureFromCwd () {
588
+ const cwd = process.cwd();
589
+ const match = cwd.match(/features[\\/]([^\\/]+)/);
590
+ if (match && match[1]) {
591
+ return match[1];
592
+ }
593
+ return null;
594
+ }
595
+
@@ -22,8 +22,6 @@ if type compdef &>/dev/null; then
22
22
  'a:添加组件 (add 别名)'
23
23
  'templates:管理模板'
24
24
  't:管理模板 (templates 别名)'
25
- 'generate-sm:生成状态管理代码'
26
- 'g:生成状态管理代码 (generate-sm 别名)'
27
25
  'update-templates:更新模板缓存'
28
26
  'u:更新模板缓存 (update-templates 别名)'
29
27
  'cache:管理缓存'
@@ -0,0 +1,70 @@
1
+ /**
2
+ * config 命令
3
+ * 职责:初始化和管理项目配置文件
4
+ */
5
+
6
+ import { ProjectConfigManager, detectProjectTemplate } from '@flu-cli/core';
7
+ import { writeFileSync, existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import chalk from 'chalk';
10
+
11
+ /**
12
+ * 初始化配置文件
13
+ */
14
+ export function initConfig (options) {
15
+ try {
16
+ const projectDir = options.dir || process.cwd();
17
+ const configPath = join(projectDir, '.flu-cli.json');
18
+
19
+ if (existsSync(configPath) && !options.force) {
20
+ console.log(chalk.yellow('⚠️ 配置文件已存在 (.flu-cli.json)'));
21
+ console.log(chalk.gray('使用 --force 覆盖'));
22
+ return;
23
+ }
24
+
25
+ // 检测当前项目模板,生成对应的默认配置
26
+ const template = detectProjectTemplate(projectDir) || 'custom';
27
+ let config = ProjectConfigManager.getDefaultConfigTemplate(template);
28
+
29
+ // Smart Init: 现实检查,防止默认配置依赖的 BasePage/BaseViewModel 在项目中不存在
30
+ try {
31
+ const pageConfig = config.generators && config.generators.page;
32
+ if (pageConfig && pageConfig.withBasePage) {
33
+ const possiblePaths = [
34
+ join(projectDir, 'lib/base/base_page.dart'),
35
+ join(projectDir, 'lib/core/base/base_page.dart'),
36
+ join(projectDir, 'lib/core/presentation/base/base_page.dart')
37
+ ];
38
+
39
+ if (pageConfig.basePageImport && pageConfig.basePageImport.startsWith('lib/')) {
40
+ possiblePaths.unshift(join(projectDir, pageConfig.basePageImport));
41
+ }
42
+
43
+ const basePageExists = possiblePaths.some(p => existsSync(p));
44
+
45
+ if (!basePageExists) {
46
+ console.log(chalk.yellow('Smart Init: 未检测到 BasePage 文件,降级为原生模式 (Native Mode)'));
47
+ if (config.generators && config.generators.page) {
48
+ config.generators.page.withBasePage = false;
49
+ config.generators.page.withViewModel = false;
50
+ }
51
+ if (config.generators && config.generators.viewModel) {
52
+ config.generators.viewModel.withBaseViewModel = false;
53
+ }
54
+ }
55
+ }
56
+ } catch (e) {
57
+ console.log(chalk.gray(`Smart Init 检查失败: ${e.message || e}`));
58
+ }
59
+
60
+ writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
61
+
62
+ console.log(chalk.green(`✓ 配置文件已生成: ${configPath}`));
63
+ console.log(chalk.cyan(`ℹ️ 基于检测到的模板类型: ${template}`));
64
+ console.log(chalk.gray('你可以编辑 .flu-cli.json 来自定义生成规则'));
65
+
66
+ } catch (error) {
67
+ console.error(chalk.red(`初始化配置失败: ${error.message}`));
68
+ process.exit(1);
69
+ }
70
+ }
@@ -9,10 +9,11 @@ import chalk from 'chalk';
9
9
  import { join, dirname } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { existsSync } from 'fs';
12
- import { getTemplate, isValidTemplate } from '../../config/templates.js';
12
+ import { getTemplate, isValidTemplate, getAllTemplates } from '../../config/templates.js';
13
13
  import { selectTemplateWithEnquirer } from '../utils/templateSelectorEnquirer.js';
14
14
  import { cloneOrUpdateTemplate } from '../templates/templateManager.js';
15
- import { getAuthorName, saveAuthorName, findCustomTemplateById, saveDefaultTemplate } from '../utils/config.js';
15
+ import { getAuthorName, saveAuthorName, saveDefaultTemplate } from '../utils/config.js';
16
+ import { ConfigManager } from '@flu-cli/core';
16
17
  import { copyTemplate, replaceVariables, copyCustomTemplate, ensurePubspecName, mergePubspecFromTemplate, cleanupTemplateFiles } from '../templates/templateCopier.js';
17
18
  import { runFlutterCreate } from '../utils/flutterHelper.js';
18
19
  import { ProjectGenerator } from '../generators/project_generator.js';
@@ -150,7 +151,7 @@ export async function newProjectWithClack (projectName, options) {
150
151
  // Step 7: 显示信息并确认
151
152
  const templateDisplay = templateSelection?.kind === 'builtin'
152
153
  ? getTemplate(templateSelection.name).displayName
153
- : (findCustomTemplateById(templateSelection.id)?.name || '自定义模板');
154
+ : (ConfigManager.getInstance().getTemplate(templateSelection.id)?.name || '自定义模板');
154
155
 
155
156
  console.log('');
156
157
  console.log(chalk.cyan('📋 项目信息确认:'));
@@ -288,7 +289,9 @@ async function createProject (projectName, templateSelection, projectDir, projec
288
289
  sourceLocalPath
289
290
  );
290
291
  } else if (templateSelection?.kind === 'custom') {
291
- const ct = findCustomTemplateById(templateSelection.id);
292
+ const configManager = ConfigManager.getInstance();
293
+ const ct = configManager.getTemplate(templateSelection.id);
294
+
292
295
  if (!ct) {
293
296
  s.stop('模板准备失败');
294
297
  throw new Error('未找到自定义模板');
@@ -300,12 +303,12 @@ async function createProject (projectName, templateSelection, projectDir, projec
300
303
  'main',
301
304
  !options.cache,
302
305
  true,
303
- ct.pathOrUrl
306
+ ct.path || ct.url // 兼容旧配置
304
307
  );
305
308
  } else {
306
309
  templatePath = await cloneOrUpdateTemplate(
307
310
  ct.id,
308
- ct.pathOrUrl,
311
+ ct.url,
309
312
  ct.branch || 'main',
310
313
  !options.cache,
311
314
  false