nodebbs 0.0.8 → 0.0.9
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/dist/commands/clean/index.d.ts +1 -0
- package/dist/commands/clean/index.js +15 -2
- package/dist/commands/pack/index.js +2 -7
- package/dist/commands/restart/index.js +1 -2
- package/dist/commands/start/index.js +1 -2
- package/dist/commands/stop/index.js +1 -3
- package/dist/interactive.js +2 -2
- package/dist/utils/docker.d.ts +47 -0
- package/dist/utils/docker.js +56 -2
- package/dist/utils/env.js +102 -20
- package/dist/utils/selection.js +2 -1
- package/oclif.manifest.json +8 -2
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ export default class Clean extends Command {
|
|
|
5
5
|
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
6
|
cache: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
images: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
};
|
|
10
11
|
run(): Promise<void>;
|
|
@@ -2,12 +2,13 @@ import { Command, Flags } from '@oclif/core';
|
|
|
2
2
|
import { checkbox, confirm } from '@inquirer/prompts';
|
|
3
3
|
import { logger } from '../../utils/logger.js';
|
|
4
4
|
import { execa } from 'execa';
|
|
5
|
+
import { clearStoredEnv } from '../../utils/selection.js';
|
|
5
6
|
export default class Clean extends Command {
|
|
6
7
|
static description = '清理 Docker 缓存和残留资源';
|
|
7
8
|
static flags = {
|
|
8
9
|
all: Flags.boolean({
|
|
9
10
|
char: 'a',
|
|
10
|
-
description: '清理所有 (
|
|
11
|
+
description: '清理所有 (构建缓存、无用镜像、网络、环境锁定)',
|
|
11
12
|
default: false,
|
|
12
13
|
}),
|
|
13
14
|
cache: Flags.boolean({
|
|
@@ -18,6 +19,10 @@ export default class Clean extends Command {
|
|
|
18
19
|
description: '清理无用镜像 (dangling)',
|
|
19
20
|
default: false,
|
|
20
21
|
}),
|
|
22
|
+
env: Flags.boolean({
|
|
23
|
+
description: '清理环境锁定 (Environment Lock)',
|
|
24
|
+
default: false,
|
|
25
|
+
}),
|
|
21
26
|
force: Flags.boolean({
|
|
22
27
|
char: 'f',
|
|
23
28
|
description: '跳过确认提示',
|
|
@@ -29,13 +34,15 @@ export default class Clean extends Command {
|
|
|
29
34
|
let targets = [];
|
|
30
35
|
// 根据标志确定清理目标
|
|
31
36
|
if (flags.all) {
|
|
32
|
-
targets = ['cache', 'images', 'networks'];
|
|
37
|
+
targets = ['cache', 'images', 'networks', 'env'];
|
|
33
38
|
}
|
|
34
39
|
else {
|
|
35
40
|
if (flags.cache)
|
|
36
41
|
targets.push('cache');
|
|
37
42
|
if (flags.images)
|
|
38
43
|
targets.push('images');
|
|
44
|
+
if (flags.env)
|
|
45
|
+
targets.push('env');
|
|
39
46
|
}
|
|
40
47
|
// 如果未提供标志,进行交互式选择
|
|
41
48
|
if (targets.length === 0) {
|
|
@@ -45,6 +52,7 @@ export default class Clean extends Command {
|
|
|
45
52
|
{ name: '构建缓存 (Build Cache)', value: 'cache' },
|
|
46
53
|
{ name: '无用镜像 (Dangling Images)', value: 'images' },
|
|
47
54
|
{ name: '无用网络 (Unused Networks)', value: 'networks' },
|
|
55
|
+
{ name: '环境锁定 (Environment Lock)', value: 'env' },
|
|
48
56
|
],
|
|
49
57
|
});
|
|
50
58
|
}
|
|
@@ -81,6 +89,11 @@ export default class Clean extends Command {
|
|
|
81
89
|
await execa('docker', ['network', 'prune', '-f'], { stdio: 'inherit' });
|
|
82
90
|
logger.success('无用网络已清理');
|
|
83
91
|
}
|
|
92
|
+
if (targets.includes('env')) {
|
|
93
|
+
logger.info('正在清除环境锁定...');
|
|
94
|
+
await clearStoredEnv();
|
|
95
|
+
logger.success('环境锁定已移除');
|
|
96
|
+
}
|
|
84
97
|
}
|
|
85
98
|
catch (error) {
|
|
86
99
|
logger.error('清理过程中发生错误');
|
|
@@ -4,6 +4,7 @@ import * as fs from 'fs';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as os from 'os';
|
|
6
6
|
import { getTemplatePath } from '../../utils/template.js';
|
|
7
|
+
import { checkDocker } from '../../utils/docker.js';
|
|
7
8
|
export default class Pack extends Command {
|
|
8
9
|
static description = '生成离线部署包';
|
|
9
10
|
static flags = {
|
|
@@ -13,12 +14,7 @@ export default class Pack extends Command {
|
|
|
13
14
|
const { flags } = await this.parse(Pack);
|
|
14
15
|
const outputPath = path.resolve(flags.output);
|
|
15
16
|
// 检查 docker 是否可用
|
|
16
|
-
|
|
17
|
-
await execa('docker', ['--version']);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
this.error('无法执行 docker 命令,请确保 Docker 已安装并运行。');
|
|
21
|
-
}
|
|
17
|
+
await checkDocker();
|
|
22
18
|
// 1. 准备临时目录
|
|
23
19
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nodebbs-pack-'));
|
|
24
20
|
this.log(`正在准备构建环境: ${tmpDir}`);
|
|
@@ -30,7 +26,6 @@ export default class Pack extends Command {
|
|
|
30
26
|
// 但既然是打包,通常是想把当前开发好的代码打包。
|
|
31
27
|
// 所以我们假设在项目根目录运行。
|
|
32
28
|
if (!fs.existsSync('docker-compose.yml')) {
|
|
33
|
-
this.warn('当前目录未找到 docker-compose.yml,将使用内置模板。');
|
|
34
29
|
// TODO: 从 templates 复制 (简化起见,我们假设用户在项目根目录,或者我们强制要求)
|
|
35
30
|
// 如果没找到,我们实际上无法 build api/web,因为 build context 需要源码。
|
|
36
31
|
// 所以必须要求在项目根目录运行。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
import { runCompose, getComposeFiles } from '../../utils/docker.js';
|
|
3
3
|
import { logger } from '../../utils/logger.js';
|
|
4
|
-
import { EnvFlag, selectEnvironment
|
|
4
|
+
import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
|
|
5
5
|
export default class Restart extends Command {
|
|
6
6
|
static description = '重启所有服务 (up --force-recreate)';
|
|
7
7
|
static flags = {
|
|
@@ -11,7 +11,6 @@ export default class Restart extends Command {
|
|
|
11
11
|
const { flags } = await this.parse(Restart);
|
|
12
12
|
// 1. 选择环境
|
|
13
13
|
const env = await selectEnvironment(flags.env);
|
|
14
|
-
await setStoredEnv(env);
|
|
15
14
|
const { files, isBuiltIn } = await getComposeFiles(env);
|
|
16
15
|
logger.info('正在重启服务 (使用现有镜像)...');
|
|
17
16
|
logger.info('环境: ' + env);
|
|
@@ -4,7 +4,7 @@ import { checkDocker, runCompose, waitForHealth, execCompose, getComposeFiles }
|
|
|
4
4
|
import { initEnv, checkEnv } from '../../utils/env.js';
|
|
5
5
|
import { logger } from '../../utils/logger.js';
|
|
6
6
|
import dotenv from 'dotenv';
|
|
7
|
-
import { EnvFlag, selectEnvironment
|
|
7
|
+
import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
|
|
8
8
|
export default class Start extends Command {
|
|
9
9
|
static description = '开始部署';
|
|
10
10
|
static flags = {
|
|
@@ -25,7 +25,6 @@ export default class Start extends Command {
|
|
|
25
25
|
}
|
|
26
26
|
// 1. 选择环境
|
|
27
27
|
const env = await selectEnvironment(flags.env);
|
|
28
|
-
await setStoredEnv(env);
|
|
29
28
|
// 2. 获取 Compose 文件
|
|
30
29
|
const { files: composeFiles, isBuiltIn } = await getComposeFiles(env);
|
|
31
30
|
if (isBuiltIn) {
|
|
@@ -2,7 +2,7 @@ import { Command, Flags } from '@oclif/core';
|
|
|
2
2
|
import { runCompose, getComposeFiles } from '../../utils/docker.js';
|
|
3
3
|
import { logger } from '../../utils/logger.js';
|
|
4
4
|
import { confirm } from '@inquirer/prompts';
|
|
5
|
-
import { EnvFlag, selectEnvironment
|
|
5
|
+
import { EnvFlag, selectEnvironment } from '../../utils/selection.js';
|
|
6
6
|
export default class Stop extends Command {
|
|
7
7
|
static description = '停止服务';
|
|
8
8
|
static flags = {
|
|
@@ -32,13 +32,11 @@ export default class Stop extends Command {
|
|
|
32
32
|
}
|
|
33
33
|
logger.info('正在停止服务并删除数据卷...');
|
|
34
34
|
await runCompose(files, ['down', '-v'], isBuiltIn);
|
|
35
|
-
await clearStoredEnv();
|
|
36
35
|
logger.success('服务已停止,数据卷已删除');
|
|
37
36
|
}
|
|
38
37
|
else {
|
|
39
38
|
logger.info('正在停止服务...');
|
|
40
39
|
await runCompose(files, ['down'], isBuiltIn);
|
|
41
|
-
await clearStoredEnv();
|
|
42
40
|
logger.success('服务已停止');
|
|
43
41
|
}
|
|
44
42
|
}
|
package/dist/interactive.js
CHANGED
|
@@ -147,7 +147,7 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
147
147
|
console.log('\n操作已取消。');
|
|
148
148
|
continue;
|
|
149
149
|
}
|
|
150
|
-
console.error(error);
|
|
150
|
+
console.error(error.message);
|
|
151
151
|
}
|
|
152
152
|
console.log('\n命令执行完成。');
|
|
153
153
|
// 动态导入 input 以保持轻量
|
|
@@ -178,7 +178,7 @@ async function navigate(node, breadcrumbs, config) {
|
|
|
178
178
|
console.log('\n操作已取消。');
|
|
179
179
|
continue;
|
|
180
180
|
}
|
|
181
|
-
console.error(error);
|
|
181
|
+
console.error(error.message);
|
|
182
182
|
}
|
|
183
183
|
console.log('\n命令执行完成。');
|
|
184
184
|
// 动态导入 input 以保持轻量
|
package/dist/utils/docker.d.ts
CHANGED
|
@@ -1,8 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 获取 Docker Compose 文件路径配置
|
|
3
|
+
*
|
|
4
|
+
* 自动检测当前目录是否存在 `docker-compose.yml`。
|
|
5
|
+
* - 如果存在,则优先使用项目根目录的配置。
|
|
6
|
+
* - 如果不存在,则使用 CLI 内置的模板文件。
|
|
7
|
+
*
|
|
8
|
+
* @param env - 运行环境 ('production' | 'lowmem' | 'basic')
|
|
9
|
+
* @returns 对象包含文件路径数组和是否使用内置模板的标志
|
|
10
|
+
*/
|
|
1
11
|
export declare function getComposeFiles(env: string): Promise<{
|
|
2
12
|
files: string[];
|
|
3
13
|
isBuiltIn: boolean;
|
|
4
14
|
}>;
|
|
15
|
+
/**
|
|
16
|
+
* 检查 Docker 环境是否可用
|
|
17
|
+
*
|
|
18
|
+
* 验证 `docker` 和 `docker compose` 命令是否已安装并能正常响应。
|
|
19
|
+
* 如果检查失败,会抛出错误并提示用户安装。
|
|
20
|
+
*/
|
|
5
21
|
export declare function checkDocker(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* 执行 Docker Compose 管理命令 (Lifecycle Commands)
|
|
24
|
+
*
|
|
25
|
+
* 用于执行如 `up`, `down`, `logs`, `build` 等管理整个服务堆栈的命令。
|
|
26
|
+
* 如果使用的是内置模板,会自动注入必要的环境变量(如数据库初始化脚本路径)。
|
|
27
|
+
*
|
|
28
|
+
* @param files - Docker Compose 文件路径数组
|
|
29
|
+
* @param args - 传递给 docker compose 的参数列表 (e.g. `['up', '-d']`)
|
|
30
|
+
* @param isBuiltIn - 是否使用内置模板 (决定是否注入额外环境变量)
|
|
31
|
+
*/
|
|
6
32
|
export declare function runCompose(files: string[], args: string[], isBuiltIn?: boolean): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* 在容器内执行命令 (Exec)
|
|
35
|
+
*
|
|
36
|
+
* 相当于执行 `docker compose exec [service] [command]`。
|
|
37
|
+
* 用于在正在运行的容器中运行脚本或命令(例如数据库迁移、种子数据填充)。
|
|
38
|
+
*
|
|
39
|
+
* @param files - Docker Compose 文件路径数组
|
|
40
|
+
* @param service - 目标服务名称 (e.g. 'api', 'postgres')
|
|
41
|
+
* @param command - 要执行的命令数组 (e.g. `['npm', 'run', 'seed']`)
|
|
42
|
+
* @param isBuiltIn - 是否使用内置模板
|
|
43
|
+
*/
|
|
7
44
|
export declare function execCompose(files: string[], service: string, command: string[], isBuiltIn?: boolean): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* 等待服务健康检查通过
|
|
47
|
+
*
|
|
48
|
+
* 轮询关键服务 (PostgreSQL, Redis, API) 直到它们准备就绪或超时。
|
|
49
|
+
* - PostgreSQL: 使用 `pg_isready` 检查
|
|
50
|
+
* - Redis & API: 使用简单的延时等待 (后续可优化为端口/API探测)
|
|
51
|
+
*
|
|
52
|
+
* @param files - Docker Compose 文件路径数组
|
|
53
|
+
* @param isBuiltIn - 是否使用内置模板
|
|
54
|
+
*/
|
|
8
55
|
export declare function waitForHealth(files: string[], isBuiltIn?: boolean): Promise<void>;
|
package/dist/utils/docker.js
CHANGED
|
@@ -5,6 +5,16 @@ import { exists } from 'node:fs';
|
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { getTemplateDir, getTemplatePath } from './template.js';
|
|
7
7
|
const fileExists = promisify(exists);
|
|
8
|
+
/**
|
|
9
|
+
* 获取 Docker Compose 文件路径配置
|
|
10
|
+
*
|
|
11
|
+
* 自动检测当前目录是否存在 `docker-compose.yml`。
|
|
12
|
+
* - 如果存在,则优先使用项目根目录的配置。
|
|
13
|
+
* - 如果不存在,则使用 CLI 内置的模板文件。
|
|
14
|
+
*
|
|
15
|
+
* @param env - 运行环境 ('production' | 'lowmem' | 'basic')
|
|
16
|
+
* @returns 对象包含文件路径数组和是否使用内置模板的标志
|
|
17
|
+
*/
|
|
8
18
|
export async function getComposeFiles(env) {
|
|
9
19
|
const workDir = process.cwd();
|
|
10
20
|
let isBuiltIn = false;
|
|
@@ -26,6 +36,12 @@ export async function getComposeFiles(env) {
|
|
|
26
36
|
}
|
|
27
37
|
return { files, isBuiltIn };
|
|
28
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* 检查 Docker 环境是否可用
|
|
41
|
+
*
|
|
42
|
+
* 验证 `docker` 和 `docker compose` 命令是否已安装并能正常响应。
|
|
43
|
+
* 如果检查失败,会抛出错误并提示用户安装。
|
|
44
|
+
*/
|
|
29
45
|
export async function checkDocker() {
|
|
30
46
|
logger.info('正在检查 Docker 环境...');
|
|
31
47
|
try {
|
|
@@ -38,6 +54,16 @@ export async function checkDocker() {
|
|
|
38
54
|
throw error;
|
|
39
55
|
}
|
|
40
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* 执行 Docker Compose 管理命令 (Lifecycle Commands)
|
|
59
|
+
*
|
|
60
|
+
* 用于执行如 `up`, `down`, `logs`, `build` 等管理整个服务堆栈的命令。
|
|
61
|
+
* 如果使用的是内置模板,会自动注入必要的环境变量(如数据库初始化脚本路径)。
|
|
62
|
+
*
|
|
63
|
+
* @param files - Docker Compose 文件路径数组
|
|
64
|
+
* @param args - 传递给 docker compose 的参数列表 (e.g. `['up', '-d']`)
|
|
65
|
+
* @param isBuiltIn - 是否使用内置模板 (决定是否注入额外环境变量)
|
|
66
|
+
*/
|
|
41
67
|
export async function runCompose(files, args, isBuiltIn = false) {
|
|
42
68
|
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
43
69
|
if (isBuiltIn) {
|
|
@@ -48,16 +74,44 @@ export async function runCompose(files, args, isBuiltIn = false) {
|
|
|
48
74
|
}
|
|
49
75
|
composeArgs.push(...args);
|
|
50
76
|
// 使用 stdio: 'inherit' 实时显示输出
|
|
51
|
-
|
|
77
|
+
// 设置 COMPOSE_LOG_LEVEL=ERROR 抑制变量未设置的警告
|
|
78
|
+
await execa('docker', ['compose', ...composeArgs], {
|
|
79
|
+
stdio: 'inherit',
|
|
80
|
+
env: { ...process.env, COMPOSE_LOG_LEVEL: 'ERROR' }
|
|
81
|
+
});
|
|
52
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* 在容器内执行命令 (Exec)
|
|
85
|
+
*
|
|
86
|
+
* 相当于执行 `docker compose exec [service] [command]`。
|
|
87
|
+
* 用于在正在运行的容器中运行脚本或命令(例如数据库迁移、种子数据填充)。
|
|
88
|
+
*
|
|
89
|
+
* @param files - Docker Compose 文件路径数组
|
|
90
|
+
* @param service - 目标服务名称 (e.g. 'api', 'postgres')
|
|
91
|
+
* @param command - 要执行的命令数组 (e.g. `['npm', 'run', 'seed']`)
|
|
92
|
+
* @param isBuiltIn - 是否使用内置模板
|
|
93
|
+
*/
|
|
53
94
|
export async function execCompose(files, service, command, isBuiltIn = false) {
|
|
54
95
|
const composeArgs = files.flatMap(f => ['-f', f]);
|
|
55
96
|
if (isBuiltIn) {
|
|
56
97
|
composeArgs.push('--project-directory', process.cwd());
|
|
57
98
|
}
|
|
58
99
|
composeArgs.push('exec', service, ...command);
|
|
59
|
-
await execa('docker', ['compose', ...composeArgs], {
|
|
100
|
+
await execa('docker', ['compose', ...composeArgs], {
|
|
101
|
+
stdio: 'inherit',
|
|
102
|
+
env: { ...process.env, COMPOSE_LOG_LEVEL: 'ERROR' }
|
|
103
|
+
});
|
|
60
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* 等待服务健康检查通过
|
|
107
|
+
*
|
|
108
|
+
* 轮询关键服务 (PostgreSQL, Redis, API) 直到它们准备就绪或超时。
|
|
109
|
+
* - PostgreSQL: 使用 `pg_isready` 检查
|
|
110
|
+
* - Redis & API: 使用简单的延时等待 (后续可优化为端口/API探测)
|
|
111
|
+
*
|
|
112
|
+
* @param files - Docker Compose 文件路径数组
|
|
113
|
+
* @param isBuiltIn - 是否使用内置模板
|
|
114
|
+
*/
|
|
61
115
|
export async function waitForHealth(files, isBuiltIn = false) {
|
|
62
116
|
logger.info('正在等待服务就绪...');
|
|
63
117
|
// 等待 Postgres
|
package/dist/utils/env.js
CHANGED
|
@@ -12,39 +12,121 @@ export async function initEnv() {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
logger.info('正在创建 .env 文件...');
|
|
15
|
+
// 检查模板文件
|
|
16
|
+
let templateContent = '';
|
|
15
17
|
let sourceFile = '';
|
|
16
|
-
let isBuiltIn = false;
|
|
17
18
|
if (await fileExists('.env.docker.example')) {
|
|
18
19
|
sourceFile = '.env.docker.example';
|
|
20
|
+
templateContent = await fs.readFile(sourceFile, 'utf-8');
|
|
19
21
|
}
|
|
20
22
|
else {
|
|
21
23
|
// 使用内置模板
|
|
22
24
|
sourceFile = getTemplatePath('env');
|
|
23
|
-
|
|
25
|
+
templateContent = await fs.readFile(sourceFile, 'utf-8');
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const { input, password } = await import('@inquirer/prompts');
|
|
28
|
+
const crypto = await import('node:crypto');
|
|
29
|
+
function generateSecret(length = 32) {
|
|
30
|
+
return crypto.randomBytes(length).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, length);
|
|
31
|
+
}
|
|
32
|
+
console.log('\n请配置环境变量(按 Enter 使用默认值或自动生成):\n');
|
|
33
|
+
// 1. 收集用户输入
|
|
34
|
+
const postgresPassword = await password({
|
|
35
|
+
message: '设置 POSTGRES_PASSWORD (数据库密码) [空值自动生成]:',
|
|
36
|
+
mask: '*',
|
|
37
|
+
validate: (value) => true, // 允许为空以自动生成
|
|
38
|
+
}) || generateSecret();
|
|
39
|
+
const redisPassword = await password({
|
|
40
|
+
message: '设置 REDIS_PASSWORD (Redis 密码) [空值自动生成]:',
|
|
41
|
+
mask: '*',
|
|
42
|
+
validate: (value) => true,
|
|
43
|
+
}) || generateSecret();
|
|
44
|
+
const jwtSecret = await password({
|
|
45
|
+
message: '设置 JWT_SECRET (JWT 密钥) [空值自动生成]:',
|
|
46
|
+
mask: '*',
|
|
47
|
+
validate: (value) => true,
|
|
48
|
+
}) || generateSecret(64);
|
|
49
|
+
const webPort = await input({
|
|
50
|
+
message: '设置 WEB_PORT (前端端口):',
|
|
51
|
+
default: '3100'
|
|
52
|
+
});
|
|
53
|
+
const apiPort = await input({
|
|
54
|
+
message: '设置 API_PORT (后端端口):',
|
|
55
|
+
default: '7100'
|
|
56
|
+
});
|
|
57
|
+
const defaultApiUrl = `http://localhost:${apiPort}`;
|
|
58
|
+
const nextPublicApiUrl = await input({
|
|
59
|
+
message: '设置 NEXT_PUBLIC_API_URL (前端访问后端的地址):',
|
|
60
|
+
default: defaultApiUrl
|
|
61
|
+
});
|
|
62
|
+
const defaultAppUrl = `http://localhost:${webPort}`;
|
|
63
|
+
const nextPublicAppUrl = await input({
|
|
64
|
+
message: '设置 NEXT_PUBLIC_APP_URL (应用访问地址):',
|
|
65
|
+
default: defaultAppUrl
|
|
66
|
+
});
|
|
67
|
+
const corsOrigin = await input({
|
|
68
|
+
message: '设置 CORS_ORIGIN (允许跨域的域名):',
|
|
69
|
+
default: '*'
|
|
70
|
+
});
|
|
71
|
+
// 2. 显示配置预览
|
|
72
|
+
console.log('\n================ 配置预览 ================');
|
|
73
|
+
console.log(`POSTGRES_PASSWORD: ${postgresPassword.substring(0, 3)}******`);
|
|
74
|
+
console.log(`REDIS_PASSWORD: ${redisPassword.substring(0, 3)}******`);
|
|
75
|
+
console.log(`JWT_SECRET: ${jwtSecret.substring(0, 3)}******`);
|
|
76
|
+
console.log(`WEB_PORT: ${webPort}`);
|
|
77
|
+
console.log(`API_PORT: ${apiPort}`);
|
|
78
|
+
console.log(`NEXT_PUBLIC_API_URL: ${nextPublicApiUrl}`);
|
|
79
|
+
console.log(`NEXT_PUBLIC_APP_URL: ${nextPublicAppUrl}`);
|
|
80
|
+
console.log(`CORS_ORIGIN: ${corsOrigin}`);
|
|
81
|
+
console.log('==========================================\n');
|
|
82
|
+
// 3. 确认生成
|
|
83
|
+
const confirmed = await confirm({
|
|
84
|
+
message: '确认生成 .env 文件?',
|
|
85
|
+
default: true
|
|
86
|
+
});
|
|
87
|
+
if (!confirmed) {
|
|
88
|
+
logger.info('已取消生成 .env 文件。');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
// 4. 替换模板内容
|
|
92
|
+
// 为了保留注释,我们使用简单的字符串替换,而不是 dotenv 解析
|
|
93
|
+
// 注意:这里假设模板中的 key 遵循 standard env format (KEY=value)
|
|
94
|
+
let newEnv = templateContent;
|
|
95
|
+
const replacements = {
|
|
96
|
+
'POSTGRES_PASSWORD': postgresPassword,
|
|
97
|
+
'REDIS_PASSWORD': redisPassword,
|
|
98
|
+
'JWT_SECRET': jwtSecret,
|
|
99
|
+
'WEB_PORT': webPort,
|
|
100
|
+
'API_PORT': apiPort,
|
|
101
|
+
'NEXT_PUBLIC_API_URL': nextPublicApiUrl,
|
|
102
|
+
'NEXT_PUBLIC_APP_URL': nextPublicAppUrl,
|
|
103
|
+
'CORS_ORIGIN': corsOrigin,
|
|
104
|
+
// 如果 API_PORT 变了,模板里可能还需要联动修改 APP_URL 用于 OAuth 回调,这里简单处理
|
|
105
|
+
'APP_URL': nextPublicAppUrl
|
|
106
|
+
};
|
|
107
|
+
// 针对每个 Key 进行替换。
|
|
108
|
+
// 策略:查找 `KEY=...` 并替换为 `KEY=newValue`
|
|
109
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
110
|
+
// 匹配 KEY=任意非换行字符
|
|
111
|
+
const regex = new RegExp(`^${key}=.*`, 'gm');
|
|
112
|
+
if (regex.test(newEnv)) {
|
|
113
|
+
newEnv = newEnv.replace(regex, `${key}=${value}`);
|
|
29
114
|
}
|
|
30
115
|
else {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
logger.warning('请编辑 .env 文件并修改以下配置:');
|
|
34
|
-
logger.warning(' - POSTGRES_PASSWORD (数据库密码)');
|
|
35
|
-
logger.warning(' - REDIS_PASSWORD (Redis 密码)');
|
|
36
|
-
logger.warning(' - JWT_SECRET (JWT 密钥)');
|
|
37
|
-
const edit = await confirm({
|
|
38
|
-
message: '是否现在编辑 .env 文件?',
|
|
39
|
-
default: false
|
|
40
|
-
});
|
|
41
|
-
if (edit) {
|
|
42
|
-
logger.info('请手动编辑 .env 文件后再次运行命令。');
|
|
43
|
-
process.exit(0);
|
|
116
|
+
// 如果模板里没这个 key,追加到末尾(虽然理论上模板应该有)
|
|
117
|
+
newEnv += `\n${key}=${value}`;
|
|
44
118
|
}
|
|
45
119
|
}
|
|
120
|
+
// 5. 写入文件
|
|
121
|
+
await fs.writeFile('.env', newEnv, 'utf-8');
|
|
122
|
+
// isBuiltIn is not defined in the new logic, need to determine it
|
|
123
|
+
// based on whether sourceFile was .env.docker.example or a template path
|
|
124
|
+
const isBuiltIn = sourceFile === getTemplatePath('env');
|
|
125
|
+
if (isBuiltIn) {
|
|
126
|
+
logger.success('已使用内置模板并填充配置生成 .env');
|
|
127
|
+
}
|
|
46
128
|
else {
|
|
47
|
-
logger.
|
|
129
|
+
logger.success(`已从 ${sourceFile} 复制并填充配置生成 .env`);
|
|
48
130
|
}
|
|
49
131
|
}
|
|
50
132
|
export async function checkEnv(envType) {
|
package/dist/utils/selection.js
CHANGED
|
@@ -3,7 +3,7 @@ import { select } from '@inquirer/prompts';
|
|
|
3
3
|
import { logger } from './logger.js';
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
const ENV_MARKER_FILE = '.
|
|
6
|
+
const ENV_MARKER_FILE = '.env.lock';
|
|
7
7
|
export const EnvFlag = Flags.string({
|
|
8
8
|
char: 'e',
|
|
9
9
|
description: '部署环境 (production, lowmem, basic)',
|
|
@@ -64,5 +64,6 @@ export async function selectEnvironment(env, options = {}) {
|
|
|
64
64
|
throw error;
|
|
65
65
|
}
|
|
66
66
|
logger.info(`已选择环境: ${selected}`);
|
|
67
|
+
await setStoredEnv(selected);
|
|
67
68
|
return selected;
|
|
68
69
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"flags": {
|
|
8
8
|
"all": {
|
|
9
9
|
"char": "a",
|
|
10
|
-
"description": "清理所有 (
|
|
10
|
+
"description": "清理所有 (构建缓存、无用镜像、网络、环境锁定)",
|
|
11
11
|
"name": "all",
|
|
12
12
|
"allowNo": false,
|
|
13
13
|
"type": "boolean"
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
"allowNo": false,
|
|
25
25
|
"type": "boolean"
|
|
26
26
|
},
|
|
27
|
+
"env": {
|
|
28
|
+
"description": "清理环境锁定 (Environment Lock)",
|
|
29
|
+
"name": "env",
|
|
30
|
+
"allowNo": false,
|
|
31
|
+
"type": "boolean"
|
|
32
|
+
},
|
|
27
33
|
"force": {
|
|
28
34
|
"char": "f",
|
|
29
35
|
"description": "跳过确认提示",
|
|
@@ -718,5 +724,5 @@
|
|
|
718
724
|
]
|
|
719
725
|
}
|
|
720
726
|
},
|
|
721
|
-
"version": "0.0.
|
|
727
|
+
"version": "0.0.9"
|
|
722
728
|
}
|