nodebbs 0.1.0 → 0.3.0-beta.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 +65 -310
- package/dist/commands/db/import.d.ts +1 -2
- package/dist/commands/db/import.js +73 -45
- package/dist/commands/pack/index.js +60 -44
- package/dist/commands/restart/index.js +13 -7
- package/dist/commands/start/index.d.ts +1 -1
- package/dist/commands/start/index.js +83 -73
- package/dist/commands/upgrade/index.d.ts +8 -0
- package/dist/commands/upgrade/index.js +39 -0
- package/dist/interactive.js +1 -1
- package/dist/templates/docker-compose.build.yml +10 -0
- package/dist/templates/docker-compose.lowmem.yml +25 -14
- package/dist/templates/docker-compose.prod.yml +89 -0
- package/dist/templates/docker-compose.yml +34 -93
- package/dist/templates/dockerignore +120 -0
- package/dist/templates/env +11 -30
- package/dist/utils/docker.d.ts +1 -1
- package/dist/utils/docker.js +46 -10
- package/dist/utils/env.d.ts +6 -1
- package/dist/utils/env.js +90 -16
- package/dist/utils/selection.d.ts +3 -1
- package/dist/utils/selection.js +22 -23
- package/oclif.manifest.json +103 -80
- package/package.json +1 -1
- package/dist/commands/rebuild/index.d.ts +0 -6
- package/dist/commands/rebuild/index.js +0 -11
|
@@ -3,37 +3,22 @@ services:
|
|
|
3
3
|
postgres:
|
|
4
4
|
image: postgres:16-alpine
|
|
5
5
|
container_name: nodebbs-postgres
|
|
6
|
-
restart:
|
|
6
|
+
restart: unless-stopped
|
|
7
7
|
environment:
|
|
8
8
|
POSTGRES_USER: postgres
|
|
9
|
-
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
9
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_password}
|
|
10
10
|
POSTGRES_DB: ${POSTGRES_DB:-nodebbs}
|
|
11
|
-
TZ:
|
|
11
|
+
TZ: Asia/Shanghai
|
|
12
12
|
volumes:
|
|
13
13
|
- postgres_data:/var/lib/postgresql/data
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# - "${POSTGRES_PORT:-5432}:5432"
|
|
14
|
+
|
|
15
|
+
ports:
|
|
16
|
+
- "${POSTGRES_PORT:-5432}:5432"
|
|
18
17
|
healthcheck:
|
|
19
18
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
20
|
-
interval:
|
|
21
|
-
timeout:
|
|
19
|
+
interval: 10s
|
|
20
|
+
timeout: 5s
|
|
22
21
|
retries: 5
|
|
23
|
-
start_period: 40s
|
|
24
|
-
deploy:
|
|
25
|
-
resources:
|
|
26
|
-
limits:
|
|
27
|
-
cpus: '1'
|
|
28
|
-
memory: 512M
|
|
29
|
-
reservations:
|
|
30
|
-
cpus: '0.25'
|
|
31
|
-
memory: 256M
|
|
32
|
-
logging:
|
|
33
|
-
driver: "json-file"
|
|
34
|
-
options:
|
|
35
|
-
max-size: "10m"
|
|
36
|
-
max-file: "3"
|
|
37
22
|
networks:
|
|
38
23
|
- nodebbs-network
|
|
39
24
|
|
|
@@ -41,62 +26,46 @@ services:
|
|
|
41
26
|
redis:
|
|
42
27
|
image: redis:7-alpine
|
|
43
28
|
container_name: nodebbs-redis
|
|
44
|
-
restart:
|
|
45
|
-
command:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
--appendonly yes
|
|
49
|
-
--appendfsync everysec
|
|
50
|
-
--maxmemory 256mb
|
|
51
|
-
--maxmemory-policy allkeys-lru
|
|
29
|
+
restart: unless-stopped
|
|
30
|
+
command: redis-server --requirepass ${REDIS_PASSWORD:-redis_password} --appendonly yes
|
|
31
|
+
environment:
|
|
32
|
+
TZ: Asia/Shanghai
|
|
52
33
|
volumes:
|
|
53
34
|
- redis_data:/data
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# - "${REDIS_PORT:-6379}:6379"
|
|
35
|
+
ports:
|
|
36
|
+
- "${REDIS_PORT:-6379}:6379"
|
|
57
37
|
healthcheck:
|
|
58
38
|
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
|
59
|
-
interval:
|
|
60
|
-
timeout:
|
|
39
|
+
interval: 10s
|
|
40
|
+
timeout: 5s
|
|
61
41
|
retries: 5
|
|
62
|
-
start_period: 20s
|
|
63
|
-
deploy:
|
|
64
|
-
resources:
|
|
65
|
-
limits:
|
|
66
|
-
cpus: '0.5'
|
|
67
|
-
memory: 256M
|
|
68
|
-
reservations:
|
|
69
|
-
cpus: '0.1'
|
|
70
|
-
memory: 128M
|
|
71
|
-
logging:
|
|
72
|
-
driver: "json-file"
|
|
73
|
-
options:
|
|
74
|
-
max-size: "10m"
|
|
75
|
-
max-file: "3"
|
|
76
42
|
networks:
|
|
77
43
|
- nodebbs-network
|
|
78
44
|
|
|
79
45
|
# API 服务
|
|
80
46
|
api:
|
|
81
|
-
|
|
47
|
+
build:
|
|
48
|
+
context: ../../
|
|
49
|
+
dockerfile: apps/api/Dockerfile
|
|
50
|
+
image: ${API_IMAGE:-nodebbs-api:local}
|
|
82
51
|
container_name: nodebbs-api
|
|
83
|
-
restart:
|
|
52
|
+
restart: unless-stopped
|
|
84
53
|
environment:
|
|
85
54
|
NODE_ENV: production
|
|
86
55
|
APP_NAME: ${APP_NAME:-nodebbs}
|
|
87
56
|
HOST: 0.0.0.0
|
|
88
57
|
PORT: 7100
|
|
89
|
-
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-nodebbs}
|
|
90
|
-
REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379/0
|
|
91
|
-
USER_CACHE_TTL: ${USER_CACHE_TTL:-
|
|
92
|
-
JWT_SECRET: ${JWT_SECRET}
|
|
58
|
+
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:-postgres_password}@postgres:5432/${POSTGRES_DB:-nodebbs}
|
|
59
|
+
REDIS_URL: redis://default:${REDIS_PASSWORD:-redis_password}@redis:6379/0
|
|
60
|
+
USER_CACHE_TTL: ${USER_CACHE_TTL:-120}
|
|
61
|
+
JWT_SECRET: ${JWT_SECRET:-change-this-to-a-secure-random-string-in-production}
|
|
93
62
|
JWT_ACCESS_TOKEN_EXPIRES_IN: ${JWT_ACCESS_TOKEN_EXPIRES_IN:-1y}
|
|
94
63
|
CORS_ORIGIN: ${CORS_ORIGIN:-*}
|
|
95
64
|
APP_URL: ${APP_URL:-http://localhost:3100}
|
|
96
|
-
TZ:
|
|
97
|
-
NODE_OPTIONS: "--max-old-space-size=512"
|
|
65
|
+
TZ: Asia/Shanghai
|
|
98
66
|
volumes:
|
|
99
67
|
- api_uploads:/app/apps/api/uploads
|
|
68
|
+
# - ./apps/api/src:/app/apps/api/src:ro
|
|
100
69
|
ports:
|
|
101
70
|
- "${API_PORT:-7100}:7100"
|
|
102
71
|
depends_on:
|
|
@@ -110,35 +79,23 @@ services:
|
|
|
110
79
|
timeout: 10s
|
|
111
80
|
retries: 3
|
|
112
81
|
start_period: 40s
|
|
113
|
-
deploy:
|
|
114
|
-
resources:
|
|
115
|
-
limits:
|
|
116
|
-
cpus: '1'
|
|
117
|
-
memory: 768M
|
|
118
|
-
reservations:
|
|
119
|
-
cpus: '0.3'
|
|
120
|
-
memory: 384M
|
|
121
|
-
logging:
|
|
122
|
-
driver: "json-file"
|
|
123
|
-
options:
|
|
124
|
-
max-size: "20m"
|
|
125
|
-
max-file: "5"
|
|
126
82
|
networks:
|
|
127
83
|
- nodebbs-network
|
|
128
84
|
|
|
129
85
|
# Web 前端服务
|
|
130
86
|
web:
|
|
131
|
-
|
|
87
|
+
build:
|
|
88
|
+
context: ../../
|
|
89
|
+
dockerfile: apps/web/Dockerfile
|
|
90
|
+
image: ${WEB_IMAGE:-nodebbs-web:local}
|
|
132
91
|
container_name: nodebbs-web
|
|
133
|
-
restart:
|
|
92
|
+
restart: unless-stopped
|
|
134
93
|
environment:
|
|
135
94
|
NODE_ENV: production
|
|
136
95
|
APP_NAME: ${APP_NAME:-nodebbs}
|
|
137
96
|
PORT: 3100
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
TZ: ${TZ:-Asia/Shanghai}
|
|
141
|
-
NODE_OPTIONS: "--max-old-space-size=512"
|
|
97
|
+
SERVER_API_URL: ${SERVER_API_URL:-http://api:7100}
|
|
98
|
+
TZ: Asia/Shanghai
|
|
142
99
|
ports:
|
|
143
100
|
- "${WEB_PORT:-3100}:3100"
|
|
144
101
|
depends_on:
|
|
@@ -150,19 +107,6 @@ services:
|
|
|
150
107
|
timeout: 10s
|
|
151
108
|
retries: 3
|
|
152
109
|
start_period: 40s
|
|
153
|
-
deploy:
|
|
154
|
-
resources:
|
|
155
|
-
limits:
|
|
156
|
-
cpus: '1'
|
|
157
|
-
memory: 768M
|
|
158
|
-
reservations:
|
|
159
|
-
cpus: '0.3'
|
|
160
|
-
memory: 384M
|
|
161
|
-
logging:
|
|
162
|
-
driver: "json-file"
|
|
163
|
-
options:
|
|
164
|
-
max-size: "20m"
|
|
165
|
-
max-file: "5"
|
|
166
110
|
networks:
|
|
167
111
|
- nodebbs-network
|
|
168
112
|
|
|
@@ -179,6 +123,3 @@ volumes:
|
|
|
179
123
|
networks:
|
|
180
124
|
nodebbs-network:
|
|
181
125
|
driver: bridge
|
|
182
|
-
ipam:
|
|
183
|
-
config:
|
|
184
|
-
- subnet: 172.28.0.0/16
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# ============================================
|
|
2
|
+
# .dockerignore - Turborepo Monorepo
|
|
3
|
+
# ============================================
|
|
4
|
+
# 作用:优化 Docker 构建上下文
|
|
5
|
+
# 原则:只把源码和必要文件打包进镜像
|
|
6
|
+
# 构建缓存、依赖、日志、运行时数据等全部排除
|
|
7
|
+
# ============================================
|
|
8
|
+
|
|
9
|
+
# ========================
|
|
10
|
+
# Node / pnpm 依赖
|
|
11
|
+
# ========================
|
|
12
|
+
# node_modules 会在 Docker 内重新安装
|
|
13
|
+
node_modules
|
|
14
|
+
# pnpm 全局存储目录
|
|
15
|
+
.pnpm-store
|
|
16
|
+
|
|
17
|
+
# ========================
|
|
18
|
+
# Turborepo 缓存
|
|
19
|
+
# ========================
|
|
20
|
+
.turbo
|
|
21
|
+
|
|
22
|
+
# ========================
|
|
23
|
+
# 构建输出
|
|
24
|
+
# ========================
|
|
25
|
+
.next # Next.js 构建目录
|
|
26
|
+
dist # Fastify / TS 构建目录
|
|
27
|
+
build
|
|
28
|
+
out # Next.js 静态导出目录
|
|
29
|
+
coverage # 测试覆盖率报告
|
|
30
|
+
|
|
31
|
+
# ========================
|
|
32
|
+
# 日志文件
|
|
33
|
+
# ========================
|
|
34
|
+
*.log
|
|
35
|
+
npm-debug.log*
|
|
36
|
+
yarn-debug.log*
|
|
37
|
+
yarn-error.log*
|
|
38
|
+
pnpm-debug.log*
|
|
39
|
+
lerna-debug.log*
|
|
40
|
+
|
|
41
|
+
# ========================
|
|
42
|
+
# 环境变量文件(运行时通过 docker-compose 注入)
|
|
43
|
+
# ========================
|
|
44
|
+
.env* # 排除所有本地环境文件
|
|
45
|
+
!.env.example # 保留示例文件
|
|
46
|
+
|
|
47
|
+
# ========================
|
|
48
|
+
# 测试相关文件
|
|
49
|
+
# ========================
|
|
50
|
+
__tests__ # 单元测试目录
|
|
51
|
+
*.test.* # 测试文件
|
|
52
|
+
*.spec.* # 测试文件
|
|
53
|
+
.nyc_output # 测试覆盖率缓存
|
|
54
|
+
|
|
55
|
+
# ========================
|
|
56
|
+
# 版本控制相关
|
|
57
|
+
# ========================
|
|
58
|
+
.git
|
|
59
|
+
.gitignore
|
|
60
|
+
.gitattributes
|
|
61
|
+
|
|
62
|
+
# ========================
|
|
63
|
+
# 编辑器 / 操作系统临时文件
|
|
64
|
+
# ========================
|
|
65
|
+
.vscode
|
|
66
|
+
.idea
|
|
67
|
+
.DS_Store
|
|
68
|
+
Thumbs.db
|
|
69
|
+
desktop.ini
|
|
70
|
+
*.swp
|
|
71
|
+
*.swo
|
|
72
|
+
*.swn
|
|
73
|
+
.history
|
|
74
|
+
|
|
75
|
+
# ========================
|
|
76
|
+
# CI/CD 和文档
|
|
77
|
+
# ========================
|
|
78
|
+
.github
|
|
79
|
+
.gitlab-ci.yml
|
|
80
|
+
.travis.yml
|
|
81
|
+
.circleci
|
|
82
|
+
docs
|
|
83
|
+
*.md
|
|
84
|
+
!README.md # 保留 README
|
|
85
|
+
|
|
86
|
+
# ========================
|
|
87
|
+
# 缓存 / 临时文件
|
|
88
|
+
# ========================
|
|
89
|
+
.cache
|
|
90
|
+
tmp
|
|
91
|
+
temp
|
|
92
|
+
.temp
|
|
93
|
+
*.tsbuildinfo # TypeScript 构建缓存
|
|
94
|
+
|
|
95
|
+
# ========================
|
|
96
|
+
# 运行时数据(使用 Docker 卷挂载)
|
|
97
|
+
# ========================
|
|
98
|
+
apps/api/uploads/ # 用户上传文件
|
|
99
|
+
|
|
100
|
+
# ========================
|
|
101
|
+
# 本地数据库文件(开发用)
|
|
102
|
+
# ========================
|
|
103
|
+
*.db
|
|
104
|
+
*.sqlite
|
|
105
|
+
|
|
106
|
+
# ========================
|
|
107
|
+
# 数据库 dump(非 migration)
|
|
108
|
+
# ========================
|
|
109
|
+
dump/
|
|
110
|
+
backups/
|
|
111
|
+
|
|
112
|
+
# ========================
|
|
113
|
+
# 进程管理器
|
|
114
|
+
# ========================
|
|
115
|
+
.pm2
|
|
116
|
+
|
|
117
|
+
# ========================
|
|
118
|
+
# docker-compose 文件(不打包进镜像)
|
|
119
|
+
# ========================
|
|
120
|
+
docker-compose*.yml
|
package/dist/templates/env
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
# ========================================
|
|
2
|
-
# NodeBBS
|
|
2
|
+
# NodeBBS Docker Compose 环境变量配置
|
|
3
3
|
# ========================================
|
|
4
4
|
|
|
5
|
-
#
|
|
6
|
-
# 基础配置
|
|
7
|
-
# ========================================
|
|
8
|
-
# 应用名称(用于 Docker 容器前缀)
|
|
5
|
+
# 应用名称
|
|
9
6
|
APP_NAME=nodebbs
|
|
10
7
|
|
|
11
|
-
# 部署环境 (prod, dev)
|
|
12
|
-
NODE_ENV=production
|
|
13
|
-
|
|
14
|
-
# 镜像配置
|
|
15
|
-
# 默认为 GitHub Container Registry
|
|
16
|
-
# 注意:使用 `nodebbs start` 时,可以选择版本 tag 覆盖此处的默认 latest tag
|
|
17
|
-
# 如果需要固定特定版本 (如 v1.0),请修改以下配置或在部署时输入对应的 tag
|
|
18
|
-
API_IMAGE=ghcr.io/aiprojecthub/nodebbs-api:latest
|
|
19
|
-
WEB_IMAGE=ghcr.io/aiprojecthub/nodebbs-web:latest
|
|
20
|
-
|
|
21
8
|
# ========================================
|
|
22
9
|
# 数据库配置
|
|
23
10
|
# ========================================
|
|
24
|
-
# 务必修改为强密码!
|
|
25
11
|
POSTGRES_PASSWORD=your_secure_postgres_password_here
|
|
26
12
|
POSTGRES_DB=nodebbs
|
|
27
13
|
POSTGRES_PORT=5432
|
|
@@ -29,7 +15,6 @@ POSTGRES_PORT=5432
|
|
|
29
15
|
# ========================================
|
|
30
16
|
# Redis 配置
|
|
31
17
|
# ========================================
|
|
32
|
-
# 务必修改为强密码!
|
|
33
18
|
REDIS_PASSWORD=your_secure_redis_password_here
|
|
34
19
|
REDIS_PORT=6379
|
|
35
20
|
|
|
@@ -39,8 +24,8 @@ REDIS_PORT=6379
|
|
|
39
24
|
API_PORT=7100
|
|
40
25
|
|
|
41
26
|
# 用户缓存 TTL(秒)
|
|
42
|
-
#
|
|
43
|
-
USER_CACHE_TTL=
|
|
27
|
+
# 开发环境: 30-60, 生产环境: 120-300
|
|
28
|
+
USER_CACHE_TTL=120
|
|
44
29
|
|
|
45
30
|
# JWT 配置
|
|
46
31
|
# 使用 `openssl rand -base64 32` 生成安全的密钥
|
|
@@ -52,7 +37,6 @@ JWT_ACCESS_TOKEN_EXPIRES_IN=1y
|
|
|
52
37
|
CORS_ORIGIN=*
|
|
53
38
|
|
|
54
39
|
# 应用 URL(OAuth 回调使用)
|
|
55
|
-
# 生产环境请修改为公网域名
|
|
56
40
|
APP_URL=http://localhost:3100
|
|
57
41
|
|
|
58
42
|
# ========================================
|
|
@@ -60,17 +44,14 @@ APP_URL=http://localhost:3100
|
|
|
60
44
|
# ========================================
|
|
61
45
|
WEB_PORT=3100
|
|
62
46
|
|
|
63
|
-
# API
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
NEXT_PUBLIC_API_URL=http://localhost:7100
|
|
67
|
-
|
|
68
|
-
# 应用 URL(公网访问地址)
|
|
69
|
-
# 生产环境请修改为公网域名,例如: https://yourdomain.com
|
|
70
|
-
NEXT_PUBLIC_APP_URL=http://localhost:3100
|
|
47
|
+
# Web 服务内部访问 API 的地址
|
|
48
|
+
# 通常保持默认 http://api:7100 即可,走 Docker 内部网络
|
|
49
|
+
SERVER_API_URL=http://api:7100
|
|
71
50
|
|
|
72
51
|
# ========================================
|
|
73
|
-
#
|
|
52
|
+
# 时区配置
|
|
74
53
|
# ========================================
|
|
75
|
-
# 时区
|
|
76
54
|
TZ=Asia/Shanghai
|
|
55
|
+
|
|
56
|
+
API_IMAGE=ghcr.io/aiprojecthub/nodebbs-api:latest
|
|
57
|
+
WEB_IMAGE=ghcr.io/aiprojecthub/nodebbs-web:latest
|
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' | 'basic')
|
|
9
9
|
* @returns 对象包含文件路径数组和是否使用内置模板的标志
|
|
10
10
|
*/
|
|
11
11
|
export declare function getComposeFiles(env: string): Promise<{
|
package/dist/utils/docker.js
CHANGED
|
@@ -4,7 +4,17 @@ import path from 'node:path';
|
|
|
4
4
|
import { exists } from 'node:fs';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { getTemplateDir, getTemplatePath } from './template.js';
|
|
7
|
+
import { getDeploymentMode } from './selection.js';
|
|
7
8
|
const fileExists = promisify(exists);
|
|
9
|
+
async function getDockerEnv() {
|
|
10
|
+
const mode = await getDeploymentMode();
|
|
11
|
+
const envs = { ...process.env, COMPOSE_LOG_LEVEL: 'ERROR' };
|
|
12
|
+
if (mode === 'source') {
|
|
13
|
+
envs['API_IMAGE'] = 'nodebbs-api:local';
|
|
14
|
+
envs['WEB_IMAGE'] = 'nodebbs-web:local';
|
|
15
|
+
}
|
|
16
|
+
return envs;
|
|
17
|
+
}
|
|
8
18
|
/**
|
|
9
19
|
* 获取 Docker Compose 文件路径配置
|
|
10
20
|
*
|
|
@@ -12,27 +22,52 @@ const fileExists = promisify(exists);
|
|
|
12
22
|
* - 如果存在,则优先使用项目根目录的配置。
|
|
13
23
|
* - 如果不存在,则使用 CLI 内置的模板文件。
|
|
14
24
|
*
|
|
15
|
-
* @param env - 运行环境 ('production' | 'lowmem')
|
|
25
|
+
* @param env - 运行环境 ('production' | 'lowmem' | 'basic')
|
|
16
26
|
* @returns 对象包含文件路径数组和是否使用内置模板的标志
|
|
17
27
|
*/
|
|
18
28
|
export async function getComposeFiles(env) {
|
|
19
29
|
const workDir = process.cwd();
|
|
20
30
|
let isBuiltIn = false;
|
|
31
|
+
// 1. 确定 Base 文件
|
|
21
32
|
// 检查当前目录是否存在 docker-compose.yml
|
|
22
33
|
let baseFile = path.join(workDir, 'docker-compose.yml');
|
|
23
|
-
|
|
34
|
+
const templateDir = getTemplateDir();
|
|
24
35
|
if (!await fileExists(baseFile)) {
|
|
25
36
|
// 使用内置模板
|
|
26
37
|
isBuiltIn = true;
|
|
27
|
-
templateDir = getTemplateDir();
|
|
28
38
|
baseFile = getTemplatePath('docker-compose.yml');
|
|
29
39
|
}
|
|
30
40
|
const files = [baseFile];
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
// 2. 确定环境 Override 文件 (production / lowmem)
|
|
42
|
+
if (env === 'production' || env === 'lowmem') {
|
|
43
|
+
const fileName = `docker-compose.${env}.yml`;
|
|
44
|
+
const localOverride = path.join(workDir, fileName);
|
|
45
|
+
const templateOverride = path.join(templateDir, fileName);
|
|
46
|
+
if (await fileExists(localOverride)) {
|
|
47
|
+
files.push(localOverride);
|
|
48
|
+
}
|
|
49
|
+
else if (await fileExists(templateOverride)) {
|
|
50
|
+
// 即使 Base 是本地的,如果本地没有 override,我们也尝试用内置的 override 补全吗?
|
|
51
|
+
// 通常为了混用方便,是可以的。但如果 Base 是本地,通常期望全套本地。
|
|
52
|
+
// 策略:如果 Base 是内置,则一定用内置 Override。如果 Base 是本地,仅当本地也有 Override 时才用?
|
|
53
|
+
// 不,为了方便用户只覆盖 Base 而复用我们的资源配置,如果本地没 Override,可以用内置的。
|
|
54
|
+
files.push(templateOverride);
|
|
55
|
+
}
|
|
33
56
|
}
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
// 3. 确定构建配置 (仅源码模式)
|
|
58
|
+
const mode = await getDeploymentMode();
|
|
59
|
+
if (mode === 'source') {
|
|
60
|
+
const buildFileName = 'docker-compose.build.yml';
|
|
61
|
+
const localBuild = path.join(workDir, buildFileName);
|
|
62
|
+
const templateBuild = path.join(templateDir, buildFileName);
|
|
63
|
+
if (await fileExists(localBuild)) {
|
|
64
|
+
files.push(localBuild);
|
|
65
|
+
}
|
|
66
|
+
else if (isBuiltIn && await fileExists(templateBuild)) {
|
|
67
|
+
// 只有在使用内置 Base 时,才默认加载内置 Build。
|
|
68
|
+
// 如果用户自定义了 Base,通常他们会自己把 build 写进去,或者自己提供 build.yml
|
|
69
|
+
files.push(templateBuild);
|
|
70
|
+
}
|
|
36
71
|
}
|
|
37
72
|
return { files, isBuiltIn };
|
|
38
73
|
}
|
|
@@ -74,7 +109,7 @@ export async function runCompose(files, args, isBuiltIn = false) {
|
|
|
74
109
|
// 设置 COMPOSE_LOG_LEVEL=ERROR 抑制变量未设置的警告
|
|
75
110
|
await execa('docker', ['compose', ...composeArgs], {
|
|
76
111
|
stdio: 'inherit',
|
|
77
|
-
env:
|
|
112
|
+
env: await getDockerEnv()
|
|
78
113
|
});
|
|
79
114
|
}
|
|
80
115
|
/**
|
|
@@ -96,7 +131,7 @@ export async function execCompose(files, service, command, isBuiltIn = false) {
|
|
|
96
131
|
composeArgs.push('exec', service, ...command);
|
|
97
132
|
await execa('docker', ['compose', ...composeArgs], {
|
|
98
133
|
stdio: 'inherit',
|
|
99
|
-
env:
|
|
134
|
+
env: await getDockerEnv()
|
|
100
135
|
});
|
|
101
136
|
}
|
|
102
137
|
/**
|
|
@@ -118,10 +153,11 @@ export async function waitForHealth(files, isBuiltIn = false) {
|
|
|
118
153
|
composeArgs.push('--project-directory', process.cwd());
|
|
119
154
|
}
|
|
120
155
|
const pgArgs = [...composeArgs, 'exec', '-T', 'postgres', 'pg_isready', '-U', 'postgres'];
|
|
156
|
+
const envs = await getDockerEnv();
|
|
121
157
|
let retries = 15;
|
|
122
158
|
while (retries > 0) {
|
|
123
159
|
try {
|
|
124
|
-
await execa('docker', ['compose', ...pgArgs]);
|
|
160
|
+
await execa('docker', ['compose', ...pgArgs], { env: envs });
|
|
125
161
|
logger.success('PostgreSQL 已就绪');
|
|
126
162
|
break;
|
|
127
163
|
}
|
package/dist/utils/env.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
+
export declare function getEnvValue(key: string): Promise<string | undefined>;
|
|
2
|
+
export declare function setEnvValue(key: string, value: string): Promise<void>;
|
|
3
|
+
export declare function unsetEnvValue(key: string): Promise<void>;
|
|
1
4
|
export declare function initEnv(): Promise<void>;
|
|
2
|
-
export declare function
|
|
5
|
+
export declare function initDockerIgnore(): Promise<void>;
|
|
6
|
+
export declare function initImageEnv(): Promise<void>;
|
|
7
|
+
export declare function checkEnv(envType: 'production' | 'lowmem' | 'basic'): Promise<void>;
|
package/dist/utils/env.js
CHANGED
|
@@ -6,12 +6,52 @@ import { confirm } from '@inquirer/prompts';
|
|
|
6
6
|
import dotenv from 'dotenv';
|
|
7
7
|
import { getTemplatePath } from './template.js';
|
|
8
8
|
const fileExists = promisify(exists);
|
|
9
|
-
export async function
|
|
9
|
+
export async function getEnvValue(key) {
|
|
10
|
+
if (!await fileExists('.env'))
|
|
11
|
+
return undefined;
|
|
12
|
+
try {
|
|
13
|
+
const content = await fs.readFile('.env', 'utf-8');
|
|
14
|
+
const config = dotenv.parse(content);
|
|
15
|
+
return config[key];
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function setEnvValue(key, value) {
|
|
22
|
+
let content = '';
|
|
10
23
|
if (await fileExists('.env')) {
|
|
11
|
-
|
|
24
|
+
content = await fs.readFile('.env', 'utf-8');
|
|
25
|
+
}
|
|
26
|
+
const regex = new RegExp(`^${key}=.*`, 'm');
|
|
27
|
+
if (regex.test(content)) {
|
|
28
|
+
content = content.replace(regex, `${key}=${value}`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
if (content && !content.endsWith('\n'))
|
|
32
|
+
content += '\n';
|
|
33
|
+
content += `${key}=${value}\n`;
|
|
34
|
+
}
|
|
35
|
+
await fs.writeFile('.env', content, 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
export async function unsetEnvValue(key) {
|
|
38
|
+
if (!await fileExists('.env'))
|
|
12
39
|
return;
|
|
40
|
+
let content = await fs.readFile('.env', 'utf-8');
|
|
41
|
+
const regex = new RegExp(`^${key}=.*\n?`, 'm');
|
|
42
|
+
if (regex.test(content)) {
|
|
43
|
+
content = content.replace(regex, '');
|
|
44
|
+
await fs.writeFile('.env', content, 'utf-8');
|
|
13
45
|
}
|
|
14
|
-
|
|
46
|
+
}
|
|
47
|
+
export async function initEnv() {
|
|
48
|
+
const currentEnv = await fileExists('.env') ? dotenv.parse(await fs.readFile('.env', 'utf-8')) : {};
|
|
49
|
+
// 如果已存在关键配置(如 POSTGRES_PASSWORD),则视为无需初始化
|
|
50
|
+
if (currentEnv.POSTGRES_PASSWORD) {
|
|
51
|
+
logger.info('.env 文件已配置,跳过创建');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
logger.info('正在创建/更新 .env 文件...');
|
|
15
55
|
// 检查模板文件
|
|
16
56
|
let templateContent = '';
|
|
17
57
|
let sourceFile = '';
|
|
@@ -54,14 +94,10 @@ export async function initEnv() {
|
|
|
54
94
|
message: '设置 API_PORT (后端端口):',
|
|
55
95
|
default: '7100'
|
|
56
96
|
});
|
|
57
|
-
const
|
|
58
|
-
const nextPublicApiUrl = await input({
|
|
59
|
-
message: '设置 NEXT_PUBLIC_API_URL (前端访问后端的地址):',
|
|
60
|
-
default: defaultApiUrl
|
|
61
|
-
});
|
|
97
|
+
const serverApiUrl = `http://api:${apiPort}`;
|
|
62
98
|
const defaultAppUrl = `http://localhost:${webPort}`;
|
|
63
|
-
const
|
|
64
|
-
message: '设置
|
|
99
|
+
const appUrl = await input({
|
|
100
|
+
message: '设置 APP_URL (应用访问地址):',
|
|
65
101
|
default: defaultAppUrl
|
|
66
102
|
});
|
|
67
103
|
const corsOrigin = await input({
|
|
@@ -75,8 +111,8 @@ export async function initEnv() {
|
|
|
75
111
|
console.log(`JWT_SECRET: ${jwtSecret.substring(0, 3)}******`);
|
|
76
112
|
console.log(`WEB_PORT: ${webPort}`);
|
|
77
113
|
console.log(`API_PORT: ${apiPort}`);
|
|
78
|
-
console.log(`
|
|
79
|
-
console.log(`
|
|
114
|
+
console.log(`APP_URL: ${appUrl}`);
|
|
115
|
+
console.log(`SERVER_API_URL: ${serverApiUrl}`);
|
|
80
116
|
console.log(`CORS_ORIGIN: ${corsOrigin}`);
|
|
81
117
|
console.log('==========================================\n');
|
|
82
118
|
// 3. 确认生成
|
|
@@ -98,11 +134,9 @@ export async function initEnv() {
|
|
|
98
134
|
'JWT_SECRET': jwtSecret,
|
|
99
135
|
'WEB_PORT': webPort,
|
|
100
136
|
'API_PORT': apiPort,
|
|
101
|
-
'
|
|
102
|
-
'
|
|
137
|
+
'SERVER_API_URL': serverApiUrl,
|
|
138
|
+
'APP_URL': appUrl,
|
|
103
139
|
'CORS_ORIGIN': corsOrigin,
|
|
104
|
-
// 如果 API_PORT 变了,模板里可能还需要联动修改 APP_URL 用于 OAuth 回调,这里简单处理
|
|
105
|
-
'APP_URL': nextPublicAppUrl
|
|
106
140
|
};
|
|
107
141
|
// 针对每个 Key 进行替换。
|
|
108
142
|
// 策略:查找 `KEY=...` 并替换为 `KEY=newValue`
|
|
@@ -118,6 +152,11 @@ export async function initEnv() {
|
|
|
118
152
|
}
|
|
119
153
|
}
|
|
120
154
|
// 5. 写入文件
|
|
155
|
+
// 如果原文件中有 DEPLOY_ENV 等其他配置,保留它们
|
|
156
|
+
// 简单策略:如果 currentEnv 有 DEPLOY_ENV,确保 newEnv 里也有
|
|
157
|
+
if (currentEnv.DEPLOY_ENV && !newEnv.includes('DEPLOY_ENV=')) {
|
|
158
|
+
newEnv += `\nDEPLOY_ENV=${currentEnv.DEPLOY_ENV}`;
|
|
159
|
+
}
|
|
121
160
|
await fs.writeFile('.env', newEnv, 'utf-8');
|
|
122
161
|
// isBuiltIn is not defined in the new logic, need to determine it
|
|
123
162
|
// based on whether sourceFile was .env.docker.example or a template path
|
|
@@ -129,6 +168,41 @@ export async function initEnv() {
|
|
|
129
168
|
logger.success(`已从 ${sourceFile} 复制并填充配置生成 .env`);
|
|
130
169
|
}
|
|
131
170
|
}
|
|
171
|
+
export async function initDockerIgnore() {
|
|
172
|
+
if (await fileExists('.dockerignore')) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
logger.info('正在创建 .dockerignore 文件...');
|
|
176
|
+
const templatePath = getTemplatePath('dockerignore');
|
|
177
|
+
const content = await fs.readFile(templatePath, 'utf-8');
|
|
178
|
+
await fs.writeFile('.dockerignore', content, 'utf-8');
|
|
179
|
+
logger.success('已创建 .dockerignore');
|
|
180
|
+
}
|
|
181
|
+
export async function initImageEnv() {
|
|
182
|
+
const { getDeploymentMode } = await import('./selection.js');
|
|
183
|
+
const mode = await getDeploymentMode();
|
|
184
|
+
logger.info(`当前部署模式: ${mode === 'image' ? '镜像部署' : '源码部署'}`);
|
|
185
|
+
if (mode !== 'image')
|
|
186
|
+
return;
|
|
187
|
+
logger.info('检测到镜像部署模式,请配置镜像地址...');
|
|
188
|
+
// initEnv 已经执行,尝试从 .env 获取默认值
|
|
189
|
+
const currentApiImage = await getEnvValue('API_IMAGE') || 'ghcr.io/aiprojecthub/nodebbs-api:latest';
|
|
190
|
+
const currentWebImage = await getEnvValue('WEB_IMAGE') || 'ghcr.io/aiprojecthub/nodebbs-web:latest';
|
|
191
|
+
let defaultVersion = 'latest';
|
|
192
|
+
const match = currentApiImage.match(/:([^:]+)$/);
|
|
193
|
+
if (match)
|
|
194
|
+
defaultVersion = match[1];
|
|
195
|
+
const { input } = await import('@inquirer/prompts');
|
|
196
|
+
const version = await input({
|
|
197
|
+
message: '设置镜像版本 (默认为 latest):',
|
|
198
|
+
default: defaultVersion
|
|
199
|
+
});
|
|
200
|
+
// 仅替换版本号部分
|
|
201
|
+
const newApiImage = currentApiImage.replace(/:[^:]+$/, `:${version}`);
|
|
202
|
+
const newWebImage = currentWebImage.replace(/:[^:]+$/, `:${version}`);
|
|
203
|
+
await setEnvValue('API_IMAGE', newApiImage);
|
|
204
|
+
await setEnvValue('WEB_IMAGE', newWebImage);
|
|
205
|
+
}
|
|
132
206
|
export async function checkEnv(envType) {
|
|
133
207
|
logger.info('正在检查环境配置...');
|
|
134
208
|
// 加载 .env
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
export type EnvType = 'production' | 'lowmem';
|
|
1
|
+
export type EnvType = 'production' | 'lowmem' | 'basic';
|
|
2
|
+
export type DeploymentMode = 'source' | 'image';
|
|
2
3
|
export declare const EnvFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
3
4
|
export declare function setStoredEnv(env: EnvType): Promise<void>;
|
|
5
|
+
export declare function getDeploymentMode(): Promise<DeploymentMode>;
|
|
4
6
|
export declare function clearStoredEnv(): Promise<void>;
|
|
5
7
|
export declare function selectEnvironment(env?: string, options?: {
|
|
6
8
|
prompt?: string;
|