nodebbs 0.0.9 → 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 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, basic
69
- - `-b, --build` - 重新构建镜像并启动(跳过健康检查和初始化)
64
+ - `-e, --env` - 部署环境(production, lowmem)
65
+ - `-t, --tag` - 镜像版本 tag (默认: latest)
70
66
 
71
67
  启动流程包含:
72
68
  - Docker 环境检查
73
69
  - 环境变量验证
74
- - 镜像构建
70
+ - 拉取最新镜像
75
71
  - 服务启动
76
- - 健康检查(默认开启,使用 `--build` 跳过)
77
- - 数据库初始化(默认开启,使用 `--build` 跳过)
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** - 生产环境(2C4G+,推荐)
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 basic`。
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.prod.yml` - 生产环境配置
335
- - `docker-compose.lowmem.yml` - 低配环境配置
336
+ - `docker-compose.yml` - 基础/生产环境配置
337
+ - `docker-compose.lowmem.yml` - 低配环境配置 (Override)
336
338
 
337
339
  CLI 会优先使用本地配置文件。
338
340
 
@@ -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
+ }
@@ -74,9 +74,6 @@ export default class Pack extends Command {
74
74
  return line;
75
75
  });
76
76
  fs.writeFileSync(path.join(tmpDir, 'docker-compose.yml'), cleanedLines.join('\n'));
77
- if (fs.existsSync('docker-compose.prod.yml')) {
78
- fs.copyFileSync('docker-compose.prod.yml', path.join(tmpDir, 'docker-compose.prod.yml'));
79
- }
80
77
  if (fs.existsSync('.env')) {
81
78
  this.warn('检测到 .env 文件,出于安全考虑,不会默认打包 .env 文件。请在部署时手动配置环境变量。');
82
79
  }
@@ -117,7 +114,9 @@ services:
117
114
  image: nodebbs-web:latest
118
115
  EOF
119
116
 
120
- docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.override.yml up -d
117
+ COMPOSE_FILES="-f docker-compose.yml -f docker-compose.override.yml"
118
+
119
+ docker compose \$COMPOSE_FILES up -d
121
120
 
122
121
  echo "部署完成!"
123
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 = '重新构建并启动服务 (start --build)';
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 with --build flag and any other arguments
9
- await Start.run(['--build', ...this.argv]);
8
+ // Invoke Start command to pull latest images and restart
9
+ await Start.run(this.argv);
10
10
  }
11
11
  }
@@ -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
- build: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
+ tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  };
8
8
  run(): Promise<void>;
9
9
  }
@@ -4,25 +4,20 @@ 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 { input } from '@inquirer/prompts';
7
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
- build: Flags.boolean({
13
- char: 'b',
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
- if (flags.build) {
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
23
  // 2. 获取 Compose 文件
@@ -39,69 +34,83 @@ export default class Start extends Command {
39
34
  else if (env === 'lowmem') {
40
35
  logger.success('已选择:低配环境');
41
36
  }
42
- else {
43
- logger.success('已选择:基础环境');
44
- if (!flags.build) {
45
- logger.warning('注意:无资源限制,不推荐用于生产环境。');
46
- }
47
- }
48
37
  // 3. 检查 Docker 和环境变量
49
38
  await checkDocker();
50
39
  // initEnv 保证 .env 存在(或退出)
51
40
  await initEnv();
52
41
  await checkEnv(env);
53
- if (!flags.build) {
54
- const continueDeploy = await confirm({
55
- message: '是否继续启动?',
56
- default: true
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'
57
50
  });
58
- if (!continueDeploy) {
59
- logger.info('操作已取消。');
60
- this.exit(0);
61
- }
62
51
  }
63
- // 4. 构建并启动服务
64
- if (flags.build) {
65
- logger.info('正在重新构建并启动服务...');
66
- await runCompose(composeFiles, ['up', '-d', '--build'], isBuiltIn);
67
- logger.success('服务已重新构建并启动');
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);
68
79
  }
69
- else {
70
- logger.info('正在构建 Docker 镜像...');
71
- await runCompose(composeFiles, ['build', '--no-cache'], isBuiltIn);
72
- logger.success('镜像构建完成');
73
- logger.info('正在启动服务...');
74
- await runCompose(composeFiles, ['up', '-d'], isBuiltIn);
75
- logger.success('服务启动指令已发送');
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 推送完成');
76
97
  }
77
- // 5. 启动后操作 (仅完整部署)
78
- if (!flags.build) {
79
- await waitForHealth(composeFiles, isBuiltIn);
80
- const pushDb = await confirm({
81
- message: '是否推送数据库 schema?',
82
- default: false
83
- });
84
- if (pushDb) {
85
- logger.info('正在推送数据库 schema...');
86
- await execCompose(composeFiles, 'api', ['npm', 'run', 'db:push'], isBuiltIn);
87
- logger.success('数据库 schema 推送完成');
88
- }
89
- const seedDb = await confirm({
90
- message: '是否初始化种子数据?',
91
- default: false
92
- });
93
- if (seedDb) {
94
- logger.info('正在初始化数据...');
95
- await execCompose(composeFiles, 'api', ['npm', 'run', 'seed'], isBuiltIn);
96
- logger.success('数据初始化完成');
97
- }
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('数据初始化完成');
98
106
  }
99
- logger.header(flags.build ? '启动成功!' : 'NodeBBS 启动成功!');
107
+ logger.header('NodeBBS 启动成功!');
100
108
  // 6. 显示信息
101
109
  logger.info(`环境: ${env}`);
102
- const envConfig = dotenv.config().parsed || {};
103
- const webPort = envConfig.WEB_PORT || '3100';
104
- const apiPort = envConfig.API_PORT || '7100';
110
+ // 复用之前的 envConfig 或重新读取 (这里直接复用即可,或者为了保险重新读取但不要 redeclare)
111
+ const finalEnvConfig = dotenv.config().parsed || {};
112
+ const webPort = finalEnvConfig.WEB_PORT || '3100';
113
+ const apiPort = finalEnvConfig.API_PORT || '7100';
105
114
  console.log(` Web 前端: http://localhost:${webPort}`);
106
115
  console.log(` API 服务: http://localhost:${apiPort}`);
107
116
  console.log(` API 文档: http://localhost:${apiPort}/docs`);
@@ -1,19 +1,6 @@
1
- # 低内存环境 Docker Compose 覆盖配置
2
- # 适用于 1C1G 或 1C2G 的低配服务器
3
- # 使用方式: docker compose -f docker-compose.yml -f docker-compose.lowmem.yml up -d
4
-
5
1
  services:
6
2
  # PostgreSQL 数据库 - 低内存优化
7
3
  postgres:
8
- restart: always
9
- ports: [] # 不暴露端口到主机,只在内部网络访问
10
- environment:
11
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
12
- healthcheck:
13
- interval: 30s
14
- timeout: 10s
15
- retries: 5
16
- start_period: 40s
17
4
  deploy:
18
5
  resources:
19
6
  limits:
@@ -23,14 +10,13 @@ services:
23
10
  cpus: '0.1'
24
11
  memory: 128M
25
12
  logging:
26
- driver: "json-file"
27
13
  options:
28
14
  max-size: "5m"
29
15
  max-file: "2"
30
16
 
31
17
  # Redis 缓存 - 低内存优化
32
18
  redis:
33
- restart: always
19
+ # 降低内存限制
34
20
  command: >
35
21
  redis-server
36
22
  --requirepass ${REDIS_PASSWORD}
@@ -40,12 +26,6 @@ services:
40
26
  --maxmemory-policy allkeys-lru
41
27
  --save 900 1
42
28
  --save 300 10
43
- ports: [] # 不暴露端口到主机
44
- healthcheck:
45
- interval: 30s
46
- timeout: 10s
47
- retries: 5
48
- start_period: 20s
49
29
  deploy:
50
30
  resources:
51
31
  limits:
@@ -55,23 +35,15 @@ services:
55
35
  cpus: '0.1'
56
36
  memory: 64M
57
37
  logging:
58
- driver: "json-file"
59
38
  options:
60
39
  max-size: "5m"
61
40
  max-file: "2"
62
41
 
63
42
  # API 服务 - 低内存优化
64
43
  api:
65
- restart: always
66
44
  environment:
67
- USER_CACHE_TTL: ${USER_CACHE_TTL:-300}
68
- JWT_SECRET: ${JWT_SECRET}
69
- CORS_ORIGIN: ${CORS_ORIGIN}
70
- APP_URL: ${APP_URL}
71
- # Node.js 内存限制
45
+ # 降低 Node.js 内存限制
72
46
  NODE_OPTIONS: "--max-old-space-size=384"
73
- volumes:
74
- - api_uploads:/app/apps/api/uploads
75
47
  healthcheck:
76
48
  start_period: 90s # 低内存环境启动更慢
77
49
  interval: 60s
@@ -84,22 +56,13 @@ services:
84
56
  cpus: '0.2'
85
57
  memory: 256M
86
58
  logging:
87
- driver: "json-file"
88
59
  options:
89
60
  max-size: "10m"
90
- max-file: "3"
91
61
 
92
62
  # Web 前端服务 - 低内存优化
93
63
  web:
94
- restart: always
95
- build:
96
- args:
97
- NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
98
- NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
99
64
  environment:
100
- NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
101
- NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
102
- # Node.js 内存限制
65
+ # 降低 Node.js 内存限制
103
66
  NODE_OPTIONS: "--max-old-space-size=384"
104
67
  healthcheck:
105
68
  start_period: 90s # 低内存环境启动更慢
@@ -113,14 +76,5 @@ services:
113
76
  cpus: '0.2'
114
77
  memory: 256M
115
78
  logging:
116
- driver: "json-file"
117
79
  options:
118
80
  max-size: "10m"
119
- max-file: "3"
120
-
121
- # 网络配置
122
- networks:
123
- nodebbs-network:
124
- ipam:
125
- config:
126
- - subnet: 172.28.0.0/16