nodebbs 0.3.3 → 0.4.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 +22 -17
- 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} +8 -8
- package/dist/commands/backup/uploads.d.ts +13 -0
- package/dist/commands/backup/uploads.js +116 -0
- package/dist/commands/clean/index.d.ts +1 -1
- package/dist/commands/clean/index.js +12 -12
- package/dist/commands/db/reset.js +4 -4
- package/dist/commands/import/all.d.ts +9 -0
- package/dist/commands/import/all.js +123 -0
- package/dist/commands/{db/import.d.ts → import/db.d.ts} +1 -1
- package/dist/commands/{db/import.js → import/db.js} +16 -22
- package/dist/commands/import/uploads.d.ts +13 -0
- package/dist/commands/import/uploads.js +141 -0
- package/dist/commands/logs/all.js +2 -2
- package/dist/commands/logs/api.js +2 -2
- package/dist/commands/logs/db.js +1 -1
- package/dist/commands/logs/redis.js +1 -1
- package/dist/commands/logs/web.js +1 -1
- package/dist/commands/pack/index.js +30 -27
- package/dist/commands/restart/index.js +6 -8
- package/dist/commands/shell/api.js +1 -1
- package/dist/commands/shell/db.js +2 -2
- package/dist/commands/shell/redis.js +2 -2
- package/dist/commands/shell/web.js +1 -1
- package/dist/commands/start/index.d.ts +1 -1
- package/dist/commands/start/index.js +12 -22
- package/dist/commands/status/index.js +2 -2
- package/dist/commands/stop/index.d.ts +1 -1
- package/dist/commands/stop/index.js +7 -7
- package/dist/commands/upgrade/index.js +5 -5
- package/dist/interactive.js +28 -28
- package/dist/templates/config.yml +22 -0
- package/dist/templates/docker-compose.yml +3 -13
- package/dist/templates/env +1 -2
- package/dist/utils/docker.js +28 -20
- package/dist/utils/env.d.ts +1 -1
- package/dist/utils/env.js +40 -42
- package/dist/utils/logger.d.ts +2 -2
- package/dist/utils/logger.js +8 -8
- package/dist/utils/selection.d.ts +2 -2
- package/dist/utils/selection.js +7 -10
- package/oclif.manifest.json +213 -67
- package/package.json +9 -3
package/dist/interactive.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { Config } from '@oclif/core';
|
|
2
1
|
import { select, Separator } from '@inquirer/prompts';
|
|
3
|
-
import {
|
|
2
|
+
import { Config } from '@oclif/core';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
export async function runInteractive(root) {
|
|
5
6
|
// Config.load 需要文件路径而不是 URL
|
|
6
7
|
// 如果 root 是 import.meta.url,则进行转换
|
|
7
|
-
|
|
8
|
+
const rootPath = root.startsWith('file://') ? fileURLToPath(root) : root;
|
|
8
9
|
const config = await Config.load(rootPath);
|
|
9
10
|
// 构建命令树
|
|
10
|
-
const tree = { name: 'root'
|
|
11
|
+
const tree = { children: {}, name: 'root' };
|
|
11
12
|
for (const cmd of config.commands) {
|
|
12
13
|
// 跳过隐藏命令
|
|
13
14
|
if (cmd.hidden)
|
|
14
15
|
continue;
|
|
15
16
|
const parts = cmd.id.split(':');
|
|
16
17
|
let currentNode = tree;
|
|
17
|
-
for (
|
|
18
|
-
const part = parts[i];
|
|
18
|
+
for (const part of parts) {
|
|
19
19
|
if (!currentNode.children[part]) {
|
|
20
20
|
currentNode.children[part] = {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
children: {},
|
|
22
|
+
name: part
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
currentNode = currentNode.children[part];
|
|
@@ -29,8 +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
|
+
'backup': ['all', 'db', 'uploads'],
|
|
35
|
+
'db': ['push', 'seed', 'reset'],
|
|
36
|
+
'import': ['all', 'db', 'uploads'],
|
|
34
37
|
'logs': ['all', 'web', 'api', 'db', 'redis'],
|
|
35
38
|
};
|
|
36
39
|
async function navigate(node, breadcrumbs, config) {
|
|
@@ -57,8 +60,8 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
57
60
|
if (node.command) {
|
|
58
61
|
choices.push({
|
|
59
62
|
name: `${node.command.description || node.command.id}`,
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
short: 'all',
|
|
64
|
+
value: '__SELF__'
|
|
62
65
|
});
|
|
63
66
|
}
|
|
64
67
|
const subChoices = keys.map(key => {
|
|
@@ -82,7 +85,7 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
if (description) {
|
|
85
|
-
const shortDesc = description.split('.')[0].
|
|
88
|
+
const shortDesc = description.split('.')[0].slice(0, 50);
|
|
86
89
|
label = `${key.padEnd(12)} ${shortDesc}`;
|
|
87
90
|
}
|
|
88
91
|
else {
|
|
@@ -93,18 +96,17 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
93
96
|
}
|
|
94
97
|
return {
|
|
95
98
|
name: label,
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
short: key,
|
|
100
|
+
value: key
|
|
98
101
|
};
|
|
99
102
|
});
|
|
100
103
|
choices.push(...subChoices);
|
|
101
104
|
// 添加导航选项
|
|
102
|
-
|
|
105
|
+
choices.push(new Separator());
|
|
103
106
|
if (breadcrumbs.length > 0) {
|
|
104
|
-
choices.push(
|
|
105
|
-
choices.push({ name: '⬅️ 返回', value: '__BACK__', short: '返回' });
|
|
107
|
+
choices.push({ name: chalk.dim('返回'), short: '返回', value: '__BACK__' });
|
|
106
108
|
}
|
|
107
|
-
choices.push({ name: '
|
|
109
|
+
choices.push({ name: chalk.dim('退出'), short: '退出', value: '__EXIT__' });
|
|
108
110
|
const controller = new AbortController();
|
|
109
111
|
// 监听 Esc 键
|
|
110
112
|
const onKeypress = (_str, key) => {
|
|
@@ -118,10 +120,10 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
118
120
|
process.stdin.on('keypress', onKeypress);
|
|
119
121
|
try {
|
|
120
122
|
const selection = await select({
|
|
121
|
-
message: breadcrumbs.length ? `选择命令 (${breadcrumbs.join(' > ')}):` : '选择命令:',
|
|
122
123
|
choices,
|
|
123
|
-
pageSize: 15,
|
|
124
124
|
loop: true, // 允许循环导航
|
|
125
|
+
message: breadcrumbs.length > 0 ? `选择命令 (${breadcrumbs.join(' > ')}):` : '选择命令:',
|
|
126
|
+
pageSize: 15,
|
|
125
127
|
// @ts-ignore: prompt signal support might be strict on types
|
|
126
128
|
signal: controller.signal
|
|
127
129
|
});
|
|
@@ -149,10 +151,10 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
149
151
|
}
|
|
150
152
|
console.error(error.message);
|
|
151
153
|
}
|
|
152
|
-
console.log('\n命令执行完成。');
|
|
154
|
+
console.log('\n✨ 命令执行完成。');
|
|
153
155
|
// 动态导入 input 以保持轻量
|
|
154
156
|
const { input } = await import('@inquirer/prompts');
|
|
155
|
-
await input({ message: '
|
|
157
|
+
await input({ message: '按回车键返回菜单...' });
|
|
156
158
|
// 继续循环
|
|
157
159
|
continue;
|
|
158
160
|
}
|
|
@@ -180,10 +182,10 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
180
182
|
}
|
|
181
183
|
console.error(error.message);
|
|
182
184
|
}
|
|
183
|
-
console.log('\n命令执行完成。');
|
|
185
|
+
console.log('\n✨ 命令执行完成。');
|
|
184
186
|
// 动态导入 input 以保持轻量
|
|
185
187
|
const { input } = await import('@inquirer/prompts');
|
|
186
|
-
await input({ message: '
|
|
188
|
+
await input({ message: '按回车键返回菜单...' });
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
catch (error) {
|
|
@@ -193,10 +195,8 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
193
195
|
// 子菜单 -> 返回
|
|
194
196
|
return;
|
|
195
197
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
process.exit(0);
|
|
199
|
-
}
|
|
198
|
+
// 顶级菜单 -> 退出
|
|
199
|
+
process.exit(0);
|
|
200
200
|
}
|
|
201
201
|
// 处理 Ctrl+C (ExitPromptError)
|
|
202
202
|
if (error.name === 'ExitPromptError') {
|
|
@@ -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
|
|
@@ -42,10 +42,7 @@ services:
|
|
|
42
42
|
|
|
43
43
|
# API 服务
|
|
44
44
|
api:
|
|
45
|
-
|
|
46
|
-
context: .
|
|
47
|
-
dockerfile: apps/api/Dockerfile
|
|
48
|
-
image: ${API_IMAGE:-nodebbs-api:local}
|
|
45
|
+
image: ${API_IMAGE:-ghcr.io/aiprojecthub/nodebbs-api:latest}
|
|
49
46
|
container_name: ${COMPOSE_PROJECT_NAME:-nodebbs}-api
|
|
50
47
|
restart: unless-stopped
|
|
51
48
|
environment:
|
|
@@ -80,10 +77,7 @@ services:
|
|
|
80
77
|
|
|
81
78
|
# Web 前端服务
|
|
82
79
|
web:
|
|
83
|
-
|
|
84
|
-
context: .
|
|
85
|
-
dockerfile: apps/web/Dockerfile
|
|
86
|
-
image: ${WEB_IMAGE:-nodebbs-web:local}
|
|
80
|
+
image: ${WEB_IMAGE:-ghcr.io/aiprojecthub/nodebbs-web:latest}
|
|
87
81
|
container_name: ${COMPOSE_PROJECT_NAME:-nodebbs}-web
|
|
88
82
|
restart: unless-stopped
|
|
89
83
|
environment:
|
|
@@ -91,11 +85,7 @@ services:
|
|
|
91
85
|
PORT: 3100
|
|
92
86
|
SERVER_API_URL: ${SERVER_API_URL:-http://api:7100}
|
|
93
87
|
TZ: ${TZ:-Asia/Shanghai}
|
|
94
|
-
#
|
|
95
|
-
# 自定义资源挂载(如有需要,请取消注释并在同级目录下创建 web 文件夹及对应文件)
|
|
96
|
-
# - ./web/favicon.ico:/app/apps/web/public/favicon.ico
|
|
97
|
-
# - ./web/logo.svg:/app/apps/web/public/logo.svg
|
|
98
|
-
# - ./web/apple-touch-icon.png:/app/apps/web/public/apple-touch-icon.png
|
|
88
|
+
# 自定义资源挂载请使用 config.yml,参见 src/templates/config.yml
|
|
99
89
|
ports:
|
|
100
90
|
- "${WEB_PORT:-3100}:3100"
|
|
101
91
|
depends_on:
|
package/dist/templates/env
CHANGED
package/dist/utils/docker.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { execa } from 'execa';
|
|
2
1
|
import { confirm } from '@inquirer/prompts';
|
|
3
|
-
import {
|
|
4
|
-
import path from 'node:path';
|
|
2
|
+
import { execa } from 'execa';
|
|
5
3
|
import { exists } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
6
5
|
import { promisify } from 'node:util';
|
|
7
|
-
import {
|
|
6
|
+
import { logger } from './logger.js';
|
|
8
7
|
import { getDeploymentMode } from './selection.js';
|
|
8
|
+
import { getTemplateDir, getTemplatePath } from './template.js';
|
|
9
9
|
const fileExists = promisify(exists);
|
|
10
10
|
async function getDockerEnv() {
|
|
11
11
|
const mode = await getDeploymentMode();
|
|
12
12
|
const envs = { ...process.env, COMPOSE_LOG_LEVEL: 'ERROR' };
|
|
13
13
|
if (mode === 'source') {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// 镜像名与容器名保持一致的命名逻辑
|
|
15
|
+
// 优先使用 COMPOSE_PROJECT_NAME,回退到 APP_NAME,最后使用默认值
|
|
16
|
+
const projectName = process.env.COMPOSE_PROJECT_NAME
|
|
17
|
+
|| process.env.APP_NAME
|
|
18
|
+
|| 'nodebbs';
|
|
19
|
+
envs.API_IMAGE = `${projectName}-api:local`;
|
|
20
|
+
envs.WEB_IMAGE = `${projectName}-web:local`;
|
|
16
21
|
}
|
|
17
22
|
return envs;
|
|
18
23
|
}
|
|
@@ -42,8 +47,8 @@ export async function getComposeFiles(env) {
|
|
|
42
47
|
const files = [baseFile];
|
|
43
48
|
// 映射 environment 到对应的 override 文件名
|
|
44
49
|
const envFileMap = {
|
|
45
|
-
'
|
|
46
|
-
'
|
|
50
|
+
'lowmem': 'docker-compose.lowmem.yml',
|
|
51
|
+
'production': 'docker-compose.prod.yml'
|
|
47
52
|
};
|
|
48
53
|
if (env in envFileMap) {
|
|
49
54
|
const fileName = envFileMap[env];
|
|
@@ -75,6 +80,12 @@ export async function getComposeFiles(env) {
|
|
|
75
80
|
files.push(templateBuild);
|
|
76
81
|
}
|
|
77
82
|
}
|
|
83
|
+
// 4. 检测自定义配置文件 (config.yml)
|
|
84
|
+
// 用于挂载自定义静态资源(如 logo、favicon 等),仅当文件存在时加载
|
|
85
|
+
const configFile = path.join(workDir, 'config.yml');
|
|
86
|
+
if (await fileExists(configFile)) {
|
|
87
|
+
files.push(configFile);
|
|
88
|
+
}
|
|
78
89
|
return { files, isBuiltIn };
|
|
79
90
|
}
|
|
80
91
|
/**
|
|
@@ -124,8 +135,8 @@ export async function checkDocker() {
|
|
|
124
135
|
throw error;
|
|
125
136
|
}
|
|
126
137
|
const shouldInstall = await confirm({
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
default: true,
|
|
139
|
+
message: '检测到未安装 Docker,是否尝试自动安装?'
|
|
129
140
|
});
|
|
130
141
|
if (shouldInstall) {
|
|
131
142
|
try {
|
|
@@ -171,7 +182,7 @@ export async function checkDocker() {
|
|
|
171
182
|
* 安装完成后会尝试启动 Docker 服务并清理临时文件。
|
|
172
183
|
*/
|
|
173
184
|
async function installDocker() {
|
|
174
|
-
const platform = process
|
|
185
|
+
const { platform } = process;
|
|
175
186
|
if (platform === 'linux') {
|
|
176
187
|
const scriptPath = 'get-docker.sh';
|
|
177
188
|
try {
|
|
@@ -266,8 +277,8 @@ export async function runCompose(files, args, isBuiltIn = false) {
|
|
|
266
277
|
// 使用 stdio: 'inherit' 实时显示输出
|
|
267
278
|
// 设置 COMPOSE_LOG_LEVEL=ERROR 抑制变量未设置的警告
|
|
268
279
|
await execa('docker', ['compose', ...composeArgs], {
|
|
269
|
-
|
|
270
|
-
|
|
280
|
+
env: await getDockerEnv(),
|
|
281
|
+
stdio: 'inherit'
|
|
271
282
|
});
|
|
272
283
|
}
|
|
273
284
|
/**
|
|
@@ -288,8 +299,8 @@ export async function execCompose(files, service, command, isBuiltIn = false) {
|
|
|
288
299
|
}
|
|
289
300
|
composeArgs.push('exec', service, ...command);
|
|
290
301
|
await execa('docker', ['compose', ...composeArgs], {
|
|
291
|
-
|
|
292
|
-
|
|
302
|
+
env: await getDockerEnv(),
|
|
303
|
+
stdio: 'inherit'
|
|
293
304
|
});
|
|
294
305
|
}
|
|
295
306
|
/**
|
|
@@ -303,9 +314,8 @@ export async function execCompose(files, service, command, isBuiltIn = false) {
|
|
|
303
314
|
* @param isBuiltIn - 是否使用内置模板
|
|
304
315
|
*/
|
|
305
316
|
export async function waitForHealth(files, isBuiltIn = false) {
|
|
306
|
-
logger.info('
|
|
317
|
+
logger.info('正在等待各项依赖服务就绪...');
|
|
307
318
|
// 等待 Postgres
|
|
308
|
-
logger.info('等待 PostgreSQL...');
|
|
309
319
|
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
310
320
|
if (isBuiltIn) {
|
|
311
321
|
composeArgs.push('--project-directory', process.cwd());
|
|
@@ -328,11 +338,9 @@ export async function waitForHealth(files, isBuiltIn = false) {
|
|
|
328
338
|
logger.warning('PostgreSQL 可能尚未就绪');
|
|
329
339
|
}
|
|
330
340
|
// 等待 Redis
|
|
331
|
-
logger.info('等待 Redis...');
|
|
332
341
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
333
342
|
logger.success('Redis 已就绪');
|
|
334
343
|
// 等待 API
|
|
335
|
-
|
|
336
|
-
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
344
|
+
await new Promise(resolve => setTimeout(resolve, 10_000));
|
|
337
345
|
logger.success('API 服务已就绪');
|
|
338
346
|
}
|
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: '
|
|
7
|
+
export declare function checkEnv(envType: 'default' | 'lowmem' | 'production'): Promise<void>;
|
package/dist/utils/env.js
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
2
3
|
import { exists } from 'node:fs';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
3
5
|
import { promisify } from 'node:util';
|
|
4
6
|
import { logger } from './logger.js';
|
|
5
|
-
import { confirm } from '@inquirer/prompts';
|
|
6
|
-
import dotenv from 'dotenv';
|
|
7
7
|
import { getTemplatePath } from './template.js';
|
|
8
8
|
const fileExists = promisify(exists);
|
|
9
9
|
export async function getEnvValue(key) {
|
|
10
10
|
if (!await fileExists('.env'))
|
|
11
11
|
return undefined;
|
|
12
12
|
try {
|
|
13
|
-
const content = await fs.readFile('.env', '
|
|
13
|
+
const content = await fs.readFile('.env', 'utf8');
|
|
14
14
|
const config = dotenv.parse(content);
|
|
15
15
|
return config[key];
|
|
16
16
|
}
|
|
17
|
-
catch
|
|
17
|
+
catch {
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
export async function setEnvValue(key, value) {
|
|
22
22
|
let content = '';
|
|
23
23
|
if (await fileExists('.env')) {
|
|
24
|
-
content = await fs.readFile('.env', '
|
|
24
|
+
content = await fs.readFile('.env', 'utf8');
|
|
25
25
|
}
|
|
26
26
|
const regex = new RegExp(`^${key}=.*`, 'm');
|
|
27
27
|
if (regex.test(content)) {
|
|
@@ -37,7 +37,7 @@ export async function setEnvValue(key, value) {
|
|
|
37
37
|
export async function unsetEnvValue(key) {
|
|
38
38
|
if (!await fileExists('.env'))
|
|
39
39
|
return;
|
|
40
|
-
let content = await fs.readFile('.env', '
|
|
40
|
+
let content = await fs.readFile('.env', 'utf8');
|
|
41
41
|
const regex = new RegExp(`^${key}=.*\n?`, 'm');
|
|
42
42
|
if (regex.test(content)) {
|
|
43
43
|
content = content.replace(regex, '');
|
|
@@ -45,7 +45,7 @@ export async function unsetEnvValue(key) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
export async function initEnv() {
|
|
48
|
-
const currentEnv = await fileExists('.env') ? dotenv.parse(await fs.readFile('.env', '
|
|
48
|
+
const currentEnv = await fileExists('.env') ? dotenv.parse(await fs.readFile('.env', 'utf8')) : {};
|
|
49
49
|
// 如果已存在关键配置(如 POSTGRES_PASSWORD),则视为无需初始化
|
|
50
50
|
if (currentEnv.POSTGRES_PASSWORD) {
|
|
51
51
|
logger.info('.env 文件已配置,跳过创建');
|
|
@@ -57,53 +57,53 @@ export async function initEnv() {
|
|
|
57
57
|
let sourceFile = '';
|
|
58
58
|
if (await fileExists('.env.docker.example')) {
|
|
59
59
|
sourceFile = '.env.docker.example';
|
|
60
|
-
templateContent = await fs.readFile(sourceFile, '
|
|
60
|
+
templateContent = await fs.readFile(sourceFile, 'utf8');
|
|
61
61
|
}
|
|
62
62
|
else {
|
|
63
63
|
// 使用内置模板
|
|
64
64
|
sourceFile = getTemplatePath('env');
|
|
65
|
-
templateContent = await fs.readFile(sourceFile, '
|
|
65
|
+
templateContent = await fs.readFile(sourceFile, 'utf8');
|
|
66
66
|
}
|
|
67
67
|
const { input, password } = await import('@inquirer/prompts');
|
|
68
68
|
const crypto = await import('node:crypto');
|
|
69
69
|
function generateSecret(length = 32) {
|
|
70
|
-
return crypto.randomBytes(length).toString('base64').
|
|
70
|
+
return crypto.randomBytes(length).toString('base64').replaceAll(/[^a-zA-Z0-9]/g, '').slice(0, Math.max(0, length));
|
|
71
71
|
}
|
|
72
72
|
console.log('\n请配置环境变量(按 Enter 使用默认值或自动生成):\n');
|
|
73
73
|
// 1. 收集用户输入
|
|
74
74
|
const postgresPassword = await password({
|
|
75
|
-
message: '设置 POSTGRES_PASSWORD (数据库密码) [空值自动生成]:',
|
|
76
75
|
mask: '*',
|
|
76
|
+
message: '设置 POSTGRES_PASSWORD (数据库密码) [空值自动生成]:',
|
|
77
77
|
validate: (value) => true, // 允许为空以自动生成
|
|
78
78
|
}) || generateSecret();
|
|
79
79
|
const redisPassword = await password({
|
|
80
|
-
message: '设置 REDIS_PASSWORD (Redis 密码) [空值自动生成]:',
|
|
81
80
|
mask: '*',
|
|
81
|
+
message: '设置 REDIS_PASSWORD (Redis 密码) [空值自动生成]:',
|
|
82
82
|
validate: (value) => true,
|
|
83
83
|
}) || generateSecret();
|
|
84
84
|
const jwtSecret = await password({
|
|
85
|
-
message: '设置 JWT_SECRET (JWT 密钥) [空值自动生成]:',
|
|
86
85
|
mask: '*',
|
|
86
|
+
message: '设置 JWT_SECRET (JWT 密钥) [空值自动生成]:',
|
|
87
87
|
validate: (value) => true,
|
|
88
88
|
}) || generateSecret(64);
|
|
89
89
|
const webPort = await input({
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
default: '3100',
|
|
91
|
+
message: '设置 WEB_PORT (前端端口):'
|
|
92
92
|
});
|
|
93
93
|
const apiPort = await input({
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
default: '7100',
|
|
95
|
+
message: '设置 API_PORT (后端端口):'
|
|
96
96
|
});
|
|
97
97
|
const serverApiUrl = `http://api:${apiPort}`;
|
|
98
98
|
const corsOrigin = await input({
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
default: '*',
|
|
100
|
+
message: '设置 CORS_ORIGIN (允许跨域的域名):'
|
|
101
101
|
});
|
|
102
102
|
// 2. 显示配置预览
|
|
103
103
|
console.log('\n================ 配置预览 ================');
|
|
104
|
-
console.log(`POSTGRES_PASSWORD: ${postgresPassword.
|
|
105
|
-
console.log(`REDIS_PASSWORD: ${redisPassword.
|
|
106
|
-
console.log(`JWT_SECRET: ${jwtSecret.
|
|
104
|
+
console.log(`POSTGRES_PASSWORD: ${postgresPassword.slice(0, 3)}******`);
|
|
105
|
+
console.log(`REDIS_PASSWORD: ${redisPassword.slice(0, 3)}******`);
|
|
106
|
+
console.log(`JWT_SECRET: ${jwtSecret.slice(0, 3)}******`);
|
|
107
107
|
console.log(`WEB_PORT: ${webPort}`);
|
|
108
108
|
console.log(`API_PORT: ${apiPort}`);
|
|
109
109
|
console.log(`SERVER_API_URL: ${serverApiUrl}`);
|
|
@@ -111,8 +111,8 @@ export async function initEnv() {
|
|
|
111
111
|
console.log('==========================================\n');
|
|
112
112
|
// 3. 确认生成
|
|
113
113
|
const confirmed = await confirm({
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
default: true,
|
|
115
|
+
message: '确认生成 .env 文件?'
|
|
116
116
|
});
|
|
117
117
|
if (!confirmed) {
|
|
118
118
|
logger.info('已取消生成 .env 文件。');
|
|
@@ -123,13 +123,13 @@ export async function initEnv() {
|
|
|
123
123
|
// 注意:这里假设模板中的 key 遵循 standard env format (KEY=value)
|
|
124
124
|
let newEnv = templateContent;
|
|
125
125
|
const replacements = {
|
|
126
|
+
'API_PORT': apiPort,
|
|
127
|
+
'CORS_ORIGIN': corsOrigin,
|
|
128
|
+
'JWT_SECRET': jwtSecret,
|
|
126
129
|
'POSTGRES_PASSWORD': postgresPassword,
|
|
127
130
|
'REDIS_PASSWORD': redisPassword,
|
|
128
|
-
'JWT_SECRET': jwtSecret,
|
|
129
|
-
'WEB_PORT': webPort,
|
|
130
|
-
'API_PORT': apiPort,
|
|
131
131
|
'SERVER_API_URL': serverApiUrl,
|
|
132
|
-
'
|
|
132
|
+
'WEB_PORT': webPort,
|
|
133
133
|
};
|
|
134
134
|
// 针对每个 Key 进行替换。
|
|
135
135
|
// 策略:查找 `KEY=...` 并替换为 `KEY=newValue`
|
|
@@ -167,7 +167,7 @@ export async function initDockerIgnore() {
|
|
|
167
167
|
}
|
|
168
168
|
logger.info('正在创建 .dockerignore 文件...');
|
|
169
169
|
const templatePath = getTemplatePath('dockerignore');
|
|
170
|
-
const content = await fs.readFile(templatePath, '
|
|
170
|
+
const content = await fs.readFile(templatePath, 'utf8');
|
|
171
171
|
await fs.writeFile('.dockerignore', content, 'utf-8');
|
|
172
172
|
logger.success('已创建 .dockerignore');
|
|
173
173
|
}
|
|
@@ -177,7 +177,7 @@ export async function initImageEnv() {
|
|
|
177
177
|
logger.info(`当前部署模式: ${mode === 'image' ? '镜像部署' : '源码部署'}`);
|
|
178
178
|
if (mode !== 'image')
|
|
179
179
|
return;
|
|
180
|
-
logger.info('
|
|
180
|
+
logger.info('请配置镜像地址...');
|
|
181
181
|
// initEnv 已经执行,尝试从 .env 获取默认值
|
|
182
182
|
const currentApiImage = await getEnvValue('API_IMAGE') || 'ghcr.io/aiprojecthub/nodebbs-api:latest';
|
|
183
183
|
const currentWebImage = await getEnvValue('WEB_IMAGE') || 'ghcr.io/aiprojecthub/nodebbs-web:latest';
|
|
@@ -187,8 +187,8 @@ export async function initImageEnv() {
|
|
|
187
187
|
defaultVersion = match[1];
|
|
188
188
|
const { input } = await import('@inquirer/prompts');
|
|
189
189
|
const version = await input({
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
default: defaultVersion,
|
|
191
|
+
message: '设置镜像版本 (默认为 latest):'
|
|
192
192
|
});
|
|
193
193
|
// 仅替换版本号部分
|
|
194
194
|
const newApiImage = currentApiImage.replace(/:[^:]+$/, `:${version}`);
|
|
@@ -203,9 +203,9 @@ export async function checkEnv(envType) {
|
|
|
203
203
|
let warnings = 0;
|
|
204
204
|
let errors = 0;
|
|
205
205
|
const defaults = {
|
|
206
|
+
JWT_SECRET: ['change-this-to-a-secure-random-string-in-production'],
|
|
206
207
|
POSTGRES_PASSWORD: ['your_secure_postgres_password_here', 'postgres_password'],
|
|
207
|
-
REDIS_PASSWORD: ['your_secure_redis_password_here', 'redis_password']
|
|
208
|
-
JWT_SECRET: ['change-this-to-a-secure-random-string-in-production']
|
|
208
|
+
REDIS_PASSWORD: ['your_secure_redis_password_here', 'redis_password']
|
|
209
209
|
};
|
|
210
210
|
const isProd = envType === 'production' || envType === 'lowmem';
|
|
211
211
|
if (defaults.POSTGRES_PASSWORD.includes(envConfig.POSTGRES_PASSWORD)) {
|
|
@@ -238,11 +238,9 @@ export async function checkEnv(envType) {
|
|
|
238
238
|
warnings++;
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
-
if (isProd) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
warnings++;
|
|
245
|
-
}
|
|
241
|
+
if (isProd && (!envConfig.CORS_ORIGIN || envConfig.CORS_ORIGIN === '*')) {
|
|
242
|
+
logger.warning('生产环境建议设置具体的 CORS_ORIGIN');
|
|
243
|
+
warnings++;
|
|
246
244
|
}
|
|
247
245
|
if (errors > 0) {
|
|
248
246
|
logger.error(`发现 ${errors} 个配置错误,无法继续。`);
|
|
@@ -251,8 +249,8 @@ export async function checkEnv(envType) {
|
|
|
251
249
|
if (warnings > 0) {
|
|
252
250
|
logger.warning(`发现 ${warnings} 个配置警告。`);
|
|
253
251
|
const cont = await confirm({
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
default: false,
|
|
253
|
+
message: '是否继续?'
|
|
256
254
|
});
|
|
257
255
|
if (!cont)
|
|
258
256
|
process.exit(0);
|
package/dist/utils/logger.d.ts
CHANGED
package/dist/utils/logger.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
export const logger = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
warning: (msg) => console.log(chalk.yellow('[警告]'), msg),
|
|
6
|
-
error: (msg) => console.log(chalk.red('[错误]'), msg),
|
|
7
|
-
header: (msg) => {
|
|
3
|
+
error: (msg) => console.log(chalk.red('✖'), msg),
|
|
4
|
+
header(msg) {
|
|
8
5
|
console.log('');
|
|
9
|
-
console.log(chalk.blue('
|
|
6
|
+
console.log(chalk.blue('='.repeat(40)));
|
|
10
7
|
console.log(chalk.blue(` ${msg}`));
|
|
11
|
-
console.log(chalk.blue('
|
|
8
|
+
console.log(chalk.blue('='.repeat(40)));
|
|
12
9
|
console.log('');
|
|
13
|
-
}
|
|
10
|
+
},
|
|
11
|
+
info: (msg) => console.log(chalk.blue('ℹ'), msg),
|
|
12
|
+
success: (msg) => console.log(chalk.green('✔'), msg),
|
|
13
|
+
warning: (msg) => console.log(chalk.yellow('⚠'), msg)
|
|
14
14
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type EnvType = '
|
|
2
|
-
export type DeploymentMode = '
|
|
1
|
+
export type EnvType = 'default' | 'lowmem' | 'production';
|
|
2
|
+
export type DeploymentMode = 'image' | 'source';
|
|
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>;
|
|
5
5
|
export declare function getDeploymentMode(): Promise<DeploymentMode>;
|
package/dist/utils/selection.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Flags } from '@oclif/core';
|
|
2
1
|
import { select } from '@inquirer/prompts';
|
|
3
|
-
import {
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
4
3
|
import fs from 'node:fs/promises';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import { getEnvValue, setEnvValue, unsetEnvValue } from './env.js';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
7
|
export const EnvFlag = Flags.string({
|
|
8
8
|
char: 'e',
|
|
9
9
|
description: '部署环境 (production, lowmem, default)',
|
|
@@ -11,7 +11,7 @@ export const EnvFlag = Flags.string({
|
|
|
11
11
|
});
|
|
12
12
|
async function getStoredEnv() {
|
|
13
13
|
const env = await getEnvValue('DEPLOY_ENV');
|
|
14
|
-
if (env && ['
|
|
14
|
+
if (env && ['default', 'lowmem', 'production'].includes(env)) {
|
|
15
15
|
return env;
|
|
16
16
|
}
|
|
17
17
|
return null;
|
|
@@ -20,14 +20,11 @@ export async function setStoredEnv(env) {
|
|
|
20
20
|
await setEnvValue('DEPLOY_ENV', env);
|
|
21
21
|
}
|
|
22
22
|
export async function getDeploymentMode() {
|
|
23
|
-
// 自动检测:检查当前目录下是否有 package.json
|
|
23
|
+
// 自动检测:检查当前目录下是否有 package.json
|
|
24
24
|
try {
|
|
25
25
|
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (pkg.name === 'nodebbs-forum') {
|
|
29
|
-
return 'source';
|
|
30
|
-
}
|
|
26
|
+
await fs.access(packageJsonPath);
|
|
27
|
+
return 'source';
|
|
31
28
|
}
|
|
32
29
|
catch { }
|
|
33
30
|
return 'image';
|
|
@@ -47,7 +44,6 @@ export async function selectEnvironment(env, options = {}) {
|
|
|
47
44
|
return storedEnv;
|
|
48
45
|
}
|
|
49
46
|
const selected = await select({
|
|
50
|
-
message: options.prompt || '请选择运行环境:',
|
|
51
47
|
choices: [
|
|
52
48
|
{ name: '标准生产环境 (2C4G+) [推荐]', value: 'production' },
|
|
53
49
|
{ name: '低配环境 (1C1G/1C2G)', value: 'lowmem' },
|
|
@@ -55,6 +51,7 @@ export async function selectEnvironment(env, options = {}) {
|
|
|
55
51
|
{ name: '❌ 取消', value: '__CANCEL__' },
|
|
56
52
|
],
|
|
57
53
|
loop: true,
|
|
54
|
+
message: options.prompt || '请选择运行环境:',
|
|
58
55
|
});
|
|
59
56
|
if (selected === '__CANCEL__') {
|
|
60
57
|
const error = new Error('用户取消操作');
|