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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ImportUploads extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
input: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
};
|
|
7
|
+
run(): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* 获取或创建上传文件数据卷
|
|
10
|
+
* 优先级: 1. .env 中的 COMPOSE_PROJECT_NAME 2. 当前目录名 3. 自动搜索 4. 创建新卷
|
|
11
|
+
*/
|
|
12
|
+
private getOrCreateUploadsVolume;
|
|
13
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { confirm, input, select } from '@inquirer/prompts';
|
|
2
|
+
import { Command, Flags } from '@oclif/core';
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { logger } from '../../utils/logger.js';
|
|
8
|
+
export default class ImportUploads extends Command {
|
|
9
|
+
static description = '恢复上传文件';
|
|
10
|
+
static flags = {
|
|
11
|
+
input: Flags.string({
|
|
12
|
+
char: 'i',
|
|
13
|
+
description: '输入文件路径 (.tar.gz)',
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
async run() {
|
|
17
|
+
const { flags } = await this.parse(ImportUploads);
|
|
18
|
+
// 确定输入文件
|
|
19
|
+
let inputFile = flags.input;
|
|
20
|
+
if (!inputFile) {
|
|
21
|
+
// 尝试自动查找当前目录下的 .tar.gz 文件
|
|
22
|
+
const files = fs.readdirSync(process.cwd())
|
|
23
|
+
.filter(f => f.endsWith('.tar.gz') && f.includes('uploads'))
|
|
24
|
+
.sort((a, b) => {
|
|
25
|
+
const statA = fs.statSync(a);
|
|
26
|
+
const statB = fs.statSync(b);
|
|
27
|
+
return statB.mtime.getTime() - statA.mtime.getTime();
|
|
28
|
+
});
|
|
29
|
+
if (files.length > 0) {
|
|
30
|
+
const choices = files.map(f => ({
|
|
31
|
+
name: `${f} (${(fs.statSync(f).size / 1024 / 1024).toFixed(2)} MB)`,
|
|
32
|
+
value: f
|
|
33
|
+
}));
|
|
34
|
+
choices.push({ name: '手动输入路径', value: '__MANUAL__' });
|
|
35
|
+
const selection = await select({
|
|
36
|
+
choices,
|
|
37
|
+
message: '请选择上传文件备份:',
|
|
38
|
+
});
|
|
39
|
+
inputFile = selection === '__MANUAL__' ? (await input({ message: '请输入文件路径:' })) : selection;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
inputFile = await input({ message: '未找到上传备份文件 (.tar.gz),请输入路径:' });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// 确保绝对路径并检查是否存在
|
|
46
|
+
const inputPath = path.resolve(process.cwd(), inputFile);
|
|
47
|
+
if (!fs.existsSync(inputPath)) {
|
|
48
|
+
logger.error(`文件未找到: ${inputPath}`);
|
|
49
|
+
this.exit(1);
|
|
50
|
+
}
|
|
51
|
+
// 获取或创建上传文件卷
|
|
52
|
+
const volumeName = await this.getOrCreateUploadsVolume();
|
|
53
|
+
// 危险操作确认
|
|
54
|
+
if (process.env.NODEBBS_SKIP_CONFIRM !== 'true') {
|
|
55
|
+
logger.warning('此操作将清空当前上传目录的所有文件并从备份恢复!');
|
|
56
|
+
logger.warning(`目标文件: ${inputPath}`);
|
|
57
|
+
logger.warning(`数据卷: ${volumeName}`);
|
|
58
|
+
const isConfirmed = await confirm({
|
|
59
|
+
default: false,
|
|
60
|
+
message: '确认继续?(文件丢失不可撤销)'
|
|
61
|
+
});
|
|
62
|
+
if (!isConfirmed) {
|
|
63
|
+
logger.info('操作已取消');
|
|
64
|
+
this.exit(0);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
logger.info(`正在恢复上传文件 (卷: ${volumeName})...`);
|
|
68
|
+
const inputDir = path.dirname(inputPath);
|
|
69
|
+
const inputFileName = path.basename(inputPath);
|
|
70
|
+
try {
|
|
71
|
+
// 使用 alpine 容器恢复卷内容
|
|
72
|
+
await execa('docker', [
|
|
73
|
+
'run', '--rm',
|
|
74
|
+
'-v', `${volumeName}:/data`,
|
|
75
|
+
'-v', `${inputDir}:/backup:ro`,
|
|
76
|
+
'alpine',
|
|
77
|
+
'sh', '-c', `rm -rf /data/* && tar xzf /backup/${inputFileName} -C /data`
|
|
78
|
+
], {
|
|
79
|
+
stdio: 'inherit'
|
|
80
|
+
});
|
|
81
|
+
logger.success('上传文件恢复成功!');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
logger.error('上传文件恢复失败');
|
|
85
|
+
if (error instanceof Error) {
|
|
86
|
+
logger.error(error.message);
|
|
87
|
+
}
|
|
88
|
+
this.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 获取或创建上传文件数据卷
|
|
93
|
+
* 优先级: 1. .env 中的 COMPOSE_PROJECT_NAME 2. 当前目录名 3. 自动搜索 4. 创建新卷
|
|
94
|
+
*/
|
|
95
|
+
async getOrCreateUploadsVolume() {
|
|
96
|
+
// 1. 优先从 .env 获取项目名称
|
|
97
|
+
const envConfig = dotenv.config().parsed || {};
|
|
98
|
+
if (envConfig.COMPOSE_PROJECT_NAME) {
|
|
99
|
+
const volumeName = `${envConfig.COMPOSE_PROJECT_NAME}_api_uploads`;
|
|
100
|
+
try {
|
|
101
|
+
await execa('docker', ['volume', 'inspect', volumeName]);
|
|
102
|
+
return volumeName;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// 卷不存在,继续
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 2. 尝试使用当前目录名(与 Docker Compose 默认行为一致)
|
|
109
|
+
const dirName = path.basename(process.cwd());
|
|
110
|
+
const volumeByDir = `${dirName}_api_uploads`;
|
|
111
|
+
try {
|
|
112
|
+
await execa('docker', ['volume', 'inspect', volumeByDir]);
|
|
113
|
+
return volumeByDir;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// 卷不存在,继续
|
|
117
|
+
}
|
|
118
|
+
// 3. 自动搜索包含 api_uploads 的卷
|
|
119
|
+
try {
|
|
120
|
+
const result = await execa('docker', ['volume', 'ls', '--format', '{{.Name}}']);
|
|
121
|
+
const volumes = result.stdout.split('\n').filter(v => v.includes('api_uploads'));
|
|
122
|
+
if (volumes.length === 1) {
|
|
123
|
+
return volumes[0];
|
|
124
|
+
}
|
|
125
|
+
if (volumes.length > 1) {
|
|
126
|
+
logger.warning('找到多个可能的上传卷:');
|
|
127
|
+
for (const v of volumes)
|
|
128
|
+
logger.info(` - ${v}`);
|
|
129
|
+
logger.info('请在 .env 中设置 COMPOSE_PROJECT_NAME 以指定正确的卷');
|
|
130
|
+
return volumes[0];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// 忽略错误
|
|
135
|
+
}
|
|
136
|
+
// 4. 基于目录名创建新卷
|
|
137
|
+
logger.info(`数据卷 "${volumeByDir}" 不存在,正在创建...`);
|
|
138
|
+
await execa('docker', ['volume', 'create', volumeByDir]);
|
|
139
|
+
return volumeByDir;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import {
|
|
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 Logs extends Command {
|
|
6
|
-
static description = '
|
|
6
|
+
static description = '查看日志';
|
|
7
7
|
static flags = {
|
|
8
8
|
env: EnvFlag,
|
|
9
9
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import {
|
|
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 LogsApi extends Command {
|
|
6
|
-
static description = '查看 API
|
|
6
|
+
static description = '查看 API 日志';
|
|
7
7
|
static flags = {
|
|
8
8
|
env: EnvFlag,
|
|
9
9
|
};
|
package/dist/commands/logs/db.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import {
|
|
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 LogsDb extends Command {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import {
|
|
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 LogsRedis extends Command {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import {
|
|
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 LogsWeb extends Command {
|
|
@@ -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
|
|
5
|
-
import * as
|
|
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',
|
|
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
|
-
|
|
22
|
+
logger.info(`正在准备构建环境: ${tmpDir}`);
|
|
22
23
|
try {
|
|
23
24
|
const mode = await getDeploymentMode();
|
|
24
25
|
if (mode !== 'source') {
|
|
25
|
-
|
|
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
|
-
|
|
34
|
+
logger.warning('当前目录未找到 docker-compose.yml,将使用内置模板。');
|
|
33
35
|
}
|
|
34
36
|
// 3. 构建应用镜像
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
logger.info('正在导出镜像到文件 (nodebbs-images.tar)...');
|
|
56
58
|
// 获取所有相关镜像的列表
|
|
57
|
-
const projectName = path.basename(process.cwd()).toLowerCase().
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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, '
|
|
93
|
+
envExample = fs.readFileSync(envTemplatePath, 'utf8');
|
|
92
94
|
}
|
|
93
|
-
catch
|
|
94
|
-
|
|
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
|
-
|
|
142
|
+
logger.info(`正在生成最终压缩包: ${outputPath}`);
|
|
141
143
|
await execa('tar', ['-czf', outputPath, '-C', tmpDir, '.']);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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, {
|
|
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 {
|
|
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(
|
|
21
|
-
//
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
27
|
+
// 为了简单,我们遵循 restart 的模式
|
|
28
28
|
const env = await selectEnvironment(flags.env);
|
|
29
29
|
// 确保镜像配置存在
|
|
30
30
|
await initImageEnv();
|