nodebbs 0.0.3 → 0.0.4
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 +24 -16
- package/bin/dev.js +6 -1
- package/bin/run.js +6 -1
- package/dist/commands/clean/index.js +4 -4
- package/dist/commands/db/backup.js +10 -10
- package/dist/commands/rebuild/index.d.ts +6 -0
- package/dist/commands/rebuild/index.js +11 -0
- package/dist/commands/restart/index.js +1 -1
- package/dist/commands/start/index.js +10 -9
- package/dist/commands/stop/index.js +4 -2
- package/dist/interactive.d.ts +1 -0
- package/dist/interactive.js +171 -0
- package/dist/utils/docker.js +7 -7
- package/dist/utils/env.js +2 -2
- package/dist/utils/selection.d.ts +2 -0
- package/dist/utils/selection.js +45 -1
- package/dist/utils/template.d.ts +5 -5
- package/dist/utils/template.js +5 -5
- package/oclif.manifest.json +51 -65
- package/package.json +10 -5
- package/dist/commands/db/index.d.ts +0 -8
- package/dist/commands/db/index.js +0 -17
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# NodeBBS CLI
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> NodeBBS 论坛系统专业运维工具
|
|
4
4
|
|
|
5
5
|
[](https://oclif.io)
|
|
6
6
|
[](https://npmjs.org/package/nodebbs)
|
|
@@ -11,7 +11,8 @@ NodeBBS CLI 是一个专为全栈开发者设计的命令行工具,用于简
|
|
|
11
11
|
|
|
12
12
|
### 特性
|
|
13
13
|
|
|
14
|
-
-
|
|
14
|
+
- �️ **交互式菜单** - 支持键盘导航的可视化命令选择(新增)
|
|
15
|
+
- �🚀 **快速启动** - 一键启动开发环境
|
|
15
16
|
- 🎯 **全栈友好** - 命令设计贴近开发者思维
|
|
16
17
|
- 🔧 **服务级控制** - 可以单独管理每个服务
|
|
17
18
|
- 📊 **实时日志** - 方便查看各服务日志
|
|
@@ -32,9 +33,13 @@ pnpm add -g nodebbs
|
|
|
32
33
|
yarn global add nodebbs
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
或者直接使用 npx
|
|
36
|
+
或者直接使用 npx(推荐):
|
|
36
37
|
|
|
37
38
|
```bash
|
|
39
|
+
# 进入交互式菜单(推荐)
|
|
40
|
+
npx nodebbs
|
|
41
|
+
|
|
42
|
+
# 运行特定命令
|
|
38
43
|
npx nodebbs [command]
|
|
39
44
|
```
|
|
40
45
|
|
|
@@ -66,10 +71,10 @@ npx nodebbs status
|
|
|
66
71
|
## 📚 命令参考
|
|
67
72
|
|
|
68
73
|
#### `nodebbs start`
|
|
69
|
-
|
|
74
|
+
开始部署
|
|
70
75
|
|
|
71
76
|
```bash
|
|
72
|
-
#
|
|
77
|
+
# 交互式启动
|
|
73
78
|
npx nodebbs start
|
|
74
79
|
|
|
75
80
|
# 指定环境启动
|
|
@@ -105,7 +110,7 @@ npx nodebbs restart -e production
|
|
|
105
110
|
```bash
|
|
106
111
|
npx nodebbs stop
|
|
107
112
|
npx nodebbs stop -e production
|
|
108
|
-
|
|
113
|
+
|
|
109
114
|
# 停止服务并删除数据卷(危险!)
|
|
110
115
|
npx nodebbs stop --volumes
|
|
111
116
|
npx nodebbs stop -v
|
|
@@ -198,12 +203,7 @@ npx nodebbs shell:redis
|
|
|
198
203
|
|
|
199
204
|
### 数据库管理
|
|
200
205
|
|
|
201
|
-
#### `nodebbs db`
|
|
202
|
-
打开数据库管理界面(Drizzle Studio)
|
|
203
206
|
|
|
204
|
-
```bash
|
|
205
|
-
npx nodebbs db
|
|
206
|
-
```
|
|
207
207
|
|
|
208
208
|
#### `nodebbs db:generate`
|
|
209
209
|
生成数据库迁移文件
|
|
@@ -290,8 +290,7 @@ npx nodebbs logs:api
|
|
|
290
290
|
# 进入 API 容器调试
|
|
291
291
|
npx nodebbs shell:api
|
|
292
292
|
|
|
293
|
-
|
|
294
|
-
npx nodebbs db
|
|
293
|
+
|
|
295
294
|
|
|
296
295
|
# 重置测试数据
|
|
297
296
|
npx nodebbs db:reset
|
|
@@ -329,8 +328,7 @@ npx nodebbs db:migrate
|
|
|
329
328
|
### 场景 5:数据库操作
|
|
330
329
|
|
|
331
330
|
```bash
|
|
332
|
-
|
|
333
|
-
npx nodebbs db
|
|
331
|
+
|
|
334
332
|
|
|
335
333
|
# 运行迁移
|
|
336
334
|
npx nodebbs db:migrate
|
|
@@ -357,7 +355,7 @@ npx nodebbs shell:db
|
|
|
357
355
|
| `make logs` | `npx nodebbs logs` |
|
|
358
356
|
| `make logs-api` | `npx nodebbs logs:api` |
|
|
359
357
|
| `make exec-api` | `npx nodebbs shell:api` |
|
|
360
|
-
|
|
358
|
+
|
|
361
359
|
| `make clean-all` | `npx nodebbs stop --volumes` |
|
|
362
360
|
|
|
363
361
|
## ⚙️ 环境配置
|
|
@@ -393,6 +391,16 @@ CORS_ORIGIN=*
|
|
|
393
391
|
|
|
394
392
|
> **提示**:可以使用 `openssl rand -hex 32` 命令生成安全的随机密钥。
|
|
395
393
|
|
|
394
|
+
### 环境持久化
|
|
395
|
+
|
|
396
|
+
CLI 会自动记住您上次启动的环境:
|
|
397
|
+
|
|
398
|
+
1. 当您运行 `nodebbs start` 并选择环境(如 `production`)后,CLI 会在当前目录创建 `.nodebbs-env` 文件记录该选择。
|
|
399
|
+
2. 后续运行 `nodebbs logs`, `nodebbs status` 等命令时,会自动使用该环境,无需再次指定 `-e production`。
|
|
400
|
+
3. 运行 `nodebbs stop` 成功停止服务后,会自动删除 `.nodebbs-env` 文件。
|
|
401
|
+
|
|
402
|
+
**注意**:如果您需要临时操作其他环境,仍然可以使用 `-e` 参数强制指定,例如 `nodebbs logs -e basic`。
|
|
403
|
+
|
|
396
404
|
## 🛠️ 高级用法
|
|
397
405
|
|
|
398
406
|
### 内置模板
|
package/bin/dev.js
CHANGED
|
@@ -2,4 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import {execute} from '@oclif/core'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
if (process.argv.length <= 2) {
|
|
6
|
+
const {runInteractive} = await import('../src/interactive.ts')
|
|
7
|
+
await runInteractive(import.meta.url)
|
|
8
|
+
} else {
|
|
9
|
+
await execute({development: true, dir: import.meta.url})
|
|
10
|
+
}
|
package/bin/run.js
CHANGED
|
@@ -2,4 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import {execute} from '@oclif/core'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
if (process.argv.length <= 2) {
|
|
6
|
+
const {runInteractive} = await import('../dist/interactive.js')
|
|
7
|
+
await runInteractive(import.meta.url)
|
|
8
|
+
} else {
|
|
9
|
+
await execute({dir: import.meta.url})
|
|
10
|
+
}
|
|
@@ -27,7 +27,7 @@ export default class Clean extends Command {
|
|
|
27
27
|
async run() {
|
|
28
28
|
const { flags } = await this.parse(Clean);
|
|
29
29
|
let targets = [];
|
|
30
|
-
//
|
|
30
|
+
// 根据标志确定清理目标
|
|
31
31
|
if (flags.all) {
|
|
32
32
|
targets = ['cache', 'images', 'networks'];
|
|
33
33
|
}
|
|
@@ -37,7 +37,7 @@ export default class Clean extends Command {
|
|
|
37
37
|
if (flags.images)
|
|
38
38
|
targets.push('images');
|
|
39
39
|
}
|
|
40
|
-
//
|
|
40
|
+
// 如果未提供标志,进行交互式选择
|
|
41
41
|
if (targets.length === 0) {
|
|
42
42
|
targets = await checkbox({
|
|
43
43
|
message: '请选择要清理的项目:',
|
|
@@ -52,7 +52,7 @@ export default class Clean extends Command {
|
|
|
52
52
|
logger.info('未选择任何清理项目。');
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
-
//
|
|
55
|
+
// 确认提示
|
|
56
56
|
if (!flags.force) {
|
|
57
57
|
logger.warning(`即将清理: ${targets.join(', ')}`);
|
|
58
58
|
const confirmed = await confirm({
|
|
@@ -64,7 +64,7 @@ export default class Clean extends Command {
|
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
//
|
|
67
|
+
// 执行清理
|
|
68
68
|
try {
|
|
69
69
|
if (targets.includes('cache')) {
|
|
70
70
|
logger.info('正在清理构建缓存...');
|
|
@@ -17,33 +17,33 @@ export default class DbBackup extends Command {
|
|
|
17
17
|
};
|
|
18
18
|
async run() {
|
|
19
19
|
const { flags } = await this.parse(DbBackup);
|
|
20
|
-
// 1.
|
|
20
|
+
// 1. 选择环境
|
|
21
21
|
const env = await selectEnvironment(flags.env);
|
|
22
|
-
// 2.
|
|
22
|
+
// 2. 确定输出文件
|
|
23
23
|
let outputFile = flags.output;
|
|
24
24
|
if (!outputFile) {
|
|
25
25
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
26
26
|
outputFile = `backup_${timestamp}.sql`;
|
|
27
27
|
}
|
|
28
|
-
//
|
|
28
|
+
// 确保绝对路径
|
|
29
29
|
const outputPath = path.resolve(process.cwd(), outputFile);
|
|
30
|
-
// 3.
|
|
30
|
+
// 3. 获取 Compose 文件
|
|
31
31
|
const { files, isBuiltIn } = await getComposeFiles(env);
|
|
32
32
|
logger.info(`正在备份数据库到: ${outputPath}`);
|
|
33
33
|
logger.info('环境: ' + env);
|
|
34
|
-
//
|
|
34
|
+
// 加载环境配置
|
|
35
35
|
const envConfig = dotenv.config().parsed || {};
|
|
36
36
|
const dbUser = envConfig.POSTGRES_USER || 'postgres';
|
|
37
37
|
const dbName = envConfig.POSTGRES_DB || 'nodebbs';
|
|
38
38
|
const dbPassword = envConfig.POSTGRES_PASSWORD;
|
|
39
|
-
// 4.
|
|
39
|
+
// 4. 构建 Compose 参数
|
|
40
40
|
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
41
41
|
if (isBuiltIn) {
|
|
42
42
|
composeArgs.push('--project-directory', process.cwd());
|
|
43
43
|
}
|
|
44
|
-
// 5.
|
|
44
|
+
// 5. 运行 pg_dump
|
|
45
45
|
const dumpArgs = [...composeArgs, 'exec', '-T'];
|
|
46
|
-
//
|
|
46
|
+
// 如果有密码则注入
|
|
47
47
|
if (dbPassword) {
|
|
48
48
|
dumpArgs.push('-e', `PGPASSWORD=${dbPassword}`);
|
|
49
49
|
}
|
|
@@ -54,7 +54,7 @@ export default class DbBackup extends Command {
|
|
|
54
54
|
subprocess.stdout.pipe(fs.createWriteStream(outputPath));
|
|
55
55
|
}
|
|
56
56
|
await subprocess;
|
|
57
|
-
//
|
|
57
|
+
// 检查文件大小
|
|
58
58
|
const stats = fs.statSync(outputPath);
|
|
59
59
|
if (stats.size === 0) {
|
|
60
60
|
logger.error('备份文件为空,备份可能失败。');
|
|
@@ -68,7 +68,7 @@ export default class DbBackup extends Command {
|
|
|
68
68
|
if (error instanceof Error) {
|
|
69
69
|
logger.error(error.message);
|
|
70
70
|
}
|
|
71
|
-
//
|
|
71
|
+
// 如果失败则清理空文件
|
|
72
72
|
if (fs.existsSync(outputPath) && fs.statSync(outputPath).size === 0) {
|
|
73
73
|
fs.unlinkSync(outputPath);
|
|
74
74
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import Start from '../start/index.js';
|
|
3
|
+
export default class Rebuild extends Command {
|
|
4
|
+
static description = '重新构建并启动服务 (start --build)';
|
|
5
|
+
// Allow passing flags like -e to the underlying start command
|
|
6
|
+
static strict = false;
|
|
7
|
+
async run() {
|
|
8
|
+
// Invoke Start command with --build flag and any other arguments
|
|
9
|
+
await Start.run(['--build', ...this.argv]);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -9,7 +9,7 @@ export default class Restart extends Command {
|
|
|
9
9
|
};
|
|
10
10
|
async run() {
|
|
11
11
|
const { flags } = await this.parse(Restart);
|
|
12
|
-
// 1.
|
|
12
|
+
// 1. 选择环境
|
|
13
13
|
const env = await selectEnvironment(flags.env);
|
|
14
14
|
const { files, isBuiltIn } = await getComposeFiles(env);
|
|
15
15
|
logger.info('正在重启服务...');
|
|
@@ -4,9 +4,9 @@ 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 } from '../../utils/selection.js';
|
|
7
|
+
import { EnvFlag, selectEnvironment, setStoredEnv } from '../../utils/selection.js';
|
|
8
8
|
export default class Start extends Command {
|
|
9
|
-
static description = '
|
|
9
|
+
static description = '开始部署';
|
|
10
10
|
static flags = {
|
|
11
11
|
env: EnvFlag,
|
|
12
12
|
build: Flags.boolean({
|
|
@@ -23,9 +23,10 @@ export default class Start extends Command {
|
|
|
23
23
|
else {
|
|
24
24
|
logger.header('NodeBBS Docker 部署');
|
|
25
25
|
}
|
|
26
|
-
// 1.
|
|
26
|
+
// 1. 选择环境
|
|
27
27
|
const env = await selectEnvironment(flags.env);
|
|
28
|
-
|
|
28
|
+
await setStoredEnv(env);
|
|
29
|
+
// 2. 获取 Compose 文件
|
|
29
30
|
const { files: composeFiles, isBuiltIn } = await getComposeFiles(env);
|
|
30
31
|
if (isBuiltIn) {
|
|
31
32
|
logger.info('使用内置 Docker Compose 模板...');
|
|
@@ -45,9 +46,9 @@ export default class Start extends Command {
|
|
|
45
46
|
logger.warning('注意:无资源限制,不推荐用于生产环境。');
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
|
-
// 3.
|
|
49
|
+
// 3. 检查 Docker 和环境变量
|
|
49
50
|
await checkDocker();
|
|
50
|
-
// initEnv
|
|
51
|
+
// initEnv 保证 .env 存在(或退出)
|
|
51
52
|
await initEnv();
|
|
52
53
|
await checkEnv(env);
|
|
53
54
|
if (!flags.build) {
|
|
@@ -60,7 +61,7 @@ export default class Start extends Command {
|
|
|
60
61
|
this.exit(0);
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
|
-
// 4.
|
|
64
|
+
// 4. 构建并启动服务
|
|
64
65
|
if (flags.build) {
|
|
65
66
|
logger.info('正在重新构建并启动服务...');
|
|
66
67
|
await runCompose(composeFiles, ['up', '-d', '--build'], isBuiltIn);
|
|
@@ -74,7 +75,7 @@ export default class Start extends Command {
|
|
|
74
75
|
await runCompose(composeFiles, ['up', '-d'], isBuiltIn);
|
|
75
76
|
logger.success('服务启动指令已发送');
|
|
76
77
|
}
|
|
77
|
-
// 5.
|
|
78
|
+
// 5. 启动后操作 (仅完整部署)
|
|
78
79
|
if (!flags.build) {
|
|
79
80
|
await waitForHealth(composeFiles, isBuiltIn);
|
|
80
81
|
const pushDb = await confirm({
|
|
@@ -97,7 +98,7 @@ export default class Start extends Command {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
logger.header(flags.build ? '启动成功!' : 'NodeBBS 启动成功!');
|
|
100
|
-
// 6.
|
|
101
|
+
// 6. 显示信息
|
|
101
102
|
logger.info(`环境: ${env}`);
|
|
102
103
|
const envConfig = dotenv.config().parsed || {};
|
|
103
104
|
const webPort = envConfig.WEB_PORT || '3100';
|
|
@@ -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 } from '../../utils/selection.js';
|
|
5
|
+
import { EnvFlag, selectEnvironment, clearStoredEnv } from '../../utils/selection.js';
|
|
6
6
|
export default class Stop extends Command {
|
|
7
7
|
static description = '停止服务';
|
|
8
8
|
static flags = {
|
|
@@ -15,7 +15,7 @@ export default class Stop extends Command {
|
|
|
15
15
|
};
|
|
16
16
|
async run() {
|
|
17
17
|
const { flags } = await this.parse(Stop);
|
|
18
|
-
// 1.
|
|
18
|
+
// 1. 选择环境
|
|
19
19
|
const env = await selectEnvironment(flags.env, {
|
|
20
20
|
prompt: '请选择运行环境(停止服务需匹配启动环境):'
|
|
21
21
|
});
|
|
@@ -32,11 +32,13 @@ export default class Stop extends Command {
|
|
|
32
32
|
}
|
|
33
33
|
logger.info('正在停止服务并删除数据卷...');
|
|
34
34
|
await runCompose(files, ['down', '-v'], isBuiltIn);
|
|
35
|
+
await clearStoredEnv();
|
|
35
36
|
logger.success('服务已停止,数据卷已删除');
|
|
36
37
|
}
|
|
37
38
|
else {
|
|
38
39
|
logger.info('正在停止服务...');
|
|
39
40
|
await runCompose(files, ['down'], isBuiltIn);
|
|
41
|
+
await clearStoredEnv();
|
|
40
42
|
logger.success('服务已停止');
|
|
41
43
|
}
|
|
42
44
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runInteractive(root: string): Promise<void>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Config } from '@oclif/core';
|
|
2
|
+
import { select, Separator } from '@inquirer/prompts';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
export async function runInteractive(root) {
|
|
5
|
+
// Config.load 需要文件路径而不是 URL
|
|
6
|
+
// 如果 root 是 import.meta.url,则进行转换
|
|
7
|
+
let rootPath = root.startsWith('file://') ? fileURLToPath(root) : root;
|
|
8
|
+
const config = await Config.load(rootPath);
|
|
9
|
+
// 构建命令树
|
|
10
|
+
const tree = { name: 'root', children: {} };
|
|
11
|
+
for (const cmd of config.commands) {
|
|
12
|
+
// 跳过隐藏命令
|
|
13
|
+
if (cmd.hidden)
|
|
14
|
+
continue;
|
|
15
|
+
const parts = cmd.id.split(':');
|
|
16
|
+
let currentNode = tree;
|
|
17
|
+
for (let i = 0; i < parts.length; i++) {
|
|
18
|
+
const part = parts[i];
|
|
19
|
+
if (!currentNode.children[part]) {
|
|
20
|
+
currentNode.children[part] = {
|
|
21
|
+
name: part,
|
|
22
|
+
children: {}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
currentNode = currentNode.children[part];
|
|
26
|
+
}
|
|
27
|
+
// 将命令分配给叶节点
|
|
28
|
+
currentNode.command = cmd;
|
|
29
|
+
}
|
|
30
|
+
await navigate(tree, [], config);
|
|
31
|
+
}
|
|
32
|
+
async function navigate(node, breadcrumbs, config) {
|
|
33
|
+
const priorityOrder = ['start', 'rebuild', 'stop', 'restart', 'status', 'logs', 'shell', 'db'];
|
|
34
|
+
while (true) {
|
|
35
|
+
const keys = Object.keys(node.children).sort((a, b) => {
|
|
36
|
+
const indexA = priorityOrder.indexOf(a);
|
|
37
|
+
const indexB = priorityOrder.indexOf(b);
|
|
38
|
+
// If both are in priority list, sort by index
|
|
39
|
+
if (indexA !== -1 && indexB !== -1)
|
|
40
|
+
return indexA - indexB;
|
|
41
|
+
// If only A is in priority list, A comes first
|
|
42
|
+
if (indexA !== -1)
|
|
43
|
+
return -1;
|
|
44
|
+
// If only B is in priority list, B comes first
|
|
45
|
+
if (indexB !== -1)
|
|
46
|
+
return 1;
|
|
47
|
+
// Otherwise sort alphabetically
|
|
48
|
+
return a.localeCompare(b);
|
|
49
|
+
});
|
|
50
|
+
const choices = keys.map(key => {
|
|
51
|
+
const child = node.children[key];
|
|
52
|
+
const hasSubcommands = Object.keys(child.children).length > 0;
|
|
53
|
+
let label = key;
|
|
54
|
+
let description = '';
|
|
55
|
+
// 尝试获取命令描述
|
|
56
|
+
if (key === 'help') {
|
|
57
|
+
description = '显示帮助信息';
|
|
58
|
+
}
|
|
59
|
+
else if (child.command?.description) {
|
|
60
|
+
description = child.command.description;
|
|
61
|
+
}
|
|
62
|
+
// 或者获取主题描述
|
|
63
|
+
else if (hasSubcommands) {
|
|
64
|
+
const fullId = [...breadcrumbs, key].join(':');
|
|
65
|
+
const topic = config.topics.find(t => t.name === fullId);
|
|
66
|
+
if (topic) {
|
|
67
|
+
description = topic.description || '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (description) {
|
|
71
|
+
const shortDesc = description.split('.')[0].substring(0, 50);
|
|
72
|
+
label = `${key.padEnd(12)} ${shortDesc}`;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
label = key.padEnd(12);
|
|
76
|
+
}
|
|
77
|
+
if (hasSubcommands) {
|
|
78
|
+
label = `${label} [+]`;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
name: label,
|
|
82
|
+
value: key,
|
|
83
|
+
short: key
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
// 添加导航选项
|
|
87
|
+
// 即使支持 Esc,保留显式选项也有助于发现性
|
|
88
|
+
if (breadcrumbs.length > 0) {
|
|
89
|
+
choices.push(new Separator());
|
|
90
|
+
choices.push({ name: '⬅️ 返回', value: '__BACK__', short: '返回' });
|
|
91
|
+
}
|
|
92
|
+
choices.push({ name: '❌ 退出', value: '__EXIT__', short: '退出' });
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
// 监听 Esc 键
|
|
95
|
+
const onKeypress = (_str, key) => {
|
|
96
|
+
if (key && key.name === 'escape') {
|
|
97
|
+
controller.abort();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
// 确保 keypress 事件被触发(inquirer 内部也会这样做,但为了安全起见)
|
|
101
|
+
// 注意:process.stdin 可能已经是 raw mode,inquirer 会处理它
|
|
102
|
+
// 我们只需要监听
|
|
103
|
+
process.stdin.on('keypress', onKeypress);
|
|
104
|
+
try {
|
|
105
|
+
const selection = await select({
|
|
106
|
+
message: breadcrumbs.length ? `选择命令 (${breadcrumbs.join(' > ')}):` : '选择命令:',
|
|
107
|
+
choices,
|
|
108
|
+
pageSize: 15,
|
|
109
|
+
loop: true, // 允许循环导航
|
|
110
|
+
// @ts-ignore: prompt signal support might be strict on types
|
|
111
|
+
signal: controller.signal
|
|
112
|
+
});
|
|
113
|
+
if (selection === '__EXIT__') {
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
if (selection === '__BACK__') {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const selectedNode = node.children[selection];
|
|
120
|
+
if (Object.keys(selectedNode.children).length > 0) {
|
|
121
|
+
// 有子命令,进入子菜单
|
|
122
|
+
await navigate(selectedNode, [...breadcrumbs, selection], config);
|
|
123
|
+
}
|
|
124
|
+
else if (selectedNode.command) {
|
|
125
|
+
// 可运行的命令
|
|
126
|
+
console.log(`正在运行: ${selectedNode.command.id}`);
|
|
127
|
+
try {
|
|
128
|
+
await config.runCommand(selectedNode.command.id);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
// Ctrl+C: 退出程序
|
|
132
|
+
if (error.name === 'ExitPromptError') {
|
|
133
|
+
console.log('\n用户退出。');
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
// 手动取消: 返回菜单
|
|
137
|
+
if (error.name === 'CancelError') {
|
|
138
|
+
console.log('\n操作已取消。');
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
console.error(error);
|
|
142
|
+
}
|
|
143
|
+
console.log('\n命令执行完成。');
|
|
144
|
+
// 动态导入 input 以保持轻量
|
|
145
|
+
const { input } = await import('@inquirer/prompts');
|
|
146
|
+
await input({ message: '按回车键继续...' });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
// 处理 AbortError (Esc)
|
|
151
|
+
if (error.name === 'AbortError' || error.message?.includes('aborted')) {
|
|
152
|
+
if (breadcrumbs.length > 0) {
|
|
153
|
+
// 子菜单 -> 返回
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// 顶级菜单 -> 退出
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// 处理 Ctrl+C (ExitPromptError)
|
|
162
|
+
if (error.name === 'ExitPromptError') {
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
process.stdin.off('keypress', onKeypress);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
package/dist/utils/docker.js
CHANGED
|
@@ -8,11 +8,11 @@ const fileExists = promisify(exists);
|
|
|
8
8
|
export async function getComposeFiles(env) {
|
|
9
9
|
const workDir = process.cwd();
|
|
10
10
|
let isBuiltIn = false;
|
|
11
|
-
//
|
|
11
|
+
// 检查当前目录是否存在 docker-compose.yml
|
|
12
12
|
let baseFile = path.join(workDir, 'docker-compose.yml');
|
|
13
13
|
let templateDir = workDir;
|
|
14
14
|
if (!await fileExists(baseFile)) {
|
|
15
|
-
//
|
|
15
|
+
// 使用内置模板
|
|
16
16
|
isBuiltIn = true;
|
|
17
17
|
templateDir = getTemplateDir();
|
|
18
18
|
baseFile = getTemplatePath('docker-compose.yml');
|
|
@@ -42,12 +42,12 @@ export async function runCompose(files, args, isBuiltIn = false) {
|
|
|
42
42
|
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
43
43
|
if (isBuiltIn) {
|
|
44
44
|
composeArgs.push('--project-directory', process.cwd());
|
|
45
|
-
//
|
|
45
|
+
// 将 INIT_DB_PATH 设置为内置 sql 文件
|
|
46
46
|
const templateDir = path.dirname(files[0]);
|
|
47
47
|
process.env.INIT_DB_PATH = path.join(templateDir, 'init-db.sql');
|
|
48
48
|
}
|
|
49
49
|
composeArgs.push(...args);
|
|
50
|
-
//
|
|
50
|
+
// 使用 stdio: 'inherit' 实时显示输出
|
|
51
51
|
await execa('docker', ['compose', ...composeArgs], { stdio: 'inherit' });
|
|
52
52
|
}
|
|
53
53
|
export async function execCompose(files, service, command, isBuiltIn = false) {
|
|
@@ -60,7 +60,7 @@ export async function execCompose(files, service, command, isBuiltIn = false) {
|
|
|
60
60
|
}
|
|
61
61
|
export async function waitForHealth(files, isBuiltIn = false) {
|
|
62
62
|
logger.info('正在等待服务就绪...');
|
|
63
|
-
//
|
|
63
|
+
// 等待 Postgres
|
|
64
64
|
logger.info('等待 PostgreSQL...');
|
|
65
65
|
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
66
66
|
if (isBuiltIn) {
|
|
@@ -82,11 +82,11 @@ export async function waitForHealth(files, isBuiltIn = false) {
|
|
|
82
82
|
if (retries === 0) {
|
|
83
83
|
logger.warning('PostgreSQL 可能尚未就绪');
|
|
84
84
|
}
|
|
85
|
-
//
|
|
85
|
+
// 等待 Redis
|
|
86
86
|
logger.info('等待 Redis...');
|
|
87
87
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
88
88
|
logger.success('Redis 已就绪');
|
|
89
|
-
//
|
|
89
|
+
// 等待 API
|
|
90
90
|
logger.info('等待 API 服务...');
|
|
91
91
|
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
92
92
|
logger.success('API 服务已就绪');
|
package/dist/utils/env.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function initEnv() {
|
|
|
18
18
|
sourceFile = '.env.docker.example';
|
|
19
19
|
}
|
|
20
20
|
else {
|
|
21
|
-
//
|
|
21
|
+
// 使用内置模板
|
|
22
22
|
sourceFile = getTemplatePath('env');
|
|
23
23
|
isBuiltIn = true;
|
|
24
24
|
}
|
|
@@ -49,7 +49,7 @@ export async function initEnv() {
|
|
|
49
49
|
}
|
|
50
50
|
export async function checkEnv(envType) {
|
|
51
51
|
logger.info('正在检查环境配置...');
|
|
52
|
-
//
|
|
52
|
+
// 加载 .env
|
|
53
53
|
const envConfig = dotenv.config().parsed || {};
|
|
54
54
|
let warnings = 0;
|
|
55
55
|
let errors = 0;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type EnvType = 'production' | 'lowmem' | 'basic';
|
|
2
2
|
export declare const EnvFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
3
|
+
export declare function setStoredEnv(env: EnvType): Promise<void>;
|
|
4
|
+
export declare function clearStoredEnv(): Promise<void>;
|
|
3
5
|
export declare function selectEnvironment(env?: string, options?: {
|
|
4
6
|
prompt?: string;
|
|
5
7
|
}): Promise<EnvType>;
|
package/dist/utils/selection.js
CHANGED
|
@@ -1,24 +1,68 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { select } from '@inquirer/prompts';
|
|
3
3
|
import { logger } from './logger.js';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
const ENV_MARKER_FILE = '.nodebbs-env';
|
|
4
7
|
export const EnvFlag = Flags.string({
|
|
5
8
|
char: 'e',
|
|
6
9
|
description: '部署环境 (production, lowmem, basic)',
|
|
7
10
|
options: ['production', 'lowmem', 'basic'],
|
|
8
11
|
});
|
|
12
|
+
async function getStoredEnv() {
|
|
13
|
+
try {
|
|
14
|
+
const content = await fs.readFile(path.resolve(process.cwd(), ENV_MARKER_FILE), 'utf-8');
|
|
15
|
+
const env = content.trim();
|
|
16
|
+
if (['production', 'lowmem', 'basic'].includes(env)) {
|
|
17
|
+
return env;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch { }
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export async function setStoredEnv(env) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.writeFile(path.resolve(process.cwd(), ENV_MARKER_FILE), env, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
logger.warning(`无法保存环境标记: ${error}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function clearStoredEnv() {
|
|
32
|
+
try {
|
|
33
|
+
const filepath = path.resolve(process.cwd(), ENV_MARKER_FILE);
|
|
34
|
+
await fs.rm(filepath, { force: true });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger.warning(`无法清除环境标记: ${error}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
9
40
|
export async function selectEnvironment(env, options = {}) {
|
|
10
41
|
if (env) {
|
|
11
|
-
// oclif
|
|
42
|
+
// oclif 选项验证会处理有效值,但仍需类型转换
|
|
12
43
|
return env;
|
|
13
44
|
}
|
|
45
|
+
// 检查已存储的环境配置
|
|
46
|
+
const storedEnv = await getStoredEnv();
|
|
47
|
+
if (storedEnv) {
|
|
48
|
+
logger.info(`检测到运行环境: ${storedEnv}`);
|
|
49
|
+
return storedEnv;
|
|
50
|
+
}
|
|
14
51
|
const selected = await select({
|
|
15
52
|
message: options.prompt || '请选择运行环境:',
|
|
16
53
|
choices: [
|
|
17
54
|
{ name: '标准生产环境 (2C4G+) [推荐]', value: 'production' },
|
|
18
55
|
{ name: '低配环境 (1C1G/1C2G)', value: 'lowmem' },
|
|
19
56
|
{ name: '基础环境 (仅用于测试)', value: 'basic' },
|
|
57
|
+
{ name: '❌ 取消', value: '__CANCEL__' },
|
|
20
58
|
],
|
|
59
|
+
loop: true,
|
|
21
60
|
});
|
|
61
|
+
if (selected === '__CANCEL__') {
|
|
62
|
+
const error = new Error('用户取消操作');
|
|
63
|
+
error.name = 'CancelError';
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
22
66
|
logger.info(`已选择环境: ${selected}`);
|
|
23
67
|
return selected;
|
|
24
68
|
}
|
package/dist/utils/template.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 获取模板目录的绝对路径。
|
|
3
|
+
* 在生产环境 (dist/utils) 中,模板位于 dist/templates。
|
|
4
|
+
* 在开发环境 (src/utils) 中,模板位于 src/templates。
|
|
5
5
|
*/
|
|
6
6
|
export declare function getTemplateDir(): string;
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* @param fileName
|
|
8
|
+
* 获取特定模板文件的绝对路径。
|
|
9
|
+
* @param fileName 模板文件名称 (例如 'env', 'docker-compose.yml')
|
|
10
10
|
*/
|
|
11
11
|
export declare function getTemplatePath(fileName: string): string;
|
package/dist/utils/template.js
CHANGED
|
@@ -2,16 +2,16 @@ import path from 'node:path';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 获取模板目录的绝对路径。
|
|
6
|
+
* 在生产环境 (dist/utils) 中,模板位于 dist/templates。
|
|
7
|
+
* 在开发环境 (src/utils) 中,模板位于 src/templates。
|
|
8
8
|
*/
|
|
9
9
|
export function getTemplateDir() {
|
|
10
10
|
return path.join(__dirname, '..', 'templates');
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
* @param fileName
|
|
13
|
+
* 获取特定模板文件的绝对路径。
|
|
14
|
+
* @param fileName 模板文件名称 (例如 'env', 'docker-compose.yml')
|
|
15
15
|
*/
|
|
16
16
|
export function getTemplatePath(fileName) {
|
|
17
17
|
return path.join(getTemplateDir(), fileName);
|
package/oclif.manifest.json
CHANGED
|
@@ -126,41 +126,6 @@
|
|
|
126
126
|
"generate.js"
|
|
127
127
|
]
|
|
128
128
|
},
|
|
129
|
-
"db": {
|
|
130
|
-
"aliases": [],
|
|
131
|
-
"args": {},
|
|
132
|
-
"description": "数据库管理命令",
|
|
133
|
-
"flags": {
|
|
134
|
-
"env": {
|
|
135
|
-
"char": "e",
|
|
136
|
-
"description": "部署环境 (production, lowmem, basic)",
|
|
137
|
-
"name": "env",
|
|
138
|
-
"hasDynamicHelp": false,
|
|
139
|
-
"multiple": false,
|
|
140
|
-
"options": [
|
|
141
|
-
"production",
|
|
142
|
-
"lowmem",
|
|
143
|
-
"basic"
|
|
144
|
-
],
|
|
145
|
-
"type": "option"
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
"hasDynamicHelp": false,
|
|
149
|
-
"hiddenAliases": [],
|
|
150
|
-
"id": "db",
|
|
151
|
-
"pluginAlias": "nodebbs",
|
|
152
|
-
"pluginName": "nodebbs",
|
|
153
|
-
"pluginType": "core",
|
|
154
|
-
"strict": true,
|
|
155
|
-
"enableJsonFlag": false,
|
|
156
|
-
"isESM": true,
|
|
157
|
-
"relativePath": [
|
|
158
|
-
"dist",
|
|
159
|
-
"commands",
|
|
160
|
-
"db",
|
|
161
|
-
"index.js"
|
|
162
|
-
]
|
|
163
|
-
},
|
|
164
129
|
"db:migrate": {
|
|
165
130
|
"aliases": [],
|
|
166
131
|
"args": {},
|
|
@@ -476,6 +441,27 @@
|
|
|
476
441
|
"web.js"
|
|
477
442
|
]
|
|
478
443
|
},
|
|
444
|
+
"rebuild": {
|
|
445
|
+
"aliases": [],
|
|
446
|
+
"args": {},
|
|
447
|
+
"description": "重新构建并启动服务 (start --build)",
|
|
448
|
+
"flags": {},
|
|
449
|
+
"hasDynamicHelp": false,
|
|
450
|
+
"hiddenAliases": [],
|
|
451
|
+
"id": "rebuild",
|
|
452
|
+
"pluginAlias": "nodebbs",
|
|
453
|
+
"pluginName": "nodebbs",
|
|
454
|
+
"pluginType": "core",
|
|
455
|
+
"strict": false,
|
|
456
|
+
"enableJsonFlag": false,
|
|
457
|
+
"isESM": true,
|
|
458
|
+
"relativePath": [
|
|
459
|
+
"dist",
|
|
460
|
+
"commands",
|
|
461
|
+
"rebuild",
|
|
462
|
+
"index.js"
|
|
463
|
+
]
|
|
464
|
+
},
|
|
479
465
|
"restart": {
|
|
480
466
|
"aliases": [],
|
|
481
467
|
"args": {},
|
|
@@ -511,10 +497,10 @@
|
|
|
511
497
|
"index.js"
|
|
512
498
|
]
|
|
513
499
|
},
|
|
514
|
-
"
|
|
500
|
+
"start": {
|
|
515
501
|
"aliases": [],
|
|
516
502
|
"args": {},
|
|
517
|
-
"description": "
|
|
503
|
+
"description": "开始部署",
|
|
518
504
|
"flags": {
|
|
519
505
|
"env": {
|
|
520
506
|
"char": "e",
|
|
@@ -528,11 +514,18 @@
|
|
|
528
514
|
"basic"
|
|
529
515
|
],
|
|
530
516
|
"type": "option"
|
|
517
|
+
},
|
|
518
|
+
"build": {
|
|
519
|
+
"char": "b",
|
|
520
|
+
"description": "重新构建并启动服务 (跳过健康检查和数据初始化)",
|
|
521
|
+
"name": "build",
|
|
522
|
+
"allowNo": false,
|
|
523
|
+
"type": "boolean"
|
|
531
524
|
}
|
|
532
525
|
},
|
|
533
526
|
"hasDynamicHelp": false,
|
|
534
527
|
"hiddenAliases": [],
|
|
535
|
-
"id": "
|
|
528
|
+
"id": "start",
|
|
536
529
|
"pluginAlias": "nodebbs",
|
|
537
530
|
"pluginName": "nodebbs",
|
|
538
531
|
"pluginType": "core",
|
|
@@ -542,14 +535,14 @@
|
|
|
542
535
|
"relativePath": [
|
|
543
536
|
"dist",
|
|
544
537
|
"commands",
|
|
545
|
-
"
|
|
546
|
-
"
|
|
538
|
+
"start",
|
|
539
|
+
"index.js"
|
|
547
540
|
]
|
|
548
541
|
},
|
|
549
|
-
"shell:
|
|
542
|
+
"shell:api": {
|
|
550
543
|
"aliases": [],
|
|
551
544
|
"args": {},
|
|
552
|
-
"description": "
|
|
545
|
+
"description": "进入 API 服务 shell",
|
|
553
546
|
"flags": {
|
|
554
547
|
"env": {
|
|
555
548
|
"char": "e",
|
|
@@ -567,7 +560,7 @@
|
|
|
567
560
|
},
|
|
568
561
|
"hasDynamicHelp": false,
|
|
569
562
|
"hiddenAliases": [],
|
|
570
|
-
"id": "shell:
|
|
563
|
+
"id": "shell:api",
|
|
571
564
|
"pluginAlias": "nodebbs",
|
|
572
565
|
"pluginName": "nodebbs",
|
|
573
566
|
"pluginType": "core",
|
|
@@ -578,13 +571,13 @@
|
|
|
578
571
|
"dist",
|
|
579
572
|
"commands",
|
|
580
573
|
"shell",
|
|
581
|
-
"
|
|
574
|
+
"api.js"
|
|
582
575
|
]
|
|
583
576
|
},
|
|
584
|
-
"shell:
|
|
577
|
+
"shell:db": {
|
|
585
578
|
"aliases": [],
|
|
586
579
|
"args": {},
|
|
587
|
-
"description": "
|
|
580
|
+
"description": "进入数据库 shell (psql)",
|
|
588
581
|
"flags": {
|
|
589
582
|
"env": {
|
|
590
583
|
"char": "e",
|
|
@@ -602,7 +595,7 @@
|
|
|
602
595
|
},
|
|
603
596
|
"hasDynamicHelp": false,
|
|
604
597
|
"hiddenAliases": [],
|
|
605
|
-
"id": "shell:
|
|
598
|
+
"id": "shell:db",
|
|
606
599
|
"pluginAlias": "nodebbs",
|
|
607
600
|
"pluginName": "nodebbs",
|
|
608
601
|
"pluginType": "core",
|
|
@@ -613,13 +606,13 @@
|
|
|
613
606
|
"dist",
|
|
614
607
|
"commands",
|
|
615
608
|
"shell",
|
|
616
|
-
"
|
|
609
|
+
"db.js"
|
|
617
610
|
]
|
|
618
611
|
},
|
|
619
|
-
"shell:
|
|
612
|
+
"shell:redis": {
|
|
620
613
|
"aliases": [],
|
|
621
614
|
"args": {},
|
|
622
|
-
"description": "进入
|
|
615
|
+
"description": "进入 Redis shell (redis-cli)",
|
|
623
616
|
"flags": {
|
|
624
617
|
"env": {
|
|
625
618
|
"char": "e",
|
|
@@ -637,7 +630,7 @@
|
|
|
637
630
|
},
|
|
638
631
|
"hasDynamicHelp": false,
|
|
639
632
|
"hiddenAliases": [],
|
|
640
|
-
"id": "shell:
|
|
633
|
+
"id": "shell:redis",
|
|
641
634
|
"pluginAlias": "nodebbs",
|
|
642
635
|
"pluginName": "nodebbs",
|
|
643
636
|
"pluginType": "core",
|
|
@@ -648,13 +641,13 @@
|
|
|
648
641
|
"dist",
|
|
649
642
|
"commands",
|
|
650
643
|
"shell",
|
|
651
|
-
"
|
|
644
|
+
"redis.js"
|
|
652
645
|
]
|
|
653
646
|
},
|
|
654
|
-
"
|
|
647
|
+
"shell:web": {
|
|
655
648
|
"aliases": [],
|
|
656
649
|
"args": {},
|
|
657
|
-
"description": "
|
|
650
|
+
"description": "进入 Web 前端 shell",
|
|
658
651
|
"flags": {
|
|
659
652
|
"env": {
|
|
660
653
|
"char": "e",
|
|
@@ -668,18 +661,11 @@
|
|
|
668
661
|
"basic"
|
|
669
662
|
],
|
|
670
663
|
"type": "option"
|
|
671
|
-
},
|
|
672
|
-
"build": {
|
|
673
|
-
"char": "b",
|
|
674
|
-
"description": "重新构建并启动服务 (跳过健康检查和数据初始化)",
|
|
675
|
-
"name": "build",
|
|
676
|
-
"allowNo": false,
|
|
677
|
-
"type": "boolean"
|
|
678
664
|
}
|
|
679
665
|
},
|
|
680
666
|
"hasDynamicHelp": false,
|
|
681
667
|
"hiddenAliases": [],
|
|
682
|
-
"id": "
|
|
668
|
+
"id": "shell:web",
|
|
683
669
|
"pluginAlias": "nodebbs",
|
|
684
670
|
"pluginName": "nodebbs",
|
|
685
671
|
"pluginType": "core",
|
|
@@ -689,8 +675,8 @@
|
|
|
689
675
|
"relativePath": [
|
|
690
676
|
"dist",
|
|
691
677
|
"commands",
|
|
692
|
-
"
|
|
693
|
-
"
|
|
678
|
+
"shell",
|
|
679
|
+
"web.js"
|
|
694
680
|
]
|
|
695
681
|
},
|
|
696
682
|
"status": {
|
|
@@ -771,5 +757,5 @@
|
|
|
771
757
|
]
|
|
772
758
|
}
|
|
773
759
|
},
|
|
774
|
-
"version": "0.0.
|
|
760
|
+
"version": "0.0.4"
|
|
775
761
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebbs",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.0.
|
|
3
|
+
"description": "NodeBBS 论坛系统专业运维工具",
|
|
4
|
+
"version": "0.0.4",
|
|
5
5
|
"author": "wengqianshan",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nodebbs": "./bin/run.js"
|
|
@@ -54,13 +54,18 @@
|
|
|
54
54
|
"dirname": "nodebbs",
|
|
55
55
|
"commands": "./dist/commands",
|
|
56
56
|
"plugins": [
|
|
57
|
-
"@oclif/plugin-help"
|
|
58
|
-
"@oclif/plugin-plugins"
|
|
57
|
+
"@oclif/plugin-help"
|
|
59
58
|
],
|
|
60
59
|
"topicSeparator": " ",
|
|
61
60
|
"topics": {
|
|
62
61
|
"hello": {
|
|
63
|
-
"description": "
|
|
62
|
+
"description": "向世界和他人问好"
|
|
63
|
+
},
|
|
64
|
+
"db": {
|
|
65
|
+
"description": "数据库操作 (备份, 迁移, 种子数据等)"
|
|
66
|
+
},
|
|
67
|
+
"shell": {
|
|
68
|
+
"description": "服务的交互式 Shell"
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
},
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
|
-
export default class Db extends Command {
|
|
3
|
-
static description: string;
|
|
4
|
-
static flags: {
|
|
5
|
-
env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
-
};
|
|
7
|
-
run(): Promise<void>;
|
|
8
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { Command } from '@oclif/core';
|
|
2
|
-
import { execCompose, getComposeFiles } from '../../utils/docker.js';
|
|
3
|
-
import { logger } from '../../utils/logger.js';
|
|
4
|
-
import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
|
|
5
|
-
export default class Db extends Command {
|
|
6
|
-
static description = '数据库管理命令';
|
|
7
|
-
static flags = {
|
|
8
|
-
env: EnvFlag,
|
|
9
|
-
};
|
|
10
|
-
async run() {
|
|
11
|
-
const { flags } = await this.parse(Db);
|
|
12
|
-
const env = await selectEnvironment(flags.env);
|
|
13
|
-
const { files, isBuiltIn } = await getComposeFiles(env);
|
|
14
|
-
logger.info('正在进入数据库 shell...');
|
|
15
|
-
await execCompose(files, 'api', ['npm', 'run', 'db:studio'], isBuiltIn);
|
|
16
|
-
}
|
|
17
|
-
}
|