@zhipu/zp-cli 0.0.0 → 0.0.2
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 +114 -8
- package/dist/build-dashboard-CqZVHPH7.mjs +502 -0
- package/dist/build-jobs-m5jbiNTx.mjs +191 -0
- package/dist/cli.d.mts +1 -4
- package/dist/cli.mjs +981 -11
- package/dist/config-BZc9pg7J.mjs +105 -0
- package/dist/index.d.mts +23 -3
- package/dist/index.mjs +2 -6
- package/package.json +59 -4
- package/src/templates/nginx-proxy.conf.template +16 -0
- package/src/templates/zpc.config.cjs.template +20 -0
- package/src/templates/zpc.config.cts.template +21 -0
- package/src/templates/zpc.config.js.template +21 -0
- package/src/templates/zpc.config.json.template +20 -0
- package/src/templates/zpc.config.mjs.template +21 -0
- package/src/templates/zpc.config.mts.template +21 -0
- package/src/templates/zpc.config.schema.json +67 -0
- package/src/templates/zpc.config.ts.template +21 -0
package/README.md
CHANGED
|
@@ -1,23 +1,129 @@
|
|
|
1
|
-
#
|
|
1
|
+
# zp-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
内部单命令部署工具(`@zhipu/zp-cli`)。用 `zpc pack` 构建打包、`zpc deploy` 在服务器上解压并挂到 Nginx / PM2。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
路线图见 [docs/ROADMAP.md](./docs/ROADMAP.md)。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 使用
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
# 在项目根生成 zpc.config
|
|
11
|
+
zpc init
|
|
12
|
+
|
|
13
|
+
# 构建并打出 <projectName>-dist.zip(依据 ~/.zpc 部署记录与部署目录自动判定全量/增量)
|
|
14
|
+
zpc pack
|
|
15
|
+
|
|
16
|
+
# 在服务器上:目录内需有 <projectName>-dist.zip + zpc.config.*(配置不要打进压缩包)
|
|
17
|
+
zpc deploy
|
|
18
|
+
zpc deploy --force # 允许抢占冲突的 nginx proxy
|
|
11
19
|
```
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
可选环境变量:`DEPLOY_DIR`、`DEPLOY_FORCE`;`ZPC_HOME` 覆盖 `~/.zpc`(部署状态记录);测试可用 `ZPC_API_ROOT` / `ZPC_WEB_ROOT` / `ZPC_NGINX_DEFAULT_D` / `ZPC_DEPLOY_SKIP_RUNTIME`。
|
|
22
|
+
|
|
23
|
+
## 部署约定
|
|
24
|
+
|
|
25
|
+
`zpc.config` 中的 `projectName` 即下文 `<system-tag>`。
|
|
26
|
+
|
|
27
|
+
### 目录结构
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
─┬─ etc/nginx/
|
|
31
|
+
│ ├─ default.d/
|
|
32
|
+
│ │ └─ <system-tag>.conf
|
|
33
|
+
│ └─ nginx.conf
|
|
34
|
+
└─ usr/share/nginx/
|
|
35
|
+
├─ api/
|
|
36
|
+
│ └─ <system-tag>/
|
|
37
|
+
│ ├─ <api-1>/
|
|
38
|
+
│ │ ├─ dist
|
|
39
|
+
│ │ ├─ public
|
|
40
|
+
│ │ ├─ bootstrap.js
|
|
41
|
+
│ │ └─ package.json
|
|
42
|
+
│ ├─ <api-2>/
|
|
43
|
+
│ │ ├─ dist
|
|
44
|
+
│ │ ├─ public
|
|
45
|
+
│ │ ├─ bootstrap.js
|
|
46
|
+
│ │ └─ package.json
|
|
47
|
+
│ └─ ...
|
|
48
|
+
└─ html/
|
|
49
|
+
└─ <system-tag>/
|
|
50
|
+
├─ <web-1>/
|
|
51
|
+
│ ├─ assets
|
|
52
|
+
│ └─ index.html
|
|
53
|
+
├─ <web-2>/
|
|
54
|
+
│ ├─ assets
|
|
55
|
+
│ └─ index.html
|
|
56
|
+
└─ ...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Nginx 配置
|
|
60
|
+
|
|
61
|
+
1. 单系统配置放在 `/etc/nginx/default.d/`,文件名为 `<system-tag>.conf`(当前实现尚未完全对齐,见路线图「进行中」):
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
location /<API_PROXY>/ {
|
|
65
|
+
proxy_pass http://127.0.0.1:<PORT>/;
|
|
66
|
+
proxy_http_version 1.1;
|
|
67
|
+
proxy_set_header Host $host;
|
|
68
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
69
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
70
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
71
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
72
|
+
proxy_set_header Connection "upgrade";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
location = /<API_PROXY> {
|
|
76
|
+
return 301 /<API_PROXY>/;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
2. 宿主机 `nginx.conf` 的 `server` 块须包含 `include`,以便加载系统配置(机器初始化时完成,非 `zpc deploy` 职责):
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
server {
|
|
84
|
+
listen 80;
|
|
85
|
+
listen [::]:80;
|
|
86
|
+
server_name _;
|
|
87
|
+
root /usr/share/nginx/html;
|
|
88
|
+
|
|
89
|
+
# Load configuration files for the default server block.
|
|
90
|
+
include /etc/nginx/default.d/*.conf;
|
|
91
|
+
|
|
92
|
+
error_page 404 /404.html;
|
|
93
|
+
location = /404.html {
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
error_page 500 502 503 504 /50x.html;
|
|
97
|
+
location = /50x.html {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
服务器软件栈(Nginx、Node、pnpm、PM2 等)的一键安装不在本仓库范围,见路线图「展望」。
|
|
103
|
+
|
|
104
|
+
## 开发
|
|
14
105
|
|
|
15
106
|
```bash
|
|
107
|
+
# 安装依赖
|
|
108
|
+
vp install
|
|
109
|
+
# 校验(格式 / lint / 类型)
|
|
110
|
+
vp check
|
|
111
|
+
# 单元测试
|
|
16
112
|
vp test
|
|
113
|
+
# 构建可发布产物
|
|
114
|
+
vp pack
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 发布
|
|
118
|
+
|
|
119
|
+
因 pnpm 安全策略,access token 需配在用户级 npmrc(`~/.npmrc`):
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
|
17
123
|
```
|
|
18
124
|
|
|
19
|
-
|
|
125
|
+
在环境变量中设置 `NPM_TOKEN` 后发布:
|
|
20
126
|
|
|
21
127
|
```bash
|
|
22
|
-
|
|
128
|
+
pnpm publish
|
|
23
129
|
```
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { createBuildJobs, createBuildJobsContext, runBuildJobs, summarizeBuildFailures } from "./build-jobs-m5jbiNTx.mjs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
import { Box, Text, createApp, useApp, useInput, useLayoutSize } from "@vue-tui/runtime";
|
|
5
|
+
import { Fragment, computed, createBlock, createElementBlock, createTextVNode, createVNode, defineComponent, nextTick, onScopeDispose, openBlock, ref, renderList, shallowRef, toDisplayString, unref, watch, withCtx } from "vue";
|
|
6
|
+
import { ScrollBox } from "@vue-tui/components";
|
|
7
|
+
import stripAnsi from "strip-ansi";
|
|
8
|
+
//#region src/utils/clipboard.ts
|
|
9
|
+
/** Windows clip.exe 从 stdin 读取 UTF-16LE;直接写 UTF-8 会在中文系统上乱码。 */
|
|
10
|
+
function encodeWindowsClipboardInput(text) {
|
|
11
|
+
return Buffer.from(text, "utf16le");
|
|
12
|
+
}
|
|
13
|
+
function getClipboardCommand() {
|
|
14
|
+
switch (platform()) {
|
|
15
|
+
case "win32": return {
|
|
16
|
+
cmd: "clip",
|
|
17
|
+
args: []
|
|
18
|
+
};
|
|
19
|
+
case "darwin": return {
|
|
20
|
+
cmd: "pbcopy",
|
|
21
|
+
args: []
|
|
22
|
+
};
|
|
23
|
+
case "linux": return {
|
|
24
|
+
cmd: "xclip",
|
|
25
|
+
args: ["-selection", "clipboard"]
|
|
26
|
+
};
|
|
27
|
+
default: return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function writeClipboardStdin(text) {
|
|
31
|
+
if (platform() === "win32") return encodeWindowsClipboardInput(text);
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
34
|
+
function tryLinuxClipboard(text) {
|
|
35
|
+
const commands = [
|
|
36
|
+
{
|
|
37
|
+
cmd: "wl-copy",
|
|
38
|
+
args: []
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
cmd: "xclip",
|
|
42
|
+
args: ["-selection", "clipboard"]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
cmd: "xsel",
|
|
46
|
+
args: ["--clipboard", "--input"]
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
let index = 0;
|
|
51
|
+
function tryNext() {
|
|
52
|
+
if (index >= commands.length) {
|
|
53
|
+
resolve(false);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const { cmd, args } = commands[index];
|
|
57
|
+
index += 1;
|
|
58
|
+
const proc = spawn(cmd, args, { stdio: [
|
|
59
|
+
"pipe",
|
|
60
|
+
"ignore",
|
|
61
|
+
"ignore"
|
|
62
|
+
] });
|
|
63
|
+
proc.on("error", () => tryNext());
|
|
64
|
+
proc.stdin.write(text, "utf8");
|
|
65
|
+
proc.stdin.end();
|
|
66
|
+
proc.on("close", (code) => {
|
|
67
|
+
if (code === 0) resolve(true);
|
|
68
|
+
else tryNext();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
tryNext();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function copyWithCommand(text, cmd, args) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const proc = spawn(cmd, args, {
|
|
77
|
+
stdio: [
|
|
78
|
+
"pipe",
|
|
79
|
+
"ignore",
|
|
80
|
+
"ignore"
|
|
81
|
+
],
|
|
82
|
+
windowsHide: true
|
|
83
|
+
});
|
|
84
|
+
proc.on("error", () => resolve(false));
|
|
85
|
+
const input = writeClipboardStdin(text);
|
|
86
|
+
if (typeof input === "string") proc.stdin.write(input, "utf8");
|
|
87
|
+
else proc.stdin.write(input);
|
|
88
|
+
proc.stdin.end();
|
|
89
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function copyToClipboard(text) {
|
|
93
|
+
if (!text) return Promise.resolve(false);
|
|
94
|
+
const command = getClipboardCommand();
|
|
95
|
+
if (!command) return Promise.resolve(false);
|
|
96
|
+
if (platform() === "linux") return tryLinuxClipboard(text);
|
|
97
|
+
return copyWithCommand(text, command.cmd, command.args);
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/tui/pack/build-output.ts
|
|
101
|
+
const ERROR_LINE_PATTERN = /\b(error|failed|failure|fatal|exception|err!|warn)\b|错误|失败|✗/i;
|
|
102
|
+
function normalizeOutput(output) {
|
|
103
|
+
return stripAnsi(output).trimEnd();
|
|
104
|
+
}
|
|
105
|
+
function formatBuildInfo(job) {
|
|
106
|
+
return [
|
|
107
|
+
`[${job.label}]`,
|
|
108
|
+
`cwd: ${job.cwd}`,
|
|
109
|
+
`status: ${job.status}`,
|
|
110
|
+
`exit: ${job.exitCode ?? "-"}`,
|
|
111
|
+
"",
|
|
112
|
+
normalizeOutput(job.output)
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
function extractErrorOutput(output) {
|
|
116
|
+
const trimmed = normalizeOutput(output);
|
|
117
|
+
if (!trimmed) return "";
|
|
118
|
+
const lines = trimmed.split("\n");
|
|
119
|
+
const matched = lines.filter((line) => ERROR_LINE_PATTERN.test(line));
|
|
120
|
+
if (matched.length > 0) return matched.join("\n");
|
|
121
|
+
return lines.slice(-40).join("\n").trim();
|
|
122
|
+
}
|
|
123
|
+
function formatErrorInfo(job) {
|
|
124
|
+
const body = extractErrorOutput(job.output);
|
|
125
|
+
if (!body) return `[${job.label}] 暂无错误输出`;
|
|
126
|
+
return [
|
|
127
|
+
`[${job.label}]`,
|
|
128
|
+
`exit: ${job.exitCode ?? "-"}`,
|
|
129
|
+
"",
|
|
130
|
+
body
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/tui/pack/dashboard-input.ts
|
|
135
|
+
function isFocusLog(event) {
|
|
136
|
+
if (event.type !== "key") return false;
|
|
137
|
+
return event.key.name === "tab" && !event.key.shift || event.key.name === "right";
|
|
138
|
+
}
|
|
139
|
+
function isFocusProjects(event) {
|
|
140
|
+
if (event.type !== "key") return false;
|
|
141
|
+
return event.key.name === "tab" && event.key.shift || event.key.name === "left";
|
|
142
|
+
}
|
|
143
|
+
function isLogPaneEscape(event) {
|
|
144
|
+
return event.type === "key" && event.key.name === "escape";
|
|
145
|
+
}
|
|
146
|
+
function isSingleKey(event, key) {
|
|
147
|
+
if (event.type === "text" && event.text === key) return true;
|
|
148
|
+
if (event.type !== "key") return false;
|
|
149
|
+
if (event.key.ctrl || event.key.meta || event.key.alt) return false;
|
|
150
|
+
const normalized = key.toLowerCase();
|
|
151
|
+
if (event.key.character === normalized) return true;
|
|
152
|
+
return event.key.name === normalized;
|
|
153
|
+
}
|
|
154
|
+
function isCopyBuildInfo(event) {
|
|
155
|
+
return isSingleKey(event, "y");
|
|
156
|
+
}
|
|
157
|
+
function isCopyErrorInfo(event) {
|
|
158
|
+
return isSingleKey(event, "e");
|
|
159
|
+
}
|
|
160
|
+
function isCancelDuringBuild(event) {
|
|
161
|
+
if (event.type === "text" && event.text === "q") return true;
|
|
162
|
+
if (event.type !== "key") return false;
|
|
163
|
+
if (event.key.name === "escape") return true;
|
|
164
|
+
if (event.key.character === "c" && event.key.ctrl) return true;
|
|
165
|
+
return event.key.character === "q" && !event.key.ctrl && !event.key.meta;
|
|
166
|
+
}
|
|
167
|
+
function isDismissAfterFailure(event) {
|
|
168
|
+
if (event.type === "text" && event.text === "q") return true;
|
|
169
|
+
if (event.type !== "key") return false;
|
|
170
|
+
return event.key.name === "escape" || event.key.name === "enter" || event.key.character === "q" && !event.key.ctrl && !event.key.meta;
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/tui/pack/use-build-dashboard.ts
|
|
174
|
+
const statusIcon = {
|
|
175
|
+
waiting: "…",
|
|
176
|
+
running: "●",
|
|
177
|
+
success: "✓",
|
|
178
|
+
failed: "✗"
|
|
179
|
+
};
|
|
180
|
+
const COPY_HINT = " · y 复制日志 · e 复制错误";
|
|
181
|
+
function useBuildDashboard(props) {
|
|
182
|
+
const selectedIndex = shallowRef(0);
|
|
183
|
+
const focusPane = shallowRef("projects");
|
|
184
|
+
const dismissed = shallowRef(false);
|
|
185
|
+
const copyNotice = shallowRef(null);
|
|
186
|
+
const logBox = ref(null);
|
|
187
|
+
const logFollowOutput = shallowRef(true);
|
|
188
|
+
const { exit } = useApp();
|
|
189
|
+
let copyNoticeTimer;
|
|
190
|
+
const doneCount = computed(() => props.jobs.filter((job) => job.status === "success" || job.status === "failed").length);
|
|
191
|
+
const failCount = computed(() => props.jobs.filter((job) => job.status === "failed").length);
|
|
192
|
+
const allDone = computed(() => doneCount.value === props.jobs.length);
|
|
193
|
+
const selectedJob = computed(() => props.jobs[selectedIndex.value]);
|
|
194
|
+
const footerText = computed(() => {
|
|
195
|
+
if (copyNotice.value) return copyNotice.value;
|
|
196
|
+
if (!allDone.value) {
|
|
197
|
+
if (focusPane.value === "log") return `构建中… ${doneCount.value}/${props.jobs.length} 完成 · ↑↓ 滚动日志 · ←/Shift+Tab 返回 · Ctrl+C/q 取消${COPY_HINT}`;
|
|
198
|
+
return `构建中… ${doneCount.value}/${props.jobs.length} 完成 · ↑↓ 切换项目 · Tab/→ 查看日志 · Ctrl+C/q 取消${COPY_HINT}`;
|
|
199
|
+
}
|
|
200
|
+
if (failCount.value > 0) return `${failCount.value} 个失败 · 按 q / Enter / Esc 退出${COPY_HINT}`;
|
|
201
|
+
if (props.stayOnSuccess) return `全部构建成功 · 按 q / Enter / Esc 退出${COPY_HINT}`;
|
|
202
|
+
return "全部构建成功";
|
|
203
|
+
});
|
|
204
|
+
function showCopyNotice(message) {
|
|
205
|
+
copyNotice.value = message;
|
|
206
|
+
clearTimeout(copyNoticeTimer);
|
|
207
|
+
copyNoticeTimer = setTimeout(() => {
|
|
208
|
+
copyNotice.value = null;
|
|
209
|
+
}, 2e3);
|
|
210
|
+
}
|
|
211
|
+
async function copyCurrentBuildInfo() {
|
|
212
|
+
const job = selectedJob.value;
|
|
213
|
+
if (!job) return;
|
|
214
|
+
showCopyNotice(await copyToClipboard(formatBuildInfo(job)) ? `已复制 ${job.label} 构建日志` : "复制失败,请检查系统剪贴板工具");
|
|
215
|
+
}
|
|
216
|
+
async function copyCurrentErrorInfo() {
|
|
217
|
+
const job = selectedJob.value;
|
|
218
|
+
if (!job) return;
|
|
219
|
+
showCopyNotice(await copyToClipboard(formatErrorInfo(job)) ? `已复制 ${job.label} 错误信息` : "复制失败,请检查系统剪贴板工具");
|
|
220
|
+
}
|
|
221
|
+
function selectJob(index) {
|
|
222
|
+
selectedIndex.value = index;
|
|
223
|
+
logFollowOutput.value = true;
|
|
224
|
+
logBox.value?.scrollToBottom();
|
|
225
|
+
}
|
|
226
|
+
function requestExit() {
|
|
227
|
+
if (dismissed.value) return;
|
|
228
|
+
dismissed.value = true;
|
|
229
|
+
exit();
|
|
230
|
+
}
|
|
231
|
+
onScopeDispose(() => {
|
|
232
|
+
clearTimeout(copyNoticeTimer);
|
|
233
|
+
});
|
|
234
|
+
watch(allDone, (done) => {
|
|
235
|
+
if (!done || failCount.value > 0 || props.stayOnSuccess) return;
|
|
236
|
+
requestExit();
|
|
237
|
+
}, { immediate: true });
|
|
238
|
+
useInput((event) => {
|
|
239
|
+
if (dismissed.value) return;
|
|
240
|
+
if (isCopyBuildInfo(event)) {
|
|
241
|
+
copyCurrentBuildInfo();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (isCopyErrorInfo(event)) {
|
|
245
|
+
copyCurrentErrorInfo();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (isFocusLog(event)) {
|
|
249
|
+
focusPane.value = "log";
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (isFocusProjects(event)) {
|
|
253
|
+
focusPane.value = "projects";
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (!allDone.value && focusPane.value === "log" && isLogPaneEscape(event)) {
|
|
257
|
+
focusPane.value = "projects";
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (event.type === "key") if (focusPane.value === "projects") {
|
|
261
|
+
if (event.key.name === "up") {
|
|
262
|
+
selectJob(Math.max(0, selectedIndex.value - 1));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (event.key.name === "down") {
|
|
266
|
+
selectJob(Math.min(props.jobs.length - 1, selectedIndex.value + 1));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
const handle = logBox.value;
|
|
271
|
+
if (!handle) return;
|
|
272
|
+
if (event.key.name === "up") {
|
|
273
|
+
if (handle.scrollByLines(-1)) logFollowOutput.value = false;
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (event.key.name === "down") {
|
|
277
|
+
handle.scrollByLines(1);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (event.key.name === "page-up") {
|
|
281
|
+
if (handle.scrollByLines(-10)) logFollowOutput.value = false;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (event.key.name === "page-down") {
|
|
285
|
+
handle.scrollByLines(10);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!allDone.value) {
|
|
290
|
+
if (!isCancelDuringBuild(event)) return;
|
|
291
|
+
props.onCancel?.();
|
|
292
|
+
requestExit();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (failCount.value === 0) {
|
|
296
|
+
if (!props.stayOnSuccess || !allDone.value) return;
|
|
297
|
+
if (!isDismissAfterFailure(event)) return;
|
|
298
|
+
requestExit();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!isDismissAfterFailure(event)) return;
|
|
302
|
+
requestExit();
|
|
303
|
+
});
|
|
304
|
+
watch(() => selectedJob.value?.output, () => {
|
|
305
|
+
if (selectedJob.value?.status !== "running" || !logFollowOutput.value) return;
|
|
306
|
+
nextTick(() => {
|
|
307
|
+
logBox.value?.scrollToBottom();
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
statusIcon,
|
|
312
|
+
selectedIndex,
|
|
313
|
+
focusPane,
|
|
314
|
+
doneCount,
|
|
315
|
+
failCount,
|
|
316
|
+
allDone,
|
|
317
|
+
selectedJob,
|
|
318
|
+
footerText,
|
|
319
|
+
logBox
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/tui/pack/BuildDashboard.vue
|
|
324
|
+
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
325
|
+
__name: "BuildDashboard",
|
|
326
|
+
props: {
|
|
327
|
+
jobs: {},
|
|
328
|
+
onCancel: { type: Function },
|
|
329
|
+
stayOnSuccess: { type: Boolean }
|
|
330
|
+
},
|
|
331
|
+
setup(__props) {
|
|
332
|
+
const props = __props;
|
|
333
|
+
const { width, height } = useLayoutSize();
|
|
334
|
+
const { statusIcon, selectedIndex, focusPane, doneCount, failCount, allDone, selectedJob, footerText, logBox } = useBuildDashboard(props);
|
|
335
|
+
return (_ctx, _cache) => {
|
|
336
|
+
return openBlock(), createBlock(unref(Box), {
|
|
337
|
+
flexDirection: "column",
|
|
338
|
+
width: unref(width),
|
|
339
|
+
height: unref(height),
|
|
340
|
+
minHeight: 0
|
|
341
|
+
}, {
|
|
342
|
+
default: withCtx(() => [
|
|
343
|
+
createVNode(unref(Box), {
|
|
344
|
+
paddingY: 0,
|
|
345
|
+
paddingX: 1,
|
|
346
|
+
flexShrink: 0
|
|
347
|
+
}, {
|
|
348
|
+
default: withCtx(() => [
|
|
349
|
+
createVNode(unref(Text), { bold: "" }, {
|
|
350
|
+
default: withCtx(() => [..._cache[0] || (_cache[0] = [createTextVNode("zpc pack", -1)])]),
|
|
351
|
+
_: 1
|
|
352
|
+
}),
|
|
353
|
+
createVNode(unref(Text), { dimColor: "" }, {
|
|
354
|
+
default: withCtx(() => [..._cache[1] || (_cache[1] = [createTextVNode(" · 并行构建 · ", -1)])]),
|
|
355
|
+
_: 1
|
|
356
|
+
}),
|
|
357
|
+
createVNode(unref(Text), null, {
|
|
358
|
+
default: withCtx(() => [createTextVNode(toDisplayString(unref(doneCount)) + "/" + toDisplayString(__props.jobs.length) + " 完成", 1)]),
|
|
359
|
+
_: 1
|
|
360
|
+
})
|
|
361
|
+
]),
|
|
362
|
+
_: 1
|
|
363
|
+
}),
|
|
364
|
+
createVNode(unref(Box), {
|
|
365
|
+
flexDirection: "row",
|
|
366
|
+
flexGrow: 1,
|
|
367
|
+
minHeight: 0,
|
|
368
|
+
width: "100%",
|
|
369
|
+
overflow: "hidden",
|
|
370
|
+
marginTop: 1
|
|
371
|
+
}, {
|
|
372
|
+
default: withCtx(() => [createVNode(unref(Box), {
|
|
373
|
+
width: 28,
|
|
374
|
+
flexDirection: "column",
|
|
375
|
+
borderStyle: "single",
|
|
376
|
+
borderColor: unref(focusPane) === "projects" ? "cyan" : void 0,
|
|
377
|
+
borderDimColor: unref(focusPane) !== "projects",
|
|
378
|
+
paddingX: 1,
|
|
379
|
+
paddingY: 0,
|
|
380
|
+
marginRight: 1,
|
|
381
|
+
flexShrink: 0,
|
|
382
|
+
minHeight: 0,
|
|
383
|
+
overflow: "hidden"
|
|
384
|
+
}, {
|
|
385
|
+
default: withCtx(() => [createVNode(unref(Text), {
|
|
386
|
+
bold: "",
|
|
387
|
+
dimColor: unref(focusPane) !== "projects"
|
|
388
|
+
}, {
|
|
389
|
+
default: withCtx(() => [..._cache[2] || (_cache[2] = [createTextVNode("项目", -1)])]),
|
|
390
|
+
_: 1
|
|
391
|
+
}, 8, ["dimColor"]), createVNode(unref(Box), {
|
|
392
|
+
flexDirection: "column",
|
|
393
|
+
marginTop: 1,
|
|
394
|
+
minHeight: 0,
|
|
395
|
+
overflow: "hidden"
|
|
396
|
+
}, {
|
|
397
|
+
default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.jobs, (job, index) => {
|
|
398
|
+
return openBlock(), createBlock(unref(Box), { key: job.id }, {
|
|
399
|
+
default: withCtx(() => [createVNode(unref(Text), {
|
|
400
|
+
color: index === unref(selectedIndex) ? "cyan" : job.status === "waiting" ? "gray" : void 0,
|
|
401
|
+
bold: index === unref(selectedIndex),
|
|
402
|
+
dimColor: job.status === "waiting" && index !== unref(selectedIndex)
|
|
403
|
+
}, {
|
|
404
|
+
default: withCtx(() => [createTextVNode(toDisplayString(unref(statusIcon)[job.status]) + " " + toDisplayString(job.label), 1)]),
|
|
405
|
+
_: 2
|
|
406
|
+
}, 1032, [
|
|
407
|
+
"color",
|
|
408
|
+
"bold",
|
|
409
|
+
"dimColor"
|
|
410
|
+
])]),
|
|
411
|
+
_: 2
|
|
412
|
+
}, 1024);
|
|
413
|
+
}), 128))]),
|
|
414
|
+
_: 1
|
|
415
|
+
})]),
|
|
416
|
+
_: 1
|
|
417
|
+
}, 8, ["borderColor", "borderDimColor"]), createVNode(unref(Box), {
|
|
418
|
+
flexDirection: "column",
|
|
419
|
+
flexGrow: 1,
|
|
420
|
+
minHeight: 0,
|
|
421
|
+
width: "100%",
|
|
422
|
+
overflow: "hidden",
|
|
423
|
+
borderStyle: "single",
|
|
424
|
+
borderColor: unref(focusPane) === "log" ? "cyan" : void 0,
|
|
425
|
+
borderDimColor: unref(focusPane) !== "log",
|
|
426
|
+
paddingX: 1,
|
|
427
|
+
paddingY: 0
|
|
428
|
+
}, {
|
|
429
|
+
default: withCtx(() => [createVNode(unref(Box), { flexShrink: 0 }, {
|
|
430
|
+
default: withCtx(() => [createVNode(unref(Text), {
|
|
431
|
+
bold: "",
|
|
432
|
+
dimColor: unref(focusPane) !== "log"
|
|
433
|
+
}, {
|
|
434
|
+
default: withCtx(() => [createTextVNode(toDisplayString(unref(selectedJob)?.label ?? ""), 1)]),
|
|
435
|
+
_: 1
|
|
436
|
+
}, 8, ["dimColor"])]),
|
|
437
|
+
_: 1
|
|
438
|
+
}), createVNode(unref(Box), {
|
|
439
|
+
flexGrow: 1,
|
|
440
|
+
minHeight: 0,
|
|
441
|
+
width: "100%",
|
|
442
|
+
overflow: "hidden",
|
|
443
|
+
marginTop: 1
|
|
444
|
+
}, {
|
|
445
|
+
default: withCtx(() => [createVNode(unref(ScrollBox), {
|
|
446
|
+
ref_key: "logBox",
|
|
447
|
+
ref: logBox
|
|
448
|
+
}, {
|
|
449
|
+
default: withCtx(() => [createVNode(unref(Text), { wrap: "wrap" }, {
|
|
450
|
+
default: withCtx(() => [createTextVNode(toDisplayString(unref(selectedJob)?.status === "waiting" ? "(等待中…)" : unref(selectedJob)?.output || "(无输出)"), 1)]),
|
|
451
|
+
_: 1
|
|
452
|
+
})]),
|
|
453
|
+
_: 1
|
|
454
|
+
}, 512)]),
|
|
455
|
+
_: 1
|
|
456
|
+
})]),
|
|
457
|
+
_: 1
|
|
458
|
+
}, 8, ["borderColor", "borderDimColor"])]),
|
|
459
|
+
_: 1
|
|
460
|
+
}),
|
|
461
|
+
createVNode(unref(Box), {
|
|
462
|
+
paddingY: 0,
|
|
463
|
+
paddingX: 1,
|
|
464
|
+
marginTop: 1,
|
|
465
|
+
flexShrink: 0
|
|
466
|
+
}, {
|
|
467
|
+
default: withCtx(() => [createVNode(unref(Text), { color: unref(failCount) > 0 && unref(allDone) ? "red" : "gray" }, {
|
|
468
|
+
default: withCtx(() => [createTextVNode(toDisplayString(unref(footerText)), 1)]),
|
|
469
|
+
_: 1
|
|
470
|
+
}, 8, ["color"])]),
|
|
471
|
+
_: 1
|
|
472
|
+
})
|
|
473
|
+
]),
|
|
474
|
+
_: 1
|
|
475
|
+
}, 8, ["width", "height"]);
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/tui/pack/build-dashboard.ts
|
|
481
|
+
async function runBuildDashboard(specs, options = {}) {
|
|
482
|
+
const jobs = createBuildJobs(specs);
|
|
483
|
+
const ctx = createBuildJobsContext();
|
|
484
|
+
const app = createApp(_sfc_main, {
|
|
485
|
+
jobs,
|
|
486
|
+
onCancel: () => ctx.abort(),
|
|
487
|
+
stayOnSuccess: options.stayOnSuccess
|
|
488
|
+
});
|
|
489
|
+
app.mount({
|
|
490
|
+
mode: "fullscreen",
|
|
491
|
+
patchConsole: false,
|
|
492
|
+
exitOnCtrlC: false
|
|
493
|
+
});
|
|
494
|
+
const builds = runBuildJobs(jobs, ctx, options);
|
|
495
|
+
await app.waitUntilExit();
|
|
496
|
+
await builds;
|
|
497
|
+
if (ctx.isAborted()) throw new Error("[pack] 构建已取消");
|
|
498
|
+
const failure = summarizeBuildFailures(jobs);
|
|
499
|
+
if (failure) throw new Error(failure);
|
|
500
|
+
}
|
|
501
|
+
//#endregion
|
|
502
|
+
export { runBuildDashboard };
|