@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 CHANGED
@@ -1,23 +1,129 @@
1
- # vite-plus-starter
1
+ # zp-cli
2
2
 
3
- A starter for creating a Vite Plus project.
3
+ 内部单命令部署工具(`@zhipu/zp-cli`)。用 `zpc pack` 构建打包、`zpc deploy` 在服务器上解压并挂到 Nginx / PM2。
4
4
 
5
- ## Development
5
+ 路线图见 [docs/ROADMAP.md](./docs/ROADMAP.md)。
6
6
 
7
- - Install dependencies:
7
+ ## 使用
8
8
 
9
9
  ```bash
10
- vp install
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
- - Run the unit tests:
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
- - Build the library:
125
+ 在环境变量中设置 `NPM_TOKEN` 后发布:
20
126
 
21
127
  ```bash
22
- vp pack
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 };