ai-yuca 1.6.3 → 1.6.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/CHANGELOG.md +8 -0
- package/README.md +60 -0
- package/dist/bin/cli.js +28 -0
- package/dist/package.json +1 -1
- package/dist/src/pm2Release.d.ts +2 -0
- package/dist/src/pm2Release.js +359 -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 +32 -0
- package/dist/src/types/pm2Release.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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` 在 pnpm 项目缺少 `pnpm-lock.yaml` 时会发布前询问;远端分支没有 lockfile 时自动降级为 `pnpm install --no-frozen-lockfile`。
|
|
9
|
+
- `pm2-release` 支持 SSH alias 预检、SSH ControlMaster 复用、发布 ref 覆盖、PM2 命令覆盖,以及 test/prod/all 目标。
|
|
10
|
+
|
|
3
11
|
## [1.6.3] - 2026-06-05
|
|
4
12
|
|
|
5
13
|
### 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 |
|
|
@@ -182,6 +186,62 @@ ai-yuca deploy -e test --show-config
|
|
|
182
186
|
ai-yuca deploy -e test --replace-config-name-with-package
|
|
183
187
|
```
|
|
184
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
|
+
"allowNoPnpmLockfile": false,
|
|
223
|
+
"deployName": "server",
|
|
224
|
+
"environments": {
|
|
225
|
+
"test": {
|
|
226
|
+
"hosts": "test",
|
|
227
|
+
"path": "/home/echooo/fed/project-test",
|
|
228
|
+
"ref": "origin/main",
|
|
229
|
+
"script": "ai:test"
|
|
230
|
+
},
|
|
231
|
+
"prod": {
|
|
232
|
+
"hosts": "test",
|
|
233
|
+
"path": "/home/echooo/fed/project",
|
|
234
|
+
"ref": "origin/main",
|
|
235
|
+
"script": "ai"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
pnpm 项目默认优先使用 `pnpm install --frozen-lockfile`。如果本地没有 `pnpm-lock.yaml`,`pm2-release deploy` 会先询问是否继续;如果远端目标分支没有 lockfile,会自动降级为 `pnpm install --no-frozen-lockfile`。如果项目明确不提交 lockfile,可设置 `"allowNoPnpmLockfile": true`。
|
|
244
|
+
|
|
185
245
|
## 资源处理
|
|
186
246
|
|
|
187
247
|
### sharp 本地压缩
|
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
|
|
@@ -230,6 +231,33 @@ program
|
|
|
230
231
|
process.exit(1);
|
|
231
232
|
}
|
|
232
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(async (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
|
+
await (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
|
+
});
|
|
233
261
|
// 使用从types导入的UploadCommandOptions接口
|
|
234
262
|
// 添加upload命令
|
|
235
263
|
program
|
package/dist/package.json
CHANGED
|
@@ -0,0 +1,359 @@
|
|
|
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
|
+
const readline = __importStar(require("readline"));
|
|
42
|
+
function readJson(filePath) {
|
|
43
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
44
|
+
}
|
|
45
|
+
function findPackageJson(startDir) {
|
|
46
|
+
let currentDir = path.resolve(startDir);
|
|
47
|
+
while (true) {
|
|
48
|
+
const packagePath = path.join(currentDir, 'package.json');
|
|
49
|
+
if (fs.existsSync(packagePath)) {
|
|
50
|
+
return packagePath;
|
|
51
|
+
}
|
|
52
|
+
const parentDir = path.dirname(currentDir);
|
|
53
|
+
if (parentDir === currentDir) {
|
|
54
|
+
throw new Error('未找到 package.json,请在项目根目录或子目录中执行。');
|
|
55
|
+
}
|
|
56
|
+
currentDir = parentDir;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function normalizeRef(ref) {
|
|
60
|
+
if (ref.startsWith('origin/') || ref.startsWith('refs/')) {
|
|
61
|
+
return ref;
|
|
62
|
+
}
|
|
63
|
+
return `origin/${ref}`;
|
|
64
|
+
}
|
|
65
|
+
function normalizeHosts(hosts) {
|
|
66
|
+
const hostList = Array.isArray(hosts) ? hosts : (hosts || 'test').split(',');
|
|
67
|
+
return hostList.map((host) => host.trim()).filter(Boolean);
|
|
68
|
+
}
|
|
69
|
+
function getRepoUrl(packageRoot, config) {
|
|
70
|
+
if (config.repo) {
|
|
71
|
+
return config.repo;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return (0, child_process_1.execFileSync)('git', ['remote', 'get-url', 'origin'], {
|
|
75
|
+
cwd: packageRoot,
|
|
76
|
+
encoding: 'utf8',
|
|
77
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
78
|
+
}).trim();
|
|
79
|
+
}
|
|
80
|
+
catch (_a) {
|
|
81
|
+
throw new Error('未检测到 git origin,请在 package.json 的 ai-yuca.pm2Release.repo 中配置仓库地址。');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function buildSshControlPath(config, projectName, packageRoot) {
|
|
85
|
+
if (config.sshControlPath) {
|
|
86
|
+
return config.sshControlPath;
|
|
87
|
+
}
|
|
88
|
+
const controlDir = config.sshControlDir || process.env.PM2_SSH_CONTROL_DIR || '/tmp';
|
|
89
|
+
fs.mkdirSync(controlDir, { recursive: true });
|
|
90
|
+
if (controlDir !== '/tmp') {
|
|
91
|
+
fs.chmodSync(controlDir, 0o700);
|
|
92
|
+
}
|
|
93
|
+
// ControlPath 使用项目名隔离,避免多个项目共享 master 连接时互相覆盖。
|
|
94
|
+
const safeName = projectName || path.basename(packageRoot);
|
|
95
|
+
return path.join(controlDir, `${safeName}-pm2-%C`);
|
|
96
|
+
}
|
|
97
|
+
function ensureSshAliases(hosts) {
|
|
98
|
+
var _a;
|
|
99
|
+
for (const host of hosts) {
|
|
100
|
+
let sshConfig = '';
|
|
101
|
+
try {
|
|
102
|
+
sshConfig = (0, child_process_1.execFileSync)('ssh', ['-G', host], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
103
|
+
}
|
|
104
|
+
catch (_b) {
|
|
105
|
+
throw new Error(`未找到 ssh 配置别名: ${host},请先在 ~/.ssh/config 中配置 Host ${host}。`);
|
|
106
|
+
}
|
|
107
|
+
const hostname = (_a = sshConfig
|
|
108
|
+
.split('\n')
|
|
109
|
+
.map((line) => line.trim().split(/\s+/))
|
|
110
|
+
.find(([key]) => key === 'hostname')) === null || _a === void 0 ? void 0 : _a[1];
|
|
111
|
+
if (!hostname || hostname === host) {
|
|
112
|
+
throw new Error(`ssh ${host} 未配置有效 HostName,请先在 ~/.ssh/config 中配置 Host ${host}。`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function warmSshMaster(hosts, controlPath) {
|
|
117
|
+
for (const host of hosts) {
|
|
118
|
+
const sharedOptions = [
|
|
119
|
+
'-o',
|
|
120
|
+
'ControlMaster=auto',
|
|
121
|
+
'-o',
|
|
122
|
+
'ControlPersist=10m',
|
|
123
|
+
'-o',
|
|
124
|
+
`ControlPath=${controlPath}`,
|
|
125
|
+
];
|
|
126
|
+
try {
|
|
127
|
+
(0, child_process_1.execFileSync)('ssh', [...sharedOptions, '-O', 'check', host], { stdio: 'ignore' });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
catch (_a) {
|
|
131
|
+
// check 失败时建立 master 连接,后续 PM2 可复用登录态。
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
(0, child_process_1.execFileSync)('ssh', ['-MNf', '-o', 'ControlMaster=yes', '-o', 'ControlPersist=10m', '-o', `ControlPath=${controlPath}`, host], {
|
|
135
|
+
stdio: 'inherit',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (_b) {
|
|
139
|
+
console.warn(`SSH master 连接预热失败,继续由 PM2 直接连接: ${host}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function shellQuote(value) {
|
|
144
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
145
|
+
}
|
|
146
|
+
function hasRemoteSource(hosts, controlPath, deployPath) {
|
|
147
|
+
const sourcePath = path.posix.join(deployPath, 'source', '.git');
|
|
148
|
+
for (const host of hosts) {
|
|
149
|
+
try {
|
|
150
|
+
const output = (0, child_process_1.execFileSync)('ssh', [
|
|
151
|
+
'-o',
|
|
152
|
+
'ControlMaster=auto',
|
|
153
|
+
'-o',
|
|
154
|
+
'ControlPersist=10m',
|
|
155
|
+
'-o',
|
|
156
|
+
`ControlPath=${controlPath}`,
|
|
157
|
+
host,
|
|
158
|
+
'sh',
|
|
159
|
+
'-lc',
|
|
160
|
+
`test -d ${shellQuote(sourcePath)} && echo exists || echo missing`,
|
|
161
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
162
|
+
if (!output.split(/\s+/).includes('exists')) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (_a) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
function getPm2Command(config, options) {
|
|
173
|
+
const command = options.pm2Command || config.pm2Command;
|
|
174
|
+
if (command) {
|
|
175
|
+
const [bin, ...args] = command.split(/\s+/).filter(Boolean);
|
|
176
|
+
return [bin, args];
|
|
177
|
+
}
|
|
178
|
+
const packageManager = options.packageManager || config.packageManager || 'npm';
|
|
179
|
+
if (packageManager === 'pnpm') {
|
|
180
|
+
return ['pnpm', ['exec', 'pm2']];
|
|
181
|
+
}
|
|
182
|
+
return ['npx', ['pm2']];
|
|
183
|
+
}
|
|
184
|
+
function getInstallCommand(config) {
|
|
185
|
+
if (config.installCommand) {
|
|
186
|
+
return config.installCommand;
|
|
187
|
+
}
|
|
188
|
+
if (config.packageManager === 'pnpm') {
|
|
189
|
+
return 'if test -f pnpm-lock.yaml; then pnpm install --frozen-lockfile; else echo pnpm_lock_missing_use_no_frozen_lockfile; pnpm install --no-frozen-lockfile; fi';
|
|
190
|
+
}
|
|
191
|
+
return 'npm ci';
|
|
192
|
+
}
|
|
193
|
+
function getRunCommand(config) {
|
|
194
|
+
if (config.runCommand) {
|
|
195
|
+
return config.runCommand;
|
|
196
|
+
}
|
|
197
|
+
return config.packageManager === 'pnpm' ? 'pnpm run' : 'npm run';
|
|
198
|
+
}
|
|
199
|
+
function escapeJsString(value) {
|
|
200
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
201
|
+
}
|
|
202
|
+
function createTempEcosystem(packageRoot, packageJson, config, controlPath) {
|
|
203
|
+
const projectName = packageJson.name || path.basename(packageRoot);
|
|
204
|
+
const repo = getRepoUrl(packageRoot, config);
|
|
205
|
+
const prodConfig = getEnvConfig(config, 'prod');
|
|
206
|
+
const prodHosts = normalizeHosts(prodConfig.hosts).join(',');
|
|
207
|
+
const prodRef = normalizeRef(prodConfig.ref || 'origin/main');
|
|
208
|
+
const prodScript = prodConfig.script || 'ai';
|
|
209
|
+
const installCommand = getInstallCommand(config);
|
|
210
|
+
const runCommand = getRunCommand(config);
|
|
211
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-yuca-pm2-'));
|
|
212
|
+
const ecosystemPath = path.join(tempDir, 'ecosystem.config.cjs');
|
|
213
|
+
const content = `const REPO = '${escapeJsString(repo)}';
|
|
214
|
+
const DEPLOY_HOST = (process.env.PM2_DEPLOY_HOST || '${escapeJsString(prodHosts || 'test')}')
|
|
215
|
+
.split(',')
|
|
216
|
+
.map((host) => host.trim())
|
|
217
|
+
.filter(Boolean);
|
|
218
|
+
const DEPLOY_PATH = process.env.PM2_DEPLOY_PATH || '${escapeJsString(prodConfig.path)}';
|
|
219
|
+
const DEPLOY_REF = process.env.PM2_DEPLOY_REF || '${escapeJsString(prodRef)}';
|
|
220
|
+
const DEPLOY_ENV = process.env.PM2_DEPLOY_ENV || 'prod';
|
|
221
|
+
const DEPLOY_SCRIPT = process.env.PM2_DEPLOY_SCRIPT || '${escapeJsString(prodScript)}';
|
|
222
|
+
const SSH_CONTROL_PATH =
|
|
223
|
+
process.env.PM2_SSH_CONTROL_PATH || '${escapeJsString(controlPath)}';
|
|
224
|
+
const SSH_OPTIONS = [
|
|
225
|
+
'ControlMaster=auto',
|
|
226
|
+
'ControlPersist=10m',
|
|
227
|
+
\`ControlPath=\${SSH_CONTROL_PATH}\`,
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const withNvm = (command) =>
|
|
231
|
+
[
|
|
232
|
+
'. ~/.nvm/nvm.sh',
|
|
233
|
+
'corepack enable',
|
|
234
|
+
'export PATH=$(pnpm config get global-bin-dir 2>/dev/null || npm config get prefix)/bin:$HOME/.npm-global/bin:$HOME/.local/bin:$PATH',
|
|
235
|
+
\`echo pm2-release-env=\${DEPLOY_ENV} script=\${DEPLOY_SCRIPT} path=$PWD\`,
|
|
236
|
+
'echo node=$(command -v node) pnpm=$(command -v pnpm || true) npm=$(command -v npm) ai-yuca=$(command -v ai-yuca || true)',
|
|
237
|
+
command,
|
|
238
|
+
].join(' && ');
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
deploy: {
|
|
242
|
+
server: {
|
|
243
|
+
host: DEPLOY_HOST.length === 1 ? DEPLOY_HOST[0] : DEPLOY_HOST,
|
|
244
|
+
ref: DEPLOY_REF,
|
|
245
|
+
repo: REPO,
|
|
246
|
+
path: DEPLOY_PATH,
|
|
247
|
+
// 发布目标由 ai-yuca pm2-release 注入,避免项目维护 ecosystem.config.cjs。
|
|
248
|
+
ssh_options: SSH_OPTIONS,
|
|
249
|
+
'post-deploy': withNvm(
|
|
250
|
+
\`${escapeJsString(installCommand)} && ${escapeJsString(runCommand)} \${DEPLOY_SCRIPT}\`,
|
|
251
|
+
),
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
`;
|
|
256
|
+
fs.writeFileSync(ecosystemPath, content);
|
|
257
|
+
return ecosystemPath;
|
|
258
|
+
}
|
|
259
|
+
function getEnvConfig(config, target) {
|
|
260
|
+
var _a;
|
|
261
|
+
const envConfig = (_a = config.environments) === null || _a === void 0 ? void 0 : _a[target];
|
|
262
|
+
if (!(envConfig === null || envConfig === void 0 ? void 0 : envConfig.path)) {
|
|
263
|
+
throw new Error(`pm2Release.environments.${target}.path 不能为空。`);
|
|
264
|
+
}
|
|
265
|
+
return envConfig;
|
|
266
|
+
}
|
|
267
|
+
function confirm(question) {
|
|
268
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
269
|
+
return Promise.resolve(false);
|
|
270
|
+
}
|
|
271
|
+
const rl = readline.createInterface({
|
|
272
|
+
input: process.stdin,
|
|
273
|
+
output: process.stdout,
|
|
274
|
+
});
|
|
275
|
+
return new Promise((resolve) => {
|
|
276
|
+
rl.question(`${question} (y/N) `, (answer) => {
|
|
277
|
+
rl.close();
|
|
278
|
+
resolve(['y', 'yes'].includes(answer.trim().toLowerCase()));
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
async function ensurePnpmLockfile(packageRoot, config, action) {
|
|
283
|
+
if (action !== 'deploy' || config.installCommand || config.packageManager !== 'pnpm') {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (fs.existsSync(path.join(packageRoot, 'pnpm-lock.yaml'))) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (config.allowNoPnpmLockfile) {
|
|
290
|
+
console.warn('未找到 pnpm-lock.yaml,将允许远端使用 pnpm install --no-frozen-lockfile。');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const shouldContinue = await confirm('未找到 pnpm-lock.yaml。是否继续发布,并在远端使用 pnpm install --no-frozen-lockfile?');
|
|
294
|
+
if (!shouldContinue) {
|
|
295
|
+
throw new Error('已取消发布:pnpm 项目缺少 pnpm-lock.yaml。');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function runPm2(packageRoot, packageJson, config, action, target, options) {
|
|
299
|
+
const envConfig = getEnvConfig(config, target);
|
|
300
|
+
const hosts = normalizeHosts(envConfig.hosts);
|
|
301
|
+
const controlPath = buildSshControlPath(config, packageJson.name || '', packageRoot);
|
|
302
|
+
const deployName = config.deployName || 'server';
|
|
303
|
+
const ref = normalizeRef(options.ref || envConfig.ref || 'origin/main');
|
|
304
|
+
const script = envConfig.script || (target === 'test' ? 'ai:test' : 'ai');
|
|
305
|
+
const [pm2Bin, pm2Args] = getPm2Command(config, options);
|
|
306
|
+
const ecosystem = createTempEcosystem(packageRoot, packageJson, config, controlPath);
|
|
307
|
+
ensureSshAliases(hosts);
|
|
308
|
+
warmSshMaster(hosts, controlPath);
|
|
309
|
+
const env = {
|
|
310
|
+
...process.env,
|
|
311
|
+
PM2_DEPLOY_HOST: hosts.join(','),
|
|
312
|
+
PM2_DEPLOY_PATH: envConfig.path,
|
|
313
|
+
PM2_DEPLOY_REF: action === 'setup' ? normalizeRef(envConfig.setupRef || ref) : ref,
|
|
314
|
+
PM2_DEPLOY_ENV: options.envName || target,
|
|
315
|
+
PM2_DEPLOY_SCRIPT: action === 'setup' ? envConfig.setupScript || script : script,
|
|
316
|
+
PM2_SSH_CONTROL_PATH: controlPath,
|
|
317
|
+
};
|
|
318
|
+
console.log(`PM2 ${action} target: env=${target} host=${hosts.join(',')} path=${envConfig.path} script=${env.PM2_DEPLOY_SCRIPT} ref=${env.PM2_DEPLOY_REF}`);
|
|
319
|
+
if (action === 'setup' && hasRemoteSource(hosts, controlPath, envConfig.path)) {
|
|
320
|
+
console.log(`PM2 setup skipped: ${envConfig.path}/source already exists on ${hosts.join(',')}`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const args = [...pm2Args, 'deploy', ecosystem, deployName];
|
|
325
|
+
if (action === 'setup') {
|
|
326
|
+
args.push('setup');
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
args.push('--force');
|
|
330
|
+
}
|
|
331
|
+
(0, child_process_1.execFileSync)(pm2Bin, args, {
|
|
332
|
+
cwd: packageRoot,
|
|
333
|
+
env,
|
|
334
|
+
stdio: 'inherit',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
fs.rmSync(path.dirname(ecosystem), { recursive: true, force: true });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function pm2Release(action, target, options = {}) {
|
|
342
|
+
var _a;
|
|
343
|
+
const packagePath = options.config
|
|
344
|
+
? path.resolve(options.config)
|
|
345
|
+
: findPackageJson(process.cwd());
|
|
346
|
+
const packageRoot = path.dirname(packagePath);
|
|
347
|
+
const packageJson = readJson(packagePath);
|
|
348
|
+
const config = (_a = packageJson['ai-yuca']) === null || _a === void 0 ? void 0 : _a.pm2Release;
|
|
349
|
+
if (!config) {
|
|
350
|
+
throw new Error('package.json 缺少 ai-yuca.pm2Release 配置。');
|
|
351
|
+
}
|
|
352
|
+
const targets = target === 'all' ? ['test', 'prod'] : [target];
|
|
353
|
+
return (async () => {
|
|
354
|
+
await ensurePnpmLockfile(packageRoot, config, action);
|
|
355
|
+
for (const releaseTarget of targets) {
|
|
356
|
+
runPm2(packageRoot, packageJson, config, action, releaseTarget, options);
|
|
357
|
+
}
|
|
358
|
+
})();
|
|
359
|
+
}
|
package/dist/src/types/index.js
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
allowNoPnpmLockfile?: boolean;
|
|
28
|
+
environments: {
|
|
29
|
+
test: Pm2ReleaseEnvConfig;
|
|
30
|
+
prod: Pm2ReleaseEnvConfig;
|
|
31
|
+
};
|
|
32
|
+
}
|