nodebbs 0.0.8 → 0.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/README.md +19 -17
- package/dist/commands/clean/index.d.ts +1 -0
- package/dist/commands/clean/index.js +15 -2
- package/dist/commands/db/import.d.ts +10 -0
- package/dist/commands/db/import.js +87 -0
- package/dist/commands/pack/index.js +5 -11
- package/dist/commands/rebuild/index.js +3 -3
- package/dist/commands/restart/index.js +1 -2
- package/dist/commands/start/index.d.ts +1 -1
- package/dist/commands/start/index.js +71 -63
- package/dist/commands/stop/index.js +1 -3
- package/dist/interactive.js +2 -2
- package/dist/templates/docker-compose.lowmem.yml +3 -49
- package/dist/templates/docker-compose.yml +91 -34
- package/dist/templates/env +26 -11
- package/dist/utils/docker.d.ts +47 -0
- package/dist/utils/docker.js +57 -6
- package/dist/utils/env.d.ts +1 -1
- package/dist/utils/env.js +102 -20
- package/dist/utils/selection.d.ts +1 -1
- package/dist/utils/selection.js +7 -7
- package/oclif.manifest.json +100 -60
- package/package.json +1 -1
- package/dist/templates/docker-compose.prod.yml +0 -120
- package/dist/templates/init-db.sql +0 -14
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ NodeBBS CLI 是一个专为全栈开发者设计的命令行工具,用于简
|
|
|
15
15
|
- 🎯 **全栈友好** - 命令设计贴近开发者思维
|
|
16
16
|
- 📊 **实时日志** - 方便查看各服务日志
|
|
17
17
|
- 💾 **数据库管理** - 内置数据库迁移和管理工具
|
|
18
|
-
- 🌐 **内置模板** -
|
|
18
|
+
- 🌐 **内置模板** - 无需本地配置文件即可通过远程镜像快速部署
|
|
19
19
|
|
|
20
20
|
## 📦 安装
|
|
21
21
|
|
|
@@ -46,7 +46,7 @@ npx nodebbs [command]
|
|
|
46
46
|
npx nodebbs
|
|
47
47
|
```
|
|
48
48
|
- **start**: 开始部署(首次使用推荐选择此项)
|
|
49
|
-
- **rebuild**:
|
|
49
|
+
- **rebuild**: 更新并重启(Pull & Restart,代码/镜像更新后使用)
|
|
50
50
|
|
|
51
51
|
## 📚 命令参考
|
|
52
52
|
|
|
@@ -60,21 +60,25 @@ npx nodebbs start
|
|
|
60
60
|
# 指定环境启动
|
|
61
61
|
npx nodebbs start -e production
|
|
62
62
|
|
|
63
|
-
# 重新构建并启动(跳过检查)
|
|
64
|
-
npx nodebbs start --build
|
|
65
|
-
```
|
|
66
|
-
|
|
67
63
|
**参数**:
|
|
68
|
-
- `-e, --env` - 部署环境(production, lowmem
|
|
69
|
-
- `-
|
|
64
|
+
- `-e, --env` - 部署环境(production, lowmem)
|
|
65
|
+
- `-t, --tag` - 镜像版本 tag (默认: latest)
|
|
70
66
|
|
|
71
67
|
启动流程包含:
|
|
72
68
|
- Docker 环境检查
|
|
73
69
|
- 环境变量验证
|
|
74
|
-
-
|
|
70
|
+
- 拉取最新镜像
|
|
75
71
|
- 服务启动
|
|
76
|
-
-
|
|
77
|
-
-
|
|
72
|
+
- 健康检查(默认开启)
|
|
73
|
+
- 数据库初始化(默认开启)
|
|
74
|
+
|
|
75
|
+
#### `nodebbs rebuild`
|
|
76
|
+
拉取最新镜像并重启服务 (Update & Restart)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx nodebbs rebuild
|
|
80
|
+
```
|
|
81
|
+
> 此命令是 `start` 的别名,用于快速更新部署。
|
|
78
82
|
|
|
79
83
|
#### `nodebbs restart`
|
|
80
84
|
重启所有服务(不更新镜像,支持环境选择)
|
|
@@ -272,9 +276,8 @@ vi .env
|
|
|
272
276
|
|
|
273
277
|
### 支持的环境
|
|
274
278
|
|
|
275
|
-
- **basic** - 基础环境(仅用于测试)
|
|
276
279
|
- **lowmem** - 低配环境(1C1G/1C2G)
|
|
277
|
-
- **production** -
|
|
280
|
+
- **production** - 生产环境(标准配置,推荐)
|
|
278
281
|
|
|
279
282
|
### 环境变量
|
|
280
283
|
|
|
@@ -309,7 +312,7 @@ CLI 会自动记住您上次启动的环境:
|
|
|
309
312
|
2. 后续运行 `nodebbs logs`, `nodebbs status` 等命令时,会自动使用该环境,无需再次指定 `-e production`。
|
|
310
313
|
3. 运行 `nodebbs stop` 成功停止服务后,会自动删除 `.nodebbs-env` 文件。
|
|
311
314
|
|
|
312
|
-
**注意**:如果您需要临时操作其他环境,仍然可以使用 `-e` 参数强制指定,例如 `nodebbs logs -e
|
|
315
|
+
**注意**:如果您需要临时操作其他环境,仍然可以使用 `-e` 参数强制指定,例如 `nodebbs logs -e lowmem`。
|
|
313
316
|
|
|
314
317
|
## 🛠️ 高级用法
|
|
315
318
|
|
|
@@ -330,9 +333,8 @@ CLI 会自动:
|
|
|
330
333
|
### 自定义配置
|
|
331
334
|
|
|
332
335
|
如果需要自定义配置,在项目根目录创建:
|
|
333
|
-
- `docker-compose.yml` -
|
|
334
|
-
- `docker-compose.
|
|
335
|
-
- `docker-compose.lowmem.yml` - 低配环境配置
|
|
336
|
+
- `docker-compose.yml` - 基础/生产环境配置
|
|
337
|
+
- `docker-compose.lowmem.yml` - 低配环境配置 (Override)
|
|
336
338
|
|
|
337
339
|
CLI 会优先使用本地配置文件。
|
|
338
340
|
|
|
@@ -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('清理过程中发生错误');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class DbImport extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
file: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { confirm } from '@inquirer/prompts';
|
|
3
|
+
import { getComposeFiles } from '../../utils/docker.js';
|
|
4
|
+
import { logger } from '../../utils/logger.js';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import dotenv from 'dotenv';
|
|
9
|
+
import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
|
|
10
|
+
export default class DbImport extends Command {
|
|
11
|
+
static description = '导入数据库 (PostgreSQL)';
|
|
12
|
+
static flags = {
|
|
13
|
+
env: EnvFlag,
|
|
14
|
+
file: Flags.string({
|
|
15
|
+
char: 'f',
|
|
16
|
+
description: 'SQL 备份文件路径',
|
|
17
|
+
required: true,
|
|
18
|
+
}),
|
|
19
|
+
yes: Flags.boolean({
|
|
20
|
+
char: 'y',
|
|
21
|
+
description: '跳过确认提示',
|
|
22
|
+
default: false,
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
async run() {
|
|
26
|
+
const { flags } = await this.parse(DbImport);
|
|
27
|
+
// 1. 选择环境
|
|
28
|
+
const env = await selectEnvironment(flags.env);
|
|
29
|
+
// 2. 验证文件
|
|
30
|
+
const inputPath = path.resolve(process.cwd(), flags.file);
|
|
31
|
+
if (!fs.existsSync(inputPath)) {
|
|
32
|
+
logger.error(`找不到备份文件: ${inputPath}`);
|
|
33
|
+
this.exit(1);
|
|
34
|
+
}
|
|
35
|
+
// 3. 用户确认
|
|
36
|
+
if (!flags.yes) {
|
|
37
|
+
logger.warning('警告:此操作将覆盖当前数据库中的数据!');
|
|
38
|
+
const confirmImport = await confirm({
|
|
39
|
+
message: `确认从 ${flags.file} 恢复数据库?`,
|
|
40
|
+
default: false
|
|
41
|
+
});
|
|
42
|
+
if (!confirmImport) {
|
|
43
|
+
logger.info('操作已取消。');
|
|
44
|
+
this.exit(0);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// 4. 获取 Compose 文件
|
|
48
|
+
const { files, isBuiltIn } = await getComposeFiles(env);
|
|
49
|
+
logger.info(`正在从文件恢复数据库: ${inputPath}`);
|
|
50
|
+
logger.info('环境: ' + env);
|
|
51
|
+
// 加载环境配置
|
|
52
|
+
const envConfig = dotenv.config().parsed || {};
|
|
53
|
+
const dbUser = envConfig.POSTGRES_USER || 'postgres';
|
|
54
|
+
const dbName = envConfig.POSTGRES_DB || 'nodebbs';
|
|
55
|
+
const dbPassword = envConfig.POSTGRES_PASSWORD;
|
|
56
|
+
// 5. 构建 Compose 参数
|
|
57
|
+
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
58
|
+
if (isBuiltIn) {
|
|
59
|
+
composeArgs.push('--project-directory', process.cwd());
|
|
60
|
+
}
|
|
61
|
+
// 6. 运行 psql
|
|
62
|
+
// const dumpArgs = [...composeArgs, 'exec', '-T'] // backup uses exec
|
|
63
|
+
// import should also use exec -T to accept stdin
|
|
64
|
+
const importArgs = [...composeArgs, 'exec', '-T'];
|
|
65
|
+
// 如果有密码则注入
|
|
66
|
+
if (dbPassword) {
|
|
67
|
+
importArgs.push('-e', `PGPASSWORD=${dbPassword}`);
|
|
68
|
+
}
|
|
69
|
+
importArgs.push('postgres', 'psql', '-U', dbUser, '-d', dbName);
|
|
70
|
+
try {
|
|
71
|
+
const subprocess = execa('docker', ['compose', ...importArgs], {
|
|
72
|
+
input: fs.createReadStream(inputPath)
|
|
73
|
+
});
|
|
74
|
+
// subprocess.stdout?.pipe(process.stdout)
|
|
75
|
+
// subprocess.stderr?.pipe(process.stderr)
|
|
76
|
+
await subprocess;
|
|
77
|
+
logger.success('数据库导入成功!');
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
logger.error('数据库导入失败');
|
|
81
|
+
if (error instanceof Error) {
|
|
82
|
+
logger.error(error.message);
|
|
83
|
+
}
|
|
84
|
+
this.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -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
|
// 所以必须要求在项目根目录运行。
|
|
@@ -79,9 +74,6 @@ export default class Pack extends Command {
|
|
|
79
74
|
return line;
|
|
80
75
|
});
|
|
81
76
|
fs.writeFileSync(path.join(tmpDir, 'docker-compose.yml'), cleanedLines.join('\n'));
|
|
82
|
-
if (fs.existsSync('docker-compose.prod.yml')) {
|
|
83
|
-
fs.copyFileSync('docker-compose.prod.yml', path.join(tmpDir, 'docker-compose.prod.yml'));
|
|
84
|
-
}
|
|
85
77
|
if (fs.existsSync('.env')) {
|
|
86
78
|
this.warn('检测到 .env 文件,出于安全考虑,不会默认打包 .env 文件。请在部署时手动配置环境变量。');
|
|
87
79
|
}
|
|
@@ -122,7 +114,9 @@ services:
|
|
|
122
114
|
image: nodebbs-web:latest
|
|
123
115
|
EOF
|
|
124
116
|
|
|
125
|
-
|
|
117
|
+
COMPOSE_FILES="-f docker-compose.yml -f docker-compose.override.yml"
|
|
118
|
+
|
|
119
|
+
docker compose \$COMPOSE_FILES up -d
|
|
126
120
|
|
|
127
121
|
echo "部署完成!"
|
|
128
122
|
`;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
import Start from '../start/index.js';
|
|
3
3
|
export default class Rebuild extends Command {
|
|
4
|
-
static description = '
|
|
4
|
+
static description = '拉取最新镜像并重启服务 (Update & Restart)';
|
|
5
5
|
// Allow passing flags like -e to the underlying start command
|
|
6
6
|
static strict = false;
|
|
7
7
|
async run() {
|
|
8
|
-
// Invoke Start command
|
|
9
|
-
await Start.run(
|
|
8
|
+
// Invoke Start command to pull latest images and restart
|
|
9
|
+
await Start.run(this.argv);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -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);
|
|
@@ -3,7 +3,7 @@ export default class Start extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static flags: {
|
|
5
5
|
env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
-
|
|
6
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
};
|
|
8
8
|
run(): Promise<void>;
|
|
9
9
|
}
|
|
@@ -4,28 +4,22 @@ 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 {
|
|
7
|
+
import { input } from '@inquirer/prompts';
|
|
8
|
+
import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
|
|
8
9
|
export default class Start extends Command {
|
|
9
10
|
static description = '开始部署';
|
|
10
11
|
static flags = {
|
|
11
12
|
env: EnvFlag,
|
|
12
|
-
|
|
13
|
-
char: '
|
|
14
|
-
description: '
|
|
15
|
-
default: false,
|
|
13
|
+
tag: Flags.string({
|
|
14
|
+
char: 't',
|
|
15
|
+
description: 'Image version tag (e.g. latest, v0.1.0)',
|
|
16
16
|
}),
|
|
17
17
|
};
|
|
18
18
|
async run() {
|
|
19
19
|
const { flags } = await this.parse(Start);
|
|
20
|
-
|
|
21
|
-
logger.header('NodeBBS 重新构建启动');
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
logger.header('NodeBBS Docker 部署');
|
|
25
|
-
}
|
|
20
|
+
logger.header('NodeBBS Docker 部署');
|
|
26
21
|
// 1. 选择环境
|
|
27
22
|
const env = await selectEnvironment(flags.env);
|
|
28
|
-
await setStoredEnv(env);
|
|
29
23
|
// 2. 获取 Compose 文件
|
|
30
24
|
const { files: composeFiles, isBuiltIn } = await getComposeFiles(env);
|
|
31
25
|
if (isBuiltIn) {
|
|
@@ -40,69 +34,83 @@ export default class Start extends Command {
|
|
|
40
34
|
else if (env === 'lowmem') {
|
|
41
35
|
logger.success('已选择:低配环境');
|
|
42
36
|
}
|
|
43
|
-
else {
|
|
44
|
-
logger.success('已选择:基础环境');
|
|
45
|
-
if (!flags.build) {
|
|
46
|
-
logger.warning('注意:无资源限制,不推荐用于生产环境。');
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
37
|
// 3. 检查 Docker 和环境变量
|
|
50
38
|
await checkDocker();
|
|
51
39
|
// initEnv 保证 .env 存在(或退出)
|
|
52
40
|
await initEnv();
|
|
53
41
|
await checkEnv(env);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
// 如果不是本地构建,则询问镜像版本
|
|
43
|
+
// 先加载环境变量以获取当前的 IMAGE 配置(如果有)
|
|
44
|
+
const envConfig = dotenv.config().parsed || {};
|
|
45
|
+
let tag = flags.tag;
|
|
46
|
+
if (!tag) {
|
|
47
|
+
tag = await input({
|
|
48
|
+
message: '请选择要部署的镜像版本:',
|
|
49
|
+
default: 'latest'
|
|
58
50
|
});
|
|
59
|
-
if (!continueDeploy) {
|
|
60
|
-
logger.info('操作已取消。');
|
|
61
|
-
this.exit(0);
|
|
62
|
-
}
|
|
63
51
|
}
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
52
|
+
// 构造新的镜像名称
|
|
53
|
+
// 逻辑:如果 .env 里定义了 API_IMAGE,则解析出 registry/name 部分,然后拼接 tag
|
|
54
|
+
// 否则使用默认 registry
|
|
55
|
+
const defaultApiImage = 'ghcr.io/aiprojecthub/nodebbs-api';
|
|
56
|
+
const defaultWebImage = 'ghcr.io/aiprojecthub/nodebbs-web';
|
|
57
|
+
let currentApiImage = envConfig.API_IMAGE || defaultApiImage;
|
|
58
|
+
let currentWebImage = envConfig.WEB_IMAGE || defaultWebImage;
|
|
59
|
+
const replaceTag = (image, newTag) => {
|
|
60
|
+
const parts = image.split(':');
|
|
61
|
+
if (parts.length > 1 && !parts[parts.length - 1].includes('/')) {
|
|
62
|
+
// 最后一个部分不包含 '/',认为是 tag
|
|
63
|
+
parts.pop();
|
|
64
|
+
}
|
|
65
|
+
return `${parts.join(':')}:${newTag}`;
|
|
66
|
+
};
|
|
67
|
+
process.env.API_IMAGE = replaceTag(currentApiImage, tag);
|
|
68
|
+
process.env.WEB_IMAGE = replaceTag(currentWebImage, tag);
|
|
69
|
+
logger.info(`将部署版本: ${tag}`);
|
|
70
|
+
logger.info(`API Image: ${process.env.API_IMAGE}`);
|
|
71
|
+
logger.info(`Web Image: ${process.env.WEB_IMAGE}`);
|
|
72
|
+
const continueDeploy = await confirm({
|
|
73
|
+
message: '是否继续启动?',
|
|
74
|
+
default: true
|
|
75
|
+
});
|
|
76
|
+
if (!continueDeploy) {
|
|
77
|
+
logger.info('操作已取消。');
|
|
78
|
+
this.exit(0);
|
|
69
79
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
// 4. 拉取并启动服务
|
|
81
|
+
logger.info('正在拉取最新的 Docker 镜像...');
|
|
82
|
+
await runCompose(composeFiles, ['pull'], isBuiltIn);
|
|
83
|
+
logger.success('镜像拉取完成');
|
|
84
|
+
logger.info('正在启动服务...');
|
|
85
|
+
await runCompose(composeFiles, ['up', '-d'], isBuiltIn);
|
|
86
|
+
logger.success('服务启动指令已发送');
|
|
87
|
+
// 5. 启动后操作
|
|
88
|
+
await waitForHealth(composeFiles, isBuiltIn);
|
|
89
|
+
const pushDb = await confirm({
|
|
90
|
+
message: '是否推送数据库 schema?',
|
|
91
|
+
default: false
|
|
92
|
+
});
|
|
93
|
+
if (pushDb) {
|
|
94
|
+
logger.info('正在推送数据库 schema...');
|
|
95
|
+
await execCompose(composeFiles, 'api', ['npm', 'run', 'db:push'], isBuiltIn);
|
|
96
|
+
logger.success('数据库 schema 推送完成');
|
|
77
97
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
logger.info('正在推送数据库 schema...');
|
|
87
|
-
await execCompose(composeFiles, 'api', ['npm', 'run', 'db:push'], isBuiltIn);
|
|
88
|
-
logger.success('数据库 schema 推送完成');
|
|
89
|
-
}
|
|
90
|
-
const seedDb = await confirm({
|
|
91
|
-
message: '是否初始化种子数据?',
|
|
92
|
-
default: false
|
|
93
|
-
});
|
|
94
|
-
if (seedDb) {
|
|
95
|
-
logger.info('正在初始化数据...');
|
|
96
|
-
await execCompose(composeFiles, 'api', ['npm', 'run', 'seed'], isBuiltIn);
|
|
97
|
-
logger.success('数据初始化完成');
|
|
98
|
-
}
|
|
98
|
+
const seedDb = await confirm({
|
|
99
|
+
message: '是否初始化种子数据?',
|
|
100
|
+
default: false
|
|
101
|
+
});
|
|
102
|
+
if (seedDb) {
|
|
103
|
+
logger.info('正在初始化数据...');
|
|
104
|
+
await execCompose(composeFiles, 'api', ['npm', 'run', 'seed'], isBuiltIn);
|
|
105
|
+
logger.success('数据初始化完成');
|
|
99
106
|
}
|
|
100
|
-
logger.header(
|
|
107
|
+
logger.header('NodeBBS 启动成功!');
|
|
101
108
|
// 6. 显示信息
|
|
102
109
|
logger.info(`环境: ${env}`);
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const
|
|
110
|
+
// 复用之前的 envConfig 或重新读取 (这里直接复用即可,或者为了保险重新读取但不要 redeclare)
|
|
111
|
+
const finalEnvConfig = dotenv.config().parsed || {};
|
|
112
|
+
const webPort = finalEnvConfig.WEB_PORT || '3100';
|
|
113
|
+
const apiPort = finalEnvConfig.API_PORT || '7100';
|
|
106
114
|
console.log(` Web 前端: http://localhost:${webPort}`);
|
|
107
115
|
console.log(` API 服务: http://localhost:${apiPort}`);
|
|
108
116
|
console.log(` API 文档: http://localhost:${apiPort}/docs`);
|
|
@@ -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
|
@@ -147,7 +147,7 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
147
147
|
console.log('\n操作已取消。');
|
|
148
148
|
continue;
|
|
149
149
|
}
|
|
150
|
-
console.error(error);
|
|
150
|
+
console.error(error.message);
|
|
151
151
|
}
|
|
152
152
|
console.log('\n命令执行完成。');
|
|
153
153
|
// 动态导入 input 以保持轻量
|
|
@@ -178,7 +178,7 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
178
178
|
console.log('\n操作已取消。');
|
|
179
179
|
continue;
|
|
180
180
|
}
|
|
181
|
-
console.error(error);
|
|
181
|
+
console.error(error.message);
|
|
182
182
|
}
|
|
183
183
|
console.log('\n命令执行完成。');
|
|
184
184
|
// 动态导入 input 以保持轻量
|