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
package/index.js CHANGED
@@ -2,36 +2,37 @@
2
2
 
3
3
  /**
4
4
  * flu-cli v2.0
5
- * Flutter MVVM 脚手架工具
6
- *
5
+ * Flutter 全流程效率工具链
6
+ *
7
7
  * 支持多种架构模板 and 代码生成功能
8
8
  */
9
9
 
10
10
  // ========== 加载 .env 文件 ==========
11
- import { readFileSync, existsSync } from 'fs';
12
- import { resolve } from 'path';
11
+ import { readFileSync, writeFileSync, existsSync } from 'fs'
12
+ import { dirname, resolve, isAbsolute } from 'path'
13
13
 
14
- function loadEnvFile () {
14
+ function loadEnvFile() {
15
15
  try {
16
- const envPath = resolve(process.cwd(), '.env');
16
+ const envPath = resolve(process.cwd(), '.env')
17
17
  if (existsSync(envPath)) {
18
- const envContent = readFileSync(envPath, 'utf8');
19
- const lines = envContent.split('\n');
18
+ const envContent = readFileSync(envPath, 'utf8')
19
+ const lines = envContent.split('\n')
20
+ const envDir = dirname(envPath)
20
21
 
21
22
  for (const line of lines) {
22
- const trimmed = line.trim();
23
+ const trimmed = line.trim()
23
24
  // 跳过注释和空行
24
- if (!trimmed || trimmed.startsWith('#')) continue;
25
+ if (!trimmed || trimmed.startsWith('#')) continue
25
26
 
26
- const [key, ...valueParts] = trimmed.split('=');
27
- const value = valueParts.join('=').trim();
27
+ const [key, ...valueParts] = trimmed.split('=')
28
+ const value = valueParts.join('=').trim()
28
29
 
29
30
  // 移除引号
30
- const cleanValue = value.replace(/^["']|["']$/g, '');
31
+ const cleanValue = normalizeEnvPathValue(key, value.replace(/^["']|["']$/g, ''), envDir)
31
32
 
32
33
  // 只设置未定义的环境变量
33
34
  if (!process.env[key]) {
34
- process.env[key] = cleanValue;
35
+ process.env[key] = cleanValue
35
36
  }
36
37
  }
37
38
  }
@@ -40,37 +41,58 @@ function loadEnvFile () {
40
41
  }
41
42
  }
42
43
 
43
- loadEnvFile();
44
+ function normalizeEnvPathValue(key, value, baseDir) {
45
+ if (
46
+ (key === 'FLU_CLI_LOCAL_TEMPLATES_DIR' || key === 'FLU_CLI_BUNDLED_TEMPLATES_DIR') &&
47
+ value &&
48
+ !isAbsolute(value)
49
+ ) {
50
+ return resolve(baseDir, value)
51
+ }
52
+
53
+ return value
54
+ }
44
55
 
45
- import { program } from 'commander';
46
- import chalk from 'chalk';
47
- import { fileURLToPath } from 'url';
48
- import { dirname, join } from 'path';
49
- import { newProjectWithClack } from './lib/commands/newClack.js';
50
- import { addComponent } from './lib/commands/add.js';
51
- import { listTemplates, showTemplateDetail } from './lib/commands/templates.js';
52
- import { updateTemplates, cleanCache } from './lib/commands/cache.js';
53
- import { t } from './lib/utils/i18n.js';
56
+ loadEnvFile()
57
+
58
+ import { program } from 'commander'
59
+ import chalk from 'chalk'
60
+ import { fileURLToPath } from 'url'
61
+ import { join } from 'path'
62
+ import { newProjectWithClack } from './lib/commands/newClack.js'
63
+ import { addComponent } from './lib/commands/add.js'
64
+ import {
65
+ collectTemplateDetail,
66
+ collectTemplates,
67
+ listTemplates,
68
+ showTemplateDetail,
69
+ } from './lib/commands/templates.js'
70
+ import { updateTemplates, cleanCache } from './lib/commands/cache.js'
71
+ import { t } from './lib/utils/i18n.js'
72
+ import {
73
+ buildJsonEnvelope,
74
+ createNullLogger,
75
+ printJsonEnvelope,
76
+ runMuted,
77
+ } from './lib/utils/json-output.js'
54
78
 
55
79
  // 获取 package.json
56
- const __dirname = dirname(fileURLToPath(import.meta.url));
57
- const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
80
+ const __dirname = dirname(fileURLToPath(import.meta.url))
81
+ const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'))
58
82
 
59
83
  // 设置版本和描述
60
- program
61
- .version(packageJson.version)
62
- .description(chalk.cyan(t('cli.description')));
84
+ program.version(packageJson.version).description(chalk.cyan(t('cli.description')))
63
85
 
64
86
  // ========== 创建项目命令(V1 已废弃) ==========
65
87
  program
66
88
  .command('create')
67
89
  .description('⚠️ 已废弃:请使用 flu new 命令')
68
90
  .action(() => {
69
- console.log(chalk.yellow('\n⚠️ create 命令已在 V2 中废弃\n'));
70
- console.log(chalk.cyan('请使用新命令:'));
71
- console.log(chalk.green(' flu new <project-name>\n'));
72
- process.exit(0);
73
- });
91
+ console.log(chalk.yellow('\n⚠️ create 命令已废弃\n'))
92
+ console.log(chalk.cyan('请使用新命令:'))
93
+ console.log(chalk.green(' flu new <project-name>\n'))
94
+ process.exit(0)
95
+ })
74
96
 
75
97
  // ========== 新建项目命令(现代化交互式体验) ==========
76
98
  program
@@ -83,12 +105,46 @@ program
83
105
  .option('-p, --package <name>', '指定包名 (e.g., com.example.myapp)')
84
106
  .option('-a, --author <name>', '指定作者名称')
85
107
  .option('--no-cache', t('cmd.new.opt.no_cache'))
86
- .option('--no-network', '不包含网络层(默认包含)')
108
+ .option('--network', '包含网络层')
109
+ .option('--no-network', '不包含网络层(lite/modular/clean 默认包含,native 默认不包含)')
110
+ .option('--auth', '启用 auth capability')
111
+ .option('--helpers <list>', '启用 helpers,逗号分隔: payment,webview,permission,imagePicker')
112
+ .option(
113
+ '--examples <list>',
114
+ '创建期示例,逗号分隔: networkGallery,webview,permission,image-picker,payment',
115
+ )
116
+ .option('--platforms <list>', '目标平台,逗号分隔: android,ios,web,harmony')
117
+ .option('--architecture-mode <mode>', '项目架构模式: mvvm,native')
118
+ .option('--composition-profile <profile>', '生成风格: base,base_mixins,pure,pure_mixins')
119
+ .option('--enable-mixin-options [value]', '是否准备 mixin 候选目录: true,false')
120
+ .option('--serialization <strategy>', '序列化策略: manual,json_serializable,freezed')
121
+ .option('--serialization-build-runner', '为 serialization 启用 build_runner')
122
+ .option('--flutter-sdk <mode>', 'Flutter SDK 模式: system,fvm,custom')
123
+ .option('--flutter-sdk-version <version>', 'FVM Flutter SDK 版本')
124
+ .option('--flutter-bin <path>', 'custom Flutter SDK 的 flutter 可执行文件路径')
87
125
  .option('--remote', t('cmd.new.opt.remote'))
88
- .option('--flutter-template <type>', 'Flutter official template (app, module, package, plugin)', 'app')
89
- .action((projectName, options) => {
90
- newProjectWithClack(projectName, options);
91
- });
126
+ .option(
127
+ '--json',
128
+ '以 JSON 输出创建结果(便于 IDE/脚本消费;需提供 project-name 与 --template)',
129
+ false,
130
+ )
131
+ .option(
132
+ '--flutter-template <type>',
133
+ 'Flutter official template (app, module, package, plugin)',
134
+ 'app',
135
+ )
136
+ .action(async (projectName, options) => {
137
+ if (options.json) {
138
+ const r = await runMuted(() => newProjectWithClack(projectName, options))
139
+ if (!r.ok) {
140
+ printJsonEnvelope(buildJsonEnvelope('new', false, [r.error], null))
141
+ process.exit(1)
142
+ }
143
+ printJsonEnvelope(buildJsonEnvelope('new', true, [], r.result))
144
+ return
145
+ }
146
+ await newProjectWithClack(projectName, options)
147
+ })
92
148
 
93
149
  // ========== 添加组件命令 ==========
94
150
  program
@@ -100,24 +156,51 @@ program
100
156
  .option('--stateless', t('cmd.add.opt.stateless'))
101
157
  .option('--list-page', t('cmd.add.opt.list_page'))
102
158
  .option('--no-vm', t('cmd.add.opt.no_vm'))
159
+ .option('--mixins <list>', '要附加的 mixin 名称,逗号分隔')
103
160
  .option('--json <file>', t('cmd.add.opt.json'))
161
+ .option('--out-json', '以 JSON 输出生成结果(不影响 --json <file> 作为输入参数)', false)
104
162
  .option('--list', t('cmd.add.opt.list'))
105
- .action((type, name, options) => {
106
- addComponent(type, name, options);
107
- });
163
+ .action(async (type, name, options) => {
164
+ if (options.outJson) {
165
+ const r = await runMuted(() => addComponent(type, name, options))
166
+ const ok = r.ok && r.result?.ok === true
167
+ const diagnostics = r.ok ? (r.result?.diagnostics ?? []) : [r.error]
168
+ printJsonEnvelope(buildJsonEnvelope('add', ok, diagnostics, r.ok ? r.result : null))
169
+ if (!ok) process.exit(1)
170
+ return
171
+ }
172
+ await addComponent(type, name, options)
173
+ })
108
174
 
109
175
  // ========== 模板管理命令 ==========
110
176
  program
111
177
  .command('templates [template-name]')
112
178
  .alias('t')
113
179
  .description(t('cmd.templates.desc'))
114
- .action((templateName) => {
180
+ .option('--json', '以 JSON 输出模板信息(便于 IDE/脚本消费)', false)
181
+ .action((templateName, options) => {
182
+ if (options.json) {
183
+ if (templateName) {
184
+ const detail = collectTemplateDetail(templateName)
185
+ if (!detail) {
186
+ const diagnostics = [`模板 "${templateName}" 不存在`]
187
+ printJsonEnvelope(buildJsonEnvelope('templates.detail', false, diagnostics, null))
188
+ process.exit(1)
189
+ }
190
+ printJsonEnvelope(buildJsonEnvelope('templates.detail', true, [], detail))
191
+ return
192
+ }
193
+ const list = collectTemplates()
194
+ printJsonEnvelope(buildJsonEnvelope('templates.list', true, [], list))
195
+ return
196
+ }
197
+
115
198
  if (templateName) {
116
- showTemplateDetail(templateName);
199
+ showTemplateDetail(templateName)
117
200
  } else {
118
- listTemplates();
201
+ listTemplates()
119
202
  }
120
- });
203
+ })
121
204
 
122
205
  // ========== 缓存管理命令 ==========
123
206
  program
@@ -126,62 +209,335 @@ program
126
209
  .alias('u')
127
210
  .description(t('cmd.update.desc'))
128
211
  .option('--force', t('cmd.update.opt.force'))
129
- .action((templateName, options) => {
130
- updateTemplates(templateName, options);
131
- });
212
+ .option('--json', '以 JSON 输出更新结果(便于 IDE/脚本消费)', false)
213
+ .action(async (templateName, options) => {
214
+ if (options.json) {
215
+ const r = await runMuted(() => updateTemplates(templateName, options))
216
+ const diagnostics = r.ok ? [] : [r.error]
217
+ printJsonEnvelope(buildJsonEnvelope('update-templates', r.ok, diagnostics, null))
218
+ if (!r.ok) process.exit(1)
219
+ return
220
+ }
221
+ await updateTemplates(templateName, options)
222
+ })
132
223
 
133
224
  program
134
225
  .command('cache <action>')
135
226
  .alias('c')
136
227
  .description(t('cmd.cache.desc'))
137
- .action((action) => {
228
+ .option('--json', '以 JSON 输出执行结果(便于 IDE/脚本消费)', false)
229
+ .action(async (action, options) => {
230
+ if (options.json) {
231
+ if (action !== 'clean') {
232
+ const diagnostics = [`未知的操作: ${action}`, '支持的操作: clean']
233
+ printJsonEnvelope(buildJsonEnvelope('cache', false, diagnostics, { action }))
234
+ process.exit(1)
235
+ }
236
+ const r = await runMuted(() => cleanCache())
237
+ const diagnostics = r.ok ? [] : [r.error]
238
+ printJsonEnvelope(buildJsonEnvelope('cache.clean', r.ok, diagnostics, null))
239
+ if (!r.ok) process.exit(1)
240
+ return
241
+ }
138
242
  if (action === 'clean') {
139
- cleanCache();
243
+ cleanCache()
140
244
  } else {
141
- console.log(chalk.red(`未知的操作: ${action}`));
142
- console.log(chalk.yellow('支持的操作: clean'));
245
+ console.log(chalk.red(`未知的操作: ${action}`))
246
+ console.log(chalk.yellow('支持的操作: clean'))
143
247
  }
144
- });
248
+ })
145
249
 
146
250
  // ========== 应用资源管理 ==========
147
- import { configAssets } from './lib/commands/assets.js';
251
+ import { configAssets } from './lib/commands/assets.js'
148
252
 
149
253
  program
150
254
  .command('assets')
151
255
  .alias('as')
152
256
  .description(t('cmd.assets.desc'))
153
257
  .option('-d, --dir <path>', t('cmd.new.opt.dir'), process.cwd())
154
- .action((options) => {
155
- configAssets(options);
156
- });
258
+ .option('--json', '以 JSON 输出执行结果(便于 IDE/脚本消费)', false)
259
+ .action(async (options) => {
260
+ if (options.json) {
261
+ const r = await runMuted(() => configAssets(options))
262
+ const diagnostics = r.ok ? [] : [r.error]
263
+ printJsonEnvelope(buildJsonEnvelope('assets', r.ok, diagnostics, null))
264
+ if (!r.ok) process.exit(1)
265
+ return
266
+ }
267
+ await configAssets(options)
268
+ })
157
269
 
158
270
  // ========== 代码片段管理 ==========
159
- import { syncSnippets } from './lib/commands/snippets.js';
271
+ import { syncSnippets } from './lib/commands/snippets.js'
160
272
 
161
273
  program
162
274
  .command('sync-snippets')
163
275
  .alias('sync')
164
276
  .description(t('cmd.sync.desc'))
165
- .action(() => {
166
- syncSnippets();
167
- });
277
+ .option('--json', '以 JSON 输出执行结果(便于 IDE/脚本消费)', false)
278
+ .action(async (options) => {
279
+ if (options.json) {
280
+ const r = await runMuted(() => syncSnippets())
281
+ const diagnostics = r.ok ? [] : [r.error]
282
+ printJsonEnvelope(buildJsonEnvelope('sync-snippets', r.ok, diagnostics, null))
283
+ if (!r.ok) process.exit(1)
284
+ return
285
+ }
286
+ await syncSnippets()
287
+ })
288
+
289
+ // ========== Blueprint 批量生成 ==========
290
+ import { generateFromBlueprint, parseOpenApiToBlueprint } from 'flu-cli-core'
291
+ import yaml from 'js-yaml'
292
+ import http from 'http'
293
+ import https from 'https'
294
+
295
+ const bpProgram = program
296
+ .command('blueprint')
297
+ .alias('bp')
298
+ .description('🗺️ 结构蓝图:生成骨架 / 从 OpenAPI 导入(可选)')
299
+
300
+ // 子命令:flu blueprint gen(默认行为)
301
+ bpProgram
302
+ .command('gen')
303
+ .description('从 flu-blueprint.yaml 批量生成页面、Service、Model')
304
+ .option('-d, --dir <path>', 'Flutter 项目根目录(含 .flu-cli.json)', process.cwd())
305
+ .option('-f, --file <path>', 'Blueprint 文件路径(默认 <dir>/flu-blueprint.yaml)')
306
+ .option('--json', '以 JSON 输出生成结果(便于 IDE/脚本消费)', false)
307
+ .action(async (options) => {
308
+ if (options.json) {
309
+ const nullLogger = createNullLogger()
310
+ const r = await runMuted(() => generateFromBlueprint(options.dir, options.file, nullLogger))
311
+ if (!r.ok) {
312
+ printJsonEnvelope(buildJsonEnvelope('blueprint.gen', false, [r.error], null))
313
+ process.exit(1)
314
+ }
315
+ const result = r.result
316
+ printJsonEnvelope(buildJsonEnvelope('blueprint.gen', result.ok, result.diagnostics, result))
317
+ if (!result.ok) process.exit(1)
318
+ return
319
+ }
168
320
 
169
- // ========== 模板管理命令 (New) ==========
170
- import { listAllTemplates, addTemplate, removeTemplate } from './lib/commands/template.js';
321
+ const result = await generateFromBlueprint(options.dir, options.file)
322
+ if (!result.ok) {
323
+ result.diagnostics.forEach((d) => console.error(chalk.red(d)))
324
+ process.exit(1)
325
+ }
326
+ })
327
+
328
+ // 子命令:flu blueprint import <openapi>(可选增强)
329
+ bpProgram
330
+ .command('import <openapi>')
331
+ .description(
332
+ '从 OpenAPI 3 文件/URL 导入并写入 flu-blueprint.yaml(不影响已有手写内容,默认合并)',
333
+ )
334
+ .option('-d, --dir <path>', 'Flutter 项目根目录', process.cwd())
335
+ .option('--replace', '替换整个 Blueprint(默认为合并)', false)
336
+ .option('--json', '以 JSON 输出导入结果(便于 IDE/脚本消费)', false)
337
+ .action(async (openapi, options) => {
338
+ const asJson = options.json === true
339
+ const report = {
340
+ ok: false,
341
+ openapi,
342
+ blueprintPath: `${options.dir}/flu-blueprint.yaml`,
343
+ mode: options.replace ? 'replace' : 'merge',
344
+ source: openapi.startsWith('http://') || openapi.startsWith('https://') ? 'url' : 'file',
345
+ diagnostics: [],
346
+ featureCount: 0,
347
+ endpointCount: 0,
348
+ }
171
349
 
172
- const templateCmd = program.command('template');
173
- templateCmd
174
- .description(t('cmd.template.desc'))
175
- .action(() => {
176
- listAllTemplates();
177
- });
350
+ // 读取 OpenAPI 内容(本地文件或 URL)
351
+ let raw = ''
352
+ try {
353
+ if (openapi.startsWith('http://') || openapi.startsWith('https://')) {
354
+ raw = await _fetchUrl(openapi)
355
+ if (!asJson) console.log(chalk.gray(`📡 已从 URL 获取 OpenAPI 文档`))
356
+ } else {
357
+ if (!existsSync(openapi)) {
358
+ report.diagnostics.push(`文件不存在:${openapi}`)
359
+ if (asJson) {
360
+ printJsonEnvelope(
361
+ buildJsonEnvelope('blueprint.import', false, report.diagnostics, report),
362
+ )
363
+ } else {
364
+ console.error(chalk.red(`❌ 文件不存在:${openapi}`))
365
+ }
366
+ process.exit(1)
367
+ }
368
+ raw = readFileSync(openapi, 'utf8')
369
+ if (!asJson) console.log(chalk.gray(`📄 已读取 OpenAPI 文件:${openapi}`))
370
+ }
371
+ } catch (e) {
372
+ report.diagnostics.push(`读取失败:${e.message}`)
373
+ if (asJson) {
374
+ printJsonEnvelope(buildJsonEnvelope('blueprint.import', false, report.diagnostics, report))
375
+ } else {
376
+ console.error(chalk.red(`❌ 读取失败:${e.message}`))
377
+ }
378
+ process.exit(1)
379
+ }
380
+
381
+ // 解析 OpenAPI → Blueprint
382
+ let imported
383
+ try {
384
+ imported = parseOpenApiToBlueprint(raw)
385
+ } catch (e) {
386
+ report.diagnostics.push(`OpenAPI 解析失败:${e.message}`)
387
+ if (asJson) {
388
+ printJsonEnvelope(buildJsonEnvelope('blueprint.import', false, report.diagnostics, report))
389
+ } else {
390
+ console.error(chalk.red(`❌ OpenAPI 解析失败:${e.message}`))
391
+ }
392
+ process.exit(1)
393
+ }
394
+
395
+ const bpPath = `${options.dir}/flu-blueprint.yaml`
396
+ let final = imported
397
+
398
+ // 合并模式:若已存在 blueprint,按 feature.name 合并
399
+ if (!options.replace && existsSync(bpPath)) {
400
+ try {
401
+ const existing = yaml.load(readFileSync(bpPath, 'utf8'))
402
+ final = _mergeBlueprints(existing, imported)
403
+ if (!asJson) console.log(chalk.gray('🔀 合并模式:已与现有 flu-blueprint.yaml 合并'))
404
+ } catch {
405
+ report.diagnostics.push('现有 blueprint 解析失败,将使用导入结果覆盖')
406
+ if (!asJson) console.log(chalk.yellow('⚠️ 现有 blueprint 解析失败,将使用导入结果覆盖'))
407
+ }
408
+ } else if (options.replace) {
409
+ if (!asJson) console.log(chalk.gray('♻️ 替换模式:将覆盖现有 flu-blueprint.yaml'))
410
+ }
411
+
412
+ writeFileSync(
413
+ bpPath,
414
+ yaml.dump(final, { indent: 2, lineWidth: 120, noCompatMode: true }),
415
+ 'utf8',
416
+ )
417
+
418
+ const featureCount = final.features?.length ?? 0
419
+ const endpointCount = final.features?.reduce((s, f) => s + (f.endpoints?.length ?? 0), 0) ?? 0
420
+ report.ok = true
421
+ report.featureCount = featureCount
422
+ report.endpointCount = endpointCount
423
+ report.blueprintPath = bpPath
424
+
425
+ if (asJson) {
426
+ printJsonEnvelope(buildJsonEnvelope('blueprint.import', true, report.diagnostics, report))
427
+ } else {
428
+ console.log(chalk.green(`✅ flu-blueprint.yaml 已写入`))
429
+ console.log(chalk.gray(` 功能模块:${featureCount},接口:${endpointCount}`))
430
+ console.log(chalk.gray(` 下一步:flu blueprint gen 或在 VSCode 「结构蓝图」面板预览并生成`))
431
+ }
432
+ })
433
+
434
+ // 保持向后兼容:flu blueprint(不带子命令)= flu blueprint gen
435
+ bpProgram.action(async () => {
436
+ console.log(chalk.yellow('提示:flu blueprint 已支持子命令,直接运行等同于 flu blueprint gen'))
437
+ console.log(chalk.gray(' flu blueprint gen 生成骨架'))
438
+ console.log(chalk.gray(' flu blueprint import 从 OpenAPI 导入'))
439
+ bpProgram.help()
440
+ })
441
+
442
+ /** 简单按 feature.name 合并两个 blueprint */
443
+ function _mergeBlueprints(base, incoming) {
444
+ const featureMap = new Map((base.features ?? []).map((f) => [f.name, { ...f }]))
445
+ for (const f of incoming.features ?? []) {
446
+ if (featureMap.has(f.name)) {
447
+ const existing = featureMap.get(f.name)
448
+ // 合并 endpoints(按名称去重)
449
+ const existingNames = new Set((existing.endpoints ?? []).map((e) => e.name))
450
+ existing.endpoints = [
451
+ ...(existing.endpoints ?? []),
452
+ ...(f.endpoints ?? []).filter((e) => !existingNames.has(e.name)),
453
+ ]
454
+ } else {
455
+ featureMap.set(f.name, { ...f })
456
+ }
457
+ }
458
+ return { version: '1', features: Array.from(featureMap.values()) }
459
+ }
460
+
461
+ /** 从 URL 获取文本内容 */
462
+ function _fetchUrl(url) {
463
+ return new Promise((resolve, reject) => {
464
+ const client = url.startsWith('https') ? https : http
465
+ client
466
+ .get(url, (res) => {
467
+ let data = ''
468
+ res.on('data', (chunk) => (data += chunk))
469
+ res.on('end', () => resolve(data))
470
+ })
471
+ .on('error', reject)
472
+ })
473
+ }
474
+
475
+ // ========== AI Skill 规则同步 ==========
476
+ import { syncRules, syncRulesWithReport } from 'flu-cli-core'
477
+ import { initAiBaseCli } from './lib/commands/init-ai-base.js'
478
+
479
+ program
480
+ .command('sync-rules')
481
+ .alias('sr')
482
+ .description('🤖 从 .flu-cli.json 生成 AI Skill 规则文件(Cursor / Claude / AI_RULES.md)')
483
+ .option('-d, --dir <path>', '指定 Flutter 项目根目录', process.cwd())
484
+ .option('--json', '以 JSON 输出同步结果(便于 IDE/脚本消费)', false)
485
+ .action(async (options) => {
486
+ if (options.json) {
487
+ const nullLogger = createNullLogger()
488
+ const report = await syncRulesWithReport(options.dir, { logger: nullLogger })
489
+ const diagnostics = [...(report.diagnostics ?? [])]
490
+ if (report.error) diagnostics.push(report.error)
491
+ printJsonEnvelope(buildJsonEnvelope('sync-rules', report.ok, diagnostics, report))
492
+ if (!report.ok) process.exit(1)
493
+ return
494
+ }
495
+ const ok = await syncRules(options.dir)
496
+ if (!ok) process.exit(1)
497
+ })
498
+
499
+ program
500
+ .command('init-ai-base')
501
+ .alias('iab')
502
+ .description('🤖 初始化 .flu-cli.json 并同步 AI 规则(空目录或已有项目均可)')
503
+ .option('-d, --dir <path>', '目标目录', process.cwd())
504
+ .option('-p, --project-id <id>', 'projectId(小写字母开头,英文/数字/短横线)')
505
+ .option('--json', '以 JSON 输出初始化结果(便于 IDE/脚本消费)', false)
506
+ .option(
507
+ '-s, --starter <name>',
508
+ '起步方案:minimal | lite | modular | clean | native | custom',
509
+ 'minimal',
510
+ )
511
+ .option('--import <path>', 'starter=custom 时从该 JSON 文件导入;失败则兜底 minimal')
512
+ .action(async (options) => {
513
+ await initAiBaseCli(options)
514
+ })
515
+
516
+ // ========== 模板管理命令 (New) ==========
517
+ import {
518
+ listAllTemplates,
519
+ addTemplate,
520
+ removeTemplate,
521
+ collectAllTemplates,
522
+ } from './lib/commands/template.js'
523
+
524
+ const templateCmd = program.command('template')
525
+ templateCmd.description(t('cmd.template.desc')).action(() => {
526
+ listAllTemplates()
527
+ })
178
528
 
179
529
  templateCmd
180
530
  .command('list')
181
531
  .description(t('cmd.template.list.desc'))
182
- .action(() => {
183
- listAllTemplates();
184
- });
532
+ .option('--json', '以 JSON 输出模板列表(便于 IDE/脚本消费)', false)
533
+ .action((options) => {
534
+ if (options.json) {
535
+ const data = collectAllTemplates()
536
+ printJsonEnvelope(buildJsonEnvelope('template.list', true, [], data))
537
+ return
538
+ }
539
+ listAllTemplates()
540
+ })
185
541
 
186
542
  templateCmd
187
543
  .command('add <id> <source>')
@@ -191,64 +547,160 @@ templateCmd
191
547
  .option('-b, --branch <branch>', t('cmd.template.add.opt.branch'))
192
548
  .option('-d, --description <text>', t('cmd.template.add.opt.desc'))
193
549
  .option('-f, --force', t('cmd.template.add.opt.force'))
194
- .action((id, source, options) => {
195
- addTemplate(id, source, options);
196
- });
550
+ .option('--json', '以 JSON 输出执行结果(便于 IDE/脚本消费)', false)
551
+ .action(async (id, source, options) => {
552
+ if (options.json) {
553
+ const r = await runMuted(() => addTemplate(id, source, options))
554
+ const diagnostics = r.ok ? [] : [r.error]
555
+ printJsonEnvelope(buildJsonEnvelope('template.add', r.ok, diagnostics, { id, source }))
556
+ if (!r.ok) process.exit(1)
557
+ return
558
+ }
559
+ addTemplate(id, source, options)
560
+ })
197
561
 
198
562
  templateCmd
199
563
  .command('remove <id>')
200
564
  .description(t('cmd.template.remove.desc'))
201
- .action((id) => {
202
- removeTemplate(id);
203
- });
565
+ .option('--json', '以 JSON 输出执行结果(便于 IDE/脚本消费)', false)
566
+ .action(async (id, options) => {
567
+ if (options.json) {
568
+ const r = await runMuted(() => removeTemplate(id))
569
+ const diagnostics = r.ok ? [] : [r.error]
570
+ printJsonEnvelope(buildJsonEnvelope('template.remove', r.ok, diagnostics, { id }))
571
+ if (!r.ok) process.exit(1)
572
+ return
573
+ }
574
+ removeTemplate(id)
575
+ })
204
576
 
205
577
  // ========== 自动补全 ==========
206
- import { completion } from './lib/commands/completion.js';
578
+ import { completion, getCompletionScript } from './lib/commands/completion.js'
207
579
 
208
580
  program
209
581
  .command('completion')
210
582
  .alias('comp')
211
583
  .description(t('cmd.completion.desc'))
212
- .action(() => {
213
- completion();
214
- });
584
+ .option('--json', '以 JSON 输出补全脚本(便于 IDE/脚本消费)', false)
585
+ .action((options) => {
586
+ if (options.json) {
587
+ printJsonEnvelope(
588
+ buildJsonEnvelope('completion', true, [], { script: getCompletionScript() }),
589
+ )
590
+ return
591
+ }
592
+ completion()
593
+ })
215
594
 
216
595
  // ========== 配置管理命令 ==========
217
- import { initConfig, setConfig, getConfig } from './lib/commands/config.js';
218
-
219
- const configCmd = program.command('config');
220
- configCmd
221
- .description(t('cmd.config.desc'))
222
- .action(() => {
223
- configCmd.help();
224
- });
596
+ import {
597
+ initConfig,
598
+ setConfig,
599
+ getConfig,
600
+ initConfigWithReport,
601
+ setConfigWithReport,
602
+ getConfigWithReport,
603
+ listGeneratorSelectableOptionsWithReport,
604
+ } from './lib/commands/config.js'
605
+
606
+ const configCmd = program.command('config')
607
+ configCmd.description(t('cmd.config.desc')).action(() => {
608
+ configCmd.help()
609
+ })
225
610
 
226
611
  configCmd
227
612
  .command('init')
228
613
  .description(t('cmd.config.init.desc'))
229
614
  .option('-d, --dir <path>', t('cmd.new.opt.dir'), process.cwd())
230
615
  .option('-f, --force', '覆盖已存在的配置文件')
616
+ .option('--json', '以 JSON 输出执行结果(便于 IDE/脚本消费)', false)
231
617
  .action((options) => {
232
- initConfig(options);
233
- });
618
+ if (options.json) {
619
+ const report = initConfigWithReport(options)
620
+ printJsonEnvelope(buildJsonEnvelope('config.init', report.ok, report.diagnostics, report))
621
+ if (!report.ok) process.exit(1)
622
+ return
623
+ }
624
+ initConfig(options)
625
+ })
234
626
 
235
627
  configCmd
236
628
  .command('set <key> <value>')
237
629
  .description('Set global configuration (e.g., locale)')
238
- .action((key, value) => {
239
- setConfig(key, value);
240
- });
630
+ .option('--json', '以 JSON 输出执行结果(便于 IDE/脚本消费)', false)
631
+ .action((key, value, options) => {
632
+ if (options.json) {
633
+ const report = setConfigWithReport(key, value)
634
+ printJsonEnvelope(buildJsonEnvelope('config.set', report.ok, report.diagnostics, report))
635
+ if (!report.ok) process.exit(1)
636
+ return
637
+ }
638
+ setConfig(key, value)
639
+ })
241
640
 
242
641
  configCmd
243
642
  .command('get [key]')
244
643
  .description('Get global configuration')
245
- .action((key) => {
246
- getConfig(key);
247
- });
644
+ .option('--json', '以 JSON 输出执行结果(便于 IDE/脚本消费)', false)
645
+ .action((key, options) => {
646
+ if (options.json) {
647
+ const report = getConfigWithReport(key)
648
+ printJsonEnvelope(buildJsonEnvelope('config.get', report.ok, report.diagnostics, report))
649
+ if (!report.ok) process.exit(1)
650
+ return
651
+ }
652
+ getConfig(key)
653
+ })
654
+
655
+ configCmd
656
+ .command('options')
657
+ .description('列出某类生成器的可选 Base/Mixin(给 Blueprint/IDE 下拉使用)')
658
+ .option('-d, --dir <path>', 'Flutter 项目根目录(含 .flu-cli.json)', process.cwd())
659
+ .option(
660
+ '-t, --target <type>',
661
+ '生成目标:page | viewModel | widget | model | component | service',
662
+ 'page',
663
+ )
664
+ .option('--json', '以 JSON 输出执行结果(便于 IDE/脚本消费)', false)
665
+ .action((options) => {
666
+ if (options.json) {
667
+ const r = listGeneratorSelectableOptionsWithReport(options)
668
+ printJsonEnvelope(buildJsonEnvelope('config.options', r.ok, r.diagnostics, r.report))
669
+ if (!r.ok) process.exit(1)
670
+ return
671
+ }
672
+ const r = listGeneratorSelectableOptionsWithReport(options)
673
+ if (!r.ok) {
674
+ console.log(chalk.red(r.diagnostics.join('\n')))
675
+ process.exit(1)
676
+ }
677
+ console.log(JSON.stringify(r.report, null, 2))
678
+ })
679
+
680
+ // ========== 上传发布命令 ==========
681
+ import { uploadCommand } from './lib/commands/upload.js'
682
+
683
+ program
684
+ .command('upload')
685
+ .description(t('cmd.upload.desc') || '打包并上传应用')
686
+ .option('--only-build', '仅执行构建,不上传')
687
+ .option('--only-upload', '仅执行上传 (需指定文件或自动查找)')
688
+ .option('--file <path>', '指定要上传的文件路径')
689
+ .option('--platform <items>', '指定发布平台 (逗号分隔, e.g. pgyer,huawei)')
690
+ .option('--desc <text>', '更新说明 (Changelog)')
691
+ .option('--json', '以 JSON 输出执行结果(目前仅返回错误信息)', false)
692
+ .action((options) => {
693
+ if (options.json) {
694
+ const diagnostics = ['upload 暂不支持 --json 输出(交互式流程),请先使用非 json 模式运行']
695
+ printJsonEnvelope(buildJsonEnvelope('upload', false, diagnostics, null))
696
+ process.exit(1)
697
+ }
698
+ uploadCommand(options)
699
+ })
248
700
 
249
- program.parse(process.argv);
701
+ program.parse(process.argv)
250
702
 
251
703
  if (process.argv.length === 2) {
252
- console.log(chalk.bold.cyan('\n🚀 欢迎使用 flu-cli v2.0\n'));
253
- program.help({ error: false });
704
+ console.log(chalk.bold.cyan('\n🚀 欢迎使用 flu-cli v2.0\n'))
705
+ program.help({ error: false })
254
706
  }