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.
- package/README.md +10 -7
- package/dist/commands/backup/all.d.ts +9 -0
- package/dist/commands/backup/all.js +71 -0
- package/dist/commands/{db/backup.d.ts → backup/db.d.ts} +1 -1
- package/dist/commands/{db/backup.js → backup/db.js} +3 -3
- package/dist/commands/backup/uploads.d.ts +13 -0
- package/dist/commands/backup/uploads.js +117 -0
- package/dist/commands/import/all.d.ts +9 -0
- package/dist/commands/import/all.js +128 -0
- package/dist/commands/{db/import.d.ts → import/db.d.ts} +1 -1
- package/dist/commands/{db/import.js → import/db.js} +5 -6
- package/dist/commands/import/uploads.d.ts +13 -0
- package/dist/commands/import/uploads.js +146 -0
- package/dist/commands/shell/db.js +4 -1
- package/dist/commands/shell/redis.js +8 -1
- package/dist/interactive.js +3 -1
- package/dist/templates/config.yml +22 -0
- package/dist/templates/docker-compose.yml +9 -15
- package/dist/templates/env +0 -5
- package/dist/utils/docker.d.ts +1 -1
- package/dist/utils/docker.js +88 -12
- package/dist/utils/env.d.ts +1 -1
- package/dist/utils/selection.d.ts +1 -1
- package/dist/utils/selection.js +4 -4
- package/oclif.manifest.json +223 -77
- package/package.json +8 -2
- package/dist/templates/docker-compose.ports.yml +0 -8
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/interactive.js
CHANGED
|
@@ -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', '
|
|
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
|
-
#
|
|
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:
|
package/dist/templates/env
CHANGED
|
@@ -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 服务配置
|
package/dist/utils/docker.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - 如果存在,则优先使用项目根目录的配置。
|
|
6
6
|
* - 如果不存在,则使用 CLI 内置的模板文件。
|
|
7
7
|
*
|
|
8
|
-
* @param env - 运行环境 ('production' | 'lowmem' | '
|
|
8
|
+
* @param env - 运行环境 ('production' | 'lowmem' | 'default')
|
|
9
9
|
* @returns 对象包含文件路径数组和是否使用内置模板的标志
|
|
10
10
|
*/
|
|
11
11
|
export declare function getComposeFiles(env: string): Promise<{
|
package/dist/utils/docker.js
CHANGED
|
@@ -23,7 +23,7 @@ async function getDockerEnv() {
|
|
|
23
23
|
* - 如果存在,则优先使用项目根目录的配置。
|
|
24
24
|
* - 如果不存在,则使用 CLI 内置的模板文件。
|
|
25
25
|
*
|
|
26
|
-
* @param env - 运行环境 ('production' | 'lowmem' | '
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
156
|
-
|
|
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
|
*
|
package/dist/utils/env.d.ts
CHANGED
|
@@ -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' | '
|
|
7
|
+
export declare function checkEnv(envType: 'production' | 'lowmem' | 'default'): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type EnvType = 'production' | 'lowmem' | '
|
|
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>;
|
package/dist/utils/selection.js
CHANGED
|
@@ -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,
|
|
10
|
-
options: ['production', 'lowmem', '
|
|
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', '
|
|
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: '
|
|
54
|
+
{ name: '默认环境 (无资源限制)', value: 'default' },
|
|
55
55
|
{ name: '❌ 取消', value: '__CANCEL__' },
|
|
56
56
|
],
|
|
57
57
|
loop: true,
|