nodebbs 0.4.1 → 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 (40) hide show
  1. package/README.md +13 -11
  2. package/dist/commands/backup/all.d.ts +1 -1
  3. package/dist/commands/backup/all.js +9 -9
  4. package/dist/commands/backup/db.js +5 -5
  5. package/dist/commands/backup/uploads.js +8 -9
  6. package/dist/commands/clean/index.d.ts +1 -1
  7. package/dist/commands/clean/index.js +12 -12
  8. package/dist/commands/db/reset.js +4 -4
  9. package/dist/commands/import/all.js +10 -15
  10. package/dist/commands/import/db.js +11 -16
  11. package/dist/commands/import/uploads.js +13 -18
  12. package/dist/commands/logs/all.js +2 -2
  13. package/dist/commands/logs/api.js +2 -2
  14. package/dist/commands/logs/db.js +1 -1
  15. package/dist/commands/logs/redis.js +1 -1
  16. package/dist/commands/logs/web.js +1 -1
  17. package/dist/commands/pack/index.js +30 -27
  18. package/dist/commands/restart/index.js +6 -8
  19. package/dist/commands/shell/api.js +1 -1
  20. package/dist/commands/shell/db.js +2 -2
  21. package/dist/commands/shell/redis.js +2 -2
  22. package/dist/commands/shell/web.js +1 -1
  23. package/dist/commands/start/index.d.ts +1 -1
  24. package/dist/commands/start/index.js +12 -22
  25. package/dist/commands/status/index.js +2 -2
  26. package/dist/commands/stop/index.d.ts +1 -1
  27. package/dist/commands/stop/index.js +7 -7
  28. package/dist/commands/upgrade/index.js +5 -5
  29. package/dist/interactive.js +26 -28
  30. package/dist/templates/docker-compose.yml +2 -8
  31. package/dist/templates/env +2 -0
  32. package/dist/utils/docker.js +22 -20
  33. package/dist/utils/env.d.ts +1 -1
  34. package/dist/utils/env.js +40 -42
  35. package/dist/utils/logger.d.ts +2 -2
  36. package/dist/utils/logger.js +8 -8
  37. package/dist/utils/selection.d.ts +2 -2
  38. package/dist/utils/selection.js +7 -10
  39. package/oclif.manifest.json +42 -42
  40. package/package.json +2 -2
@@ -1,15 +1,16 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
2
  import { execa } from 'execa';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
5
- import * as os from 'os';
6
- import { getTemplatePath } from '../../utils/template.js';
3
+ import * as fs from 'node:fs';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
7
6
  import { checkDocker, getComposeFiles, runCompose } from '../../utils/docker.js';
7
+ import { logger } from '../../utils/logger.js';
8
8
  import { getDeploymentMode } from '../../utils/selection.js';
9
+ import { getTemplatePath } from '../../utils/template.js';
9
10
  export default class Pack extends Command {
10
11
  static description = '生成离线部署包';
11
12
  static flags = {
12
- output: Flags.string({ char: 'o', description: '输出文件名', default: 'nodebbs-offline.tar.gz' }),
13
+ output: Flags.string({ char: 'o', default: 'nodebbs-offline.tar.gz', description: '输出文件名' }),
13
14
  };
14
15
  async run() {
15
16
  const { flags } = await this.parse(Pack);
@@ -18,21 +19,22 @@ export default class Pack extends Command {
18
19
  await checkDocker();
19
20
  // 1. 准备临时目录
20
21
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nodebbs-pack-'));
21
- this.log(`正在准备构建环境: ${tmpDir}`);
22
+ logger.info(`正在准备构建环境: ${tmpDir}`);
22
23
  try {
23
24
  const mode = await getDeploymentMode();
24
25
  if (mode !== 'source') {
25
- this.error('打包命令仅支持源码部署模式。请在 NodeBBS 项目根目录下运行 (需要包含 package.json 和源代码)。');
26
+ logger.error('打包命令仅支持源码部署模式。请在 NodeBBS 项目根目录下运行 (需要包含 package.json 和源代码)。');
27
+ this.exit(1);
26
28
  }
27
29
  // 2. 导出配置文件
28
30
  // 使用 getComposeFiles 获取准确的配置文件路径 (包含 base 和 override)
29
31
  // 默认打包生产环境配置
30
32
  const { files, isBuiltIn } = await getComposeFiles('production');
31
33
  if (isBuiltIn) {
32
- console.warn('当前目录未找到 docker-compose.yml,将使用内置模板。');
34
+ logger.warning('当前目录未找到 docker-compose.yml,将使用内置模板。');
33
35
  }
34
36
  // 3. 构建应用镜像
35
- this.log('正在构建应用镜像 (这可能需要几分钟)...');
37
+ logger.info('正在构建应用镜像 (这可能需要几分钟)...');
36
38
  // 注意:如果使用内置模板,context 可能是错的,但 build 命令通常需要 context。
37
39
  // getComposeFiles 返回的 file path 是绝对路径。
38
40
  // 如果是 isBuiltIn,说明用的是 cli/templates/docker-compose.yml
@@ -48,13 +50,13 @@ export default class Pack extends Command {
48
50
  // 所以这里应该使用 runCompose 来执行 build,以确保参数一致性
49
51
  await runCompose(files, ['build'], isBuiltIn);
50
52
  // 4. 拉取依赖镜像
51
- this.log('正在确保数据库镜像已下载...');
53
+ logger.info('正在确保数据库镜像已下载...');
52
54
  await execa('docker', ['pull', 'postgres:16-alpine'], { stdio: 'inherit' });
53
55
  await execa('docker', ['pull', 'redis:7-alpine'], { stdio: 'inherit' });
54
56
  // 5. 导出镜像
55
- this.log('正在导出镜像到文件 (nodebbs-images.tar)...');
57
+ logger.info('正在导出镜像到文件 (nodebbs-images.tar)...');
56
58
  // 获取所有相关镜像的列表
57
- const projectName = path.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9]/g, '');
59
+ const projectName = path.basename(process.cwd()).toLowerCase().replaceAll(/[^a-z0-9]/g, '');
58
60
  // 我们最好通过 docker compose config 来确认镜像名
59
61
  // 简单策略:先 save postgres 和 redis,对于 api 和 web,我们先 tag 一下以确保名字固定。
60
62
  await execa('docker', ['tag', `${projectName}-api`, 'nodebbs-api:latest']);
@@ -65,11 +67,11 @@ export default class Pack extends Command {
65
67
  // files[0] -> Base
66
68
  // files[1] -> Override (Optional)
67
69
  const baseFile = files[0];
68
- let composeContent = fs.readFileSync(baseFile, 'utf-8');
70
+ const composeContent = fs.readFileSync(baseFile, 'utf8');
69
71
  const lines = composeContent.split('\n');
70
72
  const cleanedLines = lines.map(line => {
71
73
  // 移除 api 和 web 的源码挂载
72
- if (line.match(/- \.\/apps\/.*\/src:\/app\/apps\/.*\/src/)) {
74
+ if (/- \.\/apps\/.*\/src:\/app\/apps\/.*\/src/.test(line)) {
73
75
  return line.replace(/^/, '# [OFFLINE-PACK-REMOVED] ');
74
76
  }
75
77
  return line;
@@ -82,16 +84,16 @@ export default class Pack extends Command {
82
84
  fs.copyFileSync(overrideFile, path.join(tmpDir, 'docker-compose.prod.yml'));
83
85
  }
84
86
  if (fs.existsSync('.env')) {
85
- this.warn('检测到 .env 文件,出于安全考虑,不会默认打包 .env 文件。请在部署时手动配置环境变量。');
87
+ logger.warning('检测到 .env 文件,出于安全考虑,不会默认打包 .env 文件。请在部署时手动配置环境变量。');
86
88
  }
87
89
  // 创建 .env.example
88
90
  const envTemplatePath = getTemplatePath('env');
89
91
  let envExample = '';
90
92
  try {
91
- envExample = fs.readFileSync(envTemplatePath, 'utf-8');
93
+ envExample = fs.readFileSync(envTemplatePath, 'utf8');
92
94
  }
93
- catch (e) {
94
- this.warn(`读取 env 模板失败: ${envTemplatePath},将使用空模板`);
95
+ catch {
96
+ logger.warning(`读取 env 模板失败: ${envTemplatePath},将使用空模板`);
95
97
  }
96
98
  fs.writeFileSync(path.join(tmpDir, '.env.example'), envExample);
97
99
  // 7. 创建安装脚本
@@ -137,22 +139,23 @@ echo "部署完成!"
137
139
  fs.writeFileSync(path.join(tmpDir, 'install.sh'), installScript);
138
140
  fs.chmodSync(path.join(tmpDir, 'install.sh'), '755');
139
141
  // 8. 打包最终的 tar.gz
140
- this.log(`正在生成最终压缩包: ${outputPath}`);
142
+ logger.info(`正在生成最终压缩包: ${outputPath}`);
141
143
  await execa('tar', ['-czf', outputPath, '-C', tmpDir, '.']);
142
- this.log(`\n🎉 离线包生成成功: ${outputPath}`);
143
- this.log('使用方法:');
144
- this.log('1. 将压缩包上传到服务器');
145
- this.log('2. 解压: tar -xzf nodebbs-offline.tar.gz');
146
- this.log('3. 配置 .env');
147
- this.log('4. 运行: ./install.sh');
144
+ logger.success(`🎉 离线包生成成功: ${outputPath}`);
145
+ logger.info('使用方法:');
146
+ logger.info('1. 将压缩包上传到服务器');
147
+ logger.info('2. 解压: tar -xzf nodebbs-offline.tar.gz');
148
+ logger.info('3. 配置 .env');
149
+ logger.info('4. 运行: ./install.sh');
148
150
  }
149
151
  catch (error) {
150
- this.error(`打包失败: ${error.message}`);
152
+ logger.error(`打包失败: ${error.message}`);
153
+ this.exit(1);
151
154
  }
152
155
  finally {
153
156
  // 清理临时目录
154
157
  if (fs.existsSync(tmpDir)) {
155
- fs.rmSync(tmpDir, { recursive: true, force: true });
158
+ fs.rmSync(tmpDir, { force: true, recursive: true });
156
159
  }
157
160
  }
158
161
  }
@@ -1,10 +1,10 @@
1
1
  import { Command } from '@oclif/core';
2
- import { runCompose, getComposeFiles } from '../../utils/docker.js';
3
- import { logger } from '../../utils/logger.js';
4
- import { EnvFlag, selectEnvironment, getDeploymentMode } from '../../utils/selection.js';
2
+ import { getComposeFiles, runCompose } from '../../utils/docker.js';
5
3
  import { initImageEnv } from '../../utils/env.js';
4
+ import { logger } from '../../utils/logger.js';
5
+ import { EnvFlag, getDeploymentMode, selectEnvironment } from '../../utils/selection.js';
6
6
  export default class Restart extends Command {
7
- static description = '重启服务';
7
+ static description = '重启';
8
8
  static flags = {
9
9
  env: EnvFlag,
10
10
  };
@@ -13,13 +13,11 @@ export default class Restart extends Command {
13
13
  // 1. 选择环境
14
14
  const env = await selectEnvironment(flags.env);
15
15
  const { files, isBuiltIn } = await getComposeFiles(env);
16
- logger.info('正在重启服务 (使用现有镜像)...');
17
16
  // 确保镜像配置存在
18
17
  await initImageEnv();
19
18
  const mode = await getDeploymentMode();
20
- logger.info(`环境: ${env} (${mode === 'image' ? '镜像部署' : '源码部署'})`);
21
- // 3. 不会执行构建 (除非镜像不存在)
22
- // 在镜像模式下,我们需要确保不尝试构建 (哪怕镜像丢失,应该是 pull 而不是 build)
19
+ logger.info(`正在重启服务... (环境: ${env}, ${mode === 'image' ? '镜像部署' : '源码部署'})`);
20
+ // 在镜像模式下,不尝试构建
23
21
  const args = ['up', '-d', '--force-recreate'];
24
22
  if (mode === 'image') {
25
23
  args.push('--no-build');
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import { runCompose, getComposeFiles } from '../../utils/docker.js';
2
+ import { getComposeFiles, runCompose } from '../../utils/docker.js';
3
3
  import { logger } from '../../utils/logger.js';
4
4
  import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
5
5
  export default class ShellApi extends Command {
@@ -1,8 +1,8 @@
1
1
  import { Command } from '@oclif/core';
2
- import { runCompose, getComposeFiles } from '../../utils/docker.js';
2
+ import { getComposeFiles, runCompose } from '../../utils/docker.js';
3
+ import { getEnvValue } from '../../utils/env.js';
3
4
  import { logger } from '../../utils/logger.js';
4
5
  import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
5
- import { getEnvValue } from '../../utils/env.js';
6
6
  export default class ShellDb extends Command {
7
7
  static description = '进入数据库 shell (psql)';
8
8
  static flags = {
@@ -1,8 +1,8 @@
1
1
  import { Command } from '@oclif/core';
2
- import { runCompose, getComposeFiles } from '../../utils/docker.js';
2
+ import { getComposeFiles, runCompose } from '../../utils/docker.js';
3
+ import { getEnvValue } from '../../utils/env.js';
3
4
  import { logger } from '../../utils/logger.js';
4
5
  import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
5
- import { getEnvValue } from '../../utils/env.js';
6
6
  export default class ShellRedis extends Command {
7
7
  static description = '进入 Redis shell (redis-cli)';
8
8
  static flags = {
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import { runCompose, getComposeFiles } from '../../utils/docker.js';
2
+ import { getComposeFiles, runCompose } from '../../utils/docker.js';
3
3
  import { logger } from '../../utils/logger.js';
4
4
  import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
5
5
  export default class ShellWeb extends Command {
@@ -2,8 +2,8 @@ import { Command } from '@oclif/core';
2
2
  export default class Start extends Command {
3
3
  static description: string;
4
4
  static flags: {
5
- env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
5
  build: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
+ env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  };
8
8
  run(): Promise<void>;
9
9
  }
@@ -1,19 +1,19 @@
1
- import { Command, Flags } from '@oclif/core';
2
1
  import { confirm } from '@inquirer/prompts';
3
- import { checkDocker, runCompose, waitForHealth, execCompose, getComposeFiles } from '../../utils/docker.js';
4
- import { initEnv, checkEnv, initDockerIgnore, initImageEnv } from '../../utils/env.js';
5
- import { logger } from '../../utils/logger.js';
2
+ import { Command, Flags } from '@oclif/core';
6
3
  import dotenv from 'dotenv';
7
- import { EnvFlag, selectEnvironment, getDeploymentMode } from '../../utils/selection.js';
4
+ import { checkDocker, execCompose, getComposeFiles, runCompose, waitForHealth } from '../../utils/docker.js';
5
+ import { checkEnv, initDockerIgnore, initEnv, initImageEnv } from '../../utils/env.js';
6
+ import { logger } from '../../utils/logger.js';
7
+ import { EnvFlag, getDeploymentMode, selectEnvironment } from '../../utils/selection.js';
8
8
  export default class Start extends Command {
9
- static description = '启动服务';
9
+ static description = '启动';
10
10
  static flags = {
11
- env: EnvFlag,
12
11
  build: Flags.boolean({
13
12
  char: 'b',
14
- description: '重新构建并启动服务 (跳过健康检查和数据初始化)',
15
13
  default: false,
14
+ description: '重新构建并启动服务 (跳过健康检查和数据初始化)',
16
15
  }),
16
+ env: EnvFlag,
17
17
  };
18
18
  async run() {
19
19
  const { flags } = await this.parse(Start);
@@ -53,16 +53,6 @@ export default class Start extends Command {
53
53
  await initDockerIgnore();
54
54
  await initImageEnv();
55
55
  await checkEnv(env);
56
- if (!flags.build) {
57
- const continueDeploy = await confirm({
58
- message: '是否继续启动?',
59
- default: true
60
- });
61
- if (!continueDeploy) {
62
- logger.info('操作已取消。');
63
- this.exit(0);
64
- }
65
- }
66
56
  // 4. 构建并启动服务
67
57
  const mode = await getDeploymentMode();
68
58
  if (flags.build) {
@@ -97,8 +87,8 @@ export default class Start extends Command {
97
87
  if (!flags.build) {
98
88
  await waitForHealth(composeFiles, isBuiltIn);
99
89
  const pushDb = await confirm({
100
- message: '是否推送数据库 schema?',
101
- default: false
90
+ default: false,
91
+ message: '是否推送数据库 schema?(初次部署推荐)'
102
92
  });
103
93
  if (pushDb) {
104
94
  logger.info('正在推送数据库 schema...');
@@ -106,8 +96,8 @@ export default class Start extends Command {
106
96
  logger.success('数据库 schema 推送完成');
107
97
  }
108
98
  const seedDb = await confirm({
109
- message: '是否初始化种子数据?',
110
- default: false
99
+ default: false,
100
+ message: '是否初始化种子数据?(初次部署推荐)'
111
101
  });
112
102
  if (seedDb) {
113
103
  logger.info('正在初始化数据...');
@@ -1,9 +1,9 @@
1
1
  import { Command } from '@oclif/core';
2
- import { runCompose, getComposeFiles } from '../../utils/docker.js';
2
+ import { getComposeFiles, runCompose } from '../../utils/docker.js';
3
3
  import { logger } from '../../utils/logger.js';
4
4
  import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
5
5
  export default class Status extends Command {
6
- static description = '查看服务状态';
6
+ static description = '查看状态';
7
7
  static flags = {
8
8
  env: EnvFlag,
9
9
  };
@@ -2,8 +2,8 @@ import { Command } from '@oclif/core';
2
2
  export default class Stop extends Command {
3
3
  static description: string;
4
4
  static flags: {
5
- volumes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
5
  env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
+ volumes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
7
  };
8
8
  run(): Promise<void>;
9
9
  }
@@ -1,17 +1,17 @@
1
+ import { confirm } from '@inquirer/prompts';
1
2
  import { Command, Flags } from '@oclif/core';
2
- import { runCompose, getComposeFiles } from '../../utils/docker.js';
3
+ import { getComposeFiles, runCompose } from '../../utils/docker.js';
3
4
  import { logger } from '../../utils/logger.js';
4
- import { confirm } from '@inquirer/prompts';
5
5
  import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
6
6
  export default class Stop extends Command {
7
- static description = '停止服务';
7
+ static description = '停止';
8
8
  static flags = {
9
+ env: EnvFlag,
9
10
  volumes: Flags.boolean({
10
11
  char: 'v',
11
- description: '同时删除数据卷(危险!)',
12
12
  default: false,
13
+ description: '同时删除数据卷(危险!)',
13
14
  }),
14
- env: EnvFlag,
15
15
  };
16
16
  async run() {
17
17
  const { flags } = await this.parse(Stop);
@@ -21,10 +21,10 @@ export default class Stop extends Command {
21
21
  });
22
22
  const { files, isBuiltIn } = await getComposeFiles(env);
23
23
  if (flags.volumes) {
24
- logger.warning('警告:这将删除所有数据!');
24
+ logger.warning('这将删除所有数据!');
25
25
  const confirmDelete = await confirm({
26
- message: '确认继续?',
27
26
  default: false,
27
+ message: '确认继续?',
28
28
  });
29
29
  if (!confirmDelete) {
30
30
  logger.info('操作已取消。');
@@ -1,11 +1,11 @@
1
1
  import { Command } from '@oclif/core';
2
- import { runCompose, getComposeFiles } from '../../utils/docker.js';
3
- import { logger } from '../../utils/logger.js';
4
- import { EnvFlag, selectEnvironment, getDeploymentMode } from '../../utils/selection.js';
2
+ import { getComposeFiles, runCompose } from '../../utils/docker.js';
5
3
  import { initImageEnv } from '../../utils/env.js';
4
+ import { logger } from '../../utils/logger.js';
5
+ import { EnvFlag, getDeploymentMode, selectEnvironment } from '../../utils/selection.js';
6
6
  import Start from '../start/index.js';
7
7
  export default class Upgrade extends Command {
8
- static description = '升级服务';
8
+ static description = '升级';
9
9
  static flags = {
10
10
  env: EnvFlag,
11
11
  };
@@ -24,7 +24,7 @@ export default class Upgrade extends Command {
24
24
  // 2. 镜像模式:拉取更新并重启
25
25
  logger.header('NodeBBS 服务升级');
26
26
  // 选择环境 (通常需要与运行中一致,这里重新选择以防万一,或者可以复用 restart 的逻辑不传 env 则自选)
27
- //为了简单,我们遵循 restart 的模式
27
+ // 为了简单,我们遵循 restart 的模式
28
28
  const env = await selectEnvironment(flags.env);
29
29
  // 确保镜像配置存在
30
30
  await initImageEnv();
@@ -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];
@@ -31,9 +31,10 @@ export async function runInteractive(root) {
31
31
  }
32
32
  const GLOBAL_PRIORITY = ['start', 'stop', 'restart', 'upgrade', 'status', 'logs', 'db', 'backup', 'import', 'clean', 'shell', 'pack'];
33
33
  const SCOPED_PRIORITIES = {
34
- 'logs': ['all', 'web', 'api', 'db', 'redis'],
35
34
  'backup': ['all', 'db', 'uploads'],
35
+ 'db': ['push', 'seed', 'reset'],
36
36
  'import': ['all', 'db', 'uploads'],
37
+ 'logs': ['all', 'web', 'api', 'db', 'redis'],
37
38
  };
38
39
  async function navigate(node, breadcrumbs, config) {
39
40
  const currentPath = breadcrumbs.join(':');
@@ -59,8 +60,8 @@ async function navigate(node, breadcrumbs, config) {
59
60
  if (node.command) {
60
61
  choices.push({
61
62
  name: `${node.command.description || node.command.id}`,
62
- value: '__SELF__',
63
- short: 'all'
63
+ short: 'all',
64
+ value: '__SELF__'
64
65
  });
65
66
  }
66
67
  const subChoices = keys.map(key => {
@@ -84,7 +85,7 @@ async function navigate(node, breadcrumbs, config) {
84
85
  }
85
86
  }
86
87
  if (description) {
87
- const shortDesc = description.split('.')[0].substring(0, 50);
88
+ const shortDesc = description.split('.')[0].slice(0, 50);
88
89
  label = `${key.padEnd(12)} ${shortDesc}`;
89
90
  }
90
91
  else {
@@ -95,18 +96,17 @@ async function navigate(node, breadcrumbs, config) {
95
96
  }
96
97
  return {
97
98
  name: label,
98
- value: key,
99
- short: key
99
+ short: key,
100
+ value: key
100
101
  };
101
102
  });
102
103
  choices.push(...subChoices);
103
104
  // 添加导航选项
104
- // 即使支持 Esc,保留显式选项也有助于发现性
105
+ choices.push(new Separator());
105
106
  if (breadcrumbs.length > 0) {
106
- choices.push(new Separator());
107
- choices.push({ name: '⬅️ 返回', value: '__BACK__', short: '返回' });
107
+ choices.push({ name: chalk.dim('返回'), short: '返回', value: '__BACK__' });
108
108
  }
109
- choices.push({ name: '退出', value: '__EXIT__', short: '退出' });
109
+ choices.push({ name: chalk.dim('退出'), short: '退出', value: '__EXIT__' });
110
110
  const controller = new AbortController();
111
111
  // 监听 Esc 键
112
112
  const onKeypress = (_str, key) => {
@@ -120,10 +120,10 @@ async function navigate(node, breadcrumbs, config) {
120
120
  process.stdin.on('keypress', onKeypress);
121
121
  try {
122
122
  const selection = await select({
123
- message: breadcrumbs.length ? `选择命令 (${breadcrumbs.join(' > ')}):` : '选择命令:',
124
123
  choices,
125
- pageSize: 15,
126
124
  loop: true, // 允许循环导航
125
+ message: breadcrumbs.length > 0 ? `选择命令 (${breadcrumbs.join(' > ')}):` : '选择命令:',
126
+ pageSize: 15,
127
127
  // @ts-ignore: prompt signal support might be strict on types
128
128
  signal: controller.signal
129
129
  });
@@ -151,10 +151,10 @@ async function navigate(node, breadcrumbs, config) {
151
151
  }
152
152
  console.error(error.message);
153
153
  }
154
- console.log('\n命令执行完成。');
154
+ console.log('\n命令执行完成。');
155
155
  // 动态导入 input 以保持轻量
156
156
  const { input } = await import('@inquirer/prompts');
157
- await input({ message: '按回车键继续...' });
157
+ await input({ message: '按回车键返回菜单...' });
158
158
  // 继续循环
159
159
  continue;
160
160
  }
@@ -182,10 +182,10 @@ async function navigate(node, breadcrumbs, config) {
182
182
  }
183
183
  console.error(error.message);
184
184
  }
185
- console.log('\n命令执行完成。');
185
+ console.log('\n命令执行完成。');
186
186
  // 动态导入 input 以保持轻量
187
187
  const { input } = await import('@inquirer/prompts');
188
- await input({ message: '按回车键继续...' });
188
+ await input({ message: '按回车键返回菜单...' });
189
189
  }
190
190
  }
191
191
  catch (error) {
@@ -195,10 +195,8 @@ async function navigate(node, breadcrumbs, config) {
195
195
  // 子菜单 -> 返回
196
196
  return;
197
197
  }
198
- else {
199
- // 顶级菜单 -> 退出
200
- process.exit(0);
201
- }
198
+ // 顶级菜单 -> 退出
199
+ process.exit(0);
202
200
  }
203
201
  // 处理 Ctrl+C (ExitPromptError)
204
202
  if (error.name === 'ExitPromptError') {
@@ -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:
@@ -2,6 +2,8 @@
2
2
  # NodeBBS Docker Compose 环境变量配置
3
3
  # ========================================
4
4
 
5
+ #COMPOSE_PROJECT_NAME=nodebbs
6
+
5
7
  # ========================================
6
8
  # 数据库配置
7
9
  # ========================================