ai-yuca 1.6.2 → 1.6.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/CHANGELOG.md +12 -0
- package/README.md +60 -0
- package/dist/bin/cli.js +29 -0
- package/dist/package.json +1 -1
- package/dist/src/deploy.js +18 -5
- package/dist/src/pm2Release.d.ts +2 -0
- package/dist/src/pm2Release.js +321 -0
- package/dist/src/types/deploy.d.ts +1 -0
- package/dist/src/types/index.d.ts +1 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/types/pm2Release.d.ts +31 -0
- package/dist/src/types/pm2Release.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.6.4] - 2026-06-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- 新增 `pm2-release` 命令,可读取项目 `package.json` 的 `ai-yuca.pm2Release` 配置,统一执行 PM2 远程 `setup` / `deploy`。
|
|
7
|
+
- `pm2-release` 支持运行时生成临时 PM2 ecosystem 配置,项目侧只需维护 `package.json` 的发布差异配置。
|
|
8
|
+
- `pm2-release` 支持 SSH alias 预检、SSH ControlMaster 复用、发布 ref 覆盖、PM2 命令覆盖,以及 test/prod/all 目标。
|
|
9
|
+
|
|
10
|
+
## [1.6.3] - 2026-06-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `deploy` 命令新增 `--replace-config-name-with-package` 参数,上传环境 config.json 时可将匹配项目的 `name` 替换为当前 `package.json.name`。
|
|
14
|
+
|
|
3
15
|
## [1.6.2] - 2026-05-20
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.md
CHANGED
|
@@ -39,6 +39,9 @@ ai-yuca upload-config
|
|
|
39
39
|
# 部署到测试环境
|
|
40
40
|
ai-yuca deploy -e test
|
|
41
41
|
|
|
42
|
+
# 通过 PM2 发布当前项目
|
|
43
|
+
ai-yuca pm2-release deploy prod
|
|
44
|
+
|
|
42
45
|
# 压缩图片
|
|
43
46
|
ai-yuca sharp -s ./assets -o ./assets-compressed -q 80
|
|
44
47
|
|
|
@@ -59,6 +62,7 @@ ai-yuca proxy-upload-image -f ./logo.png --raw
|
|
|
59
62
|
| `download` | 从 GCP 存储桶下载文件或目录 |
|
|
60
63
|
| `upload-config` | 基于 `vs.config.json` 批量上传 |
|
|
61
64
|
| `deploy` | 一键部署到指定环境 |
|
|
65
|
+
| `pm2-release` | 读取项目配置并通过 PM2 远程发布 |
|
|
62
66
|
| `init` | 交互生成或更新 `vs.config.json` |
|
|
63
67
|
| `sharp` | 使用 sharp 本地压缩 jpg/png/webp |
|
|
64
68
|
| `tinify` | 使用 Tinify API 压缩 jpg/png/webp |
|
|
@@ -177,6 +181,62 @@ ai-yuca deploy -e test -f
|
|
|
177
181
|
|
|
178
182
|
# 查看部署配置
|
|
179
183
|
ai-yuca deploy -e test --show-config
|
|
184
|
+
|
|
185
|
+
# 上传环境 config.json 时,将匹配项目的 name 替换为 package.json.name
|
|
186
|
+
ai-yuca deploy -e test --replace-config-name-with-package
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### PM2 远程发布
|
|
190
|
+
|
|
191
|
+
`pm2-release` 读取当前项目 `package.json` 的 `ai-yuca.pm2Release` 配置,用于把多个前端项目统一到一套 PM2 发布命令。命令会先检查本机 SSH alias 是否配置了有效 `HostName`,再运行时生成临时 PM2 ecosystem 配置并复用 SSH master 连接执行 PM2;项目内不需要保留 `ecosystem.config.cjs`。
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# 初始化 test 和 prod 远端目录
|
|
195
|
+
ai-yuca pm2-release setup
|
|
196
|
+
|
|
197
|
+
# 发布测试环境
|
|
198
|
+
ai-yuca pm2-release deploy test
|
|
199
|
+
|
|
200
|
+
# 发布生产环境
|
|
201
|
+
ai-yuca pm2-release deploy prod
|
|
202
|
+
|
|
203
|
+
# 指定分支或 ref
|
|
204
|
+
ai-yuca pm2-release deploy test --ref feat_xxx
|
|
205
|
+
ai-yuca pm2-release deploy prod --ref origin/main
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`package.json` 配置示例:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"scripts": {
|
|
213
|
+
"pm2:setup": "ai-yuca pm2-release setup",
|
|
214
|
+
"pm2:deploy": "ai-yuca pm2-release deploy prod",
|
|
215
|
+
"pm2:deploy:test": "ai-yuca pm2-release deploy test"
|
|
216
|
+
},
|
|
217
|
+
"ai-yuca": {
|
|
218
|
+
"pm2Release": {
|
|
219
|
+
"repo": "git@github.com:HEchooo/project.git",
|
|
220
|
+
"packageManager": "pnpm",
|
|
221
|
+
"pm2Command": "pnpm exec pm2",
|
|
222
|
+
"deployName": "server",
|
|
223
|
+
"environments": {
|
|
224
|
+
"test": {
|
|
225
|
+
"hosts": "test",
|
|
226
|
+
"path": "/home/echooo/fed/project-test",
|
|
227
|
+
"ref": "origin/main",
|
|
228
|
+
"script": "ai:test"
|
|
229
|
+
},
|
|
230
|
+
"prod": {
|
|
231
|
+
"hosts": "test",
|
|
232
|
+
"path": "/home/echooo/fed/project",
|
|
233
|
+
"ref": "origin/main",
|
|
234
|
+
"script": "ai"
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
180
240
|
```
|
|
181
241
|
|
|
182
242
|
## 资源处理
|
package/dist/bin/cli.js
CHANGED
|
@@ -59,6 +59,7 @@ const gKeys_1 = require("../src/gKeys");
|
|
|
59
59
|
const font_1 = require("../src/font");
|
|
60
60
|
const ico_1 = require("../src/ico");
|
|
61
61
|
const proxyUploadImage_1 = require("../src/proxyUploadImage");
|
|
62
|
+
const pm2Release_1 = require("../src/pm2Release");
|
|
62
63
|
const program = new commander_1.Command();
|
|
63
64
|
// 设置版本和描述
|
|
64
65
|
program
|
|
@@ -205,6 +206,7 @@ program
|
|
|
205
206
|
.option('--no-cache', '禁用文件缓存检查,强制上传所有文件')
|
|
206
207
|
.option('--cache-file <path>', '指定缓存文件路径,默认为 .cdn.cache.[branch].json')
|
|
207
208
|
.option('--show-config', '仅打印读取到的配置信息,不执行真实的部署操作')
|
|
209
|
+
.option('--replace-config-name-with-package', '上传环境 config.json 时,将匹配项目的 name 替换为当前 package.json 的项目名')
|
|
208
210
|
.option('-f, --force', '强制执行部署,跳过终端交互式确认提示')
|
|
209
211
|
.action(async (options) => {
|
|
210
212
|
try {
|
|
@@ -229,6 +231,33 @@ program
|
|
|
229
231
|
process.exit(1);
|
|
230
232
|
}
|
|
231
233
|
});
|
|
234
|
+
// 添加 PM2 远程发布命令
|
|
235
|
+
program
|
|
236
|
+
.command('pm2-release')
|
|
237
|
+
.description('通过当前项目 package.json 的 ai-yuca.pm2Release 配置执行 PM2 远程发布')
|
|
238
|
+
.argument('<action>', '操作类型:setup 或 deploy')
|
|
239
|
+
.argument('[target]', '发布目标:test、prod 或 all,setup 默认 all,deploy 默认 prod')
|
|
240
|
+
.option('-c, --config <path>', '指定 package.json 路径,默认向上查找当前项目 package.json')
|
|
241
|
+
.option('--ref <ref>', '指定发布分支或 ref,例如 main、origin/main、refs/heads/main')
|
|
242
|
+
.option('--env-name <name>', '覆盖注入到 PM2_DEPLOY_ENV 的环境名')
|
|
243
|
+
.option('--package-manager <name>', '覆盖 PM2 执行器推断,支持 npm 或 pnpm')
|
|
244
|
+
.option('--pm2-command <command>', '自定义 PM2 命令,例如 "pnpm exec pm2" 或 "npx pm2"')
|
|
245
|
+
.action((action, target, options) => {
|
|
246
|
+
try {
|
|
247
|
+
if (action !== 'setup' && action !== 'deploy') {
|
|
248
|
+
throw new Error('action 只能是 setup 或 deploy。');
|
|
249
|
+
}
|
|
250
|
+
const resolvedTarget = target || (action === 'setup' ? 'all' : 'prod');
|
|
251
|
+
if (resolvedTarget !== 'test' && resolvedTarget !== 'prod' && resolvedTarget !== 'all') {
|
|
252
|
+
throw new Error('target 只能是 test、prod 或 all。');
|
|
253
|
+
}
|
|
254
|
+
(0, pm2Release_1.pm2Release)(action, resolvedTarget, options);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
console.error(`PM2 发布错误: ${err instanceof Error ? err.message : String(err)}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
232
261
|
// 使用从types导入的UploadCommandOptions接口
|
|
233
262
|
// 添加upload命令
|
|
234
263
|
program
|
package/dist/package.json
CHANGED
package/dist/src/deploy.js
CHANGED
|
@@ -217,12 +217,18 @@ async function uploadToGcp(filePath, bucketName, destination, storageClient) {
|
|
|
217
217
|
/**
|
|
218
218
|
* 更新环境配置
|
|
219
219
|
*/
|
|
220
|
-
async function updateEnvironmentConfig(env, projectName, versionInfo, config, storageClient) {
|
|
220
|
+
async function updateEnvironmentConfig(env, projectName, versionInfo, config, storageClient, replaceConfigNameWithPackage = false) {
|
|
221
221
|
try {
|
|
222
222
|
// 获取当前环境配置
|
|
223
223
|
const { data: envConfig } = await getConfigFiles(env, config, storageClient);
|
|
224
|
-
|
|
224
|
+
if (!Array.isArray(envConfig.projects)) {
|
|
225
|
+
envConfig.projects = [];
|
|
226
|
+
}
|
|
227
|
+
// 开启替换时,允许通过s3Static定位旧项目并改写name。
|
|
225
228
|
let projectConfig = envConfig.projects.find((p) => p.name === projectName);
|
|
229
|
+
if (!projectConfig && replaceConfigNameWithPackage) {
|
|
230
|
+
projectConfig = envConfig.projects.find((p) => p.s3Static === config.upload.s3Static);
|
|
231
|
+
}
|
|
226
232
|
const newProjectConfig = {
|
|
227
233
|
baseUrl: versionInfo.baseUrl,
|
|
228
234
|
s3Static: config.upload.s3Static,
|
|
@@ -237,7 +243,7 @@ async function updateEnvironmentConfig(env, projectName, versionInfo, config, st
|
|
|
237
243
|
}
|
|
238
244
|
else {
|
|
239
245
|
envConfig.projects = envConfig.projects.map((p) => {
|
|
240
|
-
if (p.name === projectName) {
|
|
246
|
+
if (p === projectConfig || p.name === projectName) {
|
|
241
247
|
return newProjectConfig;
|
|
242
248
|
}
|
|
243
249
|
return p;
|
|
@@ -461,7 +467,14 @@ async function deployFiles(options) {
|
|
|
461
467
|
// 检查项目是否存在
|
|
462
468
|
const { data: envConfig } = await getConfigFiles(options.env, config, storageClient);
|
|
463
469
|
console.log('当前环境配置:', JSON.stringify(envConfig));
|
|
464
|
-
|
|
470
|
+
let existingProject = envConfig.projects.find((p) => p.name === projectInfo.name);
|
|
471
|
+
if (!existingProject && options.replaceConfigNameWithPackage) {
|
|
472
|
+
// 参数开启时,用s3Static识别需要改名的旧项目配置。
|
|
473
|
+
existingProject = envConfig.projects.find((p) => p.s3Static === config.upload.s3Static);
|
|
474
|
+
if (existingProject && existingProject.name !== projectInfo.name) {
|
|
475
|
+
console.log(`🔁 环境配置项目名将由 "${existingProject.name}" 替换为 "${projectInfo.name}"`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
465
478
|
if (!existingProject) {
|
|
466
479
|
if (options.force) {
|
|
467
480
|
console.log(`🆕 自动创建项目 "${projectInfo.name}" 在环境 "${options.env}" 中...`);
|
|
@@ -489,7 +502,7 @@ async function deployFiles(options) {
|
|
|
489
502
|
timestamp: Date.now(),
|
|
490
503
|
links: obj.links
|
|
491
504
|
};
|
|
492
|
-
const projectConfigUpdate = await updateEnvironmentConfig(options.env, projectInfo.name, versionInfo, config, storageClient);
|
|
505
|
+
const projectConfigUpdate = await updateEnvironmentConfig(options.env, projectInfo.name, versionInfo, config, storageClient, Boolean(options.replaceConfigNameWithPackage));
|
|
493
506
|
console.log('项目配置信息上传结果:', JSON.stringify(projectConfigUpdate));
|
|
494
507
|
if (!projectConfigUpdate) {
|
|
495
508
|
throw new Error('更新项目配置失败,发布失败!');
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.pm2Release = pm2Release;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
function readJson(filePath) {
|
|
42
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
43
|
+
}
|
|
44
|
+
function findPackageJson(startDir) {
|
|
45
|
+
let currentDir = path.resolve(startDir);
|
|
46
|
+
while (true) {
|
|
47
|
+
const packagePath = path.join(currentDir, 'package.json');
|
|
48
|
+
if (fs.existsSync(packagePath)) {
|
|
49
|
+
return packagePath;
|
|
50
|
+
}
|
|
51
|
+
const parentDir = path.dirname(currentDir);
|
|
52
|
+
if (parentDir === currentDir) {
|
|
53
|
+
throw new Error('未找到 package.json,请在项目根目录或子目录中执行。');
|
|
54
|
+
}
|
|
55
|
+
currentDir = parentDir;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function normalizeRef(ref) {
|
|
59
|
+
if (ref.startsWith('origin/') || ref.startsWith('refs/')) {
|
|
60
|
+
return ref;
|
|
61
|
+
}
|
|
62
|
+
return `origin/${ref}`;
|
|
63
|
+
}
|
|
64
|
+
function normalizeHosts(hosts) {
|
|
65
|
+
const hostList = Array.isArray(hosts) ? hosts : (hosts || 'test').split(',');
|
|
66
|
+
return hostList.map((host) => host.trim()).filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
function getRepoUrl(packageRoot, config) {
|
|
69
|
+
if (config.repo) {
|
|
70
|
+
return config.repo;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return (0, child_process_1.execFileSync)('git', ['remote', 'get-url', 'origin'], {
|
|
74
|
+
cwd: packageRoot,
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
77
|
+
}).trim();
|
|
78
|
+
}
|
|
79
|
+
catch (_a) {
|
|
80
|
+
throw new Error('未检测到 git origin,请在 package.json 的 ai-yuca.pm2Release.repo 中配置仓库地址。');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function buildSshControlPath(config, projectName, packageRoot) {
|
|
84
|
+
if (config.sshControlPath) {
|
|
85
|
+
return config.sshControlPath;
|
|
86
|
+
}
|
|
87
|
+
const controlDir = config.sshControlDir || process.env.PM2_SSH_CONTROL_DIR || '/tmp';
|
|
88
|
+
fs.mkdirSync(controlDir, { recursive: true });
|
|
89
|
+
if (controlDir !== '/tmp') {
|
|
90
|
+
fs.chmodSync(controlDir, 0o700);
|
|
91
|
+
}
|
|
92
|
+
// ControlPath 使用项目名隔离,避免多个项目共享 master 连接时互相覆盖。
|
|
93
|
+
const safeName = projectName || path.basename(packageRoot);
|
|
94
|
+
return path.join(controlDir, `${safeName}-pm2-%C`);
|
|
95
|
+
}
|
|
96
|
+
function ensureSshAliases(hosts) {
|
|
97
|
+
var _a;
|
|
98
|
+
for (const host of hosts) {
|
|
99
|
+
let sshConfig = '';
|
|
100
|
+
try {
|
|
101
|
+
sshConfig = (0, child_process_1.execFileSync)('ssh', ['-G', host], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
102
|
+
}
|
|
103
|
+
catch (_b) {
|
|
104
|
+
throw new Error(`未找到 ssh 配置别名: ${host},请先在 ~/.ssh/config 中配置 Host ${host}。`);
|
|
105
|
+
}
|
|
106
|
+
const hostname = (_a = sshConfig
|
|
107
|
+
.split('\n')
|
|
108
|
+
.map((line) => line.trim().split(/\s+/))
|
|
109
|
+
.find(([key]) => key === 'hostname')) === null || _a === void 0 ? void 0 : _a[1];
|
|
110
|
+
if (!hostname || hostname === host) {
|
|
111
|
+
throw new Error(`ssh ${host} 未配置有效 HostName,请先在 ~/.ssh/config 中配置 Host ${host}。`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function warmSshMaster(hosts, controlPath) {
|
|
116
|
+
for (const host of hosts) {
|
|
117
|
+
const sharedOptions = [
|
|
118
|
+
'-o',
|
|
119
|
+
'ControlMaster=auto',
|
|
120
|
+
'-o',
|
|
121
|
+
'ControlPersist=10m',
|
|
122
|
+
'-o',
|
|
123
|
+
`ControlPath=${controlPath}`,
|
|
124
|
+
];
|
|
125
|
+
try {
|
|
126
|
+
(0, child_process_1.execFileSync)('ssh', [...sharedOptions, '-O', 'check', host], { stdio: 'ignore' });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
catch (_a) {
|
|
130
|
+
// check 失败时建立 master 连接,后续 PM2 可复用登录态。
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
(0, child_process_1.execFileSync)('ssh', ['-MNf', '-o', 'ControlMaster=yes', '-o', 'ControlPersist=10m', '-o', `ControlPath=${controlPath}`, host], {
|
|
134
|
+
stdio: 'inherit',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (_b) {
|
|
138
|
+
console.warn(`SSH master 连接预热失败,继续由 PM2 直接连接: ${host}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function shellQuote(value) {
|
|
143
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
144
|
+
}
|
|
145
|
+
function hasRemoteSource(hosts, controlPath, deployPath) {
|
|
146
|
+
const sourcePath = path.posix.join(deployPath, 'source', '.git');
|
|
147
|
+
for (const host of hosts) {
|
|
148
|
+
try {
|
|
149
|
+
const output = (0, child_process_1.execFileSync)('ssh', [
|
|
150
|
+
'-o',
|
|
151
|
+
'ControlMaster=auto',
|
|
152
|
+
'-o',
|
|
153
|
+
'ControlPersist=10m',
|
|
154
|
+
'-o',
|
|
155
|
+
`ControlPath=${controlPath}`,
|
|
156
|
+
host,
|
|
157
|
+
'sh',
|
|
158
|
+
'-lc',
|
|
159
|
+
`test -d ${shellQuote(sourcePath)} && echo exists || echo missing`,
|
|
160
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
161
|
+
if (!output.split(/\s+/).includes('exists')) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (_a) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
function getPm2Command(config, options) {
|
|
172
|
+
const command = options.pm2Command || config.pm2Command;
|
|
173
|
+
if (command) {
|
|
174
|
+
const [bin, ...args] = command.split(/\s+/).filter(Boolean);
|
|
175
|
+
return [bin, args];
|
|
176
|
+
}
|
|
177
|
+
const packageManager = options.packageManager || config.packageManager || 'npm';
|
|
178
|
+
if (packageManager === 'pnpm') {
|
|
179
|
+
return ['pnpm', ['exec', 'pm2']];
|
|
180
|
+
}
|
|
181
|
+
return ['npx', ['pm2']];
|
|
182
|
+
}
|
|
183
|
+
function getInstallCommand(config) {
|
|
184
|
+
if (config.installCommand) {
|
|
185
|
+
return config.installCommand;
|
|
186
|
+
}
|
|
187
|
+
return config.packageManager === 'pnpm' ? 'pnpm install --frozen-lockfile' : 'npm ci';
|
|
188
|
+
}
|
|
189
|
+
function getRunCommand(config) {
|
|
190
|
+
if (config.runCommand) {
|
|
191
|
+
return config.runCommand;
|
|
192
|
+
}
|
|
193
|
+
return config.packageManager === 'pnpm' ? 'pnpm run' : 'npm run';
|
|
194
|
+
}
|
|
195
|
+
function escapeJsString(value) {
|
|
196
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
197
|
+
}
|
|
198
|
+
function createTempEcosystem(packageRoot, packageJson, config, controlPath) {
|
|
199
|
+
const projectName = packageJson.name || path.basename(packageRoot);
|
|
200
|
+
const repo = getRepoUrl(packageRoot, config);
|
|
201
|
+
const prodConfig = getEnvConfig(config, 'prod');
|
|
202
|
+
const prodHosts = normalizeHosts(prodConfig.hosts).join(',');
|
|
203
|
+
const prodRef = normalizeRef(prodConfig.ref || 'origin/main');
|
|
204
|
+
const prodScript = prodConfig.script || 'ai';
|
|
205
|
+
const installCommand = getInstallCommand(config);
|
|
206
|
+
const runCommand = getRunCommand(config);
|
|
207
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-yuca-pm2-'));
|
|
208
|
+
const ecosystemPath = path.join(tempDir, 'ecosystem.config.cjs');
|
|
209
|
+
const content = `const REPO = '${escapeJsString(repo)}';
|
|
210
|
+
const DEPLOY_HOST = (process.env.PM2_DEPLOY_HOST || '${escapeJsString(prodHosts || 'test')}')
|
|
211
|
+
.split(',')
|
|
212
|
+
.map((host) => host.trim())
|
|
213
|
+
.filter(Boolean);
|
|
214
|
+
const DEPLOY_PATH = process.env.PM2_DEPLOY_PATH || '${escapeJsString(prodConfig.path)}';
|
|
215
|
+
const DEPLOY_REF = process.env.PM2_DEPLOY_REF || '${escapeJsString(prodRef)}';
|
|
216
|
+
const DEPLOY_ENV = process.env.PM2_DEPLOY_ENV || 'prod';
|
|
217
|
+
const DEPLOY_SCRIPT = process.env.PM2_DEPLOY_SCRIPT || '${escapeJsString(prodScript)}';
|
|
218
|
+
const SSH_CONTROL_PATH =
|
|
219
|
+
process.env.PM2_SSH_CONTROL_PATH || '${escapeJsString(controlPath)}';
|
|
220
|
+
const SSH_OPTIONS = [
|
|
221
|
+
'ControlMaster=auto',
|
|
222
|
+
'ControlPersist=10m',
|
|
223
|
+
\`ControlPath=\${SSH_CONTROL_PATH}\`,
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
const withNvm = (command) =>
|
|
227
|
+
[
|
|
228
|
+
'. ~/.nvm/nvm.sh',
|
|
229
|
+
'corepack enable',
|
|
230
|
+
'export PATH=$(pnpm config get global-bin-dir 2>/dev/null || npm config get prefix)/bin:$HOME/.npm-global/bin:$HOME/.local/bin:$PATH',
|
|
231
|
+
\`echo pm2-release-env=\${DEPLOY_ENV} script=\${DEPLOY_SCRIPT} path=$PWD\`,
|
|
232
|
+
'echo node=$(command -v node) pnpm=$(command -v pnpm || true) npm=$(command -v npm) ai-yuca=$(command -v ai-yuca || true)',
|
|
233
|
+
command,
|
|
234
|
+
].join(' && ');
|
|
235
|
+
|
|
236
|
+
module.exports = {
|
|
237
|
+
deploy: {
|
|
238
|
+
server: {
|
|
239
|
+
host: DEPLOY_HOST.length === 1 ? DEPLOY_HOST[0] : DEPLOY_HOST,
|
|
240
|
+
ref: DEPLOY_REF,
|
|
241
|
+
repo: REPO,
|
|
242
|
+
path: DEPLOY_PATH,
|
|
243
|
+
// 发布目标由 ai-yuca pm2-release 注入,避免项目维护 ecosystem.config.cjs。
|
|
244
|
+
ssh_options: SSH_OPTIONS,
|
|
245
|
+
'post-deploy': withNvm(
|
|
246
|
+
\`${escapeJsString(installCommand)} && ${escapeJsString(runCommand)} \${DEPLOY_SCRIPT}\`,
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
`;
|
|
252
|
+
fs.writeFileSync(ecosystemPath, content);
|
|
253
|
+
return ecosystemPath;
|
|
254
|
+
}
|
|
255
|
+
function getEnvConfig(config, target) {
|
|
256
|
+
var _a;
|
|
257
|
+
const envConfig = (_a = config.environments) === null || _a === void 0 ? void 0 : _a[target];
|
|
258
|
+
if (!(envConfig === null || envConfig === void 0 ? void 0 : envConfig.path)) {
|
|
259
|
+
throw new Error(`pm2Release.environments.${target}.path 不能为空。`);
|
|
260
|
+
}
|
|
261
|
+
return envConfig;
|
|
262
|
+
}
|
|
263
|
+
function runPm2(packageRoot, packageJson, config, action, target, options) {
|
|
264
|
+
const envConfig = getEnvConfig(config, target);
|
|
265
|
+
const hosts = normalizeHosts(envConfig.hosts);
|
|
266
|
+
const controlPath = buildSshControlPath(config, packageJson.name || '', packageRoot);
|
|
267
|
+
const deployName = config.deployName || 'server';
|
|
268
|
+
const ref = normalizeRef(options.ref || envConfig.ref || 'origin/main');
|
|
269
|
+
const script = envConfig.script || (target === 'test' ? 'ai:test' : 'ai');
|
|
270
|
+
const [pm2Bin, pm2Args] = getPm2Command(config, options);
|
|
271
|
+
const ecosystem = createTempEcosystem(packageRoot, packageJson, config, controlPath);
|
|
272
|
+
ensureSshAliases(hosts);
|
|
273
|
+
warmSshMaster(hosts, controlPath);
|
|
274
|
+
const env = {
|
|
275
|
+
...process.env,
|
|
276
|
+
PM2_DEPLOY_HOST: hosts.join(','),
|
|
277
|
+
PM2_DEPLOY_PATH: envConfig.path,
|
|
278
|
+
PM2_DEPLOY_REF: action === 'setup' ? normalizeRef(envConfig.setupRef || ref) : ref,
|
|
279
|
+
PM2_DEPLOY_ENV: options.envName || target,
|
|
280
|
+
PM2_DEPLOY_SCRIPT: action === 'setup' ? envConfig.setupScript || script : script,
|
|
281
|
+
PM2_SSH_CONTROL_PATH: controlPath,
|
|
282
|
+
};
|
|
283
|
+
console.log(`PM2 ${action} target: env=${target} host=${hosts.join(',')} path=${envConfig.path} script=${env.PM2_DEPLOY_SCRIPT} ref=${env.PM2_DEPLOY_REF}`);
|
|
284
|
+
if (action === 'setup' && hasRemoteSource(hosts, controlPath, envConfig.path)) {
|
|
285
|
+
console.log(`PM2 setup skipped: ${envConfig.path}/source already exists on ${hosts.join(',')}`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const args = [...pm2Args, 'deploy', ecosystem, deployName];
|
|
290
|
+
if (action === 'setup') {
|
|
291
|
+
args.push('setup');
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
args.push('--force');
|
|
295
|
+
}
|
|
296
|
+
(0, child_process_1.execFileSync)(pm2Bin, args, {
|
|
297
|
+
cwd: packageRoot,
|
|
298
|
+
env,
|
|
299
|
+
stdio: 'inherit',
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
fs.rmSync(path.dirname(ecosystem), { recursive: true, force: true });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function pm2Release(action, target, options = {}) {
|
|
307
|
+
var _a;
|
|
308
|
+
const packagePath = options.config
|
|
309
|
+
? path.resolve(options.config)
|
|
310
|
+
: findPackageJson(process.cwd());
|
|
311
|
+
const packageRoot = path.dirname(packagePath);
|
|
312
|
+
const packageJson = readJson(packagePath);
|
|
313
|
+
const config = (_a = packageJson['ai-yuca']) === null || _a === void 0 ? void 0 : _a.pm2Release;
|
|
314
|
+
if (!config) {
|
|
315
|
+
throw new Error('package.json 缺少 ai-yuca.pm2Release 配置。');
|
|
316
|
+
}
|
|
317
|
+
const targets = target === 'all' ? ['test', 'prod'] : [target];
|
|
318
|
+
for (const releaseTarget of targets) {
|
|
319
|
+
runPm2(packageRoot, packageJson, config, action, releaseTarget, options);
|
|
320
|
+
}
|
|
321
|
+
}
|
package/dist/src/types/index.js
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type Pm2ReleaseAction = 'setup' | 'deploy';
|
|
2
|
+
export type Pm2ReleaseTarget = 'test' | 'prod';
|
|
3
|
+
export interface Pm2ReleaseCommandOptions {
|
|
4
|
+
config?: string;
|
|
5
|
+
packageManager?: string;
|
|
6
|
+
pm2Command?: string;
|
|
7
|
+
envName?: string;
|
|
8
|
+
ref?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Pm2ReleaseEnvConfig {
|
|
11
|
+
hosts?: string | string[];
|
|
12
|
+
path: string;
|
|
13
|
+
ref?: string;
|
|
14
|
+
script?: string;
|
|
15
|
+
setupRef?: string;
|
|
16
|
+
setupScript?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface Pm2ReleaseConfig {
|
|
19
|
+
repo?: string;
|
|
20
|
+
deployName?: string;
|
|
21
|
+
sshControlDir?: string;
|
|
22
|
+
sshControlPath?: string;
|
|
23
|
+
packageManager?: string;
|
|
24
|
+
pm2Command?: string;
|
|
25
|
+
installCommand?: string;
|
|
26
|
+
runCommand?: string;
|
|
27
|
+
environments: {
|
|
28
|
+
test: Pm2ReleaseEnvConfig;
|
|
29
|
+
prod: Pm2ReleaseEnvConfig;
|
|
30
|
+
};
|
|
31
|
+
}
|