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.
- package/CHANGELOG.md +23 -0
- package/README.md +17 -4
- package/config/dev.config.js +11 -11
- package/config/templates.js +10 -10
- package/index.js +554 -102
- package/lib/commands/add.js +365 -266
- package/lib/commands/assets.js +77 -78
- package/lib/commands/cache.js +29 -52
- package/lib/commands/completion.js +13 -11
- package/lib/commands/config.js +150 -44
- package/lib/commands/init-ai-base.js +89 -0
- package/lib/commands/newClack.js +269 -178
- package/lib/commands/snippets.js +58 -43
- package/lib/commands/template.js +98 -58
- package/lib/commands/templates.js +101 -57
- package/lib/commands/upload.js +313 -0
- package/lib/commands/vnext-options.js +206 -0
- package/lib/generators/model_generator.js +91 -88
- package/lib/generators/page_generator.js +100 -93
- package/lib/generators/service_generator.js +44 -39
- package/lib/generators/viewmodel_generator.js +25 -29
- package/lib/generators/widget_generator.js +30 -35
- package/lib/templates/templateCopier.js +14 -15
- package/lib/templates/templateManager.js +22 -21
- package/lib/utils/config.js +37 -20
- package/lib/utils/flutterHelper.js +2 -2
- package/lib/utils/i18n.js +3 -3
- package/lib/utils/index_updater.js +22 -23
- package/lib/utils/json-output.js +59 -0
- package/lib/utils/logger.js +17 -17
- package/lib/utils/project_detector.js +66 -66
- package/lib/utils/snippet_loader.js +21 -19
- package/lib/utils/string_helper.js +13 -13
- package/lib/utils/templateSelectorEnquirer.js +94 -108
- package/locales/en-US.json +1 -1
- package/locales/zh-CN.json +2 -2
- package/package.json +60 -57
- package/scripts/smoke-vnext-generate.mjs +1934 -0
- package/scripts/smoke-vnext-params.mjs +92 -0
- package/CLI.md +0 -513
- package/release.sh +0 -529
- package/scripts/e2e-state-tests.js +0 -116
- package/scripts/sync-base-to-templates.js +0 -108
- package/scripts/workspace-clone-all.sh +0 -101
- 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
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
import {
|
|
48
|
-
import
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
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
|
|
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('--
|
|
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(
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
130
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
155
|
-
|
|
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
|
-
.
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
.
|
|
183
|
-
|
|
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
|
-
.
|
|
195
|
-
|
|
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
|
-
.
|
|
202
|
-
|
|
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
|
-
.
|
|
213
|
-
|
|
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 {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
239
|
-
|
|
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
|
-
.
|
|
246
|
-
|
|
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
|
}
|