nodebbs 0.0.0 → 0.0.2
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 +378 -249
- package/dist/commands/db/generate.d.ts +5 -0
- package/dist/commands/db/generate.js +12 -0
- package/dist/commands/db/index.d.ts +5 -0
- package/dist/commands/db/index.js +11 -0
- package/dist/commands/db/migrate.d.ts +5 -0
- package/dist/commands/db/migrate.js +12 -0
- package/dist/commands/db/push.d.ts +5 -0
- package/dist/commands/db/push.js +12 -0
- package/dist/commands/db/reset.d.ts +5 -0
- package/dist/commands/db/reset.js +22 -0
- package/dist/commands/db/seed.d.ts +5 -0
- package/dist/commands/db/seed.js +12 -0
- package/dist/commands/deploy/index.d.ts +8 -0
- package/dist/commands/deploy/index.js +95 -0
- package/dist/commands/dev/index.d.ts +9 -0
- package/dist/commands/dev/index.js +59 -0
- package/dist/commands/logs/api.d.ts +5 -0
- package/dist/commands/logs/api.js +11 -0
- package/dist/commands/logs/db.d.ts +5 -0
- package/dist/commands/logs/db.js +11 -0
- package/dist/commands/logs/index.d.ts +5 -0
- package/dist/commands/logs/index.js +11 -0
- package/dist/commands/logs/redis.d.ts +5 -0
- package/dist/commands/logs/redis.js +11 -0
- package/dist/commands/logs/web.d.ts +5 -0
- package/dist/commands/logs/web.js +11 -0
- package/dist/commands/restart/index.d.ts +5 -0
- package/dist/commands/restart/index.js +12 -0
- package/dist/commands/setup/index.d.ts +5 -0
- package/dist/commands/setup/index.js +12 -0
- package/dist/commands/shell/api.d.ts +5 -0
- package/dist/commands/shell/api.js +11 -0
- package/dist/commands/shell/db.d.ts +5 -0
- package/dist/commands/shell/db.js +11 -0
- package/dist/commands/shell/redis.d.ts +5 -0
- package/dist/commands/shell/redis.js +11 -0
- package/dist/commands/shell/web.d.ts +5 -0
- package/dist/commands/shell/web.js +11 -0
- package/dist/commands/status/index.d.ts +5 -0
- package/dist/commands/status/index.js +11 -0
- package/dist/commands/stop/index.d.ts +8 -0
- package/dist/commands/stop/index.js +37 -0
- package/dist/templates/docker-compose.lowmem.yml +126 -0
- package/dist/templates/docker-compose.prod.yml +120 -0
- package/dist/templates/docker-compose.yml +127 -0
- package/dist/templates/init-db.sql +14 -0
- package/dist/utils/docker.d.ts +8 -0
- package/dist/utils/docker.js +101 -0
- package/dist/utils/env.d.ts +2 -0
- package/dist/utils/env.js +108 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.js +14 -0
- package/oclif.manifest.json +441 -26
- package/package.json +9 -3
- package/dist/commands/hello/index.d.ts +0 -12
- package/dist/commands/hello/index.js +0 -19
- package/dist/commands/hello/world.d.ts +0 -8
- package/dist/commands/hello/world.js +0 -14
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# 低内存环境 Docker Compose 覆盖配置
|
|
2
|
+
# 适用于 1C1G 或 1C2G 的低配服务器
|
|
3
|
+
# 使用方式: docker compose -f docker-compose.yml -f docker-compose.lowmem.yml up -d
|
|
4
|
+
|
|
5
|
+
services:
|
|
6
|
+
# PostgreSQL 数据库 - 低内存优化
|
|
7
|
+
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
|
+
deploy:
|
|
18
|
+
resources:
|
|
19
|
+
limits:
|
|
20
|
+
cpus: '0.5'
|
|
21
|
+
memory: 256M
|
|
22
|
+
reservations:
|
|
23
|
+
cpus: '0.1'
|
|
24
|
+
memory: 128M
|
|
25
|
+
logging:
|
|
26
|
+
driver: "json-file"
|
|
27
|
+
options:
|
|
28
|
+
max-size: "5m"
|
|
29
|
+
max-file: "2"
|
|
30
|
+
|
|
31
|
+
# Redis 缓存 - 低内存优化
|
|
32
|
+
redis:
|
|
33
|
+
restart: always
|
|
34
|
+
command: >
|
|
35
|
+
redis-server
|
|
36
|
+
--requirepass ${REDIS_PASSWORD}
|
|
37
|
+
--appendonly yes
|
|
38
|
+
--appendfsync everysec
|
|
39
|
+
--maxmemory 128mb
|
|
40
|
+
--maxmemory-policy allkeys-lru
|
|
41
|
+
--save 900 1
|
|
42
|
+
--save 300 10
|
|
43
|
+
ports: [] # 不暴露端口到主机
|
|
44
|
+
healthcheck:
|
|
45
|
+
interval: 30s
|
|
46
|
+
timeout: 10s
|
|
47
|
+
retries: 5
|
|
48
|
+
start_period: 20s
|
|
49
|
+
deploy:
|
|
50
|
+
resources:
|
|
51
|
+
limits:
|
|
52
|
+
cpus: '0.3'
|
|
53
|
+
memory: 128M
|
|
54
|
+
reservations:
|
|
55
|
+
cpus: '0.1'
|
|
56
|
+
memory: 64M
|
|
57
|
+
logging:
|
|
58
|
+
driver: "json-file"
|
|
59
|
+
options:
|
|
60
|
+
max-size: "5m"
|
|
61
|
+
max-file: "2"
|
|
62
|
+
|
|
63
|
+
# API 服务 - 低内存优化
|
|
64
|
+
api:
|
|
65
|
+
restart: always
|
|
66
|
+
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 内存限制
|
|
72
|
+
NODE_OPTIONS: "--max-old-space-size=384"
|
|
73
|
+
volumes:
|
|
74
|
+
- api_uploads:/app/apps/api/uploads
|
|
75
|
+
healthcheck:
|
|
76
|
+
start_period: 90s # 低内存环境启动更慢
|
|
77
|
+
interval: 60s
|
|
78
|
+
deploy:
|
|
79
|
+
resources:
|
|
80
|
+
limits:
|
|
81
|
+
cpus: '0.7'
|
|
82
|
+
memory: 512M
|
|
83
|
+
reservations:
|
|
84
|
+
cpus: '0.2'
|
|
85
|
+
memory: 256M
|
|
86
|
+
logging:
|
|
87
|
+
driver: "json-file"
|
|
88
|
+
options:
|
|
89
|
+
max-size: "10m"
|
|
90
|
+
max-file: "3"
|
|
91
|
+
|
|
92
|
+
# Web 前端服务 - 低内存优化
|
|
93
|
+
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
|
+
environment:
|
|
100
|
+
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
|
101
|
+
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
|
|
102
|
+
# Node.js 内存限制
|
|
103
|
+
NODE_OPTIONS: "--max-old-space-size=384"
|
|
104
|
+
healthcheck:
|
|
105
|
+
start_period: 90s # 低内存环境启动更慢
|
|
106
|
+
interval: 60s
|
|
107
|
+
deploy:
|
|
108
|
+
resources:
|
|
109
|
+
limits:
|
|
110
|
+
cpus: '0.5'
|
|
111
|
+
memory: 512M
|
|
112
|
+
reservations:
|
|
113
|
+
cpus: '0.2'
|
|
114
|
+
memory: 256M
|
|
115
|
+
logging:
|
|
116
|
+
driver: "json-file"
|
|
117
|
+
options:
|
|
118
|
+
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
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# 生产环境 Docker Compose 覆盖配置
|
|
2
|
+
# 使用方式: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
|
3
|
+
# 本文件只包含与 docker-compose.yml 的差异部分
|
|
4
|
+
|
|
5
|
+
services:
|
|
6
|
+
# PostgreSQL 数据库 - 生产环境优化
|
|
7
|
+
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
|
+
deploy:
|
|
18
|
+
resources:
|
|
19
|
+
limits:
|
|
20
|
+
cpus: '1'
|
|
21
|
+
memory: 512M
|
|
22
|
+
reservations:
|
|
23
|
+
cpus: '0.25'
|
|
24
|
+
memory: 256M
|
|
25
|
+
logging:
|
|
26
|
+
driver: "json-file"
|
|
27
|
+
options:
|
|
28
|
+
max-size: "10m"
|
|
29
|
+
max-file: "3"
|
|
30
|
+
|
|
31
|
+
# Redis 缓存 - 生产环境优化
|
|
32
|
+
redis:
|
|
33
|
+
restart: always
|
|
34
|
+
command: >
|
|
35
|
+
redis-server
|
|
36
|
+
--requirepass ${REDIS_PASSWORD}
|
|
37
|
+
--appendonly yes
|
|
38
|
+
--appendfsync everysec
|
|
39
|
+
--maxmemory 256mb
|
|
40
|
+
--maxmemory-policy allkeys-lru
|
|
41
|
+
ports: [] # 生产环境不暴露端口到主机
|
|
42
|
+
healthcheck:
|
|
43
|
+
interval: 30s
|
|
44
|
+
timeout: 10s
|
|
45
|
+
retries: 5
|
|
46
|
+
start_period: 20s
|
|
47
|
+
deploy:
|
|
48
|
+
resources:
|
|
49
|
+
limits:
|
|
50
|
+
cpus: '0.5'
|
|
51
|
+
memory: 256M
|
|
52
|
+
reservations:
|
|
53
|
+
cpus: '0.1'
|
|
54
|
+
memory: 128M
|
|
55
|
+
logging:
|
|
56
|
+
driver: "json-file"
|
|
57
|
+
options:
|
|
58
|
+
max-size: "10m"
|
|
59
|
+
max-file: "3"
|
|
60
|
+
|
|
61
|
+
# API 服务 - 生产环境优化
|
|
62
|
+
api:
|
|
63
|
+
restart: always
|
|
64
|
+
environment:
|
|
65
|
+
USER_CACHE_TTL: ${USER_CACHE_TTL:-300} # 生产环境缓存时间更长
|
|
66
|
+
JWT_SECRET: ${JWT_SECRET} # 生产环境必须设置
|
|
67
|
+
CORS_ORIGIN: ${CORS_ORIGIN} # 生产环境必须明确设置
|
|
68
|
+
APP_URL: ${APP_URL} # 生产环境实际域名
|
|
69
|
+
NODE_OPTIONS: "--max-old-space-size=512" # 限制 Node.js 内存使用
|
|
70
|
+
volumes:
|
|
71
|
+
- api_uploads:/app/apps/api/uploads # 生产环境不挂载源代码
|
|
72
|
+
healthcheck:
|
|
73
|
+
start_period: 60s
|
|
74
|
+
deploy:
|
|
75
|
+
resources:
|
|
76
|
+
limits:
|
|
77
|
+
cpus: '1'
|
|
78
|
+
memory: 768M
|
|
79
|
+
reservations:
|
|
80
|
+
cpus: '0.3'
|
|
81
|
+
memory: 384M
|
|
82
|
+
logging:
|
|
83
|
+
driver: "json-file"
|
|
84
|
+
options:
|
|
85
|
+
max-size: "20m"
|
|
86
|
+
max-file: "5"
|
|
87
|
+
|
|
88
|
+
# Web 前端服务 - 生产环境优化
|
|
89
|
+
web:
|
|
90
|
+
restart: always
|
|
91
|
+
build:
|
|
92
|
+
args:
|
|
93
|
+
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
|
94
|
+
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
|
|
95
|
+
environment:
|
|
96
|
+
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
|
|
97
|
+
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
|
|
98
|
+
NODE_OPTIONS: "--max-old-space-size=512" # 限制 Node.js 内存使用
|
|
99
|
+
healthcheck:
|
|
100
|
+
start_period: 60s
|
|
101
|
+
deploy:
|
|
102
|
+
resources:
|
|
103
|
+
limits:
|
|
104
|
+
cpus: '1'
|
|
105
|
+
memory: 768M
|
|
106
|
+
reservations:
|
|
107
|
+
cpus: '0.3'
|
|
108
|
+
memory: 384M
|
|
109
|
+
logging:
|
|
110
|
+
driver: "json-file"
|
|
111
|
+
options:
|
|
112
|
+
max-size: "20m"
|
|
113
|
+
max-file: "5"
|
|
114
|
+
|
|
115
|
+
# 网络 - 生产环境使用固定子网
|
|
116
|
+
networks:
|
|
117
|
+
nodebbs-network:
|
|
118
|
+
ipam:
|
|
119
|
+
config:
|
|
120
|
+
- subnet: 172.28.0.0/16
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
services:
|
|
2
|
+
# PostgreSQL 数据库
|
|
3
|
+
postgres:
|
|
4
|
+
image: postgres:16-alpine
|
|
5
|
+
container_name: nodebbs-postgres
|
|
6
|
+
restart: unless-stopped
|
|
7
|
+
environment:
|
|
8
|
+
POSTGRES_USER: postgres
|
|
9
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_password}
|
|
10
|
+
POSTGRES_DB: ${POSTGRES_DB:-nodebbs}
|
|
11
|
+
TZ: Asia/Shanghai
|
|
12
|
+
volumes:
|
|
13
|
+
- postgres_data:/var/lib/postgresql/data
|
|
14
|
+
- ${INIT_DB_PATH:-./scripts/init-db.sql}:/docker-entrypoint-initdb.d/init.sql:ro
|
|
15
|
+
ports:
|
|
16
|
+
- "${POSTGRES_PORT:-5432}:5432"
|
|
17
|
+
healthcheck:
|
|
18
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
19
|
+
interval: 10s
|
|
20
|
+
timeout: 5s
|
|
21
|
+
retries: 5
|
|
22
|
+
networks:
|
|
23
|
+
- nodebbs-network
|
|
24
|
+
|
|
25
|
+
# Redis 缓存
|
|
26
|
+
redis:
|
|
27
|
+
image: redis:7-alpine
|
|
28
|
+
container_name: nodebbs-redis
|
|
29
|
+
restart: unless-stopped
|
|
30
|
+
command: redis-server --requirepass ${REDIS_PASSWORD:-redis_password} --appendonly yes
|
|
31
|
+
environment:
|
|
32
|
+
TZ: Asia/Shanghai
|
|
33
|
+
volumes:
|
|
34
|
+
- redis_data:/data
|
|
35
|
+
ports:
|
|
36
|
+
- "${REDIS_PORT:-6379}:6379"
|
|
37
|
+
healthcheck:
|
|
38
|
+
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
|
39
|
+
interval: 10s
|
|
40
|
+
timeout: 5s
|
|
41
|
+
retries: 5
|
|
42
|
+
networks:
|
|
43
|
+
- nodebbs-network
|
|
44
|
+
|
|
45
|
+
# API 服务
|
|
46
|
+
api:
|
|
47
|
+
build:
|
|
48
|
+
context: . # 从 monorepo 根目录构建(turbo prune 需要完整的 workspace)
|
|
49
|
+
dockerfile: apps/api/Dockerfile
|
|
50
|
+
container_name: nodebbs-api
|
|
51
|
+
restart: unless-stopped
|
|
52
|
+
environment:
|
|
53
|
+
NODE_ENV: production
|
|
54
|
+
APP_NAME: ${APP_NAME:-nodebbs}
|
|
55
|
+
HOST: 0.0.0.0
|
|
56
|
+
PORT: 7100
|
|
57
|
+
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:-postgres_password}@postgres:5432/${POSTGRES_DB:-nodebbs}
|
|
58
|
+
REDIS_URL: redis://default:${REDIS_PASSWORD:-redis_password}@redis:6379/0
|
|
59
|
+
USER_CACHE_TTL: ${USER_CACHE_TTL:-120}
|
|
60
|
+
JWT_SECRET: ${JWT_SECRET:-change-this-to-a-secure-random-string-in-production}
|
|
61
|
+
JWT_ACCESS_TOKEN_EXPIRES_IN: ${JWT_ACCESS_TOKEN_EXPIRES_IN:-1y}
|
|
62
|
+
CORS_ORIGIN: ${CORS_ORIGIN:-*}
|
|
63
|
+
APP_URL: ${APP_URL:-http://localhost:3100}
|
|
64
|
+
TZ: Asia/Shanghai
|
|
65
|
+
volumes:
|
|
66
|
+
- api_uploads:/app/apps/api/uploads
|
|
67
|
+
- ./apps/api/src:/app/apps/api/src:ro
|
|
68
|
+
ports:
|
|
69
|
+
- "${API_PORT:-7100}:7100"
|
|
70
|
+
depends_on:
|
|
71
|
+
postgres:
|
|
72
|
+
condition: service_healthy
|
|
73
|
+
redis:
|
|
74
|
+
condition: service_healthy
|
|
75
|
+
healthcheck:
|
|
76
|
+
test: ["CMD", "node", "-e", "require('http').get('http://localhost:7100/api', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
|
77
|
+
interval: 30s
|
|
78
|
+
timeout: 10s
|
|
79
|
+
retries: 3
|
|
80
|
+
start_period: 40s
|
|
81
|
+
networks:
|
|
82
|
+
- nodebbs-network
|
|
83
|
+
|
|
84
|
+
# Web 前端服务
|
|
85
|
+
web:
|
|
86
|
+
build:
|
|
87
|
+
context: . # 从 monorepo 根目录构建(turbo prune 需要完整的 workspace)
|
|
88
|
+
dockerfile: apps/web/Dockerfile
|
|
89
|
+
args:
|
|
90
|
+
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:7100}
|
|
91
|
+
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3100}
|
|
92
|
+
container_name: nodebbs-web
|
|
93
|
+
restart: unless-stopped
|
|
94
|
+
environment:
|
|
95
|
+
NODE_ENV: production
|
|
96
|
+
APP_NAME: ${APP_NAME:-nodebbs}
|
|
97
|
+
PORT: 3100
|
|
98
|
+
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:7100}
|
|
99
|
+
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3100}
|
|
100
|
+
TZ: Asia/Shanghai
|
|
101
|
+
ports:
|
|
102
|
+
- "${WEB_PORT:-3100}:3100"
|
|
103
|
+
depends_on:
|
|
104
|
+
api:
|
|
105
|
+
condition: service_healthy
|
|
106
|
+
healthcheck:
|
|
107
|
+
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3100', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
|
108
|
+
interval: 30s
|
|
109
|
+
timeout: 10s
|
|
110
|
+
retries: 3
|
|
111
|
+
start_period: 40s
|
|
112
|
+
networks:
|
|
113
|
+
- nodebbs-network
|
|
114
|
+
|
|
115
|
+
# 数据卷
|
|
116
|
+
volumes:
|
|
117
|
+
postgres_data:
|
|
118
|
+
driver: local
|
|
119
|
+
redis_data:
|
|
120
|
+
driver: local
|
|
121
|
+
api_uploads:
|
|
122
|
+
driver: local
|
|
123
|
+
|
|
124
|
+
# 网络
|
|
125
|
+
networks:
|
|
126
|
+
nodebbs-network:
|
|
127
|
+
driver: bridge
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- PostgreSQL 初始化脚本
|
|
2
|
+
-- 这个脚本会在数据库首次创建时执行
|
|
3
|
+
|
|
4
|
+
-- 创建扩展(如果需要)
|
|
5
|
+
-- CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
6
|
+
-- CREATE EXTENSION IF NOT EXISTS "pg_trgm";
|
|
7
|
+
|
|
8
|
+
-- 设置时区
|
|
9
|
+
SET timezone = 'Asia/Shanghai';
|
|
10
|
+
|
|
11
|
+
-- 数据库已由 POSTGRES_DB 环境变量创建
|
|
12
|
+
-- 这里可以添加其他初始化 SQL
|
|
13
|
+
|
|
14
|
+
\echo '数据库初始化完成'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function getComposeFiles(env: string): Promise<{
|
|
2
|
+
files: string[];
|
|
3
|
+
isBuiltIn: boolean;
|
|
4
|
+
}>;
|
|
5
|
+
export declare function checkDocker(): Promise<void>;
|
|
6
|
+
export declare function runCompose(files: string[], args: string[], isBuiltIn?: boolean): Promise<void>;
|
|
7
|
+
export declare function execCompose(files: string[], service: string, command: string[], isBuiltIn?: boolean): Promise<void>;
|
|
8
|
+
export declare function waitForHealth(files: string[], isBuiltIn?: boolean): Promise<void>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { exists } from 'node:fs';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
const fileExists = promisify(exists);
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
export async function getComposeFiles(env) {
|
|
10
|
+
const workDir = process.cwd();
|
|
11
|
+
let isBuiltIn = false;
|
|
12
|
+
// Check current directory for docker-compose.yml
|
|
13
|
+
let baseFile = path.join(workDir, 'docker-compose.yml');
|
|
14
|
+
let templateDir = workDir;
|
|
15
|
+
if (!await fileExists(baseFile)) {
|
|
16
|
+
// Use built-in templates
|
|
17
|
+
isBuiltIn = true;
|
|
18
|
+
// In production (dist/utils/docker.js), templates are in dist/templates
|
|
19
|
+
// __dirname is dist/utils, so ../templates points to dist/templates
|
|
20
|
+
templateDir = path.join(__dirname, '..', 'templates');
|
|
21
|
+
baseFile = path.join(templateDir, 'docker-compose.yml');
|
|
22
|
+
// Verify the built-in template exists
|
|
23
|
+
if (!await fileExists(baseFile)) {
|
|
24
|
+
logger.error('内置模板未找到,请确保 CLI 正确安装。');
|
|
25
|
+
throw new Error('Built-in templates not found');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const files = [baseFile];
|
|
29
|
+
if (env === 'production') {
|
|
30
|
+
files.push(path.join(templateDir, 'docker-compose.prod.yml'));
|
|
31
|
+
}
|
|
32
|
+
else if (env === 'lowmem') {
|
|
33
|
+
files.push(path.join(templateDir, 'docker-compose.lowmem.yml'));
|
|
34
|
+
}
|
|
35
|
+
return { files, isBuiltIn };
|
|
36
|
+
}
|
|
37
|
+
export async function checkDocker() {
|
|
38
|
+
logger.info('正在检查 Docker 环境...');
|
|
39
|
+
try {
|
|
40
|
+
await execa('docker', ['--version']);
|
|
41
|
+
await execa('docker', ['compose', 'version']);
|
|
42
|
+
logger.success('Docker 环境检查通过');
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
logger.error('未找到 Docker 或 Docker Compose,请先安装。');
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function runCompose(files, args, isBuiltIn = false) {
|
|
50
|
+
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
51
|
+
if (isBuiltIn) {
|
|
52
|
+
composeArgs.push('--project-directory', process.cwd());
|
|
53
|
+
// Set INIT_DB_PATH to the built-in sql file
|
|
54
|
+
const templateDir = path.dirname(files[0]);
|
|
55
|
+
process.env.INIT_DB_PATH = path.join(templateDir, 'init-db.sql');
|
|
56
|
+
}
|
|
57
|
+
composeArgs.push(...args);
|
|
58
|
+
// Use stdio: 'inherit' to show output in real-time
|
|
59
|
+
await execa('docker', ['compose', ...composeArgs], { stdio: 'inherit' });
|
|
60
|
+
}
|
|
61
|
+
export async function execCompose(files, service, command, isBuiltIn = false) {
|
|
62
|
+
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
63
|
+
if (isBuiltIn) {
|
|
64
|
+
composeArgs.push('--project-directory', process.cwd());
|
|
65
|
+
}
|
|
66
|
+
composeArgs.push('exec', service, ...command);
|
|
67
|
+
await execa('docker', ['compose', ...composeArgs], { stdio: 'inherit' });
|
|
68
|
+
}
|
|
69
|
+
export async function waitForHealth(files, isBuiltIn = false) {
|
|
70
|
+
logger.info('正在等待服务就绪...');
|
|
71
|
+
// Wait for Postgres
|
|
72
|
+
logger.info('等待 PostgreSQL...');
|
|
73
|
+
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
74
|
+
if (isBuiltIn) {
|
|
75
|
+
composeArgs.push('--project-directory', process.cwd());
|
|
76
|
+
}
|
|
77
|
+
const pgArgs = [...composeArgs, 'exec', '-T', 'postgres', 'pg_isready', '-U', 'postgres'];
|
|
78
|
+
let retries = 15;
|
|
79
|
+
while (retries > 0) {
|
|
80
|
+
try {
|
|
81
|
+
await execa('docker', ['compose', ...pgArgs]);
|
|
82
|
+
logger.success('PostgreSQL 已就绪');
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
retries--;
|
|
87
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (retries === 0) {
|
|
91
|
+
logger.warning('PostgreSQL 可能尚未就绪');
|
|
92
|
+
}
|
|
93
|
+
// Wait for Redis
|
|
94
|
+
logger.info('等待 Redis...');
|
|
95
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
96
|
+
logger.success('Redis 已就绪');
|
|
97
|
+
// Wait for API
|
|
98
|
+
logger.info('等待 API 服务...');
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
100
|
+
logger.success('API 服务已就绪');
|
|
101
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { exists } from 'node:fs';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
import { confirm } from '@inquirer/prompts';
|
|
6
|
+
import dotenv from 'dotenv';
|
|
7
|
+
const fileExists = promisify(exists);
|
|
8
|
+
export async function initEnv() {
|
|
9
|
+
if (await fileExists('.env')) {
|
|
10
|
+
logger.info('.env 文件已存在,跳过创建');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
logger.info('正在创建 .env 文件...');
|
|
14
|
+
if (await fileExists('.env.docker.example')) {
|
|
15
|
+
await fs.copyFile('.env.docker.example', '.env');
|
|
16
|
+
logger.warning('请编辑 .env 文件并修改以下配置:');
|
|
17
|
+
logger.warning(' - POSTGRES_PASSWORD (数据库密码)');
|
|
18
|
+
logger.warning(' - REDIS_PASSWORD (Redis 密码)');
|
|
19
|
+
logger.warning(' - JWT_SECRET (JWT 密钥)');
|
|
20
|
+
const edit = await confirm({
|
|
21
|
+
message: '是否现在编辑 .env 文件?',
|
|
22
|
+
default: false
|
|
23
|
+
});
|
|
24
|
+
if (edit) {
|
|
25
|
+
logger.info('请手动编辑 .env 文件后再次运行命令。');
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// If .env.docker.example doesn't exist, maybe we are in the wrong directory or need to look in project/
|
|
31
|
+
if (await fileExists('project/.env.docker.example')) {
|
|
32
|
+
await fs.copyFile('project/.env.docker.example', '.env');
|
|
33
|
+
logger.success('已从 project/.env.docker.example 复制 .env');
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
logger.warning('未找到 .env.docker.example!跳过 .env 创建。');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function checkEnv(envType) {
|
|
41
|
+
logger.info('正在检查环境配置...');
|
|
42
|
+
// Load .env
|
|
43
|
+
const envConfig = dotenv.config().parsed || {};
|
|
44
|
+
let warnings = 0;
|
|
45
|
+
let errors = 0;
|
|
46
|
+
const defaults = {
|
|
47
|
+
POSTGRES_PASSWORD: ['your_secure_postgres_password_here', 'postgres_password'],
|
|
48
|
+
REDIS_PASSWORD: ['your_secure_redis_password_here', 'redis_password'],
|
|
49
|
+
JWT_SECRET: ['change-this-to-a-secure-random-string-in-production']
|
|
50
|
+
};
|
|
51
|
+
const isProd = envType === 'production' || envType === 'lowmem';
|
|
52
|
+
if (defaults.POSTGRES_PASSWORD.includes(envConfig.POSTGRES_PASSWORD)) {
|
|
53
|
+
if (isProd) {
|
|
54
|
+
logger.error('生产环境必须修改 POSTGRES_PASSWORD');
|
|
55
|
+
errors++;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
logger.warning('建议修改 POSTGRES_PASSWORD');
|
|
59
|
+
warnings++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (defaults.REDIS_PASSWORD.includes(envConfig.REDIS_PASSWORD)) {
|
|
63
|
+
if (isProd) {
|
|
64
|
+
logger.error('生产环境必须修改 REDIS_PASSWORD');
|
|
65
|
+
errors++;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
logger.warning('建议修改 REDIS_PASSWORD');
|
|
69
|
+
warnings++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (defaults.JWT_SECRET.includes(envConfig.JWT_SECRET)) {
|
|
73
|
+
if (isProd) {
|
|
74
|
+
logger.error('生产环境必须修改 JWT_SECRET');
|
|
75
|
+
errors++;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
logger.warning('建议修改 JWT_SECRET');
|
|
79
|
+
warnings++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (isProd) {
|
|
83
|
+
if (!envConfig.CORS_ORIGIN || envConfig.CORS_ORIGIN === '*') {
|
|
84
|
+
logger.warning('生产环境建议设置具体的 CORS_ORIGIN');
|
|
85
|
+
warnings++;
|
|
86
|
+
}
|
|
87
|
+
if (!envConfig.APP_URL || envConfig.APP_URL === 'http://localhost:3100') {
|
|
88
|
+
logger.warning('生产环境建议设置实际的 APP_URL');
|
|
89
|
+
warnings++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (errors > 0) {
|
|
93
|
+
logger.error(`发现 ${errors} 个配置错误,无法继续。`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (warnings > 0) {
|
|
97
|
+
logger.warning(`发现 ${warnings} 个配置警告。`);
|
|
98
|
+
const cont = await confirm({
|
|
99
|
+
message: '是否继续?',
|
|
100
|
+
default: false
|
|
101
|
+
});
|
|
102
|
+
if (!cont)
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
logger.success('环境配置检查通过');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export const logger = {
|
|
3
|
+
info: (msg) => console.log(chalk.blue('[信息]'), msg),
|
|
4
|
+
success: (msg) => console.log(chalk.green('[成功]'), msg),
|
|
5
|
+
warning: (msg) => console.log(chalk.yellow('[警告]'), msg),
|
|
6
|
+
error: (msg) => console.log(chalk.red('[错误]'), msg),
|
|
7
|
+
header: (msg) => {
|
|
8
|
+
console.log('');
|
|
9
|
+
console.log(chalk.blue('========================================'));
|
|
10
|
+
console.log(chalk.blue(` ${msg}`));
|
|
11
|
+
console.log(chalk.blue('========================================'));
|
|
12
|
+
console.log('');
|
|
13
|
+
}
|
|
14
|
+
};
|