@zhipu/zp-cli 0.0.1 → 0.0.3

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
@@ -10,20 +10,24 @@
10
10
  # 在项目根生成 zpc.config
11
11
  zpc init
12
12
 
13
- # 构建并打出 dist.zip(全量)
13
+ # 构建并打出 <projectName>-dist.zip(依据 ~/.zpc 部署记录与部署目录自动判定全量/增量)
14
14
  zpc pack
15
15
 
16
- # api 增量包
17
- zpc pack -i
18
-
19
- # 在服务器上:目录内需有 dist.zip + zpc.config.*(配置不要打进压缩包)
16
+ # 在服务器上:目录内需有 <projectName>-dist.zip + zpc.config.*(配置不要打进压缩包)
20
17
  zpc deploy
21
- zpc deploy -i # 增量(api 保留 public,跳过 web)
22
- zpc deploy --full # 强制全量
23
- zpc deploy --force # 允许抢占冲突的 nginx proxy
18
+ zpc deploy --force # 允许抢占冲突的 nginx proxy
19
+
20
+ # 盘点部署目录与 nginx 配置,标注已记录 / 未记录 / 记录但缺失
21
+ zpc check
24
22
  ```
25
23
 
26
- 可选环境变量:`DEPLOY_DIR`、`DEPLOY_INCREMENTAL`、`DEPLOY_FORCE`;测试可用 `ZPC_API_ROOT` / `ZPC_WEB_ROOT` / `ZPC_NGINX_DEFAULT_D` / `ZPC_DEPLOY_SKIP_RUNTIME`。
24
+ `zpc deploy` 若发现无 `~/.zpc` 部署记录、但 api/web 部署目录非空或已有本项目 nginx 配置,会交互询问:
25
+
26
+ 1. 清空目标文件夹并覆盖(全量,旧目录备份为带时间戳的 `.bak-*`)
27
+ 2. 使用增量部署(api 保留 `public`,跳过 web)
28
+ 3. 取消部署(默认;非交互环境同样默认取消)
29
+
30
+ 可选环境变量:`DEPLOY_DIR`、`DEPLOY_FORCE`;`ZPC_HOME` 覆盖 `~/.zpc`(部署状态记录);测试可用 `ZPC_API_ROOT` / `ZPC_WEB_ROOT` / `ZPC_NGINX_DEFAULT_D` / `ZPC_DEPLOY_SKIP_RUNTIME`。
27
31
 
28
32
  ## 部署约定
29
33
 
@@ -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 };