befly 3.4.6 → 3.4.8

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/bin/index.ts CHANGED
@@ -3,17 +3,43 @@
3
3
  * Befly CLI - 命令行工具入口
4
4
  * 为 Befly 框架提供项目管理和脚本执行功能
5
5
  */
6
+
7
+ /**
8
+ * 环境启动器
9
+ * 必须在任何模块导入前执行
10
+ * 父进程会在这里启动子进程并退出
11
+ * 只有子进程会继续执行后面的代码
12
+ */
13
+ import { launch } from './launcher.js';
14
+ await launch(import.meta.path);
15
+
16
+ /**
17
+ * 以下代码只在子进程中执行
18
+ * 此时环境变量已正确加载
19
+ */
6
20
  import { Command } from 'commander';
7
- import { scriptCommand } from '../commands/script.js';
8
21
  import { devCommand } from '../commands/dev.js';
9
22
  import { buildCommand } from '../commands/build.js';
10
23
  import { startCommand } from '../commands/start.js';
11
24
  import { syncDbCommand } from '../commands/syncDb.js';
12
- import { addonCommand } from '../commands/addon.js';
13
25
  import { syncApiCommand } from '../commands/syncApi.js';
14
26
  import { syncMenuCommand } from '../commands/syncMenu.js';
15
27
  import { syncDevCommand } from '../commands/syncDev.js';
16
28
  import { Logger } from '../lib/logger.js';
29
+ import { join } from 'pathe';
30
+
31
+ /**
32
+ * 读取 package.json 版本号
33
+ */
34
+ function getVersion(): string {
35
+ try {
36
+ const pkgPath = join(import.meta.dir, '..', 'package.json');
37
+ const pkg = require(pkgPath);
38
+ return pkg.version || '0.0.0';
39
+ } catch (error) {
40
+ return '0.0.0';
41
+ }
42
+ }
17
43
 
18
44
  /**
19
45
  * Bun 版本要求
@@ -95,40 +121,38 @@ checkBunVersion();
95
121
 
96
122
  const program = new Command();
97
123
 
98
- program.name('befly').description('Befly CLI - 为 Befly 框架提供命令行工具').version('3.0.0');
124
+ program.name('befly').description('Befly CLI - 为 Befly 框架提供命令行工具').version(getVersion());
99
125
 
100
- // script 命令 - 执行脚本
101
- program.command('script').description('列出并执行 befly 脚本').option('--dry-run', '预演模式,只显示不执行', false).action(scriptCommand);
126
+ /**
127
+ * 包装命令处理函数,在执行前打印环境
128
+ */
129
+ function wrapCommand<T extends (...args: any[]) => any>(fn: T): T {
130
+ return (async (...args: any[]) => {
131
+ Logger.printEnv();
132
+ return await fn(...args);
133
+ }) as T;
134
+ }
102
135
 
103
136
  // dev 命令 - 开发服务器
104
- program.command('dev').description('启动开发服务器').option('-p, --port <number>', '端口号', '3000').option('-h, --host <string>', '主机地址', '0.0.0.0').option('--no-sync', '跳过表同步', false).option('-v, --verbose', '详细日志', false).action(devCommand);
137
+ program.command('dev').description('启动开发服务器').option('-p, --port <number>', '端口号', '3000').option('-h, --host <string>', '主机地址', '0.0.0.0').option('--no-sync', '跳过表同步', false).option('-v, --verbose', '详细日志', false).action(wrapCommand(devCommand));
105
138
 
106
139
  // build 命令 - 构建项目
107
- program.command('build').description('构建项目').option('-o, --outdir <path>', '输出目录', 'dist').option('--minify', '压缩代码', false).option('--sourcemap', '生成 sourcemap', false).action(buildCommand);
140
+ program.command('build').description('构建项目').option('-o, --outdir <path>', '输出目录', 'dist').option('--minify', '压缩代码', false).option('--sourcemap', '生成 sourcemap', false).action(wrapCommand(buildCommand));
108
141
 
109
142
  // start 命令 - 启动生产服务器
110
- program.command('start').description('启动生产服务器').option('-p, --port <number>', '端口号', '3000').option('-h, --host <string>', '主机地址', '0.0.0.0').option('-c, --cluster <instances>', '集群模式(数字或 max)').action(startCommand);
143
+ program.command('start').description('启动生产服务器').option('-p, --port <number>', '端口号', '3000').option('-h, --host <string>', '主机地址', '0.0.0.0').option('-c, --cluster <instances>', '集群模式(数字或 max)').action(wrapCommand(startCommand));
111
144
 
112
145
  // syncDb 命令 - 同步数据库
113
- program.command('syncDb').description('同步数据库表结构').option('-t, --table <name>', '指定表名').option('--dry-run', '预览模式,只显示不执行', false).action(syncDbCommand);
146
+ program.command('syncDb').description('同步数据库表结构').option('-t, --table <name>', '指定表名').option('--dry-run', '预览模式,只显示不执行', false).option('-e, --env <environment>', '指定环境 (development, production, test)').action(wrapCommand(syncDbCommand));
114
147
 
115
148
  // syncApi 命令 - 同步 API 接口
116
- program.command('syncApi').description('同步 API 接口到数据库').option('--plan', '计划模式,只显示不执行', false).action(syncApiCommand);
149
+ program.command('syncApi').description('同步 API 接口到数据库').option('--plan', '计划模式,只显示不执行', false).option('-e, --env <environment>', '指定环境 (development, production, test)').action(wrapCommand(syncApiCommand));
117
150
 
118
151
  // syncMenu 命令 - 同步菜单
119
- program.command('syncMenu').description('同步菜单配置到数据库').option('--plan', '计划模式,只显示不执行', false).action(syncMenuCommand);
152
+ program.command('syncMenu').description('同步菜单配置到数据库').option('--plan', '计划模式,只显示不执行', false).option('-e, --env <environment>', '指定环境 (development, production, test)').action(wrapCommand(syncMenuCommand));
120
153
 
121
154
  // syncDev 命令 - 同步开发者账号
122
- program.command('syncDev').description('同步开发者管理员账号').option('--plan', '计划模式,只显示不执行', false).action(syncDevCommand);
123
-
124
- // addon 命令 - 插件管理
125
- const addon = program.command('addon').description('管理 Befly 插件');
126
-
127
- addon.command('install <name>').description('安装插件').option('-s, --source <url>', '插件源地址').action(addonCommand.install);
128
-
129
- addon.command('uninstall <name>').description('卸载插件').option('--keep-data', '保留插件数据', false).action(addonCommand.uninstall);
130
-
131
- addon.command('list').description('列出已安装的插件').action(addonCommand.list);
155
+ program.command('syncDev').description('同步开发者管理员账号').option('--plan', '计划模式,只显示不执行', false).option('-e, --env <environment>', '指定环境 (development, production, test)').action(wrapCommand(syncDevCommand));
132
156
 
133
157
  // 显示建议和错误
134
158
  program.showSuggestionAfterError();
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Befly CLI 环境启动器
3
+ * 负责检测环境参数并在正确的环境中启动子进程
4
+ *
5
+ * 工作原理:
6
+ * 1. 父进程:检测 --env 参数 → 启动子进程 → 退出
7
+ * 2. 子进程:跳过启动逻辑 → 继续执行 CLI 命令
8
+ */
9
+
10
+ import { readdirSync, existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ /**
14
+ * 解析环境名称
15
+ * 支持前缀匹配(至少3个字符)
16
+ *
17
+ * @param input 用户输入的环境名称
18
+ * @returns 完整的环境名称
19
+ */
20
+ function resolveEnvName(input: string): string {
21
+ // 如果输入少于3个字符,直接返回(不做匹配)
22
+ if (input.length < 3) {
23
+ return input;
24
+ }
25
+
26
+ // 获取项目根目录的所有 .env.* 文件
27
+ const rootDir = process.cwd();
28
+ let envFiles: string[] = [];
29
+
30
+ try {
31
+ const files = readdirSync(rootDir);
32
+ envFiles = files.filter((file) => file.startsWith('.env.') && file !== '.env.example').map((file) => file.replace('.env.', ''));
33
+ } catch (error) {
34
+ // 读取失败时直接返回原始输入
35
+ return input;
36
+ }
37
+
38
+ // 精确匹配
39
+ if (envFiles.includes(input)) {
40
+ return input;
41
+ }
42
+
43
+ // 前缀匹配
44
+ const matches = envFiles.filter((env) => env.startsWith(input.toLowerCase()));
45
+
46
+ if (matches.length === 1) {
47
+ return matches[0];
48
+ }
49
+
50
+ if (matches.length > 1) {
51
+ console.error(`❌ 环境名称 "${input}" 匹配到多个环境: ${matches.join(', ')}`);
52
+ console.error('请使用更具体的名称');
53
+ process.exit(1);
54
+ }
55
+
56
+ // 未匹配到,返回原始输入(让 Bun 处理文件不存在的情况)
57
+ return input;
58
+ }
59
+
60
+ /**
61
+ * 启动环境检测和子进程
62
+ * 如果在父进程中,会启动子进程并退出(不返回)
63
+ * 如果在子进程中,直接返回(继续执行)
64
+ *
65
+ * @param entryFile CLI 入口文件的绝对路径(通常是 bin/index.ts)
66
+ */
67
+ export async function launch(entryFile: string): Promise<void> {
68
+ // 如果已经在子进程中,直接返回
69
+ if (process.env.BEFLY_SUBPROCESS) {
70
+ return;
71
+ }
72
+
73
+ // 确定环境名称
74
+ const envArgIndex = process.argv.indexOf('--env');
75
+ let envInput = 'development'; // 默认环境
76
+
77
+ if (envArgIndex !== -1 && process.argv[envArgIndex + 1]) {
78
+ // 如果指定了 --env 参数
79
+ envInput = process.argv[envArgIndex + 1];
80
+ } else if (process.env.NODE_ENV) {
81
+ // 如果设置了 NODE_ENV 环境变量
82
+ envInput = process.env.NODE_ENV;
83
+ }
84
+
85
+ // 解析环境名称(支持前缀匹配)
86
+ const envName = resolveEnvName(envInput);
87
+ const envFile = `.env.${envName}`;
88
+
89
+ // 过滤掉 --env 参数(已通过 --env-file 处理)
90
+ const filteredArgs = process.argv.slice(2).filter((arg, i, arr) => {
91
+ if (arg === '--env') return false;
92
+ if (arr[i - 1] === '--env') return false;
93
+ return true;
94
+ });
95
+
96
+ // 启动子进程,使用指定的环境文件
97
+ const proc = Bun.spawn(['bun', 'run', `--env-file=${envFile}`, entryFile, ...filteredArgs], {
98
+ cwd: process.cwd(),
99
+ stdio: ['inherit', 'inherit', 'inherit'],
100
+ env: {
101
+ ...process.env,
102
+ BEFLY_SUBPROCESS: '1', // 标记为子进程
103
+ NODE_ENV: envName // 同时设置 NODE_ENV
104
+ }
105
+ });
106
+
107
+ // 等待子进程结束并退出(父进程不会返回)
108
+ process.exit(await proc.exited);
109
+ }
package/lib/logger.ts CHANGED
@@ -211,4 +211,16 @@ export class Logger {
211
211
  static clearCache(): void {
212
212
  this.currentFiles.clear();
213
213
  }
214
+
215
+ /**
216
+ * 打印当前运行环境
217
+ * 用于命令开始时提示用户当前环境
218
+ */
219
+ static printEnv(): void {
220
+ const env = process.env.NODE_ENV || 'development';
221
+ const envColor = env === 'production' ? chalk.red : env === 'test' ? chalk.yellow : chalk.green;
222
+ console.log(chalk.gray('────────────────────────────────────────'));
223
+ console.log(chalk.bold('运行环境: ') + envColor.bold(env.toUpperCase()));
224
+ console.log(chalk.gray('────────────────────────────────────────'));
225
+ }
214
226
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.4.6",
3
+ "version": "3.4.8",
4
4
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
5
5
  "type": "module",
6
6
  "private": false,
@@ -81,5 +81,5 @@
81
81
  "ora": "^9.0.0",
82
82
  "pathe": "^2.0.3"
83
83
  },
84
- "gitHead": "157f48d9170ab671919a4999050531469072bddf"
84
+ "gitHead": "8b7e6187e0a0ed12656ed2a2ff4107e0f76a7e31"
85
85
  }
package/commands/addon.ts DELETED
@@ -1,57 +0,0 @@
1
- /**
2
- * Addon 命令 - 插件管理
3
- */
4
-
5
- import { existsSync, mkdirSync, rmSync } from 'node:fs';
6
- import { join } from 'pathe';
7
- import ora from 'ora';
8
- import { Logger } from '../lib/logger.js';
9
-
10
- export const addonCommand = {
11
- async install(name: string, options: { source?: string }) {
12
- const spinner = ora({
13
- text: `正在安装插件: ${name}`,
14
- color: 'cyan',
15
- spinner: 'dots'
16
- }).start();
17
-
18
- try {
19
- // TODO: 实现插件安装逻辑
20
- // 1. 从 source 或默认源下载插件
21
- // 2. 解压到 addons 目录
22
- // 3. 安装插件依赖
23
- // 4. 执行插件安装脚本
24
-
25
- spinner.warn(`插件安装功能开发中`);
26
- } catch (error) {
27
- spinner.fail(`插件 ${name} 安装失败`);
28
- throw error;
29
- }
30
- },
31
-
32
- async uninstall(name: string, options: { keepData: boolean }) {
33
- const spinner = ora({
34
- text: `正在卸载插件: ${name}`,
35
- color: 'cyan',
36
- spinner: 'dots'
37
- }).start();
38
-
39
- try {
40
- // TODO: 实现插件卸载逻辑
41
- // 1. 执行插件卸载脚本
42
- // 2. 删除插件文件
43
- // 3. 可选:删除插件数据
44
-
45
- spinner.warn(`插件卸载功能开发中`);
46
- } catch (error) {
47
- spinner.fail(`插件 ${name} 卸载失败`);
48
- throw error;
49
- }
50
- },
51
-
52
- async list() {
53
- // TODO: 读取已安装的插件列表
54
- console.log('已安装的插件:\n');
55
- console.log('(功能开发中)');
56
- }
57
- };
@@ -1,303 +0,0 @@
1
- /**
2
- * Script 命令 - 列出并执行项目脚本
3
- */
4
-
5
- import { join, dirname, parse, resolve, basename } from 'pathe';
6
- import { existsSync, readdirSync, statSync } from 'node:fs';
7
- import { Glob } from 'bun';
8
- import inquirer from 'inquirer';
9
- import { Logger } from '../lib/logger.js';
10
-
11
- /**
12
- * 脚本项接口
13
- */
14
- interface ScriptItem {
15
- /** 脚本名称 */
16
- name: string;
17
- /** 脚本来源 (core、project 或 addon 名称) */
18
- source: string;
19
- /** 显示名称 */
20
- displayName: string;
21
- /** 脚本完整路径 */
22
- path: string;
23
- }
24
-
25
- /**
26
- * 查找项目根目录(向上查找 package.json)
27
- */
28
- function findProjectRoot(startDir: string): string | null {
29
- let currentDir = startDir;
30
- const root = parse(currentDir).root;
31
-
32
- while (currentDir !== root) {
33
- const packagePath = join(currentDir, 'package.json');
34
- if (existsSync(packagePath)) {
35
- return currentDir;
36
- }
37
- currentDir = dirname(currentDir);
38
- }
39
-
40
- return null;
41
- }
42
-
43
- /**
44
- * 安全地列出目录下的所有 .ts 脚本文件
45
- */
46
- function safeList(dir: string): string[] {
47
- try {
48
- if (!existsSync(dir)) {
49
- return [];
50
- }
51
-
52
- const glob = new Glob('*.ts');
53
- const files = Array.from(
54
- glob.scanSync({
55
- cwd: dir,
56
- absolute: false,
57
- onlyFiles: true,
58
- dot: false
59
- })
60
- );
61
-
62
- return files.map((f) => basename(f).replace(/\.ts$/, '')).sort();
63
- } catch {
64
- return [];
65
- }
66
- }
67
-
68
- /**
69
- * 扫描 CLI 自带的 scripts
70
- */
71
- function scanCliScripts(): Array<{ scriptName: string; scriptPath: string }> {
72
- const results: Array<{ scriptName: string; scriptPath: string }> = [];
73
-
74
- try {
75
- // 获取 CLI 包的根目录(当前文件在 commands 目录下)
76
- const cliRoot = resolve(__dirname, '..');
77
- const scriptsDir = join(cliRoot, 'scripts');
78
-
79
- if (!existsSync(scriptsDir)) {
80
- return results;
81
- }
82
-
83
- const scriptFiles = readdirSync(scriptsDir);
84
-
85
- for (const file of scriptFiles) {
86
- // 只处理直接子级的 .ts 文件
87
- if (file.endsWith('.ts')) {
88
- const scriptName = file.replace(/\.ts$/, '');
89
- const scriptPath = join(scriptsDir, file);
90
-
91
- // 确保是文件而不是目录
92
- if (statSync(scriptPath).isFile()) {
93
- results.push({
94
- scriptName,
95
- scriptPath
96
- });
97
- }
98
- }
99
- }
100
- } catch {
101
- // 静默失败
102
- }
103
-
104
- return results;
105
- }
106
-
107
- /**
108
- * 扫描 node_modules/@befly-addon/* 下的 scripts
109
- */
110
- function scanAddonScripts(projectRoot: string): Array<{ addonName: string; scriptName: string; scriptPath: string }> {
111
- const results: Array<{ addonName: string; scriptName: string; scriptPath: string }> = [];
112
-
113
- try {
114
- const beflyAddonsDir = join(projectRoot, 'node_modules', '@befly-addon');
115
-
116
- if (!existsSync(beflyAddonsDir)) {
117
- return results;
118
- }
119
-
120
- // 读取 @befly-addon 目录下的所有 addon
121
- const addonDirs = readdirSync(beflyAddonsDir);
122
-
123
- for (const addonDir of addonDirs) {
124
- const scriptsDir = join(beflyAddonsDir, addonDir, 'scripts');
125
-
126
- if (!existsSync(scriptsDir)) {
127
- continue;
128
- }
129
-
130
- try {
131
- const scriptFiles = readdirSync(scriptsDir);
132
-
133
- for (const file of scriptFiles) {
134
- if (file.endsWith('.ts')) {
135
- const scriptName = file.replace(/\.ts$/, '');
136
- const scriptPath = join(scriptsDir, file);
137
-
138
- results.push({
139
- addonName: addonDir,
140
- scriptName,
141
- scriptPath
142
- });
143
- }
144
- }
145
- } catch {
146
- // 忽略无法读取的目录
147
- }
148
- }
149
- } catch (error) {
150
- // 静默失败
151
- }
152
-
153
- return results;
154
- }
155
-
156
- /**
157
- * 构建所有可用脚本的列表
158
- */
159
- function buildScriptItems(projectRoot: string): ScriptItem[] {
160
- const items: ScriptItem[] = [];
161
-
162
- // CLI 脚本
163
- const cliScripts = scanCliScripts();
164
- for (const script of cliScripts) {
165
- items.push({
166
- name: script.scriptName,
167
- source: 'cli',
168
- displayName: `[CLI] ${script.scriptName}`,
169
- path: script.scriptPath
170
- });
171
- }
172
-
173
- // 项目脚本
174
- const projectScriptsDir = join(projectRoot, 'scripts');
175
- const projectList = safeList(projectScriptsDir);
176
- for (const name of projectList) {
177
- items.push({
178
- name,
179
- source: 'project',
180
- displayName: `[Project] ${name}`,
181
- path: resolve(projectScriptsDir, `${name}.ts`)
182
- });
183
- }
184
-
185
- // Addon 脚本
186
- const addonScripts = scanAddonScripts(projectRoot);
187
- for (const addon of addonScripts) {
188
- items.push({
189
- name: addon.scriptName,
190
- source: `addon:${addon.addonName}`,
191
- displayName: `[${addon.addonName}] ${addon.scriptName}`,
192
- path: addon.scriptPath
193
- });
194
- }
195
-
196
- return items;
197
- }
198
-
199
- /**
200
- * 运行脚本
201
- */
202
- async function runScript(scriptPath: string, args: string[] = []): Promise<number> {
203
- const bunExe = process.execPath || 'bun';
204
- const child = Bun.spawn({
205
- cmd: [bunExe, scriptPath, ...args],
206
- stdio: ['inherit', 'inherit', 'inherit'],
207
- cwd: process.cwd(),
208
- env: { ...process.env }
209
- });
210
-
211
- const code = await child.exited;
212
- return code ?? 0;
213
- }
214
-
215
- /**
216
- * Script 命令处理函数
217
- */
218
- export async function scriptCommand(options: { dryRun?: boolean; plan?: boolean } = {}) {
219
- try {
220
- // 查找项目根目录
221
- const projectRoot = findProjectRoot();
222
- if (!projectRoot) {
223
- Logger.error('未找到项目根目录(缺少 package.json)');
224
- process.exit(1);
225
- }
226
-
227
- // 构建脚本列表
228
- const items = buildScriptItems(projectRoot);
229
-
230
- if (items.length === 0) {
231
- Logger.warn('没有找到可用的脚本');
232
- process.exit(0);
233
- }
234
-
235
- // 使用 inquirer 选择脚本
236
- const { selectedScript } = await inquirer.prompt([
237
- {
238
- type: 'list',
239
- name: 'selectedScript',
240
- message: '请选择要执行的脚本:',
241
- choices: items.map((item) => ({
242
- name: item.displayName,
243
- value: item
244
- })),
245
- loop: false
246
- }
247
- ]);
248
-
249
- // 询问是否添加 --plan 参数
250
- let scriptArgs: string[] = [];
251
- if (options.plan !== false) {
252
- const { addPlan } = await inquirer.prompt([
253
- {
254
- type: 'confirm',
255
- name: 'addPlan',
256
- message: '是否添加 --plan 参数(预演模式)?',
257
- default: false
258
- }
259
- ]);
260
-
261
- if (addPlan) {
262
- scriptArgs.push('--plan');
263
- }
264
- }
265
-
266
- // 预演模式
267
- if (options.dryRun) {
268
- Logger.info(`[预演模式] 将执行: ${selectedScript.displayName}`);
269
- Logger.info(`[预演模式] 脚本路径: ${selectedScript.path}`);
270
- if (scriptArgs.length > 0) {
271
- Logger.info(`[预演模式] 参数: ${scriptArgs.join(' ')}`);
272
- }
273
- process.exit(0);
274
- }
275
-
276
- // 确认执行
277
- const { confirm } = await inquirer.prompt([
278
- {
279
- type: 'confirm',
280
- name: 'confirm',
281
- message: `确认执行 ${selectedScript.displayName}?`,
282
- default: true
283
- }
284
- ]);
285
-
286
- if (!confirm) {
287
- Logger.info('已取消执行');
288
- process.exit(0);
289
- }
290
-
291
- // 执行脚本
292
- const argsInfo = scriptArgs.length > 0 ? ` (参数: ${scriptArgs.join(' ')})` : '';
293
- Logger.info(`正在执行: ${selectedScript.displayName}${argsInfo}\n`);
294
-
295
- const exitCode = await runScript(selectedScript.path, scriptArgs);
296
- process.exit(exitCode);
297
- } catch (error) {
298
- if (error instanceof Error) {
299
- Logger.error(`执行失败: ${error.message}`);
300
- }
301
- process.exit(1);
302
- }
303
- }