nodebbs 0.0.2 → 0.0.4
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 +103 -111
- package/bin/dev.js +6 -1
- package/bin/run.js +6 -1
- package/dist/commands/clean/index.d.ts +11 -0
- package/dist/commands/clean/index.js +93 -0
- package/dist/commands/db/backup.d.ts +9 -0
- package/dist/commands/db/backup.js +78 -0
- package/dist/commands/db/generate.d.ts +3 -0
- package/dist/commands/db/generate.js +9 -3
- package/dist/commands/db/migrate.d.ts +3 -0
- package/dist/commands/db/migrate.js +8 -2
- package/dist/commands/db/push.d.ts +3 -0
- package/dist/commands/db/push.js +8 -2
- package/dist/commands/db/reset.d.ts +3 -0
- package/dist/commands/db/reset.js +10 -4
- package/dist/commands/db/seed.d.ts +3 -0
- package/dist/commands/db/seed.js +9 -3
- package/dist/commands/logs/api.d.ts +3 -0
- package/dist/commands/logs/api.js +8 -2
- package/dist/commands/logs/db.d.ts +3 -0
- package/dist/commands/logs/db.js +8 -2
- package/dist/commands/logs/index.d.ts +3 -0
- package/dist/commands/logs/index.js +8 -2
- package/dist/commands/logs/redis.d.ts +3 -0
- package/dist/commands/logs/redis.js +8 -2
- package/dist/commands/logs/web.d.ts +3 -0
- package/dist/commands/logs/web.js +9 -3
- package/dist/commands/{db → rebuild}/index.d.ts +2 -1
- package/dist/commands/rebuild/index.js +11 -0
- package/dist/commands/restart/index.d.ts +3 -0
- package/dist/commands/restart/index.js +10 -2
- package/dist/commands/shell/api.d.ts +3 -0
- package/dist/commands/shell/api.js +9 -3
- package/dist/commands/shell/db.d.ts +3 -0
- package/dist/commands/shell/db.js +9 -3
- package/dist/commands/shell/redis.d.ts +3 -0
- package/dist/commands/shell/redis.js +9 -3
- package/dist/commands/shell/web.d.ts +3 -0
- package/dist/commands/shell/web.js +9 -3
- package/dist/commands/{dev → start}/index.d.ts +2 -2
- package/dist/commands/start/index.js +111 -0
- package/dist/commands/status/index.d.ts +3 -0
- package/dist/commands/status/index.js +8 -2
- package/dist/commands/stop/index.d.ts +1 -0
- package/dist/commands/stop/index.js +9 -1
- package/dist/interactive.d.ts +1 -0
- package/dist/interactive.js +171 -0
- package/dist/templates/env +61 -0
- package/dist/utils/docker.js +10 -18
- package/dist/utils/env.js +20 -10
- package/dist/utils/selection.d.ts +7 -0
- package/dist/utils/selection.js +68 -0
- package/dist/utils/template.d.ts +11 -0
- package/dist/utils/template.js +18 -0
- package/oclif.manifest.json +367 -90
- package/package.json +30 -25
- package/dist/commands/db/index.js +0 -11
- package/dist/commands/deploy/index.d.ts +0 -8
- package/dist/commands/deploy/index.js +0 -95
- package/dist/commands/dev/index.js +0 -59
- package/dist/commands/setup/index.d.ts +0 -5
- package/dist/commands/setup/index.js +0 -12
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Config } from '@oclif/core';
|
|
2
|
+
import { select, Separator } from '@inquirer/prompts';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
export async function runInteractive(root) {
|
|
5
|
+
// Config.load 需要文件路径而不是 URL
|
|
6
|
+
// 如果 root 是 import.meta.url,则进行转换
|
|
7
|
+
let rootPath = root.startsWith('file://') ? fileURLToPath(root) : root;
|
|
8
|
+
const config = await Config.load(rootPath);
|
|
9
|
+
// 构建命令树
|
|
10
|
+
const tree = { name: 'root', children: {} };
|
|
11
|
+
for (const cmd of config.commands) {
|
|
12
|
+
// 跳过隐藏命令
|
|
13
|
+
if (cmd.hidden)
|
|
14
|
+
continue;
|
|
15
|
+
const parts = cmd.id.split(':');
|
|
16
|
+
let currentNode = tree;
|
|
17
|
+
for (let i = 0; i < parts.length; i++) {
|
|
18
|
+
const part = parts[i];
|
|
19
|
+
if (!currentNode.children[part]) {
|
|
20
|
+
currentNode.children[part] = {
|
|
21
|
+
name: part,
|
|
22
|
+
children: {}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
currentNode = currentNode.children[part];
|
|
26
|
+
}
|
|
27
|
+
// 将命令分配给叶节点
|
|
28
|
+
currentNode.command = cmd;
|
|
29
|
+
}
|
|
30
|
+
await navigate(tree, [], config);
|
|
31
|
+
}
|
|
32
|
+
async function navigate(node, breadcrumbs, config) {
|
|
33
|
+
const priorityOrder = ['start', 'rebuild', 'stop', 'restart', 'status', 'logs', 'shell', 'db'];
|
|
34
|
+
while (true) {
|
|
35
|
+
const keys = Object.keys(node.children).sort((a, b) => {
|
|
36
|
+
const indexA = priorityOrder.indexOf(a);
|
|
37
|
+
const indexB = priorityOrder.indexOf(b);
|
|
38
|
+
// If both are in priority list, sort by index
|
|
39
|
+
if (indexA !== -1 && indexB !== -1)
|
|
40
|
+
return indexA - indexB;
|
|
41
|
+
// If only A is in priority list, A comes first
|
|
42
|
+
if (indexA !== -1)
|
|
43
|
+
return -1;
|
|
44
|
+
// If only B is in priority list, B comes first
|
|
45
|
+
if (indexB !== -1)
|
|
46
|
+
return 1;
|
|
47
|
+
// Otherwise sort alphabetically
|
|
48
|
+
return a.localeCompare(b);
|
|
49
|
+
});
|
|
50
|
+
const choices = keys.map(key => {
|
|
51
|
+
const child = node.children[key];
|
|
52
|
+
const hasSubcommands = Object.keys(child.children).length > 0;
|
|
53
|
+
let label = key;
|
|
54
|
+
let description = '';
|
|
55
|
+
// 尝试获取命令描述
|
|
56
|
+
if (key === 'help') {
|
|
57
|
+
description = '显示帮助信息';
|
|
58
|
+
}
|
|
59
|
+
else if (child.command?.description) {
|
|
60
|
+
description = child.command.description;
|
|
61
|
+
}
|
|
62
|
+
// 或者获取主题描述
|
|
63
|
+
else if (hasSubcommands) {
|
|
64
|
+
const fullId = [...breadcrumbs, key].join(':');
|
|
65
|
+
const topic = config.topics.find(t => t.name === fullId);
|
|
66
|
+
if (topic) {
|
|
67
|
+
description = topic.description || '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (description) {
|
|
71
|
+
const shortDesc = description.split('.')[0].substring(0, 50);
|
|
72
|
+
label = `${key.padEnd(12)} ${shortDesc}`;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
label = key.padEnd(12);
|
|
76
|
+
}
|
|
77
|
+
if (hasSubcommands) {
|
|
78
|
+
label = `${label} [+]`;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
name: label,
|
|
82
|
+
value: key,
|
|
83
|
+
short: key
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
// 添加导航选项
|
|
87
|
+
// 即使支持 Esc,保留显式选项也有助于发现性
|
|
88
|
+
if (breadcrumbs.length > 0) {
|
|
89
|
+
choices.push(new Separator());
|
|
90
|
+
choices.push({ name: '⬅️ 返回', value: '__BACK__', short: '返回' });
|
|
91
|
+
}
|
|
92
|
+
choices.push({ name: '❌ 退出', value: '__EXIT__', short: '退出' });
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
// 监听 Esc 键
|
|
95
|
+
const onKeypress = (_str, key) => {
|
|
96
|
+
if (key && key.name === 'escape') {
|
|
97
|
+
controller.abort();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
// 确保 keypress 事件被触发(inquirer 内部也会这样做,但为了安全起见)
|
|
101
|
+
// 注意:process.stdin 可能已经是 raw mode,inquirer 会处理它
|
|
102
|
+
// 我们只需要监听
|
|
103
|
+
process.stdin.on('keypress', onKeypress);
|
|
104
|
+
try {
|
|
105
|
+
const selection = await select({
|
|
106
|
+
message: breadcrumbs.length ? `选择命令 (${breadcrumbs.join(' > ')}):` : '选择命令:',
|
|
107
|
+
choices,
|
|
108
|
+
pageSize: 15,
|
|
109
|
+
loop: true, // 允许循环导航
|
|
110
|
+
// @ts-ignore: prompt signal support might be strict on types
|
|
111
|
+
signal: controller.signal
|
|
112
|
+
});
|
|
113
|
+
if (selection === '__EXIT__') {
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
if (selection === '__BACK__') {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const selectedNode = node.children[selection];
|
|
120
|
+
if (Object.keys(selectedNode.children).length > 0) {
|
|
121
|
+
// 有子命令,进入子菜单
|
|
122
|
+
await navigate(selectedNode, [...breadcrumbs, selection], config);
|
|
123
|
+
}
|
|
124
|
+
else if (selectedNode.command) {
|
|
125
|
+
// 可运行的命令
|
|
126
|
+
console.log(`正在运行: ${selectedNode.command.id}`);
|
|
127
|
+
try {
|
|
128
|
+
await config.runCommand(selectedNode.command.id);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
// Ctrl+C: 退出程序
|
|
132
|
+
if (error.name === 'ExitPromptError') {
|
|
133
|
+
console.log('\n用户退出。');
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
// 手动取消: 返回菜单
|
|
137
|
+
if (error.name === 'CancelError') {
|
|
138
|
+
console.log('\n操作已取消。');
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
console.error(error);
|
|
142
|
+
}
|
|
143
|
+
console.log('\n命令执行完成。');
|
|
144
|
+
// 动态导入 input 以保持轻量
|
|
145
|
+
const { input } = await import('@inquirer/prompts');
|
|
146
|
+
await input({ message: '按回车键继续...' });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
// 处理 AbortError (Esc)
|
|
151
|
+
if (error.name === 'AbortError' || error.message?.includes('aborted')) {
|
|
152
|
+
if (breadcrumbs.length > 0) {
|
|
153
|
+
// 子菜单 -> 返回
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// 顶级菜单 -> 退出
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// 处理 Ctrl+C (ExitPromptError)
|
|
162
|
+
if (error.name === 'ExitPromptError') {
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
process.stdin.off('keypress', onKeypress);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# ========================================
|
|
2
|
+
# NodeBBS Docker Compose 环境变量配置
|
|
3
|
+
# ========================================
|
|
4
|
+
|
|
5
|
+
# 应用名称
|
|
6
|
+
APP_NAME=nodebbs
|
|
7
|
+
|
|
8
|
+
# ========================================
|
|
9
|
+
# 数据库配置
|
|
10
|
+
# ========================================
|
|
11
|
+
POSTGRES_PASSWORD=your_secure_postgres_password_here
|
|
12
|
+
POSTGRES_DB=nodebbs
|
|
13
|
+
POSTGRES_PORT=5432
|
|
14
|
+
|
|
15
|
+
# ========================================
|
|
16
|
+
# Redis 配置
|
|
17
|
+
# ========================================
|
|
18
|
+
REDIS_PASSWORD=your_secure_redis_password_here
|
|
19
|
+
REDIS_PORT=6379
|
|
20
|
+
|
|
21
|
+
# ========================================
|
|
22
|
+
# API 服务配置
|
|
23
|
+
# ========================================
|
|
24
|
+
API_PORT=7100
|
|
25
|
+
|
|
26
|
+
# 用户缓存 TTL(秒)
|
|
27
|
+
# 开发环境: 30-60, 生产环境: 120-300
|
|
28
|
+
USER_CACHE_TTL=120
|
|
29
|
+
|
|
30
|
+
# JWT 配置
|
|
31
|
+
# 使用 `openssl rand -base64 32` 生成安全的密钥
|
|
32
|
+
JWT_SECRET=change-this-to-a-secure-random-string-in-production
|
|
33
|
+
JWT_ACCESS_TOKEN_EXPIRES_IN=1y
|
|
34
|
+
|
|
35
|
+
# CORS 配置
|
|
36
|
+
# 生产环境建议设置为具体的域名,例如: https://yourdomain.com
|
|
37
|
+
CORS_ORIGIN=*
|
|
38
|
+
|
|
39
|
+
# 应用 URL(OAuth 回调使用)
|
|
40
|
+
APP_URL=http://localhost:3100
|
|
41
|
+
|
|
42
|
+
# ========================================
|
|
43
|
+
# Web 前端配置
|
|
44
|
+
# ========================================
|
|
45
|
+
WEB_PORT=3100
|
|
46
|
+
|
|
47
|
+
# API 地址(公网访问地址)
|
|
48
|
+
# 使用 docker 部署必须指定 IP 或域名(避免SSR跨容器通信问题)
|
|
49
|
+
# 开发环境: http://192.168.0.100:7100
|
|
50
|
+
# 生产环境: https://api.yourdomain.com
|
|
51
|
+
NEXT_PUBLIC_API_URL=http://192.168.0.100:7100
|
|
52
|
+
|
|
53
|
+
# 应用 URL(公网访问地址)
|
|
54
|
+
# 开发环境: http://localhost:3100
|
|
55
|
+
# 生产环境: https://yourdomain.com
|
|
56
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3100
|
|
57
|
+
|
|
58
|
+
# ========================================
|
|
59
|
+
# 时区配置
|
|
60
|
+
# ========================================
|
|
61
|
+
TZ=Asia/Shanghai
|
package/dist/utils/docker.js
CHANGED
|
@@ -3,27 +3,19 @@ import { logger } from './logger.js';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { exists } from 'node:fs';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import {
|
|
6
|
+
import { getTemplateDir, getTemplatePath } from './template.js';
|
|
7
7
|
const fileExists = promisify(exists);
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
8
|
export async function getComposeFiles(env) {
|
|
10
9
|
const workDir = process.cwd();
|
|
11
10
|
let isBuiltIn = false;
|
|
12
|
-
//
|
|
11
|
+
// 检查当前目录是否存在 docker-compose.yml
|
|
13
12
|
let baseFile = path.join(workDir, 'docker-compose.yml');
|
|
14
13
|
let templateDir = workDir;
|
|
15
14
|
if (!await fileExists(baseFile)) {
|
|
16
|
-
//
|
|
15
|
+
// 使用内置模板
|
|
17
16
|
isBuiltIn = true;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
templateDir = path.join(__dirname, '..', 'templates');
|
|
21
|
-
baseFile = path.join(templateDir, 'docker-compose.yml');
|
|
22
|
-
// Verify the built-in template exists
|
|
23
|
-
if (!await fileExists(baseFile)) {
|
|
24
|
-
logger.error('内置模板未找到,请确保 CLI 正确安装。');
|
|
25
|
-
throw new Error('Built-in templates not found');
|
|
26
|
-
}
|
|
17
|
+
templateDir = getTemplateDir();
|
|
18
|
+
baseFile = getTemplatePath('docker-compose.yml');
|
|
27
19
|
}
|
|
28
20
|
const files = [baseFile];
|
|
29
21
|
if (env === 'production') {
|
|
@@ -50,12 +42,12 @@ export async function runCompose(files, args, isBuiltIn = false) {
|
|
|
50
42
|
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
51
43
|
if (isBuiltIn) {
|
|
52
44
|
composeArgs.push('--project-directory', process.cwd());
|
|
53
|
-
//
|
|
45
|
+
// 将 INIT_DB_PATH 设置为内置 sql 文件
|
|
54
46
|
const templateDir = path.dirname(files[0]);
|
|
55
47
|
process.env.INIT_DB_PATH = path.join(templateDir, 'init-db.sql');
|
|
56
48
|
}
|
|
57
49
|
composeArgs.push(...args);
|
|
58
|
-
//
|
|
50
|
+
// 使用 stdio: 'inherit' 实时显示输出
|
|
59
51
|
await execa('docker', ['compose', ...composeArgs], { stdio: 'inherit' });
|
|
60
52
|
}
|
|
61
53
|
export async function execCompose(files, service, command, isBuiltIn = false) {
|
|
@@ -68,7 +60,7 @@ export async function execCompose(files, service, command, isBuiltIn = false) {
|
|
|
68
60
|
}
|
|
69
61
|
export async function waitForHealth(files, isBuiltIn = false) {
|
|
70
62
|
logger.info('正在等待服务就绪...');
|
|
71
|
-
//
|
|
63
|
+
// 等待 Postgres
|
|
72
64
|
logger.info('等待 PostgreSQL...');
|
|
73
65
|
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
74
66
|
if (isBuiltIn) {
|
|
@@ -90,11 +82,11 @@ export async function waitForHealth(files, isBuiltIn = false) {
|
|
|
90
82
|
if (retries === 0) {
|
|
91
83
|
logger.warning('PostgreSQL 可能尚未就绪');
|
|
92
84
|
}
|
|
93
|
-
//
|
|
85
|
+
// 等待 Redis
|
|
94
86
|
logger.info('等待 Redis...');
|
|
95
87
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
96
88
|
logger.success('Redis 已就绪');
|
|
97
|
-
//
|
|
89
|
+
// 等待 API
|
|
98
90
|
logger.info('等待 API 服务...');
|
|
99
91
|
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
100
92
|
logger.success('API 服务已就绪');
|
package/dist/utils/env.js
CHANGED
|
@@ -4,6 +4,7 @@ import { promisify } from 'node:util';
|
|
|
4
4
|
import { logger } from './logger.js';
|
|
5
5
|
import { confirm } from '@inquirer/prompts';
|
|
6
6
|
import dotenv from 'dotenv';
|
|
7
|
+
import { getTemplatePath } from './template.js';
|
|
7
8
|
const fileExists = promisify(exists);
|
|
8
9
|
export async function initEnv() {
|
|
9
10
|
if (await fileExists('.env')) {
|
|
@@ -11,8 +12,24 @@ export async function initEnv() {
|
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
14
|
logger.info('正在创建 .env 文件...');
|
|
15
|
+
let sourceFile = '';
|
|
16
|
+
let isBuiltIn = false;
|
|
14
17
|
if (await fileExists('.env.docker.example')) {
|
|
15
|
-
|
|
18
|
+
sourceFile = '.env.docker.example';
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// 使用内置模板
|
|
22
|
+
sourceFile = getTemplatePath('env');
|
|
23
|
+
isBuiltIn = true;
|
|
24
|
+
}
|
|
25
|
+
if (sourceFile) {
|
|
26
|
+
await fs.copyFile(sourceFile, '.env');
|
|
27
|
+
if (isBuiltIn) {
|
|
28
|
+
logger.success('已使用内置模板创建 .env');
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
logger.success(`已从 ${sourceFile} 复制 .env`);
|
|
32
|
+
}
|
|
16
33
|
logger.warning('请编辑 .env 文件并修改以下配置:');
|
|
17
34
|
logger.warning(' - POSTGRES_PASSWORD (数据库密码)');
|
|
18
35
|
logger.warning(' - REDIS_PASSWORD (Redis 密码)');
|
|
@@ -27,19 +44,12 @@ export async function initEnv() {
|
|
|
27
44
|
}
|
|
28
45
|
}
|
|
29
46
|
else {
|
|
30
|
-
|
|
31
|
-
if (await fileExists('project/.env.docker.example')) {
|
|
32
|
-
await fs.copyFile('project/.env.docker.example', '.env');
|
|
33
|
-
logger.success('已从 project/.env.docker.example 复制 .env');
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
logger.warning('未找到 .env.docker.example!跳过 .env 创建。');
|
|
37
|
-
}
|
|
47
|
+
logger.warning('未找到 .env.docker.example 或内置模板!跳过 .env 创建。');
|
|
38
48
|
}
|
|
39
49
|
}
|
|
40
50
|
export async function checkEnv(envType) {
|
|
41
51
|
logger.info('正在检查环境配置...');
|
|
42
|
-
//
|
|
52
|
+
// 加载 .env
|
|
43
53
|
const envConfig = dotenv.config().parsed || {};
|
|
44
54
|
let warnings = 0;
|
|
45
55
|
let errors = 0;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type EnvType = 'production' | 'lowmem' | 'basic';
|
|
2
|
+
export declare const EnvFlag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
3
|
+
export declare function setStoredEnv(env: EnvType): Promise<void>;
|
|
4
|
+
export declare function clearStoredEnv(): Promise<void>;
|
|
5
|
+
export declare function selectEnvironment(env?: string, options?: {
|
|
6
|
+
prompt?: string;
|
|
7
|
+
}): Promise<EnvType>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { select } from '@inquirer/prompts';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
const ENV_MARKER_FILE = '.nodebbs-env';
|
|
7
|
+
export const EnvFlag = Flags.string({
|
|
8
|
+
char: 'e',
|
|
9
|
+
description: '部署环境 (production, lowmem, basic)',
|
|
10
|
+
options: ['production', 'lowmem', 'basic'],
|
|
11
|
+
});
|
|
12
|
+
async function getStoredEnv() {
|
|
13
|
+
try {
|
|
14
|
+
const content = await fs.readFile(path.resolve(process.cwd(), ENV_MARKER_FILE), 'utf-8');
|
|
15
|
+
const env = content.trim();
|
|
16
|
+
if (['production', 'lowmem', 'basic'].includes(env)) {
|
|
17
|
+
return env;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch { }
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export async function setStoredEnv(env) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.writeFile(path.resolve(process.cwd(), ENV_MARKER_FILE), env, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
logger.warning(`无法保存环境标记: ${error}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function clearStoredEnv() {
|
|
32
|
+
try {
|
|
33
|
+
const filepath = path.resolve(process.cwd(), ENV_MARKER_FILE);
|
|
34
|
+
await fs.rm(filepath, { force: true });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger.warning(`无法清除环境标记: ${error}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function selectEnvironment(env, options = {}) {
|
|
41
|
+
if (env) {
|
|
42
|
+
// oclif 选项验证会处理有效值,但仍需类型转换
|
|
43
|
+
return env;
|
|
44
|
+
}
|
|
45
|
+
// 检查已存储的环境配置
|
|
46
|
+
const storedEnv = await getStoredEnv();
|
|
47
|
+
if (storedEnv) {
|
|
48
|
+
logger.info(`检测到运行环境: ${storedEnv}`);
|
|
49
|
+
return storedEnv;
|
|
50
|
+
}
|
|
51
|
+
const selected = await select({
|
|
52
|
+
message: options.prompt || '请选择运行环境:',
|
|
53
|
+
choices: [
|
|
54
|
+
{ name: '标准生产环境 (2C4G+) [推荐]', value: 'production' },
|
|
55
|
+
{ name: '低配环境 (1C1G/1C2G)', value: 'lowmem' },
|
|
56
|
+
{ name: '基础环境 (仅用于测试)', value: 'basic' },
|
|
57
|
+
{ name: '❌ 取消', value: '__CANCEL__' },
|
|
58
|
+
],
|
|
59
|
+
loop: true,
|
|
60
|
+
});
|
|
61
|
+
if (selected === '__CANCEL__') {
|
|
62
|
+
const error = new Error('用户取消操作');
|
|
63
|
+
error.name = 'CancelError';
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
logger.info(`已选择环境: ${selected}`);
|
|
67
|
+
return selected;
|
|
68
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 获取模板目录的绝对路径。
|
|
3
|
+
* 在生产环境 (dist/utils) 中,模板位于 dist/templates。
|
|
4
|
+
* 在开发环境 (src/utils) 中,模板位于 src/templates。
|
|
5
|
+
*/
|
|
6
|
+
export declare function getTemplateDir(): string;
|
|
7
|
+
/**
|
|
8
|
+
* 获取特定模板文件的绝对路径。
|
|
9
|
+
* @param fileName 模板文件名称 (例如 'env', 'docker-compose.yml')
|
|
10
|
+
*/
|
|
11
|
+
export declare function getTemplatePath(fileName: string): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
4
|
+
/**
|
|
5
|
+
* 获取模板目录的绝对路径。
|
|
6
|
+
* 在生产环境 (dist/utils) 中,模板位于 dist/templates。
|
|
7
|
+
* 在开发环境 (src/utils) 中,模板位于 src/templates。
|
|
8
|
+
*/
|
|
9
|
+
export function getTemplateDir() {
|
|
10
|
+
return path.join(__dirname, '..', 'templates');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 获取特定模板文件的绝对路径。
|
|
14
|
+
* @param fileName 模板文件名称 (例如 'env', 'docker-compose.yml')
|
|
15
|
+
*/
|
|
16
|
+
export function getTemplatePath(fileName) {
|
|
17
|
+
return path.join(getTemplateDir(), fileName);
|
|
18
|
+
}
|