nodebbs 0.0.7 → 0.0.9
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/README.md +5 -149
- package/dist/commands/clean/index.d.ts +1 -0
- package/dist/commands/clean/index.js +15 -2
- package/dist/commands/pack/index.js +2 -7
- package/dist/commands/restart/index.js +1 -2
- package/dist/commands/start/index.js +1 -2
- package/dist/commands/stop/index.js +1 -3
- package/dist/interactive.js +43 -3
- package/dist/utils/docker.d.ts +47 -0
- package/dist/utils/docker.js +56 -2
- package/dist/utils/env.js +102 -20
- package/dist/utils/selection.js +2 -1
- package/oclif.manifest.json +20 -14
- package/package.json +3 -3
- /package/dist/commands/logs/{index.d.ts → all.d.ts} +0 -0
- /package/dist/commands/logs/{index.js → all.js} +0 -0
package/README.md
CHANGED
|
@@ -11,14 +11,11 @@ NodeBBS CLI 是一个专为全栈开发者设计的命令行工具,用于简
|
|
|
11
11
|
|
|
12
12
|
### 特性
|
|
13
13
|
|
|
14
|
-
- �️ **交互式菜单** -
|
|
15
|
-
- �🚀 **快速启动** - 一键启动开发环境
|
|
14
|
+
- �️ **交互式菜单** - 支持键盘导航的可视化命令选择
|
|
16
15
|
- 🎯 **全栈友好** - 命令设计贴近开发者思维
|
|
17
|
-
- 🔧 **服务级控制** - 可以单独管理每个服务
|
|
18
16
|
- 📊 **实时日志** - 方便查看各服务日志
|
|
19
17
|
- 💾 **数据库管理** - 内置数据库迁移和管理工具
|
|
20
18
|
- 🌐 **内置模板** - 无需本地配置文件即可使用
|
|
21
|
-
- 🇨🇳 **中文界面** - 所有输出均为中文
|
|
22
19
|
|
|
23
20
|
## 📦 安装
|
|
24
21
|
|
|
@@ -45,28 +42,11 @@ npx nodebbs [command]
|
|
|
45
42
|
|
|
46
43
|
## 🚀 快速开始
|
|
47
44
|
|
|
48
|
-
### 1. 启动服务
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
# 启动所有服务(生产模式,包含完整检查)
|
|
52
|
-
npx nodebbs start
|
|
53
|
-
|
|
54
|
-
# 重新构建并启动(跳过检查,用于更新)
|
|
55
|
-
npx nodebbs start --build
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### 2. 查看服务状态
|
|
59
|
-
|
|
60
45
|
```bash
|
|
61
|
-
|
|
62
|
-
npx nodebbs status
|
|
46
|
+
npx nodebbs
|
|
63
47
|
```
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- **Web 前端**: http://localhost:3100
|
|
68
|
-
- **API 文档**: http://localhost:7100/docs
|
|
69
|
-
- **健康检查**: http://localhost:7100/api
|
|
48
|
+
- **start**: 开始部署(首次使用推荐选择此项)
|
|
49
|
+
- **rebuild**: 重新构建并启动(跳过检查,通常在代码更新后选择此项)
|
|
70
50
|
|
|
71
51
|
## 📚 命令参考
|
|
72
52
|
|
|
@@ -217,22 +197,6 @@ npx nodebbs shell:redis
|
|
|
217
197
|
|
|
218
198
|
### 数据库管理
|
|
219
199
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
#### `nodebbs db:generate`
|
|
223
|
-
生成数据库迁移文件
|
|
224
|
-
|
|
225
|
-
```bash
|
|
226
|
-
npx nodebbs db:generate
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
#### `nodebbs db:migrate`
|
|
230
|
-
执行数据库迁移
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
npx nodebbs db:migrate
|
|
234
|
-
```
|
|
235
|
-
|
|
236
200
|
#### `nodebbs db:push`
|
|
237
201
|
推送数据库 schema
|
|
238
202
|
|
|
@@ -278,83 +242,7 @@ npx nodebbs clean --cache
|
|
|
278
242
|
|
|
279
243
|
## 🎯 使用场景
|
|
280
244
|
|
|
281
|
-
###
|
|
282
|
-
|
|
283
|
-
```bash
|
|
284
|
-
# 1. 启动服务
|
|
285
|
-
npx nodebbs start
|
|
286
|
-
|
|
287
|
-
# 2. 初始化数据库
|
|
288
|
-
npx nodebbs db:push
|
|
289
|
-
npx nodebbs db:seed
|
|
290
|
-
|
|
291
|
-
# 3. 查看服务状态
|
|
292
|
-
npx nodebbs status
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
### 场景 2:日常开发
|
|
296
|
-
|
|
297
|
-
```bash
|
|
298
|
-
# 启动服务
|
|
299
|
-
npx nodebbs start
|
|
300
|
-
|
|
301
|
-
# 查看 API 日志
|
|
302
|
-
npx nodebbs logs:api
|
|
303
|
-
|
|
304
|
-
# 进入 API 容器调试
|
|
305
|
-
npx nodebbs shell:api
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
# 重置测试数据
|
|
310
|
-
npx nodebbs db:reset
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### 场景 3:代码更新后重新构建
|
|
314
|
-
|
|
315
|
-
```bash
|
|
316
|
-
# 停止服务
|
|
317
|
-
npx nodebbs stop
|
|
318
|
-
|
|
319
|
-
# 重新构建并启动
|
|
320
|
-
npx nodebbs start --build
|
|
321
|
-
|
|
322
|
-
# 查看日志确认启动成功
|
|
323
|
-
npx nodebbs logs
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### 场景 4:生产部署
|
|
327
|
-
|
|
328
|
-
```bash
|
|
329
|
-
# 部署到生产环境
|
|
330
|
-
npx nodebbs start -e production
|
|
331
|
-
|
|
332
|
-
# 查看服务状态
|
|
333
|
-
npx nodebbs status
|
|
334
|
-
|
|
335
|
-
# 查看日志
|
|
336
|
-
npx nodebbs logs
|
|
337
|
-
|
|
338
|
-
# 执行数据库迁移
|
|
339
|
-
npx nodebbs db:migrate
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
### 场景 5:数据库操作
|
|
343
|
-
|
|
344
|
-
```bash
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
# 运行迁移
|
|
348
|
-
npx nodebbs db:migrate
|
|
349
|
-
|
|
350
|
-
# 填充数据
|
|
351
|
-
npx nodebbs db:seed
|
|
352
|
-
|
|
353
|
-
# 进入数据库命令行
|
|
354
|
-
npx nodebbs shell:db
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
### 场景 6:离线服务器部署
|
|
245
|
+
### 离线服务器部署
|
|
358
246
|
|
|
359
247
|
1. **在开发机打包**:
|
|
360
248
|
```bash
|
|
@@ -491,38 +379,6 @@ npx nodebbs stop
|
|
|
491
379
|
npx nodebbs stop --volumes
|
|
492
380
|
```
|
|
493
381
|
|
|
494
|
-
## 📝 开发
|
|
495
|
-
|
|
496
|
-
### 本地开发
|
|
497
|
-
|
|
498
|
-
```bash
|
|
499
|
-
# 克隆仓库
|
|
500
|
-
git clone https://github.com/aiprojecthub/nodebbs.git
|
|
501
|
-
cd nodebbs
|
|
502
|
-
|
|
503
|
-
# 安装依赖
|
|
504
|
-
pnpm install
|
|
505
|
-
|
|
506
|
-
# 构建
|
|
507
|
-
pnpm build
|
|
508
|
-
|
|
509
|
-
# 运行
|
|
510
|
-
./bin/run.js [command]
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
### 测试命令
|
|
514
|
-
|
|
515
|
-
```bash
|
|
516
|
-
# 查看帮助
|
|
517
|
-
./bin/run.js --help
|
|
518
|
-
|
|
519
|
-
# 测试 start 命令
|
|
520
|
-
./bin/run.js start --help
|
|
521
|
-
|
|
522
|
-
# 测试 logs 命令
|
|
523
|
-
./bin/run.js logs --help
|
|
524
|
-
```
|
|
525
|
-
|
|
526
382
|
## 🔗 相关链接
|
|
527
383
|
|
|
528
384
|
- [NodeBBS 项目](https://github.com/aiprojecthub/nodebbs)
|
|
@@ -5,6 +5,7 @@ export default class Clean extends Command {
|
|
|
5
5
|
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
6
|
cache: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
images: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
};
|
|
10
11
|
run(): Promise<void>;
|
|
@@ -2,12 +2,13 @@ import { Command, Flags } from '@oclif/core';
|
|
|
2
2
|
import { checkbox, confirm } from '@inquirer/prompts';
|
|
3
3
|
import { logger } from '../../utils/logger.js';
|
|
4
4
|
import { execa } from 'execa';
|
|
5
|
+
import { clearStoredEnv } from '../../utils/selection.js';
|
|
5
6
|
export default class Clean extends Command {
|
|
6
7
|
static description = '清理 Docker 缓存和残留资源';
|
|
7
8
|
static flags = {
|
|
8
9
|
all: Flags.boolean({
|
|
9
10
|
char: 'a',
|
|
10
|
-
description: '清理所有 (
|
|
11
|
+
description: '清理所有 (构建缓存、无用镜像、网络、环境锁定)',
|
|
11
12
|
default: false,
|
|
12
13
|
}),
|
|
13
14
|
cache: Flags.boolean({
|
|
@@ -18,6 +19,10 @@ export default class Clean extends Command {
|
|
|
18
19
|
description: '清理无用镜像 (dangling)',
|
|
19
20
|
default: false,
|
|
20
21
|
}),
|
|
22
|
+
env: Flags.boolean({
|
|
23
|
+
description: '清理环境锁定 (Environment Lock)',
|
|
24
|
+
default: false,
|
|
25
|
+
}),
|
|
21
26
|
force: Flags.boolean({
|
|
22
27
|
char: 'f',
|
|
23
28
|
description: '跳过确认提示',
|
|
@@ -29,13 +34,15 @@ export default class Clean extends Command {
|
|
|
29
34
|
let targets = [];
|
|
30
35
|
// 根据标志确定清理目标
|
|
31
36
|
if (flags.all) {
|
|
32
|
-
targets = ['cache', 'images', 'networks'];
|
|
37
|
+
targets = ['cache', 'images', 'networks', 'env'];
|
|
33
38
|
}
|
|
34
39
|
else {
|
|
35
40
|
if (flags.cache)
|
|
36
41
|
targets.push('cache');
|
|
37
42
|
if (flags.images)
|
|
38
43
|
targets.push('images');
|
|
44
|
+
if (flags.env)
|
|
45
|
+
targets.push('env');
|
|
39
46
|
}
|
|
40
47
|
// 如果未提供标志,进行交互式选择
|
|
41
48
|
if (targets.length === 0) {
|
|
@@ -45,6 +52,7 @@ export default class Clean extends Command {
|
|
|
45
52
|
{ name: '构建缓存 (Build Cache)', value: 'cache' },
|
|
46
53
|
{ name: '无用镜像 (Dangling Images)', value: 'images' },
|
|
47
54
|
{ name: '无用网络 (Unused Networks)', value: 'networks' },
|
|
55
|
+
{ name: '环境锁定 (Environment Lock)', value: 'env' },
|
|
48
56
|
],
|
|
49
57
|
});
|
|
50
58
|
}
|
|
@@ -81,6 +89,11 @@ export default class Clean extends Command {
|
|
|
81
89
|
await execa('docker', ['network', 'prune', '-f'], { stdio: 'inherit' });
|
|
82
90
|
logger.success('无用网络已清理');
|
|
83
91
|
}
|
|
92
|
+
if (targets.includes('env')) {
|
|
93
|
+
logger.info('正在清除环境锁定...');
|
|
94
|
+
await clearStoredEnv();
|
|
95
|
+
logger.success('环境锁定已移除');
|
|
96
|
+
}
|
|
84
97
|
}
|
|
85
98
|
catch (error) {
|
|
86
99
|
logger.error('清理过程中发生错误');
|
|
@@ -4,6 +4,7 @@ import * as fs from 'fs';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as os from 'os';
|
|
6
6
|
import { getTemplatePath } from '../../utils/template.js';
|
|
7
|
+
import { checkDocker } from '../../utils/docker.js';
|
|
7
8
|
export default class Pack extends Command {
|
|
8
9
|
static description = '生成离线部署包';
|
|
9
10
|
static flags = {
|
|
@@ -13,12 +14,7 @@ export default class Pack extends Command {
|
|
|
13
14
|
const { flags } = await this.parse(Pack);
|
|
14
15
|
const outputPath = path.resolve(flags.output);
|
|
15
16
|
// 检查 docker 是否可用
|
|
16
|
-
|
|
17
|
-
await execa('docker', ['--version']);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
this.error('无法执行 docker 命令,请确保 Docker 已安装并运行。');
|
|
21
|
-
}
|
|
17
|
+
await checkDocker();
|
|
22
18
|
// 1. 准备临时目录
|
|
23
19
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nodebbs-pack-'));
|
|
24
20
|
this.log(`正在准备构建环境: ${tmpDir}`);
|
|
@@ -30,7 +26,6 @@ export default class Pack extends Command {
|
|
|
30
26
|
// 但既然是打包,通常是想把当前开发好的代码打包。
|
|
31
27
|
// 所以我们假设在项目根目录运行。
|
|
32
28
|
if (!fs.existsSync('docker-compose.yml')) {
|
|
33
|
-
this.warn('当前目录未找到 docker-compose.yml,将使用内置模板。');
|
|
34
29
|
// TODO: 从 templates 复制 (简化起见,我们假设用户在项目根目录,或者我们强制要求)
|
|
35
30
|
// 如果没找到,我们实际上无法 build api/web,因为 build context 需要源码。
|
|
36
31
|
// 所以必须要求在项目根目录运行。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
import { runCompose, getComposeFiles } from '../../utils/docker.js';
|
|
3
3
|
import { logger } from '../../utils/logger.js';
|
|
4
|
-
import { EnvFlag, selectEnvironment
|
|
4
|
+
import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
|
|
5
5
|
export default class Restart extends Command {
|
|
6
6
|
static description = '重启所有服务 (up --force-recreate)';
|
|
7
7
|
static flags = {
|
|
@@ -11,7 +11,6 @@ export default class Restart extends Command {
|
|
|
11
11
|
const { flags } = await this.parse(Restart);
|
|
12
12
|
// 1. 选择环境
|
|
13
13
|
const env = await selectEnvironment(flags.env);
|
|
14
|
-
await setStoredEnv(env);
|
|
15
14
|
const { files, isBuiltIn } = await getComposeFiles(env);
|
|
16
15
|
logger.info('正在重启服务 (使用现有镜像)...');
|
|
17
16
|
logger.info('环境: ' + env);
|
|
@@ -4,7 +4,7 @@ import { checkDocker, runCompose, waitForHealth, execCompose, getComposeFiles }
|
|
|
4
4
|
import { initEnv, checkEnv } from '../../utils/env.js';
|
|
5
5
|
import { logger } from '../../utils/logger.js';
|
|
6
6
|
import dotenv from 'dotenv';
|
|
7
|
-
import { EnvFlag, selectEnvironment
|
|
7
|
+
import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
|
|
8
8
|
export default class Start extends Command {
|
|
9
9
|
static description = '开始部署';
|
|
10
10
|
static flags = {
|
|
@@ -25,7 +25,6 @@ export default class Start extends Command {
|
|
|
25
25
|
}
|
|
26
26
|
// 1. 选择环境
|
|
27
27
|
const env = await selectEnvironment(flags.env);
|
|
28
|
-
await setStoredEnv(env);
|
|
29
28
|
// 2. 获取 Compose 文件
|
|
30
29
|
const { files: composeFiles, isBuiltIn } = await getComposeFiles(env);
|
|
31
30
|
if (isBuiltIn) {
|
|
@@ -2,7 +2,7 @@ import { Command, Flags } from '@oclif/core';
|
|
|
2
2
|
import { runCompose, getComposeFiles } from '../../utils/docker.js';
|
|
3
3
|
import { logger } from '../../utils/logger.js';
|
|
4
4
|
import { confirm } from '@inquirer/prompts';
|
|
5
|
-
import { EnvFlag, selectEnvironment
|
|
5
|
+
import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
|
|
6
6
|
export default class Stop extends Command {
|
|
7
7
|
static description = '停止服务';
|
|
8
8
|
static flags = {
|
|
@@ -32,13 +32,11 @@ export default class Stop extends Command {
|
|
|
32
32
|
}
|
|
33
33
|
logger.info('正在停止服务并删除数据卷...');
|
|
34
34
|
await runCompose(files, ['down', '-v'], isBuiltIn);
|
|
35
|
-
await clearStoredEnv();
|
|
36
35
|
logger.success('服务已停止,数据卷已删除');
|
|
37
36
|
}
|
|
38
37
|
else {
|
|
39
38
|
logger.info('正在停止服务...');
|
|
40
39
|
await runCompose(files, ['down'], isBuiltIn);
|
|
41
|
-
await clearStoredEnv();
|
|
42
40
|
logger.success('服务已停止');
|
|
43
41
|
}
|
|
44
42
|
}
|
package/dist/interactive.js
CHANGED
|
@@ -29,8 +29,13 @@ export async function runInteractive(root) {
|
|
|
29
29
|
}
|
|
30
30
|
await navigate(tree, [], config);
|
|
31
31
|
}
|
|
32
|
+
const GLOBAL_PRIORITY = ['start', 'stop', 'restart', 'rebuild', 'status', 'logs', 'shell', 'db', 'pack'];
|
|
33
|
+
const SCOPED_PRIORITIES = {
|
|
34
|
+
'logs': ['all', 'web', 'api', 'db', 'redis'],
|
|
35
|
+
};
|
|
32
36
|
async function navigate(node, breadcrumbs, config) {
|
|
33
|
-
const
|
|
37
|
+
const currentPath = breadcrumbs.join(':');
|
|
38
|
+
const priorityOrder = SCOPED_PRIORITIES[currentPath] || GLOBAL_PRIORITY;
|
|
34
39
|
while (true) {
|
|
35
40
|
const keys = Object.keys(node.children).sort((a, b) => {
|
|
36
41
|
const indexA = priorityOrder.indexOf(a);
|
|
@@ -47,7 +52,16 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
47
52
|
// Otherwise sort alphabetically
|
|
48
53
|
return a.localeCompare(b);
|
|
49
54
|
});
|
|
50
|
-
const choices =
|
|
55
|
+
const choices = [];
|
|
56
|
+
// 如果当前节点本身就是一个命令,添加运行选项
|
|
57
|
+
if (node.command) {
|
|
58
|
+
choices.push({
|
|
59
|
+
name: `${node.command.description || node.command.id}`,
|
|
60
|
+
value: '__SELF__',
|
|
61
|
+
short: 'all'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const subChoices = keys.map(key => {
|
|
51
65
|
const child = node.children[key];
|
|
52
66
|
const hasSubcommands = Object.keys(child.children).length > 0;
|
|
53
67
|
let label = key;
|
|
@@ -83,6 +97,7 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
83
97
|
short: key
|
|
84
98
|
};
|
|
85
99
|
});
|
|
100
|
+
choices.push(...subChoices);
|
|
86
101
|
// 添加导航选项
|
|
87
102
|
// 即使支持 Esc,保留显式选项也有助于发现性
|
|
88
103
|
if (breadcrumbs.length > 0) {
|
|
@@ -116,6 +131,31 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
116
131
|
if (selection === '__BACK__') {
|
|
117
132
|
return;
|
|
118
133
|
}
|
|
134
|
+
if (selection === '__SELF__' && node.command) {
|
|
135
|
+
console.log(`正在运行: ${node.command.id}`);
|
|
136
|
+
try {
|
|
137
|
+
await config.runCommand(node.command.id);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
// Ctrl+C: 退出程序
|
|
141
|
+
if (error.name === 'ExitPromptError') {
|
|
142
|
+
console.log('\n用户退出。');
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
// 手动取消: 返回菜单
|
|
146
|
+
if (error.name === 'CancelError') {
|
|
147
|
+
console.log('\n操作已取消。');
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
console.error(error.message);
|
|
151
|
+
}
|
|
152
|
+
console.log('\n命令执行完成。');
|
|
153
|
+
// 动态导入 input 以保持轻量
|
|
154
|
+
const { input } = await import('@inquirer/prompts');
|
|
155
|
+
await input({ message: '按回车键继续...' });
|
|
156
|
+
// 继续循环
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
119
159
|
const selectedNode = node.children[selection];
|
|
120
160
|
if (Object.keys(selectedNode.children).length > 0) {
|
|
121
161
|
// 有子命令,进入子菜单
|
|
@@ -138,7 +178,7 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
138
178
|
console.log('\n操作已取消。');
|
|
139
179
|
continue;
|
|
140
180
|
}
|
|
141
|
-
console.error(error);
|
|
181
|
+
console.error(error.message);
|
|
142
182
|
}
|
|
143
183
|
console.log('\n命令执行完成。');
|
|
144
184
|
// 动态导入 input 以保持轻量
|
package/dist/utils/docker.d.ts
CHANGED
|
@@ -1,8 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 获取 Docker Compose 文件路径配置
|
|
3
|
+
*
|
|
4
|
+
* 自动检测当前目录是否存在 `docker-compose.yml`。
|
|
5
|
+
* - 如果存在,则优先使用项目根目录的配置。
|
|
6
|
+
* - 如果不存在,则使用 CLI 内置的模板文件。
|
|
7
|
+
*
|
|
8
|
+
* @param env - 运行环境 ('production' | 'lowmem' | 'basic')
|
|
9
|
+
* @returns 对象包含文件路径数组和是否使用内置模板的标志
|
|
10
|
+
*/
|
|
1
11
|
export declare function getComposeFiles(env: string): Promise<{
|
|
2
12
|
files: string[];
|
|
3
13
|
isBuiltIn: boolean;
|
|
4
14
|
}>;
|
|
15
|
+
/**
|
|
16
|
+
* 检查 Docker 环境是否可用
|
|
17
|
+
*
|
|
18
|
+
* 验证 `docker` 和 `docker compose` 命令是否已安装并能正常响应。
|
|
19
|
+
* 如果检查失败,会抛出错误并提示用户安装。
|
|
20
|
+
*/
|
|
5
21
|
export declare function checkDocker(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* 执行 Docker Compose 管理命令 (Lifecycle Commands)
|
|
24
|
+
*
|
|
25
|
+
* 用于执行如 `up`, `down`, `logs`, `build` 等管理整个服务堆栈的命令。
|
|
26
|
+
* 如果使用的是内置模板,会自动注入必要的环境变量(如数据库初始化脚本路径)。
|
|
27
|
+
*
|
|
28
|
+
* @param files - Docker Compose 文件路径数组
|
|
29
|
+
* @param args - 传递给 docker compose 的参数列表 (e.g. `['up', '-d']`)
|
|
30
|
+
* @param isBuiltIn - 是否使用内置模板 (决定是否注入额外环境变量)
|
|
31
|
+
*/
|
|
6
32
|
export declare function runCompose(files: string[], args: string[], isBuiltIn?: boolean): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* 在容器内执行命令 (Exec)
|
|
35
|
+
*
|
|
36
|
+
* 相当于执行 `docker compose exec [service] [command]`。
|
|
37
|
+
* 用于在正在运行的容器中运行脚本或命令(例如数据库迁移、种子数据填充)。
|
|
38
|
+
*
|
|
39
|
+
* @param files - Docker Compose 文件路径数组
|
|
40
|
+
* @param service - 目标服务名称 (e.g. 'api', 'postgres')
|
|
41
|
+
* @param command - 要执行的命令数组 (e.g. `['npm', 'run', 'seed']`)
|
|
42
|
+
* @param isBuiltIn - 是否使用内置模板
|
|
43
|
+
*/
|
|
7
44
|
export declare function execCompose(files: string[], service: string, command: string[], isBuiltIn?: boolean): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* 等待服务健康检查通过
|
|
47
|
+
*
|
|
48
|
+
* 轮询关键服务 (PostgreSQL, Redis, API) 直到它们准备就绪或超时。
|
|
49
|
+
* - PostgreSQL: 使用 `pg_isready` 检查
|
|
50
|
+
* - Redis & API: 使用简单的延时等待 (后续可优化为端口/API探测)
|
|
51
|
+
*
|
|
52
|
+
* @param files - Docker Compose 文件路径数组
|
|
53
|
+
* @param isBuiltIn - 是否使用内置模板
|
|
54
|
+
*/
|
|
8
55
|
export declare function waitForHealth(files: string[], isBuiltIn?: boolean): Promise<void>;
|
package/dist/utils/docker.js
CHANGED
|
@@ -5,6 +5,16 @@ import { exists } from 'node:fs';
|
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { getTemplateDir, getTemplatePath } from './template.js';
|
|
7
7
|
const fileExists = promisify(exists);
|
|
8
|
+
/**
|
|
9
|
+
* 获取 Docker Compose 文件路径配置
|
|
10
|
+
*
|
|
11
|
+
* 自动检测当前目录是否存在 `docker-compose.yml`。
|
|
12
|
+
* - 如果存在,则优先使用项目根目录的配置。
|
|
13
|
+
* - 如果不存在,则使用 CLI 内置的模板文件。
|
|
14
|
+
*
|
|
15
|
+
* @param env - 运行环境 ('production' | 'lowmem' | 'basic')
|
|
16
|
+
* @returns 对象包含文件路径数组和是否使用内置模板的标志
|
|
17
|
+
*/
|
|
8
18
|
export async function getComposeFiles(env) {
|
|
9
19
|
const workDir = process.cwd();
|
|
10
20
|
let isBuiltIn = false;
|
|
@@ -26,6 +36,12 @@ export async function getComposeFiles(env) {
|
|
|
26
36
|
}
|
|
27
37
|
return { files, isBuiltIn };
|
|
28
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* 检查 Docker 环境是否可用
|
|
41
|
+
*
|
|
42
|
+
* 验证 `docker` 和 `docker compose` 命令是否已安装并能正常响应。
|
|
43
|
+
* 如果检查失败,会抛出错误并提示用户安装。
|
|
44
|
+
*/
|
|
29
45
|
export async function checkDocker() {
|
|
30
46
|
logger.info('正在检查 Docker 环境...');
|
|
31
47
|
try {
|
|
@@ -38,6 +54,16 @@ export async function checkDocker() {
|
|
|
38
54
|
throw error;
|
|
39
55
|
}
|
|
40
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* 执行 Docker Compose 管理命令 (Lifecycle Commands)
|
|
59
|
+
*
|
|
60
|
+
* 用于执行如 `up`, `down`, `logs`, `build` 等管理整个服务堆栈的命令。
|
|
61
|
+
* 如果使用的是内置模板,会自动注入必要的环境变量(如数据库初始化脚本路径)。
|
|
62
|
+
*
|
|
63
|
+
* @param files - Docker Compose 文件路径数组
|
|
64
|
+
* @param args - 传递给 docker compose 的参数列表 (e.g. `['up', '-d']`)
|
|
65
|
+
* @param isBuiltIn - 是否使用内置模板 (决定是否注入额外环境变量)
|
|
66
|
+
*/
|
|
41
67
|
export async function runCompose(files, args, isBuiltIn = false) {
|
|
42
68
|
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
43
69
|
if (isBuiltIn) {
|
|
@@ -48,16 +74,44 @@ export async function runCompose(files, args, isBuiltIn = false) {
|
|
|
48
74
|
}
|
|
49
75
|
composeArgs.push(...args);
|
|
50
76
|
// 使用 stdio: 'inherit' 实时显示输出
|
|
51
|
-
|
|
77
|
+
// 设置 COMPOSE_LOG_LEVEL=ERROR 抑制变量未设置的警告
|
|
78
|
+
await execa('docker', ['compose', ...composeArgs], {
|
|
79
|
+
stdio: 'inherit',
|
|
80
|
+
env: { ...process.env, COMPOSE_LOG_LEVEL: 'ERROR' }
|
|
81
|
+
});
|
|
52
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* 在容器内执行命令 (Exec)
|
|
85
|
+
*
|
|
86
|
+
* 相当于执行 `docker compose exec [service] [command]`。
|
|
87
|
+
* 用于在正在运行的容器中运行脚本或命令(例如数据库迁移、种子数据填充)。
|
|
88
|
+
*
|
|
89
|
+
* @param files - Docker Compose 文件路径数组
|
|
90
|
+
* @param service - 目标服务名称 (e.g. 'api', 'postgres')
|
|
91
|
+
* @param command - 要执行的命令数组 (e.g. `['npm', 'run', 'seed']`)
|
|
92
|
+
* @param isBuiltIn - 是否使用内置模板
|
|
93
|
+
*/
|
|
53
94
|
export async function execCompose(files, service, command, isBuiltIn = false) {
|
|
54
95
|
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
55
96
|
if (isBuiltIn) {
|
|
56
97
|
composeArgs.push('--project-directory', process.cwd());
|
|
57
98
|
}
|
|
58
99
|
composeArgs.push('exec', service, ...command);
|
|
59
|
-
await execa('docker', ['compose', ...composeArgs], {
|
|
100
|
+
await execa('docker', ['compose', ...composeArgs], {
|
|
101
|
+
stdio: 'inherit',
|
|
102
|
+
env: { ...process.env, COMPOSE_LOG_LEVEL: 'ERROR' }
|
|
103
|
+
});
|
|
60
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* 等待服务健康检查通过
|
|
107
|
+
*
|
|
108
|
+
* 轮询关键服务 (PostgreSQL, Redis, API) 直到它们准备就绪或超时。
|
|
109
|
+
* - PostgreSQL: 使用 `pg_isready` 检查
|
|
110
|
+
* - Redis & API: 使用简单的延时等待 (后续可优化为端口/API探测)
|
|
111
|
+
*
|
|
112
|
+
* @param files - Docker Compose 文件路径数组
|
|
113
|
+
* @param isBuiltIn - 是否使用内置模板
|
|
114
|
+
*/
|
|
61
115
|
export async function waitForHealth(files, isBuiltIn = false) {
|
|
62
116
|
logger.info('正在等待服务就绪...');
|
|
63
117
|
// 等待 Postgres
|
package/dist/utils/env.js
CHANGED
|
@@ -12,39 +12,121 @@ export async function initEnv() {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
logger.info('正在创建 .env 文件...');
|
|
15
|
+
// 检查模板文件
|
|
16
|
+
let templateContent = '';
|
|
15
17
|
let sourceFile = '';
|
|
16
|
-
let isBuiltIn = false;
|
|
17
18
|
if (await fileExists('.env.docker.example')) {
|
|
18
19
|
sourceFile = '.env.docker.example';
|
|
20
|
+
templateContent = await fs.readFile(sourceFile, 'utf-8');
|
|
19
21
|
}
|
|
20
22
|
else {
|
|
21
23
|
// 使用内置模板
|
|
22
24
|
sourceFile = getTemplatePath('env');
|
|
23
|
-
|
|
25
|
+
templateContent = await fs.readFile(sourceFile, 'utf-8');
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const { input, password } = await import('@inquirer/prompts');
|
|
28
|
+
const crypto = await import('node:crypto');
|
|
29
|
+
function generateSecret(length = 32) {
|
|
30
|
+
return crypto.randomBytes(length).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, length);
|
|
31
|
+
}
|
|
32
|
+
console.log('\n请配置环境变量(按 Enter 使用默认值或自动生成):\n');
|
|
33
|
+
// 1. 收集用户输入
|
|
34
|
+
const postgresPassword = await password({
|
|
35
|
+
message: '设置 POSTGRES_PASSWORD (数据库密码) [空值自动生成]:',
|
|
36
|
+
mask: '*',
|
|
37
|
+
validate: (value) => true, // 允许为空以自动生成
|
|
38
|
+
}) || generateSecret();
|
|
39
|
+
const redisPassword = await password({
|
|
40
|
+
message: '设置 REDIS_PASSWORD (Redis 密码) [空值自动生成]:',
|
|
41
|
+
mask: '*',
|
|
42
|
+
validate: (value) => true,
|
|
43
|
+
}) || generateSecret();
|
|
44
|
+
const jwtSecret = await password({
|
|
45
|
+
message: '设置 JWT_SECRET (JWT 密钥) [空值自动生成]:',
|
|
46
|
+
mask: '*',
|
|
47
|
+
validate: (value) => true,
|
|
48
|
+
}) || generateSecret(64);
|
|
49
|
+
const webPort = await input({
|
|
50
|
+
message: '设置 WEB_PORT (前端端口):',
|
|
51
|
+
default: '3100'
|
|
52
|
+
});
|
|
53
|
+
const apiPort = await input({
|
|
54
|
+
message: '设置 API_PORT (后端端口):',
|
|
55
|
+
default: '7100'
|
|
56
|
+
});
|
|
57
|
+
const defaultApiUrl = `http://localhost:${apiPort}`;
|
|
58
|
+
const nextPublicApiUrl = await input({
|
|
59
|
+
message: '设置 NEXT_PUBLIC_API_URL (前端访问后端的地址):',
|
|
60
|
+
default: defaultApiUrl
|
|
61
|
+
});
|
|
62
|
+
const defaultAppUrl = `http://localhost:${webPort}`;
|
|
63
|
+
const nextPublicAppUrl = await input({
|
|
64
|
+
message: '设置 NEXT_PUBLIC_APP_URL (应用访问地址):',
|
|
65
|
+
default: defaultAppUrl
|
|
66
|
+
});
|
|
67
|
+
const corsOrigin = await input({
|
|
68
|
+
message: '设置 CORS_ORIGIN (允许跨域的域名):',
|
|
69
|
+
default: '*'
|
|
70
|
+
});
|
|
71
|
+
// 2. 显示配置预览
|
|
72
|
+
console.log('\n================ 配置预览 ================');
|
|
73
|
+
console.log(`POSTGRES_PASSWORD: ${postgresPassword.substring(0, 3)}******`);
|
|
74
|
+
console.log(`REDIS_PASSWORD: ${redisPassword.substring(0, 3)}******`);
|
|
75
|
+
console.log(`JWT_SECRET: ${jwtSecret.substring(0, 3)}******`);
|
|
76
|
+
console.log(`WEB_PORT: ${webPort}`);
|
|
77
|
+
console.log(`API_PORT: ${apiPort}`);
|
|
78
|
+
console.log(`NEXT_PUBLIC_API_URL: ${nextPublicApiUrl}`);
|
|
79
|
+
console.log(`NEXT_PUBLIC_APP_URL: ${nextPublicAppUrl}`);
|
|
80
|
+
console.log(`CORS_ORIGIN: ${corsOrigin}`);
|
|
81
|
+
console.log('==========================================\n');
|
|
82
|
+
// 3. 确认生成
|
|
83
|
+
const confirmed = await confirm({
|
|
84
|
+
message: '确认生成 .env 文件?',
|
|
85
|
+
default: true
|
|
86
|
+
});
|
|
87
|
+
if (!confirmed) {
|
|
88
|
+
logger.info('已取消生成 .env 文件。');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
// 4. 替换模板内容
|
|
92
|
+
// 为了保留注释,我们使用简单的字符串替换,而不是 dotenv 解析
|
|
93
|
+
// 注意:这里假设模板中的 key 遵循 standard env format (KEY=value)
|
|
94
|
+
let newEnv = templateContent;
|
|
95
|
+
const replacements = {
|
|
96
|
+
'POSTGRES_PASSWORD': postgresPassword,
|
|
97
|
+
'REDIS_PASSWORD': redisPassword,
|
|
98
|
+
'JWT_SECRET': jwtSecret,
|
|
99
|
+
'WEB_PORT': webPort,
|
|
100
|
+
'API_PORT': apiPort,
|
|
101
|
+
'NEXT_PUBLIC_API_URL': nextPublicApiUrl,
|
|
102
|
+
'NEXT_PUBLIC_APP_URL': nextPublicAppUrl,
|
|
103
|
+
'CORS_ORIGIN': corsOrigin,
|
|
104
|
+
// 如果 API_PORT 变了,模板里可能还需要联动修改 APP_URL 用于 OAuth 回调,这里简单处理
|
|
105
|
+
'APP_URL': nextPublicAppUrl
|
|
106
|
+
};
|
|
107
|
+
// 针对每个 Key 进行替换。
|
|
108
|
+
// 策略:查找 `KEY=...` 并替换为 `KEY=newValue`
|
|
109
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
110
|
+
// 匹配 KEY=任意非换行字符
|
|
111
|
+
const regex = new RegExp(`^${key}=.*`, 'gm');
|
|
112
|
+
if (regex.test(newEnv)) {
|
|
113
|
+
newEnv = newEnv.replace(regex, `${key}=${value}`);
|
|
29
114
|
}
|
|
30
115
|
else {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
logger.warning('请编辑 .env 文件并修改以下配置:');
|
|
34
|
-
logger.warning(' - POSTGRES_PASSWORD (数据库密码)');
|
|
35
|
-
logger.warning(' - REDIS_PASSWORD (Redis 密码)');
|
|
36
|
-
logger.warning(' - JWT_SECRET (JWT 密钥)');
|
|
37
|
-
const edit = await confirm({
|
|
38
|
-
message: '是否现在编辑 .env 文件?',
|
|
39
|
-
default: false
|
|
40
|
-
});
|
|
41
|
-
if (edit) {
|
|
42
|
-
logger.info('请手动编辑 .env 文件后再次运行命令。');
|
|
43
|
-
process.exit(0);
|
|
116
|
+
// 如果模板里没这个 key,追加到末尾(虽然理论上模板应该有)
|
|
117
|
+
newEnv += `\n${key}=${value}`;
|
|
44
118
|
}
|
|
45
119
|
}
|
|
120
|
+
// 5. 写入文件
|
|
121
|
+
await fs.writeFile('.env', newEnv, 'utf-8');
|
|
122
|
+
// isBuiltIn is not defined in the new logic, need to determine it
|
|
123
|
+
// based on whether sourceFile was .env.docker.example or a template path
|
|
124
|
+
const isBuiltIn = sourceFile === getTemplatePath('env');
|
|
125
|
+
if (isBuiltIn) {
|
|
126
|
+
logger.success('已使用内置模板并填充配置生成 .env');
|
|
127
|
+
}
|
|
46
128
|
else {
|
|
47
|
-
logger.
|
|
129
|
+
logger.success(`已从 ${sourceFile} 复制并填充配置生成 .env`);
|
|
48
130
|
}
|
|
49
131
|
}
|
|
50
132
|
export async function checkEnv(envType) {
|
package/dist/utils/selection.js
CHANGED
|
@@ -3,7 +3,7 @@ import { select } from '@inquirer/prompts';
|
|
|
3
3
|
import { logger } from './logger.js';
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
const ENV_MARKER_FILE = '.
|
|
6
|
+
const ENV_MARKER_FILE = '.env.lock';
|
|
7
7
|
export const EnvFlag = Flags.string({
|
|
8
8
|
char: 'e',
|
|
9
9
|
description: '部署环境 (production, lowmem, basic)',
|
|
@@ -64,5 +64,6 @@ export async function selectEnvironment(env, options = {}) {
|
|
|
64
64
|
throw error;
|
|
65
65
|
}
|
|
66
66
|
logger.info(`已选择环境: ${selected}`);
|
|
67
|
+
await setStoredEnv(selected);
|
|
67
68
|
return selected;
|
|
68
69
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"flags": {
|
|
8
8
|
"all": {
|
|
9
9
|
"char": "a",
|
|
10
|
-
"description": "清理所有 (
|
|
10
|
+
"description": "清理所有 (构建缓存、无用镜像、网络、环境锁定)",
|
|
11
11
|
"name": "all",
|
|
12
12
|
"allowNo": false,
|
|
13
13
|
"type": "boolean"
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
"allowNo": false,
|
|
25
25
|
"type": "boolean"
|
|
26
26
|
},
|
|
27
|
+
"env": {
|
|
28
|
+
"description": "清理环境锁定 (Environment Lock)",
|
|
29
|
+
"name": "env",
|
|
30
|
+
"allowNo": false,
|
|
31
|
+
"type": "boolean"
|
|
32
|
+
},
|
|
27
33
|
"force": {
|
|
28
34
|
"char": "f",
|
|
29
35
|
"description": "跳过确认提示",
|
|
@@ -196,10 +202,10 @@
|
|
|
196
202
|
"seed.js"
|
|
197
203
|
]
|
|
198
204
|
},
|
|
199
|
-
"logs:
|
|
205
|
+
"logs:all": {
|
|
200
206
|
"aliases": [],
|
|
201
207
|
"args": {},
|
|
202
|
-
"description": "
|
|
208
|
+
"description": "查看所有服务日志",
|
|
203
209
|
"flags": {
|
|
204
210
|
"env": {
|
|
205
211
|
"char": "e",
|
|
@@ -217,7 +223,7 @@
|
|
|
217
223
|
},
|
|
218
224
|
"hasDynamicHelp": false,
|
|
219
225
|
"hiddenAliases": [],
|
|
220
|
-
"id": "logs:
|
|
226
|
+
"id": "logs:all",
|
|
221
227
|
"pluginAlias": "nodebbs",
|
|
222
228
|
"pluginName": "nodebbs",
|
|
223
229
|
"pluginType": "core",
|
|
@@ -228,13 +234,13 @@
|
|
|
228
234
|
"dist",
|
|
229
235
|
"commands",
|
|
230
236
|
"logs",
|
|
231
|
-
"
|
|
237
|
+
"all.js"
|
|
232
238
|
]
|
|
233
239
|
},
|
|
234
|
-
"logs:
|
|
240
|
+
"logs:api": {
|
|
235
241
|
"aliases": [],
|
|
236
242
|
"args": {},
|
|
237
|
-
"description": "
|
|
243
|
+
"description": "查看 API 服务日志",
|
|
238
244
|
"flags": {
|
|
239
245
|
"env": {
|
|
240
246
|
"char": "e",
|
|
@@ -252,7 +258,7 @@
|
|
|
252
258
|
},
|
|
253
259
|
"hasDynamicHelp": false,
|
|
254
260
|
"hiddenAliases": [],
|
|
255
|
-
"id": "logs:
|
|
261
|
+
"id": "logs:api",
|
|
256
262
|
"pluginAlias": "nodebbs",
|
|
257
263
|
"pluginName": "nodebbs",
|
|
258
264
|
"pluginType": "core",
|
|
@@ -263,13 +269,13 @@
|
|
|
263
269
|
"dist",
|
|
264
270
|
"commands",
|
|
265
271
|
"logs",
|
|
266
|
-
"
|
|
272
|
+
"api.js"
|
|
267
273
|
]
|
|
268
274
|
},
|
|
269
|
-
"logs": {
|
|
275
|
+
"logs:db": {
|
|
270
276
|
"aliases": [],
|
|
271
277
|
"args": {},
|
|
272
|
-
"description": "
|
|
278
|
+
"description": "查看数据库日志",
|
|
273
279
|
"flags": {
|
|
274
280
|
"env": {
|
|
275
281
|
"char": "e",
|
|
@@ -287,7 +293,7 @@
|
|
|
287
293
|
},
|
|
288
294
|
"hasDynamicHelp": false,
|
|
289
295
|
"hiddenAliases": [],
|
|
290
|
-
"id": "logs",
|
|
296
|
+
"id": "logs:db",
|
|
291
297
|
"pluginAlias": "nodebbs",
|
|
292
298
|
"pluginName": "nodebbs",
|
|
293
299
|
"pluginType": "core",
|
|
@@ -298,7 +304,7 @@
|
|
|
298
304
|
"dist",
|
|
299
305
|
"commands",
|
|
300
306
|
"logs",
|
|
301
|
-
"
|
|
307
|
+
"db.js"
|
|
302
308
|
]
|
|
303
309
|
},
|
|
304
310
|
"logs:redis": {
|
|
@@ -718,5 +724,5 @@
|
|
|
718
724
|
]
|
|
719
725
|
}
|
|
720
726
|
},
|
|
721
|
-
"version": "0.0.
|
|
727
|
+
"version": "0.0.9"
|
|
722
728
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebbs",
|
|
3
3
|
"description": "NodeBBS 论坛系统专业运维工具",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.9",
|
|
5
5
|
"author": "wengqianshan",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nodebbs": "./bin/run.js"
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
],
|
|
59
59
|
"topicSeparator": " ",
|
|
60
60
|
"topics": {
|
|
61
|
-
"
|
|
62
|
-
"description": "
|
|
61
|
+
"logs": {
|
|
62
|
+
"description": "查看服务日志"
|
|
63
63
|
},
|
|
64
64
|
"db": {
|
|
65
65
|
"description": "数据库操作 (备份, 迁移, 种子数据等)"
|
|
File without changes
|
|
File without changes
|