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,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];
@@ -130,8 +135,8 @@ export async function checkDocker() {
130
135
  throw error;
131
136
  }
132
137
  const shouldInstall = await confirm({
133
- message: '检测到未安装 Docker,是否尝试自动安装?',
134
- default: true
138
+ default: true,
139
+ message: '检测到未安装 Docker,是否尝试自动安装?'
135
140
  });
136
141
  if (shouldInstall) {
137
142
  try {
@@ -177,7 +182,7 @@ export async function checkDocker() {
177
182
  * 安装完成后会尝试启动 Docker 服务并清理临时文件。
178
183
  */
179
184
  async function installDocker() {
180
- const platform = process.platform;
185
+ const { platform } = process;
181
186
  if (platform === 'linux') {
182
187
  const scriptPath = 'get-docker.sh';
183
188
  try {
@@ -272,8 +277,8 @@ export async function runCompose(files, args, isBuiltIn = false) {
272
277
  // 使用 stdio: 'inherit' 实时显示输出
273
278
  // 设置 COMPOSE_LOG_LEVEL=ERROR 抑制变量未设置的警告
274
279
  await execa('docker', ['compose', ...composeArgs], {
275
- stdio: 'inherit',
276
- env: await getDockerEnv()
280
+ env: await getDockerEnv(),
281
+ stdio: 'inherit'
277
282
  });
278
283
  }
279
284
  /**
@@ -294,8 +299,8 @@ export async function execCompose(files, service, command, isBuiltIn = false) {
294
299
  }
295
300
  composeArgs.push('exec', service, ...command);
296
301
  await execa('docker', ['compose', ...composeArgs], {
297
- stdio: 'inherit',
298
- env: await getDockerEnv()
302
+ env: await getDockerEnv(),
303
+ stdio: 'inherit'
299
304
  });
300
305
  }
301
306
  /**
@@ -309,9 +314,8 @@ export async function execCompose(files, service, command, isBuiltIn = false) {
309
314
  * @param isBuiltIn - 是否使用内置模板
310
315
  */
311
316
  export async function waitForHealth(files, isBuiltIn = false) {
312
- logger.info('正在等待服务就绪...');
317
+ logger.info('正在等待各项依赖服务就绪...');
313
318
  // 等待 Postgres
314
- logger.info('等待 PostgreSQL...');
315
319
  const composeArgs = files.flatMap(f => ['-f', f]);
316
320
  if (isBuiltIn) {
317
321
  composeArgs.push('--project-directory', process.cwd());
@@ -334,11 +338,9 @@ export async function waitForHealth(files, isBuiltIn = false) {
334
338
  logger.warning('PostgreSQL 可能尚未就绪');
335
339
  }
336
340
  // 等待 Redis
337
- logger.info('等待 Redis...');
338
341
  await new Promise(resolve => setTimeout(resolve, 5000));
339
342
  logger.success('Redis 已就绪');
340
343
  // 等待 API
341
- logger.info('等待 API 服务...');
342
- await new Promise(resolve => setTimeout(resolve, 10000));
344
+ await new Promise(resolve => setTimeout(resolve, 10_000));
343
345
  logger.success('API 服务已就绪');
344
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('用户取消操作');
@@ -3,16 +3,8 @@
3
3
  "backup:all": {
4
4
  "aliases": [],
5
5
  "args": {},
6
- "description": "一键备份全部数据 (数据库 + 上传文件)",
6
+ "description": "一键备份全部数据",
7
7
  "flags": {
8
- "output": {
9
- "char": "o",
10
- "description": "输出目录路径",
11
- "name": "output",
12
- "hasDynamicHelp": false,
13
- "multiple": false,
14
- "type": "option"
15
- },
16
8
  "env": {
17
9
  "char": "e",
18
10
  "description": "运行环境 (production, lowmem, default)",
@@ -25,6 +17,14 @@
25
17
  "default"
26
18
  ],
27
19
  "type": "option"
20
+ },
21
+ "output": {
22
+ "char": "o",
23
+ "description": "输出目录路径",
24
+ "name": "output",
25
+ "hasDynamicHelp": false,
26
+ "multiple": false,
27
+ "type": "option"
28
28
  }
29
29
  },
30
30
  "hasDynamicHelp": false,
@@ -46,7 +46,7 @@
46
46
  "backup:db": {
47
47
  "aliases": [],
48
48
  "args": {},
49
- "description": "备份数据库 (PostgreSQL)",
49
+ "description": "备份数据库",
50
50
  "flags": {
51
51
  "env": {
52
52
  "char": "e",
@@ -89,7 +89,7 @@
89
89
  "backup:uploads": {
90
90
  "aliases": [],
91
91
  "args": {},
92
- "description": "备份上传文件 (用户头像、附件等)",
92
+ "description": "备份上传文件",
93
93
  "flags": {
94
94
  "output": {
95
95
  "char": "o",
@@ -134,12 +134,6 @@
134
134
  "allowNo": false,
135
135
  "type": "boolean"
136
136
  },
137
- "images": {
138
- "description": "清理无用镜像 (dangling)",
139
- "name": "images",
140
- "allowNo": false,
141
- "type": "boolean"
142
- },
143
137
  "env": {
144
138
  "description": "清理环境锁定 (Environment Lock)",
145
139
  "name": "env",
@@ -152,6 +146,12 @@
152
146
  "name": "force",
153
147
  "allowNo": false,
154
148
  "type": "boolean"
149
+ },
150
+ "images": {
151
+ "description": "清理无用镜像 (dangling)",
152
+ "name": "images",
153
+ "allowNo": false,
154
+ "type": "boolean"
155
155
  }
156
156
  },
157
157
  "hasDynamicHelp": false,
@@ -278,7 +278,7 @@
278
278
  "import:all": {
279
279
  "aliases": [],
280
280
  "args": {},
281
- "description": "一键恢复全部数据 (数据库 + 上传文件)",
281
+ "description": "一键恢复全部数据",
282
282
  "flags": {
283
283
  "dir": {
284
284
  "char": "d",
@@ -321,7 +321,7 @@
321
321
  "import:db": {
322
322
  "aliases": [],
323
323
  "args": {},
324
- "description": "导入数据库 (从 SQL 文件恢复)",
324
+ "description": "导入数据库",
325
325
  "flags": {
326
326
  "env": {
327
327
  "char": "e",
@@ -364,7 +364,7 @@
364
364
  "import:uploads": {
365
365
  "aliases": [],
366
366
  "args": {},
367
- "description": "恢复上传文件 (从 tar.gz 备份恢复)",
367
+ "description": "恢复上传文件",
368
368
  "flags": {
369
369
  "input": {
370
370
  "char": "i",
@@ -394,7 +394,7 @@
394
394
  "logs:all": {
395
395
  "aliases": [],
396
396
  "args": {},
397
- "description": "查看所有服务日志",
397
+ "description": "查看日志",
398
398
  "flags": {
399
399
  "env": {
400
400
  "char": "e",
@@ -429,7 +429,7 @@
429
429
  "logs:api": {
430
430
  "aliases": [],
431
431
  "args": {},
432
- "description": "查看 API 服务日志",
432
+ "description": "查看 API 日志",
433
433
  "flags": {
434
434
  "env": {
435
435
  "char": "e",
@@ -600,7 +600,7 @@
600
600
  "restart": {
601
601
  "aliases": [],
602
602
  "args": {},
603
- "description": "重启服务",
603
+ "description": "重启",
604
604
  "flags": {
605
605
  "env": {
606
606
  "char": "e",
@@ -775,8 +775,15 @@
775
775
  "start": {
776
776
  "aliases": [],
777
777
  "args": {},
778
- "description": "启动服务",
778
+ "description": "启动",
779
779
  "flags": {
780
+ "build": {
781
+ "char": "b",
782
+ "description": "重新构建并启动服务 (跳过健康检查和数据初始化)",
783
+ "name": "build",
784
+ "allowNo": false,
785
+ "type": "boolean"
786
+ },
780
787
  "env": {
781
788
  "char": "e",
782
789
  "description": "部署环境 (production, lowmem, default)",
@@ -789,13 +796,6 @@
789
796
  "default"
790
797
  ],
791
798
  "type": "option"
792
- },
793
- "build": {
794
- "char": "b",
795
- "description": "重新构建并启动服务 (跳过健康检查和数据初始化)",
796
- "name": "build",
797
- "allowNo": false,
798
- "type": "boolean"
799
799
  }
800
800
  },
801
801
  "hasDynamicHelp": false,
@@ -817,7 +817,7 @@
817
817
  "status": {
818
818
  "aliases": [],
819
819
  "args": {},
820
- "description": "查看服务状态",
820
+ "description": "查看状态",
821
821
  "flags": {
822
822
  "env": {
823
823
  "char": "e",
@@ -852,15 +852,8 @@
852
852
  "stop": {
853
853
  "aliases": [],
854
854
  "args": {},
855
- "description": "停止服务",
855
+ "description": "停止",
856
856
  "flags": {
857
- "volumes": {
858
- "char": "v",
859
- "description": "同时删除数据卷(危险!)",
860
- "name": "volumes",
861
- "allowNo": false,
862
- "type": "boolean"
863
- },
864
857
  "env": {
865
858
  "char": "e",
866
859
  "description": "部署环境 (production, lowmem, default)",
@@ -873,6 +866,13 @@
873
866
  "default"
874
867
  ],
875
868
  "type": "option"
869
+ },
870
+ "volumes": {
871
+ "char": "v",
872
+ "description": "同时删除数据卷(危险!)",
873
+ "name": "volumes",
874
+ "allowNo": false,
875
+ "type": "boolean"
876
876
  }
877
877
  },
878
878
  "hasDynamicHelp": false,
@@ -894,7 +894,7 @@
894
894
  "upgrade": {
895
895
  "aliases": [],
896
896
  "args": {},
897
- "description": "升级服务",
897
+ "description": "升级",
898
898
  "flags": {
899
899
  "env": {
900
900
  "char": "e",
@@ -927,5 +927,5 @@
927
927
  ]
928
928
  }
929
929
  },
930
- "version": "0.4.1"
930
+ "version": "0.4.2"
931
931
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nodebbs",
3
3
  "description": "NodeBBS 论坛系统专业运维工具",
4
- "version": "0.4.1",
4
+ "version": "0.4.2",
5
5
  "author": "wengqianshan",
6
6
  "bin": {
7
7
  "nodebbs": "./bin/run.js"
@@ -59,7 +59,7 @@
59
59
  "topicSeparator": " ",
60
60
  "topics": {
61
61
  "logs": {
62
- "description": "查看服务日志"
62
+ "description": "查看日志"
63
63
  },
64
64
  "db": {
65
65
  "description": "数据库操作 (种子数据, 重置等)"