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.
Files changed (46) hide show
  1. package/README.md +22 -17
  2. package/dist/commands/backup/all.d.ts +9 -0
  3. package/dist/commands/backup/all.js +71 -0
  4. package/dist/commands/{db/backup.d.ts → backup/db.d.ts} +1 -1
  5. package/dist/commands/{db/backup.js → backup/db.js} +8 -8
  6. package/dist/commands/backup/uploads.d.ts +13 -0
  7. package/dist/commands/backup/uploads.js +116 -0
  8. package/dist/commands/clean/index.d.ts +1 -1
  9. package/dist/commands/clean/index.js +12 -12
  10. package/dist/commands/db/reset.js +4 -4
  11. package/dist/commands/import/all.d.ts +9 -0
  12. package/dist/commands/import/all.js +123 -0
  13. package/dist/commands/{db/import.d.ts → import/db.d.ts} +1 -1
  14. package/dist/commands/{db/import.js → import/db.js} +16 -22
  15. package/dist/commands/import/uploads.d.ts +13 -0
  16. package/dist/commands/import/uploads.js +141 -0
  17. package/dist/commands/logs/all.js +2 -2
  18. package/dist/commands/logs/api.js +2 -2
  19. package/dist/commands/logs/db.js +1 -1
  20. package/dist/commands/logs/redis.js +1 -1
  21. package/dist/commands/logs/web.js +1 -1
  22. package/dist/commands/pack/index.js +30 -27
  23. package/dist/commands/restart/index.js +6 -8
  24. package/dist/commands/shell/api.js +1 -1
  25. package/dist/commands/shell/db.js +2 -2
  26. package/dist/commands/shell/redis.js +2 -2
  27. package/dist/commands/shell/web.js +1 -1
  28. package/dist/commands/start/index.d.ts +1 -1
  29. package/dist/commands/start/index.js +12 -22
  30. package/dist/commands/status/index.js +2 -2
  31. package/dist/commands/stop/index.d.ts +1 -1
  32. package/dist/commands/stop/index.js +7 -7
  33. package/dist/commands/upgrade/index.js +5 -5
  34. package/dist/interactive.js +28 -28
  35. package/dist/templates/config.yml +22 -0
  36. package/dist/templates/docker-compose.yml +3 -13
  37. package/dist/templates/env +1 -2
  38. package/dist/utils/docker.js +28 -20
  39. package/dist/utils/env.d.ts +1 -1
  40. package/dist/utils/env.js +40 -42
  41. package/dist/utils/logger.d.ts +2 -2
  42. package/dist/utils/logger.js +8 -8
  43. package/dist/utils/selection.d.ts +2 -2
  44. package/dist/utils/selection.js +7 -10
  45. package/oclif.manifest.json +213 -67
  46. package/package.json +9 -3
@@ -1,25 +1,25 @@
1
- import { Config } from '@oclif/core';
2
1
  import { select, Separator } from '@inquirer/prompts';
3
- import { fileURLToPath } from 'url';
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
- let rootPath = root.startsWith('file://') ? fileURLToPath(root) : root;
8
+ const rootPath = root.startsWith('file://') ? fileURLToPath(root) : root;
8
9
  const config = await Config.load(rootPath);
9
10
  // 构建命令树
10
- const tree = { name: 'root', children: {} };
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 (let i = 0; i < parts.length; i++) {
18
- const part = parts[i];
18
+ for (const part of parts) {
19
19
  if (!currentNode.children[part]) {
20
20
  currentNode.children[part] = {
21
- name: part,
22
- children: {}
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', 'shell', 'db', 'pack'];
32
+ const GLOBAL_PRIORITY = ['start', 'stop', 'restart', 'upgrade', 'status', 'logs', 'db', 'backup', 'import', 'clean', 'shell', 'pack'];
33
33
  const SCOPED_PRIORITIES = {
34
+ '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
- value: '__SELF__',
61
- short: 'all'
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].substring(0, 50);
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
- value: key,
97
- short: key
99
+ short: key,
100
+ value: key
98
101
  };
99
102
  });
100
103
  choices.push(...subChoices);
101
104
  // 添加导航选项
102
- // 即使支持 Esc,保留显式选项也有助于发现性
105
+ choices.push(new Separator());
103
106
  if (breadcrumbs.length > 0) {
104
- choices.push(new Separator());
105
- choices.push({ name: '⬅️ 返回', value: '__BACK__', short: '返回' });
107
+ choices.push({ name: chalk.dim('返回'), short: '返回', value: '__BACK__' });
106
108
  }
107
- choices.push({ name: '退出', value: '__EXIT__', short: '退出' });
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
- else {
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
- build:
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
- build:
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
- # volumes:
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:
@@ -2,8 +2,7 @@
2
2
  # NodeBBS Docker Compose 环境变量配置
3
3
  # ========================================
4
4
 
5
- # 应用名称
6
- APP_NAME=nodebbs
5
+ #COMPOSE_PROJECT_NAME=nodebbs
7
6
 
8
7
  # ========================================
9
8
  # 数据库配置
@@ -1,18 +1,23 @@
1
- import { execa } from 'execa';
2
1
  import { confirm } from '@inquirer/prompts';
3
- import { logger } from './logger.js';
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 { getTemplateDir, getTemplatePath } from './template.js';
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
- envs['API_IMAGE'] = 'nodebbs-api:local';
15
- envs['WEB_IMAGE'] = 'nodebbs-web:local';
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
- 'production': 'docker-compose.prod.yml',
46
- 'lowmem': 'docker-compose.lowmem.yml'
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
- message: '检测到未安装 Docker,是否尝试自动安装?',
128
- default: true
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.platform;
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
- stdio: 'inherit',
270
- env: await getDockerEnv()
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
- stdio: 'inherit',
292
- env: await getDockerEnv()
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
- logger.info('等待 API 服务...');
336
- await new Promise(resolve => setTimeout(resolve, 10000));
344
+ await new Promise(resolve => setTimeout(resolve, 10_000));
337
345
  logger.success('API 服务已就绪');
338
346
  }
@@ -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' | 'default'): Promise<void>;
7
+ export declare function checkEnv(envType: 'default' | 'lowmem' | 'production'): Promise<void>;
package/dist/utils/env.js CHANGED
@@ -1,27 +1,27 @@
1
- import fs from 'node:fs/promises';
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', 'utf-8');
13
+ const content = await fs.readFile('.env', 'utf8');
14
14
  const config = dotenv.parse(content);
15
15
  return config[key];
16
16
  }
17
- catch (e) {
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', 'utf-8');
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', 'utf-8');
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', 'utf-8')) : {};
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, 'utf-8');
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, 'utf-8');
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').replace(/[^a-zA-Z0-9]/g, '').substring(0, length);
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
- message: '设置 WEB_PORT (前端端口):',
91
- default: '3100'
90
+ default: '3100',
91
+ message: '设置 WEB_PORT (前端端口):'
92
92
  });
93
93
  const apiPort = await input({
94
- message: '设置 API_PORT (后端端口):',
95
- default: '7100'
94
+ default: '7100',
95
+ message: '设置 API_PORT (后端端口):'
96
96
  });
97
97
  const serverApiUrl = `http://api:${apiPort}`;
98
98
  const corsOrigin = await input({
99
- message: '设置 CORS_ORIGIN (允许跨域的域名):',
100
- default: '*'
99
+ default: '*',
100
+ message: '设置 CORS_ORIGIN (允许跨域的域名):'
101
101
  });
102
102
  // 2. 显示配置预览
103
103
  console.log('\n================ 配置预览 ================');
104
- console.log(`POSTGRES_PASSWORD: ${postgresPassword.substring(0, 3)}******`);
105
- console.log(`REDIS_PASSWORD: ${redisPassword.substring(0, 3)}******`);
106
- console.log(`JWT_SECRET: ${jwtSecret.substring(0, 3)}******`);
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
- message: '确认生成 .env 文件?',
115
- default: true
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
- 'CORS_ORIGIN': corsOrigin,
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, 'utf-8');
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
- message: '设置镜像版本 (默认为 latest):',
191
- default: defaultVersion
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
- if (!envConfig.CORS_ORIGIN || envConfig.CORS_ORIGIN === '*') {
243
- logger.warning('生产环境建议设置具体的 CORS_ORIGIN');
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
- message: '是否继续?',
255
- default: false
252
+ default: false,
253
+ message: '是否继续?'
256
254
  });
257
255
  if (!cont)
258
256
  process.exit(0);
@@ -1,7 +1,7 @@
1
1
  export declare const logger: {
2
+ error: (msg: string) => void;
3
+ header(msg: string): void;
2
4
  info: (msg: string) => void;
3
5
  success: (msg: string) => void;
4
6
  warning: (msg: string) => void;
5
- error: (msg: string) => void;
6
- header: (msg: string) => void;
7
7
  };
@@ -1,14 +1,14 @@
1
1
  import chalk from 'chalk';
2
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) => {
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 = 'production' | 'lowmem' | 'default';
2
- export type DeploymentMode = 'source' | 'image';
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>;
@@ -1,9 +1,9 @@
1
- import { Flags } from '@oclif/core';
2
1
  import { select } from '@inquirer/prompts';
3
- import { logger } from './logger.js';
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 && ['production', 'lowmem', 'default'].includes(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 且 name='nodebbs-forum'
23
+ // 自动检测:检查当前目录下是否有 package.json
24
24
  try {
25
25
  const packageJsonPath = path.resolve(process.cwd(), 'package.json');
26
- const content = await fs.readFile(packageJsonPath, 'utf-8');
27
- const pkg = JSON.parse(content);
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('用户取消操作');