maolike-cli 1.0.7 → 2.0.1
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 +239 -101
- package/dist/src/commands/project/CreateCommand.js +48 -93
- package/dist/src/commands/project/DeleteCommand.js +2 -4
- package/dist/src/commands/project/DeployCommand.js +71 -0
- package/dist/src/commands/project/ListCommand.js +6 -4
- package/dist/src/commands/project/TestCommand.js +13 -22
- package/dist/src/commands/project/UpdateCommand.js +17 -45
- package/dist/src/commands/project/ViewCommand.js +1 -2
- package/dist/src/commands/system/ConnectCommand.js +59 -0
- package/dist/src/commands/system/CursorCommand.js +29 -0
- package/dist/src/commands/system/HelpCommand.js +11 -5
- package/dist/src/commands/system/InitCommand.js +1 -1
- package/dist/src/commands/system/LoadCommand.js +71 -139
- package/dist/src/constants/CommandRegistry.js +19 -1
- package/dist/src/core/pipelines/BaseExecutor.js +135 -0
- package/dist/src/core/pipelines/ProdExecutor.js +119 -0
- package/dist/src/core/pipelines/TestExecutor.js +83 -0
- package/dist/src/index.js +1 -1
- package/dist/src/services/ConfigManager.js +116 -129
- package/dist/src/ui/InitWizard.js +40 -142
- package/dist/src/ui/InteractiveShell.js +27 -6
- package/dist/src/utils/ConfigPrompter.js +157 -224
- package/dist/src/utils/Executor.js +18 -0
- package/dist/src/utils/RemoteServer.js +112 -71
- package/package.json +1 -1
- package/dist/src/commands/system/MenuCommand.js +0 -182
- package/dist/src/core/PipelineExecutor.js +0 -333
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const tslib_1 = require("tslib");
|
|
4
|
-
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
5
|
-
const path_1 = tslib_1.__importDefault(require("path"));
|
|
6
|
-
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
7
|
-
const ora_1 = tslib_1.__importDefault(require("ora"));
|
|
8
|
-
const RemoteServer_1 = require("../utils/RemoteServer");
|
|
9
|
-
const simple_git_1 = tslib_1.__importDefault(require("simple-git"));
|
|
10
|
-
const cli_progress_1 = tslib_1.__importDefault(require("cli-progress"));
|
|
11
|
-
const child_process_1 = require("child_process");
|
|
12
|
-
const GitProcessor_1 = tslib_1.__importDefault(require("./GitProcessor"));
|
|
13
|
-
/**
|
|
14
|
-
* 自动化流水线执行器。
|
|
15
|
-
* 封装了从构建到部署的完整生命周期管理。
|
|
16
|
-
*/
|
|
17
|
-
class PipelineExecutor {
|
|
18
|
-
constructor(repoPath, repoName, config) {
|
|
19
|
-
this.repoPath = repoPath;
|
|
20
|
-
this.repoName = repoName;
|
|
21
|
-
this.config = config;
|
|
22
|
-
const found = config["git-list"].find((t) => t.name === repoName);
|
|
23
|
-
if (!found) {
|
|
24
|
-
throw new Error(`在配置中未找到项目 ${repoName}`);
|
|
25
|
-
}
|
|
26
|
-
this.task = found;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* 启动流水线
|
|
30
|
-
*/
|
|
31
|
-
run() {
|
|
32
|
-
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
33
|
-
console.log(chalk_1.default.bold.cyan(`\n🚀 启动流水线: [${this.repoName}]`));
|
|
34
|
-
try {
|
|
35
|
-
// 1. Build (本地可选构建)
|
|
36
|
-
yield this.executeBuild();
|
|
37
|
-
// 2. 准备构建产物
|
|
38
|
-
const artifacts = this.prepareArtifacts();
|
|
39
|
-
// 3. 同步到测试环境 (测试环境: 拉取 -> 复制 -> 提交 -> 推送)
|
|
40
|
-
yield this.syncToTestProject(artifacts);
|
|
41
|
-
// 4. 远程部署 (远程: SSH -> 切换分支 -> 拉取)
|
|
42
|
-
yield this.deployToServer();
|
|
43
|
-
console.log(chalk_1.default.bold.green(`\n✨ [${this.repoName}] 流水线执行完毕!`));
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
// NVM_MISSING is a special error we threw, we don't need to double-log it, just exit.
|
|
47
|
-
if (error.message !== "NVM_MISSING") {
|
|
48
|
-
console.error(chalk_1.default.bold.red(`\n❌ 流水线中断: ${error.message}`));
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* 执行构建命令
|
|
55
|
-
*/
|
|
56
|
-
executeBuild() {
|
|
57
|
-
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
58
|
-
if (!this.task.build || !this.task.build.cmd)
|
|
59
|
-
return;
|
|
60
|
-
const spinner = (0, ora_1.default)(`执行构建: ${this.task.build.cmd}`).start();
|
|
61
|
-
// 检查 Node 版本并封装构建命令
|
|
62
|
-
let finalCmd = this.task.build.cmd;
|
|
63
|
-
if (this.task.build.nodeVersion) {
|
|
64
|
-
const requiredVer = this.task.build.nodeVersion.trim();
|
|
65
|
-
if (requiredVer) {
|
|
66
|
-
spinner.info(chalk_1.default.blue(`配置了 Node 版本 (${requiredVer}),尝试切换 (若缺失则自动安装)...`));
|
|
67
|
-
const isWin = process.platform === "win32";
|
|
68
|
-
if (isWin) {
|
|
69
|
-
// Windows: 尝试切换 -> 失败则安装 -> 再次切换 -> 执行构建
|
|
70
|
-
finalCmd = `(nvm use ${requiredVer} || nvm install ${requiredVer}) && nvm use ${requiredVer} && ${this.task.build.cmd}`;
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
// Unix (Mac/Linux): 使用交互式 shell 加载 nvm 环境
|
|
74
|
-
const userShell = process.env.SHELL || "/bin/bash";
|
|
75
|
-
finalCmd = `${userShell} -i -c "(nvm use ${requiredVer} || nvm install ${requiredVer}) && nvm use ${requiredVer} && ${this.task.build.cmd}"`;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
let outputBuffer = "";
|
|
80
|
-
try {
|
|
81
|
-
yield new Promise((resolve, reject) => {
|
|
82
|
-
var _a, _b;
|
|
83
|
-
const child = (0, child_process_1.spawn)(finalCmd, {
|
|
84
|
-
cwd: this.repoPath,
|
|
85
|
-
shell: true,
|
|
86
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
87
|
-
});
|
|
88
|
-
const updateSpinner = (data) => {
|
|
89
|
-
const str = data.toString();
|
|
90
|
-
outputBuffer += str;
|
|
91
|
-
// 获取最后一行非空内容作为进度提示
|
|
92
|
-
const lines = str
|
|
93
|
-
.split("\n")
|
|
94
|
-
.map((line) => line.trim())
|
|
95
|
-
.filter((line) => line.length > 0);
|
|
96
|
-
if (lines.length > 0) {
|
|
97
|
-
const lastLine = lines[lines.length - 1];
|
|
98
|
-
spinner.text = `正在构建... ${lastLine.slice(0, 20)}${lastLine.length > 20 ? "..." : ""}`;
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on("data", updateSpinner);
|
|
102
|
-
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on("data", updateSpinner);
|
|
103
|
-
child.on("error", (err) => reject(err));
|
|
104
|
-
child.on("close", (code) => {
|
|
105
|
-
if (code === 0)
|
|
106
|
-
resolve();
|
|
107
|
-
else
|
|
108
|
-
reject(new Error(`Exit code ${code}`));
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
spinner.succeed("构建成功");
|
|
112
|
-
}
|
|
113
|
-
catch (e) {
|
|
114
|
-
const isNvmError = e.message.includes("nvm: command not found") || e.message.includes("nvm: not found") || e.message.includes("command not found: nvm");
|
|
115
|
-
if (this.task.build.nodeVersion && isNvmError) {
|
|
116
|
-
spinner.fail(chalk_1.default.red("构建失败: 指定了 Node 版本,但设备未安装 nvm 或 nvm 无法在非交互模式下加载。"));
|
|
117
|
-
throw new Error("NVM_MISSING");
|
|
118
|
-
}
|
|
119
|
-
// 出错时,打印完整日志以便排查
|
|
120
|
-
spinner.fail(`构建失败: ${e.message}`);
|
|
121
|
-
console.error(chalk_1.default.red("\n--- 错误日志 ---"));
|
|
122
|
-
console.error(outputBuffer);
|
|
123
|
-
console.error(chalk_1.default.red("----------------"));
|
|
124
|
-
throw e;
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* 根据配置计算需要处理的构建产物清单
|
|
130
|
-
* @private
|
|
131
|
-
* @returns 产物数组,包含源路径和目标名称
|
|
132
|
-
*/
|
|
133
|
-
prepareArtifacts() {
|
|
134
|
-
const artifacts = [];
|
|
135
|
-
// 情况 A: 指定文件
|
|
136
|
-
if (this.task.build.distFile) {
|
|
137
|
-
const files = this.task.build.distFile
|
|
138
|
-
.split(",")
|
|
139
|
-
.map((s) => s.trim())
|
|
140
|
-
.filter(Boolean);
|
|
141
|
-
for (const f of files) {
|
|
142
|
-
artifacts.push({
|
|
143
|
-
src: path_1.default.resolve(this.repoPath, f),
|
|
144
|
-
destName: path_1.default.basename(f),
|
|
145
|
-
isDir: false,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// 情况 B: 目录 (默认)
|
|
150
|
-
else {
|
|
151
|
-
const dirName = this.task.build.distName || "dist";
|
|
152
|
-
artifacts.push({
|
|
153
|
-
src: path_1.default.resolve(this.repoPath, dirName),
|
|
154
|
-
destName: "", // 空字符串表示直接将目录内容平铺到目标根路径,不再保留父文件夹名
|
|
155
|
-
isDir: true,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
return artifacts;
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* 同步产物到测试环境项目
|
|
162
|
-
* 包含:确保目录存在、切换分支、拉取代码、复制文件以及提交推送
|
|
163
|
-
* @param artifacts 要同步的产物清单
|
|
164
|
-
* @private
|
|
165
|
-
*/
|
|
166
|
-
syncToTestProject(artifacts) {
|
|
167
|
-
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
168
|
-
if (!this.task.test || !this.task.test.path)
|
|
169
|
-
return;
|
|
170
|
-
const spinner = (0, ora_1.default)(`同步到测试项目: ${this.task.test.path}`).start();
|
|
171
|
-
try {
|
|
172
|
-
if (!fs_extra_1.default.existsSync(this.task.test.path)) {
|
|
173
|
-
throw new Error(`测试路径不存在: ${this.task.test.path}`);
|
|
174
|
-
}
|
|
175
|
-
// 如果配置了分支则检查
|
|
176
|
-
if (this.task.test.branch) {
|
|
177
|
-
const testGit = (0, simple_git_1.default)(this.task.test.path);
|
|
178
|
-
const isRepo = yield testGit.checkIsRepo();
|
|
179
|
-
if (isRepo) {
|
|
180
|
-
try {
|
|
181
|
-
// 1. 切换分支
|
|
182
|
-
spinner.text = `[TestEnv] 切换分支至: ${this.task.test.branch}`;
|
|
183
|
-
yield testGit.checkout(this.task.test.branch);
|
|
184
|
-
// 2. 拉取 (同步远程代码)
|
|
185
|
-
spinner.text = `[TestEnv] 拉取远程代码...`;
|
|
186
|
-
yield testGit.pull();
|
|
187
|
-
}
|
|
188
|
-
catch (err) {
|
|
189
|
-
throw new Error(`无法准备测试环境分支: ${err.message}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
// 复制产物
|
|
193
|
-
spinner.text = `[TestEnv] 复制构建产物...`;
|
|
194
|
-
for (const art of artifacts) {
|
|
195
|
-
if (!fs_extra_1.default.existsSync(art.src)) {
|
|
196
|
-
spinner.warn(`产物不存在(跳过): ${art.src}`);
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
const targetPath = path_1.default.join(this.task.test.path, art.destName);
|
|
200
|
-
yield fs_extra_1.default.copy(art.src, targetPath, { overwrite: true });
|
|
201
|
-
}
|
|
202
|
-
// 3. 提交并推送
|
|
203
|
-
if (isRepo) {
|
|
204
|
-
const commitMsg = `chore(test): 同步来自 ${this.repoName} 的构建产物`;
|
|
205
|
-
const gitProc = new GitProcessor_1.default(this.task.test.path, `${this.repoName}(测试环境)`, commitMsg);
|
|
206
|
-
yield gitProc.execute();
|
|
207
|
-
spinner.succeed("测试环境同步完成");
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
for (const art of artifacts) {
|
|
212
|
-
if (!fs_extra_1.default.existsSync(art.src)) {
|
|
213
|
-
spinner.warn(`产物不存在(跳过): ${art.src}`);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
// 如果是目录且 destName 为空(意为平铺复制),则遍历目录内容逐个复制
|
|
217
|
-
if (art.isDir && art.destName === "") {
|
|
218
|
-
const files = yield fs_extra_1.default.readdir(art.src);
|
|
219
|
-
if (files.length > 0) {
|
|
220
|
-
spinner.stop();
|
|
221
|
-
console.log(chalk_1.default.dim(`\n正在复制 ${files.length} 个文件...`));
|
|
222
|
-
const bar = new cli_progress_1.default.SingleBar({
|
|
223
|
-
format: "进度 |" + chalk_1.default.cyan("{bar}") + "| {percentage}% || {value}/{total} Files",
|
|
224
|
-
barCompleteChar: "\u2588",
|
|
225
|
-
barIncompleteChar: "\u2591",
|
|
226
|
-
hideCursor: true,
|
|
227
|
-
});
|
|
228
|
-
bar.start(files.length, 0);
|
|
229
|
-
for (const file of files) {
|
|
230
|
-
const srcFile = path_1.default.join(art.src, file);
|
|
231
|
-
const destFile = path_1.default.join(this.task.test.path, file);
|
|
232
|
-
yield fs_extra_1.default.copy(srcFile, destFile, { overwrite: true });
|
|
233
|
-
bar.increment();
|
|
234
|
-
}
|
|
235
|
-
bar.stop();
|
|
236
|
-
console.log("");
|
|
237
|
-
spinner.start("文件复制完成,准备提交...");
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
// 否则(文件或保留目录名),直接复制
|
|
242
|
-
const targetPath = path_1.default.join(this.task.test.path, art.destName);
|
|
243
|
-
yield fs_extra_1.default.copy(art.src, targetPath, { overwrite: true });
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
spinner.succeed("同步测试环境完成");
|
|
247
|
-
}
|
|
248
|
-
catch (e) {
|
|
249
|
-
spinner.fail(`同步测试失败: ${e.message}`);
|
|
250
|
-
throw e;
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* 部署产物到远程服务器
|
|
256
|
-
* 流程:建立 SSH 连接 -> 远程执行 git checkout -> 远程执行 git pull
|
|
257
|
-
* @private
|
|
258
|
-
*/
|
|
259
|
-
deployToServer() {
|
|
260
|
-
return tslib_1.__awaiter(this, void 0, void 0, function* () {
|
|
261
|
-
var _a, _b;
|
|
262
|
-
if (!this.task.server || !this.task.server.remotePath)
|
|
263
|
-
return;
|
|
264
|
-
let connectionConfig;
|
|
265
|
-
// 1. 优先检查: 预设模式 (Server Preset)
|
|
266
|
-
// 如果指定了 serverName,则完全使用预设配置,不混用全局配置
|
|
267
|
-
if (this.task.server.serverName) {
|
|
268
|
-
const preset = (_a = this.config.servers) === null || _a === void 0 ? void 0 : _a.find((s) => s.name === this.task.server.serverName);
|
|
269
|
-
if (preset) {
|
|
270
|
-
// 使用预设 (排除 name 属性以免污染 SshBaseConfig)
|
|
271
|
-
const { name } = preset, rest = tslib_1.__rest(preset, ["name"]);
|
|
272
|
-
connectionConfig = Object.assign({}, rest);
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
console.error(chalk_1.default.red(`\n❌ [Fatal] 指定了服务器预设 "${this.task.server.serverName}" 但未找到对应配置。`));
|
|
276
|
-
const available = ((_b = this.config.servers) === null || _b === void 0 ? void 0 : _b.map((s) => s.name).join(", ")) || "无";
|
|
277
|
-
console.error(chalk_1.default.dim(` 可用预设: ${available}`));
|
|
278
|
-
console.error(chalk_1.default.red(` 为安全起见,部署已终止。`));
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
// 2. 其次检查: 自定义模式 (Custom Config)
|
|
283
|
-
// 如果没有使用预设,但配置了 host,说明是独立服务器,完全独立于全局配置
|
|
284
|
-
if (!connectionConfig && this.task.server.host) {
|
|
285
|
-
const _c = this.task.server, { remotePath, serverName } = _c, customSsh = tslib_1.__rest(_c, ["remotePath", "serverName"]);
|
|
286
|
-
connectionConfig = {
|
|
287
|
-
host: customSsh.host,
|
|
288
|
-
port: customSsh.port || 22,
|
|
289
|
-
user: customSsh.user || "root",
|
|
290
|
-
password: customSsh.password,
|
|
291
|
-
gitUser: customSsh.gitUser, // 即使是 undefined 也保留,不回退
|
|
292
|
-
gitPassword: customSsh.gitPassword,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
// 3. 最后回退: 全局模式 (Global Fallback)
|
|
296
|
-
// 只有在既没有预设、也没有自定义 Host 的情况下,才使用全局配置
|
|
297
|
-
if (!connectionConfig && this.config.ssh && this.config.ssh.host) {
|
|
298
|
-
connectionConfig = Object.assign({}, this.config.ssh);
|
|
299
|
-
}
|
|
300
|
-
// 最终校验
|
|
301
|
-
if (!connectionConfig || !connectionConfig.host) {
|
|
302
|
-
console.log(chalk_1.default.yellow(`\n⚠️ [${this.repoName}] 跳过远程部署: 未找到有效的 SSH Host 配置`));
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
const connectSpinner = (0, ora_1.default)(`连接服务器并更新代码: ${connectionConfig.host}`).start();
|
|
306
|
-
let server;
|
|
307
|
-
try {
|
|
308
|
-
server = new RemoteServer_1.RemoteServer(connectionConfig);
|
|
309
|
-
yield server.connect();
|
|
310
|
-
connectSpinner.succeed("SSH 连接成功");
|
|
311
|
-
// 0. 切换分支 (使用 test.branch)
|
|
312
|
-
if (this.task.test.branch) {
|
|
313
|
-
yield server.exec(`git checkout ${this.task.test.branch}`, this.task.server.remotePath, `Git Checkout ${this.task.test.branch}`);
|
|
314
|
-
}
|
|
315
|
-
// 执行远程 Git Pull (带身份验证)
|
|
316
|
-
const pullCmd = server.getGitPullCommand();
|
|
317
|
-
yield server.exec(pullCmd, this.task.server.remotePath, "Git Pull");
|
|
318
|
-
console.log(chalk_1.default.green(`\n✅ 远程服务器代码已更新至最新状态`));
|
|
319
|
-
}
|
|
320
|
-
catch (e) {
|
|
321
|
-
if (connectSpinner.isSpinning)
|
|
322
|
-
connectSpinner.fail(`远程操作失败: ${e.message}`);
|
|
323
|
-
else
|
|
324
|
-
console.error(chalk_1.default.red(`\n❌ 远程部署出错: ${e.message}`));
|
|
325
|
-
}
|
|
326
|
-
finally {
|
|
327
|
-
if (server)
|
|
328
|
-
server.disconnect();
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
exports.default = PipelineExecutor;
|