nodebbs 0.0.4 → 0.0.5
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 +1 -17
- package/dist/commands/pack/index.d.ts +8 -0
- package/dist/commands/pack/index.js +150 -0
- package/dist/interactive.js +1 -1
- package/oclif.manifest.json +61 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -340,23 +340,7 @@ npx nodebbs db:seed
|
|
|
340
340
|
npx nodebbs shell:db
|
|
341
341
|
```
|
|
342
342
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
如果你之前使用 Makefile,这里是命令对照表:
|
|
346
|
-
|
|
347
|
-
| Makefile 命令 | NodeBBS CLI 命令 |
|
|
348
|
-
|--------------|-----------------|
|
|
349
|
-
| `make up` | `npx nodebbs start` |
|
|
350
|
-
| `make build` | `npx nodebbs start --build` |
|
|
351
|
-
| `make rebuild` | `npx nodebbs start --build` |
|
|
352
|
-
| `ENV=prod make rebuild` | `npx nodebbs start -e production --build` |
|
|
353
|
-
| `make down` | `npx nodebbs stop` |
|
|
354
|
-
| `make ps` | `npx nodebbs status` |
|
|
355
|
-
| `make logs` | `npx nodebbs logs` |
|
|
356
|
-
| `make logs-api` | `npx nodebbs logs:api` |
|
|
357
|
-
| `make exec-api` | `npx nodebbs shell:api` |
|
|
358
|
-
|
|
359
|
-
| `make clean-all` | `npx nodebbs stop --volumes` |
|
|
343
|
+
|
|
360
344
|
|
|
361
345
|
## ⚙️ 环境配置
|
|
362
346
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Pack extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
};
|
|
7
|
+
run(): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
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';
|
|
7
|
+
export default class Pack extends Command {
|
|
8
|
+
static description = '生成离线部署包';
|
|
9
|
+
static flags = {
|
|
10
|
+
output: Flags.string({ char: 'o', description: '输出文件名', default: 'nodebbs-offline.tar.gz' }),
|
|
11
|
+
};
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(Pack);
|
|
14
|
+
const outputPath = path.resolve(flags.output);
|
|
15
|
+
// 检查 docker 是否可用
|
|
16
|
+
try {
|
|
17
|
+
await execa('docker', ['--version']);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
this.error('无法执行 docker 命令,请确保 Docker 已安装并运行。');
|
|
21
|
+
}
|
|
22
|
+
// 1. 准备临时目录
|
|
23
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nodebbs-pack-'));
|
|
24
|
+
this.log(`正在准备构建环境: ${tmpDir}`);
|
|
25
|
+
try {
|
|
26
|
+
// 2. 导出配置文件
|
|
27
|
+
// 这里我们需要获取当前的 docker-compose.yml 和 prod 配置
|
|
28
|
+
// 假设用户当前目录下有这些文件,或者我们就用内置模板?
|
|
29
|
+
// 更好的方式是:如果在当前目录找到了配置文件,就用当前的;否则用内置模板。
|
|
30
|
+
// 但既然是打包,通常是想把当前开发好的代码打包。
|
|
31
|
+
// 所以我们假设在项目根目录运行。
|
|
32
|
+
if (!fs.existsSync('docker-compose.yml')) {
|
|
33
|
+
this.warn('当前目录未找到 docker-compose.yml,将使用内置模板。');
|
|
34
|
+
// TODO: 从 templates 复制 (简化起见,我们假设用户在项目根目录,或者我们强制要求)
|
|
35
|
+
// 如果没找到,我们实际上无法 build api/web,因为 build context 需要源码。
|
|
36
|
+
// 所以必须要求在项目根目录运行。
|
|
37
|
+
this.error('请在 NodeBBS 项目根目录下运行此命令 (需要包含 docker-compose.yml 和源代码)。');
|
|
38
|
+
}
|
|
39
|
+
// 3. 构建镜像
|
|
40
|
+
this.log('正在构建应用镜像 (这可能通过需要几分钟)...');
|
|
41
|
+
await execa('docker', ['compose', 'build'], { stdio: 'inherit' });
|
|
42
|
+
// 4. 拉取依赖镜像
|
|
43
|
+
this.log('正在确保数据库镜像已下载...');
|
|
44
|
+
await execa('docker', ['pull', 'postgres:16-alpine'], { stdio: 'inherit' });
|
|
45
|
+
await execa('docker', ['pull', 'redis:7-alpine'], { stdio: 'inherit' });
|
|
46
|
+
// 5. 导出镜像
|
|
47
|
+
this.log('正在导出镜像到文件 (nodebbs-images.tar)...');
|
|
48
|
+
// 获取实际的镜像名,这里假设 docker-compose build 生成的镜像名符合预期
|
|
49
|
+
// 通常是 <dir>_api 和 <dir>_web,或者我们在 compose 文件里指定了 image name?
|
|
50
|
+
// 查看 template: 没有指定 image name,所以默认为 ${dirname}-api。
|
|
51
|
+
// 为了确保准确,我们可以解析 docker compose config 的输出,或者强制指定 image name。
|
|
52
|
+
// 更稳妥的方式:直接 save 指定的服务镜像。
|
|
53
|
+
// docker compose images -q 可以获取 ID。
|
|
54
|
+
// 获取所有相关镜像的列表
|
|
55
|
+
const projectName = path.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
56
|
+
const images = [
|
|
57
|
+
'postgres:16-alpine',
|
|
58
|
+
'redis:7-alpine',
|
|
59
|
+
`${projectName}-api`, // 默认命名规则
|
|
60
|
+
`${projectName}-web`
|
|
61
|
+
];
|
|
62
|
+
// 我们最好通过 docker compose config 来确认镜像名,但这比较复杂。
|
|
63
|
+
// 简单策略:先 save postgres 和 redis,对于 api 和 web,我们先 tag 一下以确保名字固定。
|
|
64
|
+
await execa('docker', ['tag', `${projectName}-api`, 'nodebbs-api:latest']);
|
|
65
|
+
await execa('docker', ['tag', `${projectName}-web`, 'nodebbs-web:latest']);
|
|
66
|
+
const imagesToSave = ['postgres:16-alpine', 'redis:7-alpine', 'nodebbs-api:latest', 'nodebbs-web:latest'];
|
|
67
|
+
await execa('docker', ['save', '-o', path.join(tmpDir, 'nodebbs-images.tar'), ...imagesToSave], { stdio: 'inherit' });
|
|
68
|
+
// 6. 复制配置文件 (并进行修改)
|
|
69
|
+
// 读取 docker-compose.yml 并移除开发环境的源码挂载
|
|
70
|
+
// 这是一个关键步骤,因为在离线/生产环境中,我们不应该挂载 ./apps/xxx/src,
|
|
71
|
+
// 否则会覆盖掉镜像内构建好的代码,导致 "Cannot find module" 错误。
|
|
72
|
+
let composeContent = fs.readFileSync('docker-compose.yml', 'utf-8');
|
|
73
|
+
const lines = composeContent.split('\n');
|
|
74
|
+
const cleanedLines = lines.map(line => {
|
|
75
|
+
// 移除 api 和 web 的源码挂载
|
|
76
|
+
if (line.includes('./apps/api/src') || line.includes('./apps/web/src')) {
|
|
77
|
+
return line.replace(/^/, '# [OFFLINE-PACK-REMOVED] ');
|
|
78
|
+
}
|
|
79
|
+
return line;
|
|
80
|
+
});
|
|
81
|
+
fs.writeFileSync(path.join(tmpDir, 'docker-compose.yml'), cleanedLines.join('\n'));
|
|
82
|
+
if (fs.existsSync('docker-compose.prod.yml')) {
|
|
83
|
+
fs.copyFileSync('docker-compose.prod.yml', path.join(tmpDir, 'docker-compose.prod.yml'));
|
|
84
|
+
}
|
|
85
|
+
if (fs.existsSync('.env')) {
|
|
86
|
+
this.warn('检测到 .env 文件,出于安全考虑,不会默认打包 .env 文件。请在部署时手动配置环境变量。');
|
|
87
|
+
}
|
|
88
|
+
// 创建 .env.example
|
|
89
|
+
// 创建 .env.example
|
|
90
|
+
const envTemplatePath = getTemplatePath('env');
|
|
91
|
+
let envExample = '';
|
|
92
|
+
try {
|
|
93
|
+
envExample = fs.readFileSync(envTemplatePath, 'utf-8');
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
this.warn(`读取 env 模板失败: ${envTemplatePath},将使用空模板`);
|
|
97
|
+
}
|
|
98
|
+
fs.writeFileSync(path.join(tmpDir, '.env.example'), envExample);
|
|
99
|
+
// 7. 创建安装脚本
|
|
100
|
+
const installScript = `#!/bin/bash
|
|
101
|
+
echo "正在加载 Docker 镜像..."
|
|
102
|
+
docker load -i nodebbs-images.tar
|
|
103
|
+
|
|
104
|
+
echo "正在启动服务..."
|
|
105
|
+
if [ ! -f .env ]; then
|
|
106
|
+
echo "警告: 未找到 .env 文件,将使用默认配置或报错。请先复制 .env.example 为 .env 并修改配置。"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# 修改 image 名称以匹配 load 进来的 tag (如果 compose 文件里没写 image,它会尝试 build)
|
|
110
|
+
# 为了避免 build,我们需要修改 docker-compose.yml 或者使用环境变量 override
|
|
111
|
+
# 最简单的办法:我们在 install 脚本里,强制用 docker run 或者修改 compose 文件?
|
|
112
|
+
# 这里的难点是 compose file 里写的是 build: .
|
|
113
|
+
# 离线环境没法 build。
|
|
114
|
+
# 解决方案:生成一个专门的 docker-compose.offline.yml
|
|
115
|
+
|
|
116
|
+
cat > docker-compose.override.yml <<EOF
|
|
117
|
+
services:
|
|
118
|
+
api:
|
|
119
|
+
build: !reset
|
|
120
|
+
image: nodebbs-api:latest
|
|
121
|
+
web:
|
|
122
|
+
build: !reset
|
|
123
|
+
image: nodebbs-web:latest
|
|
124
|
+
EOF
|
|
125
|
+
|
|
126
|
+
docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.override.yml up -d
|
|
127
|
+
|
|
128
|
+
echo "部署完成!"
|
|
129
|
+
`;
|
|
130
|
+
fs.writeFileSync(path.join(tmpDir, 'install.sh'), installScript);
|
|
131
|
+
fs.chmodSync(path.join(tmpDir, 'install.sh'), '755');
|
|
132
|
+
// 8. 打包最终的 tar.gz
|
|
133
|
+
this.log(`正在生成最终压缩包: ${outputPath}`);
|
|
134
|
+
await execa('tar', ['-czf', outputPath, '-C', tmpDir, '.']);
|
|
135
|
+
this.log(`\n🎉 离线包生成成功: ${outputPath}`);
|
|
136
|
+
this.log('使用方法:');
|
|
137
|
+
this.log('1. 将压缩包上传到服务器');
|
|
138
|
+
this.log('2. 解压: tar -xzf nodebbs-offline.tar.gz');
|
|
139
|
+
this.log('3. 配置 .env');
|
|
140
|
+
this.log('4. 运行: ./install.sh');
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
this.error(`打包失败: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
// 清理临时目录
|
|
147
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
package/dist/interactive.js
CHANGED
|
@@ -30,7 +30,7 @@ export async function runInteractive(root) {
|
|
|
30
30
|
await navigate(tree, [], config);
|
|
31
31
|
}
|
|
32
32
|
async function navigate(node, breadcrumbs, config) {
|
|
33
|
-
const priorityOrder = ['start', 'rebuild', 'stop', 'restart', 'status', 'logs', 'shell', 'db'];
|
|
33
|
+
const priorityOrder = ['start', 'rebuild', 'stop', 'restart', 'status', 'logs', 'pack', 'shell', 'db'];
|
|
34
34
|
while (true) {
|
|
35
35
|
const keys = Object.keys(node.children).sort((a, b) => {
|
|
36
36
|
const indexA = priorityOrder.indexOf(a);
|
package/oclif.manifest.json
CHANGED
|
@@ -266,6 +266,37 @@
|
|
|
266
266
|
"seed.js"
|
|
267
267
|
]
|
|
268
268
|
},
|
|
269
|
+
"pack": {
|
|
270
|
+
"aliases": [],
|
|
271
|
+
"args": {},
|
|
272
|
+
"description": "生成离线部署包",
|
|
273
|
+
"flags": {
|
|
274
|
+
"output": {
|
|
275
|
+
"char": "o",
|
|
276
|
+
"description": "输出文件名",
|
|
277
|
+
"name": "output",
|
|
278
|
+
"default": "nodebbs-offline.tar.gz",
|
|
279
|
+
"hasDynamicHelp": false,
|
|
280
|
+
"multiple": false,
|
|
281
|
+
"type": "option"
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
"hasDynamicHelp": false,
|
|
285
|
+
"hiddenAliases": [],
|
|
286
|
+
"id": "pack",
|
|
287
|
+
"pluginAlias": "nodebbs",
|
|
288
|
+
"pluginName": "nodebbs",
|
|
289
|
+
"pluginType": "core",
|
|
290
|
+
"strict": true,
|
|
291
|
+
"enableJsonFlag": false,
|
|
292
|
+
"isESM": true,
|
|
293
|
+
"relativePath": [
|
|
294
|
+
"dist",
|
|
295
|
+
"commands",
|
|
296
|
+
"pack",
|
|
297
|
+
"index.js"
|
|
298
|
+
]
|
|
299
|
+
},
|
|
269
300
|
"logs:api": {
|
|
270
301
|
"aliases": [],
|
|
271
302
|
"args": {},
|
|
@@ -497,10 +528,10 @@
|
|
|
497
528
|
"index.js"
|
|
498
529
|
]
|
|
499
530
|
},
|
|
500
|
-
"
|
|
531
|
+
"shell:api": {
|
|
501
532
|
"aliases": [],
|
|
502
533
|
"args": {},
|
|
503
|
-
"description": "
|
|
534
|
+
"description": "进入 API 服务 shell",
|
|
504
535
|
"flags": {
|
|
505
536
|
"env": {
|
|
506
537
|
"char": "e",
|
|
@@ -514,18 +545,11 @@
|
|
|
514
545
|
"basic"
|
|
515
546
|
],
|
|
516
547
|
"type": "option"
|
|
517
|
-
},
|
|
518
|
-
"build": {
|
|
519
|
-
"char": "b",
|
|
520
|
-
"description": "重新构建并启动服务 (跳过健康检查和数据初始化)",
|
|
521
|
-
"name": "build",
|
|
522
|
-
"allowNo": false,
|
|
523
|
-
"type": "boolean"
|
|
524
548
|
}
|
|
525
549
|
},
|
|
526
550
|
"hasDynamicHelp": false,
|
|
527
551
|
"hiddenAliases": [],
|
|
528
|
-
"id": "
|
|
552
|
+
"id": "shell:api",
|
|
529
553
|
"pluginAlias": "nodebbs",
|
|
530
554
|
"pluginName": "nodebbs",
|
|
531
555
|
"pluginType": "core",
|
|
@@ -535,14 +559,14 @@
|
|
|
535
559
|
"relativePath": [
|
|
536
560
|
"dist",
|
|
537
561
|
"commands",
|
|
538
|
-
"
|
|
539
|
-
"
|
|
562
|
+
"shell",
|
|
563
|
+
"api.js"
|
|
540
564
|
]
|
|
541
565
|
},
|
|
542
|
-
"shell:
|
|
566
|
+
"shell:db": {
|
|
543
567
|
"aliases": [],
|
|
544
568
|
"args": {},
|
|
545
|
-
"description": "
|
|
569
|
+
"description": "进入数据库 shell (psql)",
|
|
546
570
|
"flags": {
|
|
547
571
|
"env": {
|
|
548
572
|
"char": "e",
|
|
@@ -560,7 +584,7 @@
|
|
|
560
584
|
},
|
|
561
585
|
"hasDynamicHelp": false,
|
|
562
586
|
"hiddenAliases": [],
|
|
563
|
-
"id": "shell:
|
|
587
|
+
"id": "shell:db",
|
|
564
588
|
"pluginAlias": "nodebbs",
|
|
565
589
|
"pluginName": "nodebbs",
|
|
566
590
|
"pluginType": "core",
|
|
@@ -571,13 +595,13 @@
|
|
|
571
595
|
"dist",
|
|
572
596
|
"commands",
|
|
573
597
|
"shell",
|
|
574
|
-
"
|
|
598
|
+
"db.js"
|
|
575
599
|
]
|
|
576
600
|
},
|
|
577
|
-
"shell:
|
|
601
|
+
"shell:redis": {
|
|
578
602
|
"aliases": [],
|
|
579
603
|
"args": {},
|
|
580
|
-
"description": "
|
|
604
|
+
"description": "进入 Redis shell (redis-cli)",
|
|
581
605
|
"flags": {
|
|
582
606
|
"env": {
|
|
583
607
|
"char": "e",
|
|
@@ -595,7 +619,7 @@
|
|
|
595
619
|
},
|
|
596
620
|
"hasDynamicHelp": false,
|
|
597
621
|
"hiddenAliases": [],
|
|
598
|
-
"id": "shell:
|
|
622
|
+
"id": "shell:redis",
|
|
599
623
|
"pluginAlias": "nodebbs",
|
|
600
624
|
"pluginName": "nodebbs",
|
|
601
625
|
"pluginType": "core",
|
|
@@ -606,13 +630,13 @@
|
|
|
606
630
|
"dist",
|
|
607
631
|
"commands",
|
|
608
632
|
"shell",
|
|
609
|
-
"
|
|
633
|
+
"redis.js"
|
|
610
634
|
]
|
|
611
635
|
},
|
|
612
|
-
"shell:
|
|
636
|
+
"shell:web": {
|
|
613
637
|
"aliases": [],
|
|
614
638
|
"args": {},
|
|
615
|
-
"description": "进入
|
|
639
|
+
"description": "进入 Web 前端 shell",
|
|
616
640
|
"flags": {
|
|
617
641
|
"env": {
|
|
618
642
|
"char": "e",
|
|
@@ -630,7 +654,7 @@
|
|
|
630
654
|
},
|
|
631
655
|
"hasDynamicHelp": false,
|
|
632
656
|
"hiddenAliases": [],
|
|
633
|
-
"id": "shell:
|
|
657
|
+
"id": "shell:web",
|
|
634
658
|
"pluginAlias": "nodebbs",
|
|
635
659
|
"pluginName": "nodebbs",
|
|
636
660
|
"pluginType": "core",
|
|
@@ -641,13 +665,13 @@
|
|
|
641
665
|
"dist",
|
|
642
666
|
"commands",
|
|
643
667
|
"shell",
|
|
644
|
-
"
|
|
668
|
+
"web.js"
|
|
645
669
|
]
|
|
646
670
|
},
|
|
647
|
-
"
|
|
671
|
+
"start": {
|
|
648
672
|
"aliases": [],
|
|
649
673
|
"args": {},
|
|
650
|
-
"description": "
|
|
674
|
+
"description": "开始部署",
|
|
651
675
|
"flags": {
|
|
652
676
|
"env": {
|
|
653
677
|
"char": "e",
|
|
@@ -661,11 +685,18 @@
|
|
|
661
685
|
"basic"
|
|
662
686
|
],
|
|
663
687
|
"type": "option"
|
|
688
|
+
},
|
|
689
|
+
"build": {
|
|
690
|
+
"char": "b",
|
|
691
|
+
"description": "重新构建并启动服务 (跳过健康检查和数据初始化)",
|
|
692
|
+
"name": "build",
|
|
693
|
+
"allowNo": false,
|
|
694
|
+
"type": "boolean"
|
|
664
695
|
}
|
|
665
696
|
},
|
|
666
697
|
"hasDynamicHelp": false,
|
|
667
698
|
"hiddenAliases": [],
|
|
668
|
-
"id": "
|
|
699
|
+
"id": "start",
|
|
669
700
|
"pluginAlias": "nodebbs",
|
|
670
701
|
"pluginName": "nodebbs",
|
|
671
702
|
"pluginType": "core",
|
|
@@ -675,8 +706,8 @@
|
|
|
675
706
|
"relativePath": [
|
|
676
707
|
"dist",
|
|
677
708
|
"commands",
|
|
678
|
-
"
|
|
679
|
-
"
|
|
709
|
+
"start",
|
|
710
|
+
"index.js"
|
|
680
711
|
]
|
|
681
712
|
},
|
|
682
713
|
"status": {
|
|
@@ -757,5 +788,5 @@
|
|
|
757
788
|
]
|
|
758
789
|
}
|
|
759
790
|
},
|
|
760
|
-
"version": "0.0.
|
|
791
|
+
"version": "0.0.5"
|
|
761
792
|
}
|