nodebbs 0.3.2 → 0.4.1

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.
@@ -2,6 +2,7 @@ import { Command } from '@oclif/core';
2
2
  import { runCompose, getComposeFiles } from '../../utils/docker.js';
3
3
  import { logger } from '../../utils/logger.js';
4
4
  import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
5
+ import { getEnvValue } from '../../utils/env.js';
5
6
  export default class ShellRedis extends Command {
6
7
  static description = '进入 Redis shell (redis-cli)';
7
8
  static flags = {
@@ -11,7 +12,13 @@ export default class ShellRedis extends Command {
11
12
  const { flags } = await this.parse(ShellRedis);
12
13
  const env = await selectEnvironment(flags.env);
13
14
  const { files, isBuiltIn } = await getComposeFiles(env);
15
+ // 从环境变量获取密码
16
+ const redisPassword = await getEnvValue('REDIS_PASSWORD');
14
17
  logger.info('正在进入 Redis shell...');
15
- await runCompose(files, ['exec', 'redis', 'redis-cli'], isBuiltIn);
18
+ // 如果有密码则使用认证参数
19
+ const redisCliArgs = redisPassword
20
+ ? ['exec', 'redis', 'redis-cli', '-a', redisPassword]
21
+ : ['exec', 'redis', 'redis-cli'];
22
+ await runCompose(files, redisCliArgs, isBuiltIn);
16
23
  }
17
24
  }
@@ -29,9 +29,11 @@ export async function runInteractive(root) {
29
29
  }
30
30
  await navigate(tree, [], config);
31
31
  }
32
- const GLOBAL_PRIORITY = ['start', 'stop', 'restart', 'upgrade', 'status', 'logs', 'shell', 'db', 'pack'];
32
+ const GLOBAL_PRIORITY = ['start', 'stop', 'restart', 'upgrade', 'status', 'logs', 'db', 'backup', 'import', 'clean', 'shell', 'pack'];
33
33
  const SCOPED_PRIORITIES = {
34
34
  'logs': ['all', 'web', 'api', 'db', 'redis'],
35
+ 'backup': ['all', 'db', 'uploads'],
36
+ 'import': ['all', 'db', 'uploads'],
35
37
  };
36
38
  async function navigate(node, breadcrumbs, config) {
37
39
  const currentPath = breadcrumbs.join(':');
@@ -0,0 +1,22 @@
1
+ # NodeBBS 自定义配置
2
+ # 此文件用于挂载自定义静态资源(如 logo、favicon 等)
3
+ # 将此文件复制到项目根目录并取消需要的注释即可生效
4
+ #
5
+ # 使用方法:
6
+ # 1. 复制此文件到项目根目录:cp config.yml ./
7
+ # 2. 在项目根目录创建 web 文件夹:mkdir -p web
8
+ # 3. 将自定义资源放入 web 文件夹
9
+ # 4. 取消下方对应行的注释
10
+ # 5. 重启服务:npx nodebbs restart
11
+
12
+ services:
13
+ web:
14
+ volumes:
15
+ # 网站图标
16
+ - ./web/favicon.ico:/app/apps/web/public/favicon.ico
17
+
18
+ # 网站 Logo
19
+ - ./web/logo.svg:/app/apps/web/public/logo.svg
20
+
21
+ # Apple Touch Icon (iOS 主屏幕图标)
22
+ - ./web/apple-touch-icon.png:/app/apps/web/public/apple-touch-icon.png
@@ -2,13 +2,13 @@ services:
2
2
  # PostgreSQL 数据库
3
3
  postgres:
4
4
  image: postgres:16-alpine
5
- container_name: nodebbs-postgres
5
+ container_name: ${COMPOSE_PROJECT_NAME:-nodebbs}-postgres
6
6
  restart: unless-stopped
7
7
  environment:
8
8
  POSTGRES_USER: postgres
9
9
  POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_password}
10
10
  POSTGRES_DB: ${POSTGRES_DB:-nodebbs}
11
- TZ: Asia/Shanghai
11
+ TZ: ${TZ:-Asia/Shanghai}
12
12
  volumes:
13
13
  - postgres_data:/var/lib/postgresql/data
14
14
 
@@ -24,11 +24,11 @@ services:
24
24
  # Redis 缓存
25
25
  redis:
26
26
  image: redis:7-alpine
27
- container_name: nodebbs-redis
27
+ container_name: ${COMPOSE_PROJECT_NAME:-nodebbs}-redis
28
28
  restart: unless-stopped
29
29
  command: redis-server --requirepass ${REDIS_PASSWORD:-redis_password} --appendonly yes
30
30
  environment:
31
- TZ: Asia/Shanghai
31
+ TZ: ${TZ:-Asia/Shanghai}
32
32
  volumes:
33
33
  - redis_data:/data
34
34
 
@@ -46,10 +46,9 @@ services:
46
46
  context: .
47
47
  dockerfile: apps/api/Dockerfile
48
48
  image: ${API_IMAGE:-nodebbs-api:local}
49
- container_name: nodebbs-api
49
+ container_name: ${COMPOSE_PROJECT_NAME:-nodebbs}-api
50
50
  restart: unless-stopped
51
51
  environment:
52
- NODE_ENV: production
53
52
  APP_NAME: ${APP_NAME:-nodebbs}
54
53
  HOST: 0.0.0.0
55
54
  PORT: 7100
@@ -59,7 +58,7 @@ services:
59
58
  JWT_SECRET: ${JWT_SECRET:-change-this-to-a-secure-random-string-in-production}
60
59
  JWT_ACCESS_TOKEN_EXPIRES_IN: ${JWT_ACCESS_TOKEN_EXPIRES_IN:-1y}
61
60
  CORS_ORIGIN: ${CORS_ORIGIN:-*}
62
- TZ: Asia/Shanghai
61
+ TZ: ${TZ:-Asia/Shanghai}
63
62
  volumes:
64
63
  - api_uploads:/app/apps/api/uploads
65
64
  # - ./apps/api/src:/app/apps/api/src:ro
@@ -85,19 +84,14 @@ services:
85
84
  context: .
86
85
  dockerfile: apps/web/Dockerfile
87
86
  image: ${WEB_IMAGE:-nodebbs-web:local}
88
- container_name: nodebbs-web
87
+ container_name: ${COMPOSE_PROJECT_NAME:-nodebbs}-web
89
88
  restart: unless-stopped
90
89
  environment:
91
- NODE_ENV: production
92
90
  APP_NAME: ${APP_NAME:-nodebbs}
93
91
  PORT: 3100
94
92
  SERVER_API_URL: ${SERVER_API_URL:-http://api:7100}
95
- TZ: Asia/Shanghai
96
- # volumes:
97
- # 自定义资源挂载(如有需要,请取消注释并在同级目录下创建 web 文件夹及对应文件)
98
- # - ./web/favicon.ico:/app/apps/web/public/favicon.ico
99
- # - ./web/logo.svg:/app/apps/web/public/logo.svg
100
- # - ./web/apple-touch-icon.png:/app/apps/web/public/apple-touch-icon.png
93
+ TZ: ${TZ:-Asia/Shanghai}
94
+ # 自定义资源挂载请使用 config.yml,参见 src/templates/config.yml
101
95
  ports:
102
96
  - "${WEB_PORT:-3100}:3100"
103
97
  depends_on:
@@ -2,21 +2,16 @@
2
2
  # NodeBBS Docker Compose 环境变量配置
3
3
  # ========================================
4
4
 
5
- # 应用名称
6
- APP_NAME=nodebbs
7
-
8
5
  # ========================================
9
6
  # 数据库配置
10
7
  # ========================================
11
8
  POSTGRES_PASSWORD=your_secure_postgres_password_here
12
9
  POSTGRES_DB=nodebbs
13
- POSTGRES_PORT=5432
14
10
 
15
11
  # ========================================
16
12
  # Redis 配置
17
13
  # ========================================
18
14
  REDIS_PASSWORD=your_secure_redis_password_here
19
- REDIS_PORT=6379
20
15
 
21
16
  # ========================================
22
17
  # API 服务配置
@@ -5,7 +5,7 @@
5
5
  * - 如果存在,则优先使用项目根目录的配置。
6
6
  * - 如果不存在,则使用 CLI 内置的模板文件。
7
7
  *
8
- * @param env - 运行环境 ('production' | 'lowmem' | 'basic')
8
+ * @param env - 运行环境 ('production' | 'lowmem' | 'default')
9
9
  * @returns 对象包含文件路径数组和是否使用内置模板的标志
10
10
  */
11
11
  export declare function getComposeFiles(env: string): Promise<{
@@ -23,7 +23,7 @@ async function getDockerEnv() {
23
23
  * - 如果存在,则优先使用项目根目录的配置。
24
24
  * - 如果不存在,则使用 CLI 内置的模板文件。
25
25
  *
26
- * @param env - 运行环境 ('production' | 'lowmem' | 'basic')
26
+ * @param env - 运行环境 ('production' | 'lowmem' | 'default')
27
27
  * @returns 对象包含文件路径数组和是否使用内置模板的标志
28
28
  */
29
29
  export async function getComposeFiles(env) {
@@ -43,18 +43,15 @@ export async function getComposeFiles(env) {
43
43
  // 映射 environment 到对应的 override 文件名
44
44
  const envFileMap = {
45
45
  'production': 'docker-compose.prod.yml',
46
- 'lowmem': 'docker-compose.lowmem.yml',
47
- 'basic': 'docker-compose.ports.yml'
46
+ 'lowmem': 'docker-compose.lowmem.yml'
48
47
  };
49
48
  if (env in envFileMap) {
50
49
  const fileName = envFileMap[env];
51
- // 注意:basic 对应的 ports.yml 是新增的,可能不存在于旧项目的根目录,
52
- // 但如果是在 CLI 内部运行,templateDir 里肯定有。
53
- // 如果是 production/lowmem,用户目录可能有 override。
50
+ // 注意:用户目录可能有 override 文件覆盖内置默认值
54
51
  const localOverride = path.join(workDir, fileName);
55
52
  const templateOverride = path.join(templateDir, fileName);
56
53
  // 策略调整:
57
- // basic -> 总是尝试加载 ports.yml (通常只存在于 template,或者用户没覆盖)
54
+ // default -> 不加载 override 文件,使用基础配置
58
55
  // production/lowmem -> 尝试加载对应的 override
59
56
  if (await fileExists(localOverride)) {
60
57
  files.push(localOverride);
@@ -78,6 +75,12 @@ export async function getComposeFiles(env) {
78
75
  files.push(templateBuild);
79
76
  }
80
77
  }
78
+ // 4. 检测自定义配置文件 (config.yml)
79
+ // 用于挂载自定义静态资源(如 logo、favicon 等),仅当文件存在时加载
80
+ const configFile = path.join(workDir, 'config.yml');
81
+ if (await fileExists(configFile)) {
82
+ files.push(configFile);
83
+ }
81
84
  return { files, isBuiltIn };
82
85
  }
83
86
  /**
@@ -94,6 +97,33 @@ export async function checkDocker() {
94
97
  logger.success('Docker 环境检查通过');
95
98
  }
96
99
  catch (error) {
100
+ // 检测是否是权限问题
101
+ const errorMessage = error.stderr || error.message || '';
102
+ const isPermissionError = errorMessage.includes('permission denied') ||
103
+ errorMessage.includes('Got permission denied') ||
104
+ errorMessage.includes('dial unix /var/run/docker.sock');
105
+ if (isPermissionError) {
106
+ logger.warning('检测到 Docker 权限问题');
107
+ logger.info('您的用户可能没有权限访问 Docker。');
108
+ // 如果是 CI 环境或非交互式环境,直接报错
109
+ if (process.env.CI || !process.stdout.isTTY) {
110
+ throw error;
111
+ }
112
+ // 尝试配置权限
113
+ await configureDockerPermissions();
114
+ // 配置后再次检查
115
+ try {
116
+ await execa('docker', ['--version']);
117
+ await execa('docker', ['compose', 'version']);
118
+ logger.success('Docker 权限配置成功!');
119
+ return;
120
+ }
121
+ catch {
122
+ logger.error('权限配置后仍无法访问 Docker');
123
+ logger.info('请尝试: 1) 运行 "newgrp docker" 2) 或重新登录');
124
+ throw error;
125
+ }
126
+ }
97
127
  logger.warning('未检测到 Docker 或 Docker Compose。');
98
128
  // 如果是 CI 环境或非交互式环境,直接报错
99
129
  if (process.env.CI || !process.stdout.isTTY) {
@@ -114,6 +144,22 @@ export async function checkDocker() {
114
144
  return;
115
145
  }
116
146
  catch (installError) {
147
+ // 特殊处理:需要 newgrp 的情况
148
+ if (installError.message === 'NEWGRP_REQUIRED') {
149
+ logger.warning('安装完成,但需要刷新权限。');
150
+ logger.info('请运行以下命令后重试:');
151
+ logger.info(' newgrp docker && npx nodebbs');
152
+ process.exit(0);
153
+ }
154
+ // 检测权限问题
155
+ const installErrMsg = installError.stderr || installError.message || '';
156
+ if (installErrMsg.includes('permission denied') || installErrMsg.includes('docker.sock')) {
157
+ logger.warning('Docker 已安装,但存在权限问题。');
158
+ logger.info('请运行以下命令后重试:');
159
+ logger.info(' newgrp docker && npx nodebbs');
160
+ logger.info('或者注销并重新登录。');
161
+ process.exit(0);
162
+ }
117
163
  logger.error(`安装失败: ${installError.message}`);
118
164
  logger.info('请访问 https://www.docker.com/get-started 手动安装。');
119
165
  throw installError;
@@ -152,11 +198,8 @@ async function installDocker() {
152
198
  logger.warning('无法自动启动 Docker 服务');
153
199
  logger.info('请手动运行: sudo systemctl start docker');
154
200
  }
155
- // 提示用户可能需要添加到 docker 组
156
- logger.warning('请确保您的用户已添加到 docker 组:');
157
- logger.info(' sudo usermod -aG docker $USER');
158
- logger.info('您可能需要注销并重新登录才能生效。');
159
- await confirm({ message: '是否已完成必要配置并准备继续?', default: true });
201
+ // 配置用户权限
202
+ await configureDockerPermissions();
160
203
  }
161
204
  finally {
162
205
  // 清理临时安装脚本
@@ -177,6 +220,39 @@ async function installDocker() {
177
220
  throw new Error(`不支持在 ${platform} 平台自动安装`);
178
221
  }
179
222
  }
223
+ /**
224
+ * 配置 Docker 用户权限
225
+ *
226
+ * 将当前用户添加到 docker 组,避免每次都需要 sudo。
227
+ */
228
+ async function configureDockerPermissions() {
229
+ const currentUser = process.env.USER || process.env.USERNAME || '';
230
+ if (!currentUser) {
231
+ logger.warning('无法获取当前用户名,请手动配置 Docker 权限');
232
+ logger.info('请运行: sudo usermod -aG docker $USER');
233
+ return;
234
+ }
235
+ logger.info(`正在将用户 "${currentUser}" 添加到 docker 组...`);
236
+ try {
237
+ await execa('sudo', ['usermod', '-aG', 'docker', currentUser], { stdio: 'inherit' });
238
+ logger.success(`已将用户 "${currentUser}" 添加到 docker 组`);
239
+ // 提示权限激活方式
240
+ logger.info('');
241
+ logger.warning('权限变更需要刷新才能生效。');
242
+ logger.info('请运行以下命令后重试:');
243
+ logger.info(' newgrp docker && npx nodebbs');
244
+ logger.info('');
245
+ logger.info('或者注销并重新登录后再运行 npx nodebbs');
246
+ throw new Error('NEWGRP_REQUIRED');
247
+ }
248
+ catch (error) {
249
+ if (error.message === 'NEWGRP_REQUIRED') {
250
+ throw error;
251
+ }
252
+ logger.error(`添加用户到 docker 组失败: ${error.message}`);
253
+ logger.info('请手动运行: sudo usermod -aG docker $USER');
254
+ }
255
+ }
180
256
  /**
181
257
  * 执行 Docker Compose 管理命令 (Lifecycle Commands)
182
258
  *
@@ -4,4 +4,4 @@ export declare function unsetEnvValue(key: string): Promise<void>;
4
4
  export declare function initEnv(): Promise<void>;
5
5
  export declare function initDockerIgnore(): Promise<void>;
6
6
  export declare function initImageEnv(): Promise<void>;
7
- export declare function checkEnv(envType: 'production' | 'lowmem' | 'basic'): Promise<void>;
7
+ export declare function checkEnv(envType: 'production' | 'lowmem' | 'default'): Promise<void>;
@@ -1,4 +1,4 @@
1
- export type EnvType = 'production' | 'lowmem' | 'basic';
1
+ export type EnvType = 'production' | 'lowmem' | 'default';
2
2
  export type DeploymentMode = 'source' | 'image';
3
3
  export declare const EnvFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
4
4
  export declare function setStoredEnv(env: EnvType): Promise<void>;
@@ -6,12 +6,12 @@ import path from 'node:path';
6
6
  import { getEnvValue, setEnvValue, unsetEnvValue } from './env.js';
7
7
  export const EnvFlag = Flags.string({
8
8
  char: 'e',
9
- description: '部署环境 (production, lowmem, basic)',
10
- options: ['production', 'lowmem', 'basic'],
9
+ description: '部署环境 (production, lowmem, default)',
10
+ options: ['production', 'lowmem', 'default'],
11
11
  });
12
12
  async function getStoredEnv() {
13
13
  const env = await getEnvValue('DEPLOY_ENV');
14
- if (env && ['production', 'lowmem', 'basic'].includes(env)) {
14
+ if (env && ['production', 'lowmem', 'default'].includes(env)) {
15
15
  return env;
16
16
  }
17
17
  return null;
@@ -51,7 +51,7 @@ export async function selectEnvironment(env, options = {}) {
51
51
  choices: [
52
52
  { name: '标准生产环境 (2C4G+) [推荐]', value: 'production' },
53
53
  { name: '低配环境 (1C1G/1C2G)', value: 'lowmem' },
54
- { name: '基础环境 (仅用于测试)', value: 'basic' },
54
+ { name: '默认环境 (无资源限制)', value: 'default' },
55
55
  { name: '❌ 取消', value: '__CANCEL__' },
56
56
  ],
57
57
  loop: true,