@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 +13 -9
- package/dist/build-dashboard-CqZVHPH7.mjs +502 -0
- package/dist/build-jobs-m5jbiNTx.mjs +191 -0
- package/dist/cli.mjs +580 -88
- package/package.json +77 -3
package/dist/cli.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { a as findConfigFile, c as loadConfig, l as templateFileName, n as SCHEMA_FILE_NAME, o as isConfigFormat, r as configFileName, s as listConfigFiles, t as CONFIG_FORMATS } from "./config-BZc9pg7J.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import fs from "node:fs";
|
|
3
|
+
import fs, { mkdtempSync, writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { Command } from "@commander-js/extra-typings";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import os, { tmpdir } from "node:os";
|
|
8
|
+
import chalk from "chalk";
|
|
7
9
|
import zlib from "node:zlib";
|
|
8
10
|
//#region package.json
|
|
9
|
-
var version = "0.0.
|
|
11
|
+
var version = "0.0.3";
|
|
10
12
|
var description = "内部单命令部署工具";
|
|
11
13
|
//#endregion
|
|
12
14
|
//#region src/utils/deploy-paths.ts
|
|
@@ -33,7 +35,82 @@ function shouldSkipDeployRuntime() {
|
|
|
33
35
|
return process.env.ZPC_DEPLOY_SKIP_RUNTIME === "1";
|
|
34
36
|
}
|
|
35
37
|
//#endregion
|
|
38
|
+
//#region src/utils/tty-shim.ts
|
|
39
|
+
/** 子进程预加载脚本:让 piped stdout/stderr 表现为 TTY,以启用 Vite 等工具的进度覆写。 */
|
|
40
|
+
const TTY_SHIM_CJS = String.raw`'use strict';
|
|
41
|
+
|
|
42
|
+
function patchStdStream(stream) {
|
|
43
|
+
if (!stream || stream.__zpcTtyPatched) return;
|
|
44
|
+
stream.__zpcTtyPatched = true;
|
|
45
|
+
Object.defineProperty(stream, 'isTTY', {
|
|
46
|
+
value: true,
|
|
47
|
+
enumerable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
});
|
|
50
|
+
if (stream.columns == null) stream.columns = 120;
|
|
51
|
+
if (stream.rows == null) stream.rows = 30;
|
|
52
|
+
if (typeof stream.clearLine !== 'function') {
|
|
53
|
+
stream.clearLine = function clearLine(dir, cb) {
|
|
54
|
+
if (typeof dir === 'function') {
|
|
55
|
+
cb = dir;
|
|
56
|
+
dir = 0;
|
|
57
|
+
}
|
|
58
|
+
if (dir === 0 || dir === undefined) stream.write('\x1b[2K\r');
|
|
59
|
+
cb?.();
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (typeof stream.cursorTo !== 'function') {
|
|
64
|
+
stream.cursorTo = function cursorTo(x, y, cb) {
|
|
65
|
+
if (typeof y === 'function') {
|
|
66
|
+
cb = y;
|
|
67
|
+
y = undefined;
|
|
68
|
+
}
|
|
69
|
+
if (typeof x === 'function') {
|
|
70
|
+
cb = x;
|
|
71
|
+
x = 0;
|
|
72
|
+
y = undefined;
|
|
73
|
+
}
|
|
74
|
+
if (y === undefined) stream.write('\x1b[' + (x + 1) + 'G');
|
|
75
|
+
else stream.write('\x1b[' + (y + 1) + ';' + (x + 1) + 'H');
|
|
76
|
+
cb?.();
|
|
77
|
+
return true;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
patchStdStream(process.stdout);
|
|
83
|
+
patchStdStream(process.stderr);
|
|
84
|
+
`;
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/utils/tty-shim-path.ts
|
|
87
|
+
let cachedShimPath;
|
|
88
|
+
/** 将 tty shim 写入临时目录并缓存路径,供 NODE_OPTIONS --require 使用。 */
|
|
89
|
+
function ensureTtyShimPath() {
|
|
90
|
+
if (cachedShimPath) return cachedShimPath;
|
|
91
|
+
const dir = mkdtempSync(path.join(tmpdir(), "zpc-tty-"));
|
|
92
|
+
cachedShimPath = path.join(dir, "tty-shim.cjs");
|
|
93
|
+
writeFileSync(cachedShimPath, TTY_SHIM_CJS, "utf8");
|
|
94
|
+
return cachedShimPath;
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
36
97
|
//#region src/utils/run.ts
|
|
98
|
+
function killProcessTree(child) {
|
|
99
|
+
if (!child.pid || child.killed) return;
|
|
100
|
+
if (process.platform === "win32") {
|
|
101
|
+
spawn("taskkill", [
|
|
102
|
+
"/pid",
|
|
103
|
+
String(child.pid),
|
|
104
|
+
"/f",
|
|
105
|
+
"/t"
|
|
106
|
+
], {
|
|
107
|
+
stdio: "ignore",
|
|
108
|
+
windowsHide: true
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
child.kill("SIGTERM");
|
|
113
|
+
}
|
|
37
114
|
function runCommand(command, args, opts = {}) {
|
|
38
115
|
return new Promise((resolve, reject) => {
|
|
39
116
|
const useCmd = opts.windowsCmd && process.platform === "win32";
|
|
@@ -64,6 +141,63 @@ function runCommand(command, args, opts = {}) {
|
|
|
64
141
|
});
|
|
65
142
|
});
|
|
66
143
|
}
|
|
144
|
+
function decodeChunk(chunk) {
|
|
145
|
+
return typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
146
|
+
}
|
|
147
|
+
function quoteNodeOption(value) {
|
|
148
|
+
if (!/[ \t"]/u.test(value)) return value;
|
|
149
|
+
return `"${value.replace(/"/g, "\\\"")}"`;
|
|
150
|
+
}
|
|
151
|
+
function buildCapturedEnv(opts) {
|
|
152
|
+
const env = opts.env ? {
|
|
153
|
+
...process.env,
|
|
154
|
+
...opts.env
|
|
155
|
+
} : { ...process.env };
|
|
156
|
+
if (!opts.fakeTty) return env;
|
|
157
|
+
const requireFlag = `--require ${quoteNodeOption(ensureTtyShimPath())}`;
|
|
158
|
+
env.NODE_OPTIONS = env.NODE_OPTIONS ? `${env.NODE_OPTIONS} ${requireFlag}` : requireFlag;
|
|
159
|
+
delete env.CI;
|
|
160
|
+
return env;
|
|
161
|
+
}
|
|
162
|
+
function runCommandCaptured(command, args, opts = {}) {
|
|
163
|
+
const useCmd = opts.windowsCmd && process.platform === "win32";
|
|
164
|
+
const child = spawn(useCmd ? process.env.ComSpec || "cmd.exe" : command, useCmd ? [
|
|
165
|
+
"/d",
|
|
166
|
+
"/s",
|
|
167
|
+
"/c",
|
|
168
|
+
command,
|
|
169
|
+
...args
|
|
170
|
+
] : args, {
|
|
171
|
+
cwd: opts.cwd,
|
|
172
|
+
stdio: [
|
|
173
|
+
"ignore",
|
|
174
|
+
"pipe",
|
|
175
|
+
"pipe"
|
|
176
|
+
],
|
|
177
|
+
windowsHide: true,
|
|
178
|
+
env: buildCapturedEnv(opts)
|
|
179
|
+
});
|
|
180
|
+
child.stdout?.on("data", (chunk) => {
|
|
181
|
+
opts.onStdout?.(decodeChunk(chunk));
|
|
182
|
+
});
|
|
183
|
+
child.stderr?.on("data", (chunk) => {
|
|
184
|
+
opts.onStderr?.(decodeChunk(chunk));
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
promise: new Promise((resolve, reject) => {
|
|
188
|
+
child.on("error", (err) => {
|
|
189
|
+
reject(/* @__PURE__ */ new Error(`无法启动 ${command}: ${err.message}`));
|
|
190
|
+
});
|
|
191
|
+
child.on("exit", (code, signal) => {
|
|
192
|
+
resolve({
|
|
193
|
+
exitCode: code,
|
|
194
|
+
signal
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}),
|
|
198
|
+
kill: () => killProcessTree(child)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
67
201
|
//#endregion
|
|
68
202
|
//#region src/utils/templates.ts
|
|
69
203
|
const require = createRequire(import.meta.url);
|
|
@@ -187,17 +321,20 @@ function listNamedDirs(parent) {
|
|
|
187
321
|
if (!fs.existsSync(parent)) return [];
|
|
188
322
|
return fs.readdirSync(parent, { withFileTypes: true }).filter((d) => d.isDirectory() && isSafeDirName(d.name) && !d.name.startsWith(".")).map((d) => d.name);
|
|
189
323
|
}
|
|
190
|
-
|
|
324
|
+
function backupTimestamp() {
|
|
325
|
+
const d = /* @__PURE__ */ new Date();
|
|
326
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
327
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 原子风格替换目录:现目录改名为带时间戳的唯一 `.bak-*` 备份 → 拷入新内容。
|
|
331
|
+
* 备份名含时间戳,多次覆盖不会互相覆盖,历史版本得以保留;失败从该备份恢复。
|
|
332
|
+
*/
|
|
191
333
|
function deployDir(src, dest, log) {
|
|
192
334
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
fs.rmSync(bak, {
|
|
197
|
-
recursive: true,
|
|
198
|
-
force: true
|
|
199
|
-
});
|
|
200
|
-
}
|
|
335
|
+
const stamp = backupTimestamp();
|
|
336
|
+
let bak = `${dest}.bak-${stamp}`;
|
|
337
|
+
for (let i = 1; fs.existsSync(bak); i++) bak = `${dest}.bak-${stamp}-${i}`;
|
|
201
338
|
if (fs.existsSync(dest)) {
|
|
202
339
|
log(`备份 ${dest} -> ${bak}`);
|
|
203
340
|
fs.renameSync(dest, bak);
|
|
@@ -304,6 +441,273 @@ async function configureNginxProxies(projectName, apis, force, log) {
|
|
|
304
441
|
log("nginx 已 reload");
|
|
305
442
|
}
|
|
306
443
|
//#endregion
|
|
444
|
+
//#region src/utils/deploy-state.ts
|
|
445
|
+
const DEPLOY_STATE_VERSION = 1;
|
|
446
|
+
const DEPLOY_STATE_FILE = "deploy-state.json";
|
|
447
|
+
function getZpcHome() {
|
|
448
|
+
return process.env.ZPC_HOME ?? path.join(os.homedir(), ".zpc");
|
|
449
|
+
}
|
|
450
|
+
function getDeployStatePath() {
|
|
451
|
+
return path.join(getZpcHome(), DEPLOY_STATE_FILE);
|
|
452
|
+
}
|
|
453
|
+
/** 以项目根目录绝对路径为键(统一分隔符,便于跨平台比对)。 */
|
|
454
|
+
function deployStateProjectKey(root) {
|
|
455
|
+
return path.resolve(root).replace(/\\/g, "/");
|
|
456
|
+
}
|
|
457
|
+
function emptyDeployState() {
|
|
458
|
+
return {
|
|
459
|
+
version: DEPLOY_STATE_VERSION,
|
|
460
|
+
projects: {}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function readDeployStateFile() {
|
|
464
|
+
const filePath = getDeployStatePath();
|
|
465
|
+
if (!fs.existsSync(filePath)) return emptyDeployState();
|
|
466
|
+
let parsed;
|
|
467
|
+
try {
|
|
468
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
469
|
+
} catch {
|
|
470
|
+
throw new Error(`[deploy] 无法解析 ${filePath},请检查 JSON 格式或删除后重试`);
|
|
471
|
+
}
|
|
472
|
+
if (typeof parsed !== "object" || parsed == null || parsed.version !== DEPLOY_STATE_VERSION || typeof parsed.projects !== "object" || parsed.projects == null) throw new Error(`[deploy] ${filePath} 格式无效,请删除后重试`);
|
|
473
|
+
return parsed;
|
|
474
|
+
}
|
|
475
|
+
function writeDeployStateFile(state) {
|
|
476
|
+
const home = getZpcHome();
|
|
477
|
+
fs.mkdirSync(home, { recursive: true });
|
|
478
|
+
fs.writeFileSync(getDeployStatePath(), `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
479
|
+
}
|
|
480
|
+
function hasFullDeployRecord(root) {
|
|
481
|
+
const state = readDeployStateFile();
|
|
482
|
+
return deployStateProjectKey(root) in state.projects;
|
|
483
|
+
}
|
|
484
|
+
function markProjectFullDeployed(root, projectName) {
|
|
485
|
+
const state = readDeployStateFile();
|
|
486
|
+
const key = deployStateProjectKey(root);
|
|
487
|
+
state.projects[key] = {
|
|
488
|
+
projectName,
|
|
489
|
+
root: key,
|
|
490
|
+
fullDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
491
|
+
};
|
|
492
|
+
writeDeployStateFile(state);
|
|
493
|
+
}
|
|
494
|
+
/** 返回 ~/.zpc/deploy-state.json 中已记录的所有 projectName(去重、排序)。 */
|
|
495
|
+
function listRecordedProjectNames() {
|
|
496
|
+
const state = readDeployStateFile();
|
|
497
|
+
return [...new Set(Object.values(state.projects).map((p) => p.projectName))].sort();
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/utils/check-state.ts
|
|
501
|
+
/** 从 nginx 配置的 `owned-by: projectName/name` 头解析出 projectName。 */
|
|
502
|
+
function nginxProjectName(text) {
|
|
503
|
+
const owned = parseNginxOwnedBy(text);
|
|
504
|
+
if (!owned) return null;
|
|
505
|
+
const idx = owned.indexOf("/");
|
|
506
|
+
return idx >= 0 ? owned.slice(0, idx) : owned;
|
|
507
|
+
}
|
|
508
|
+
/** 盘点部署痕迹(api/web 目录 + nginx 配置)与部署记录,合并成按系统分组的结果。 */
|
|
509
|
+
function collectSystemChecks() {
|
|
510
|
+
const recorded = new Set(listRecordedProjectNames());
|
|
511
|
+
const apiByProject = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const projectName of listNamedDirs(getApiRoot())) apiByProject.set(projectName, listNamedDirs(path.join(getApiRoot(), projectName)));
|
|
513
|
+
const webByProject = /* @__PURE__ */ new Map();
|
|
514
|
+
for (const projectName of listNamedDirs(getWebRoot())) webByProject.set(projectName, listNamedDirs(path.join(getWebRoot(), projectName)));
|
|
515
|
+
const nginxByProject = /* @__PURE__ */ new Map();
|
|
516
|
+
const unownedNginxConfs = [];
|
|
517
|
+
const nginxDir = getNginxDefaultD();
|
|
518
|
+
if (fs.existsSync(nginxDir)) for (const name of fs.readdirSync(nginxDir)) {
|
|
519
|
+
if (!name.endsWith(".conf")) continue;
|
|
520
|
+
const full = path.join(nginxDir, name);
|
|
521
|
+
let text = "";
|
|
522
|
+
try {
|
|
523
|
+
text = fs.readFileSync(full, "utf8");
|
|
524
|
+
} catch {}
|
|
525
|
+
const projectName = nginxProjectName(text);
|
|
526
|
+
if (projectName) {
|
|
527
|
+
const arr = nginxByProject.get(projectName) ?? [];
|
|
528
|
+
arr.push(full);
|
|
529
|
+
nginxByProject.set(projectName, arr);
|
|
530
|
+
} else unownedNginxConfs.push(full);
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
systems: [.../* @__PURE__ */ new Set([
|
|
534
|
+
...recorded,
|
|
535
|
+
...apiByProject.keys(),
|
|
536
|
+
...webByProject.keys(),
|
|
537
|
+
...nginxByProject.keys()
|
|
538
|
+
])].sort().map((projectName) => ({
|
|
539
|
+
projectName,
|
|
540
|
+
recorded: recorded.has(projectName),
|
|
541
|
+
apiDirs: apiByProject.get(projectName) ?? [],
|
|
542
|
+
webDirs: webByProject.get(projectName) ?? [],
|
|
543
|
+
nginxConfs: nginxByProject.get(projectName) ?? []
|
|
544
|
+
})),
|
|
545
|
+
unownedNginxConfs
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/utils/log.ts
|
|
550
|
+
function tagged(tag, message) {
|
|
551
|
+
return `${chalk.cyan(`[${tag}]`)} ${message}`;
|
|
552
|
+
}
|
|
553
|
+
/** 带命令前缀的彩色日志:info 默认、success 绿、warn 黄。 */
|
|
554
|
+
function createLogger(tag) {
|
|
555
|
+
return {
|
|
556
|
+
info: (message) => {
|
|
557
|
+
console.log(tagged(tag, message));
|
|
558
|
+
},
|
|
559
|
+
success: (message) => {
|
|
560
|
+
console.log(tagged(tag, chalk.green(message)));
|
|
561
|
+
},
|
|
562
|
+
warn: (message) => {
|
|
563
|
+
console.log(tagged(tag, chalk.yellow(message)));
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function logError(message) {
|
|
568
|
+
console.error(chalk.red(message));
|
|
569
|
+
}
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/commands/check.ts
|
|
572
|
+
const log$3 = createLogger("check");
|
|
573
|
+
function systemStatus(system) {
|
|
574
|
+
if (!system.recorded) return "未记录";
|
|
575
|
+
return system.apiDirs.length > 0 || system.webDirs.length > 0 || system.nginxConfs.length > 0 ? "已记录" : "记录但缺失";
|
|
576
|
+
}
|
|
577
|
+
function runCheck() {
|
|
578
|
+
const { systems, unownedNginxConfs } = collectSystemChecks();
|
|
579
|
+
if (systems.length === 0 && unownedNginxConfs.length === 0) {
|
|
580
|
+
log$3.info("未发现任何部署记录或部署痕迹");
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
for (const system of systems) {
|
|
584
|
+
const status = systemStatus(system);
|
|
585
|
+
const head = `[${status}] ${system.projectName}`;
|
|
586
|
+
if (status === "已记录") log$3.info(head);
|
|
587
|
+
else log$3.warn(head);
|
|
588
|
+
if (!(system.apiDirs.length > 0 || system.webDirs.length > 0 || system.nginxConfs.length > 0)) continue;
|
|
589
|
+
if (system.apiDirs.length > 0) log$3.info(` api: ${system.apiDirs.join(", ")}`);
|
|
590
|
+
if (system.webDirs.length > 0) log$3.info(` web: ${system.webDirs.join(", ")}`);
|
|
591
|
+
if (system.nginxConfs.length > 0) log$3.info(` nginx: ${system.nginxConfs.join(", ")}`);
|
|
592
|
+
}
|
|
593
|
+
if (unownedNginxConfs.length > 0) {
|
|
594
|
+
log$3.warn(`[未归属 nginx 配置] ${unownedNginxConfs.length} 个`);
|
|
595
|
+
for (const conf of unownedNginxConfs) log$3.warn(` ${conf}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function createCheckCommand() {
|
|
599
|
+
return new Command("check").description("盘点部署目录与 nginx 配置,标注已记录 / 未记录 / 记录但缺失").action(async () => {
|
|
600
|
+
runCheck();
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
//#endregion
|
|
604
|
+
//#region src/utils/dist-zip.ts
|
|
605
|
+
const ZPC_GITIGNORE_HEADER = "# zpc files";
|
|
606
|
+
const ZPC_DIST_ZIP_IGNORE = "*-dist.zip";
|
|
607
|
+
function distZipFileName(projectName) {
|
|
608
|
+
return `${projectName}-dist.zip`;
|
|
609
|
+
}
|
|
610
|
+
function distZipPath(projectRoot, projectName) {
|
|
611
|
+
return path.join(projectRoot, distZipFileName(projectName));
|
|
612
|
+
}
|
|
613
|
+
function gitignoreHasDistZipRule(content) {
|
|
614
|
+
return content.split(/\r?\n/u).some((line) => line.trim() === ZPC_DIST_ZIP_IGNORE);
|
|
615
|
+
}
|
|
616
|
+
/** 若项目 .gitignore 缺少 *-dist.zip 规则则追加,返回是否新写入。 */
|
|
617
|
+
function ensureZpcGitignore(projectRoot) {
|
|
618
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
619
|
+
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
620
|
+
if (gitignoreHasDistZipRule(existing)) return false;
|
|
621
|
+
const block = `${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}\n${ZPC_GITIGNORE_HEADER}\n${ZPC_DIST_ZIP_IGNORE}\n`;
|
|
622
|
+
fs.writeFileSync(gitignorePath, `${existing}${block}`, "utf8");
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/utils/deploy-conflict.ts
|
|
627
|
+
function isDirectoryNonEmpty$1(dir) {
|
|
628
|
+
if (!fs.existsSync(dir)) return false;
|
|
629
|
+
try {
|
|
630
|
+
return fs.readdirSync(dir).length > 0;
|
|
631
|
+
} catch {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/** 收集「无 ~/.zpc 记录」时应提示用户的部署痕迹:api/web 目录非空、已存在/冲突的 nginx 配置。 */
|
|
636
|
+
function collectDeployConflicts(config) {
|
|
637
|
+
const projectName = config.projectName;
|
|
638
|
+
const conflicts = [];
|
|
639
|
+
if (!projectName) return conflicts;
|
|
640
|
+
for (const item of config.api ?? []) {
|
|
641
|
+
const dir = path.join(getApiRoot(), projectName, item.name);
|
|
642
|
+
if (isDirectoryNonEmpty$1(dir)) conflicts.push({
|
|
643
|
+
kind: "api-dir",
|
|
644
|
+
message: `api 部署目录非空: ${dir}`
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
for (const item of config.web ?? []) {
|
|
648
|
+
const dir = path.join(getWebRoot(), projectName, item.name);
|
|
649
|
+
if (isDirectoryNonEmpty$1(dir)) conflicts.push({
|
|
650
|
+
kind: "web-dir",
|
|
651
|
+
message: `web 部署目录非空: ${dir}`
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const defaultD = getNginxDefaultD();
|
|
655
|
+
if (!fs.existsSync(defaultD)) return conflicts;
|
|
656
|
+
const proxies = (config.api ?? []).map((item) => item.proxy).filter((p) => !!p);
|
|
657
|
+
for (const name of fs.readdirSync(defaultD)) {
|
|
658
|
+
if (!name.endsWith(".conf")) continue;
|
|
659
|
+
const full = path.join(defaultD, name);
|
|
660
|
+
let text;
|
|
661
|
+
try {
|
|
662
|
+
text = fs.readFileSync(full, "utf8");
|
|
663
|
+
} catch {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const owned = parseNginxOwnedBy(text);
|
|
667
|
+
if (owned?.startsWith(`${projectName}/`) || name.startsWith(`${projectName}-`)) {
|
|
668
|
+
conflicts.push({
|
|
669
|
+
kind: "nginx",
|
|
670
|
+
message: `已存在本项目 nginx 配置: ${full}`
|
|
671
|
+
});
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
for (const proxy of proxies) if (confDefinesProxy(text, proxy)) {
|
|
675
|
+
conflicts.push({
|
|
676
|
+
kind: "nginx",
|
|
677
|
+
message: `nginx 配置 ${full} 已定义 proxy ${nginxLocation(proxy)}(归属 ${owned ?? "未知"})`
|
|
678
|
+
});
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return conflicts;
|
|
683
|
+
}
|
|
684
|
+
function isInteractive() {
|
|
685
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
686
|
+
}
|
|
687
|
+
/** 交互询问部署方式;非 TTY 环境直接返回取消(默认)。 */
|
|
688
|
+
async function promptDeployResolution() {
|
|
689
|
+
if (!isInteractive()) return "cancel";
|
|
690
|
+
const { select } = await import("@inquirer/prompts");
|
|
691
|
+
return await select({
|
|
692
|
+
message: "检测到已有部署痕迹(无 ~/.zpc 记录),请选择部署方式:",
|
|
693
|
+
choices: [
|
|
694
|
+
{
|
|
695
|
+
name: "清空目标文件夹并覆盖(全量,旧目录将备份)",
|
|
696
|
+
value: "full"
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: "使用增量部署(api 保留 public,跳过 web)",
|
|
700
|
+
value: "incremental"
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
name: "取消部署",
|
|
704
|
+
value: "cancel"
|
|
705
|
+
}
|
|
706
|
+
],
|
|
707
|
+
default: "cancel"
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
//#endregion
|
|
307
711
|
//#region src/utils/zip.ts
|
|
308
712
|
/** ZIP End of Central Directory 签名 */
|
|
309
713
|
const SIG_EOCD = 101010256;
|
|
@@ -419,27 +823,7 @@ async function zipDirectory(stagingDir, zipPath, cwd) {
|
|
|
419
823
|
}
|
|
420
824
|
//#endregion
|
|
421
825
|
//#region src/commands/deploy.ts
|
|
422
|
-
|
|
423
|
-
console.log(`[deploy] ${message}`);
|
|
424
|
-
}
|
|
425
|
-
function envFlag(name) {
|
|
426
|
-
const value = process.env[name];
|
|
427
|
-
return value === "1" || value === "true";
|
|
428
|
-
}
|
|
429
|
-
function resolveFlags(options) {
|
|
430
|
-
const forceFull = Boolean(options.full);
|
|
431
|
-
const force = Boolean(options.force) || envFlag("DEPLOY_FORCE");
|
|
432
|
-
let wantIncremental = Boolean(options.incremental) || envFlag("DEPLOY_INCREMENTAL");
|
|
433
|
-
if (forceFull) wantIncremental = false;
|
|
434
|
-
return {
|
|
435
|
-
wantIncremental,
|
|
436
|
-
forceFull,
|
|
437
|
-
force
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
function hasProjectFiles(dir) {
|
|
441
|
-
return fs.existsSync(path.join(dir, "dist.zip")) && Boolean(findConfigFile(dir));
|
|
442
|
-
}
|
|
826
|
+
const log$2 = createLogger("deploy");
|
|
443
827
|
function resolveProjectDir(dirArg) {
|
|
444
828
|
let dir;
|
|
445
829
|
if (dirArg) {
|
|
@@ -448,39 +832,40 @@ function resolveProjectDir(dirArg) {
|
|
|
448
832
|
dir = fs.statSync(abs).isDirectory() ? abs : path.dirname(abs);
|
|
449
833
|
} else if (process.env.DEPLOY_DIR) dir = path.resolve(process.env.DEPLOY_DIR);
|
|
450
834
|
else dir = process.cwd();
|
|
451
|
-
if (!
|
|
835
|
+
if (!findConfigFile(dir)) throw new Error(`[deploy] ${dir} 中需要 zpc.config.*(配置请单独上传,不要打进压缩包)`);
|
|
452
836
|
return dir;
|
|
453
837
|
}
|
|
454
838
|
async function deployKind(stagingDir, items, kind, destRoot, projectName, incremental) {
|
|
455
839
|
const packed = new Set(listNamedDirs(path.join(stagingDir, kind)));
|
|
456
840
|
const configured = items ?? [];
|
|
457
|
-
for (const extra of packed) if (!configured.some((item) => item.name === extra)) log$
|
|
841
|
+
for (const extra of packed) if (!configured.some((item) => item.name === extra)) log$2.warn(`跳过压缩包中的 ${kind}/${extra}(未在配置中列出)`);
|
|
458
842
|
for (const item of configured) {
|
|
459
843
|
const src = path.join(stagingDir, kind, item.name);
|
|
460
844
|
if (!fs.existsSync(src)) throw new Error(`[deploy] 配置中有 ${kind}/${item.name},但压缩包中没有对应目录`);
|
|
461
845
|
const dest = path.join(destRoot, projectName, item.name);
|
|
462
846
|
const pm2Name = `${projectName}-${item.name}`;
|
|
463
|
-
if (kind === "api" && incremental && fs.existsSync(dest)) deployApiIncremental(src, dest, log$
|
|
847
|
+
if (kind === "api" && incremental && fs.existsSync(dest)) deployApiIncremental(src, dest, log$2.info);
|
|
464
848
|
else {
|
|
465
|
-
if (kind === "api" && incremental && !fs.existsSync(dest)) log$
|
|
466
|
-
deployDir(src, dest, log$
|
|
849
|
+
if (kind === "api" && incremental && !fs.existsSync(dest)) log$2.warn(`${dest} 不存在,增量改为全量部署`);
|
|
850
|
+
deployDir(src, dest, log$2.info);
|
|
467
851
|
}
|
|
468
|
-
if (kind === "api" && !shouldSkipDeployRuntime()) await installAndReloadApi(pm2Name, dest, item, log$
|
|
852
|
+
if (kind === "api" && !shouldSkipDeployRuntime()) await installAndReloadApi(pm2Name, dest, item, log$2.info);
|
|
469
853
|
}
|
|
470
854
|
}
|
|
471
|
-
async function runDeploy(dir, options) {
|
|
472
|
-
const
|
|
855
|
+
async function runDeploy(dir, options, prompt = promptDeployResolution) {
|
|
856
|
+
const force = Boolean(options.force) || process.env.DEPLOY_FORCE === "1" || process.env.DEPLOY_FORCE === "true";
|
|
473
857
|
const projectDir = resolveProjectDir(dir);
|
|
474
858
|
const stagingDir = path.join(projectDir, ".deploy-staging");
|
|
475
|
-
const zipPath = path.join(projectDir, "dist.zip");
|
|
476
859
|
const { file, config } = await loadConfig(projectDir);
|
|
477
860
|
assertDeployConfig(config);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
log$
|
|
481
|
-
log$
|
|
861
|
+
const zipPath = distZipPath(projectDir, config.projectName);
|
|
862
|
+
if (!fs.existsSync(zipPath)) throw new Error(`[deploy] 找不到 ${zipPath},请先在该目录执行 zpc pack(配置请单独上传,不要打进压缩包)`);
|
|
863
|
+
log$2.info(`项目目录: ${projectDir}`);
|
|
864
|
+
log$2.info(`压缩包: ${zipPath}`);
|
|
865
|
+
log$2.info(`配置: ${file}`);
|
|
866
|
+
log$2.info(`项目: ${config.projectName}`);
|
|
482
867
|
try {
|
|
483
|
-
log$
|
|
868
|
+
log$2.info(`解压 ${zipPath}`);
|
|
484
869
|
await extractArchive(zipPath, stagingDir);
|
|
485
870
|
} catch (err) {
|
|
486
871
|
fs.rmSync(stagingDir, {
|
|
@@ -490,17 +875,35 @@ async function runDeploy(dir, options) {
|
|
|
490
875
|
const message = err instanceof Error ? err.message : String(err);
|
|
491
876
|
throw new Error(`[deploy] 解压失败: ${message}`);
|
|
492
877
|
}
|
|
493
|
-
const
|
|
494
|
-
|
|
878
|
+
const zipIsIncremental = fs.existsSync(path.join(stagingDir, INCREMENTAL_MARKER));
|
|
879
|
+
const hadRecord = hasFullDeployRecord(projectDir);
|
|
880
|
+
let incremental;
|
|
881
|
+
let resolvedFromConflicts = false;
|
|
882
|
+
if (hadRecord) incremental = true;
|
|
883
|
+
else {
|
|
884
|
+
const conflicts = collectDeployConflicts(config);
|
|
885
|
+
if (conflicts.length > 0) {
|
|
886
|
+
for (const conflict of conflicts) log$2.warn(conflict.message);
|
|
887
|
+
const choice = await prompt();
|
|
888
|
+
if (choice === "cancel") {
|
|
889
|
+
log$2.warn("已取消部署");
|
|
890
|
+
fs.rmSync(stagingDir, {
|
|
891
|
+
recursive: true,
|
|
892
|
+
force: true
|
|
893
|
+
});
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
incremental = choice === "incremental";
|
|
897
|
+
resolvedFromConflicts = true;
|
|
898
|
+
} else incremental = zipIsIncremental;
|
|
899
|
+
}
|
|
495
900
|
try {
|
|
496
|
-
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
log$1(`模式: ${incremental ? "增量(api 保留 public,跳过 web)" : "全量"}`);
|
|
500
|
-
if (force) log$1("已启用 --force(允许抢占冲突的 nginx proxy)");
|
|
901
|
+
log$2.info(`模式: ${incremental ? "增量(api 保留 public,跳过 web)" : "全量"}`);
|
|
902
|
+
if (!incremental && !hadRecord) log$2.info("首次部署该项目,完成后将记录到 ~/.zpc/deploy-state.json");
|
|
903
|
+
if (force) log$2.warn("已启用 --force(允许抢占冲突的 nginx proxy)");
|
|
501
904
|
await deployKind(stagingDir, config.api, "api", getApiRoot(), config.projectName, incremental);
|
|
502
905
|
if (!incremental) await deployKind(stagingDir, config.web, "web", getWebRoot(), config.projectName, false);
|
|
503
|
-
else if (config.web?.length) log$
|
|
906
|
+
else if (config.web?.length) log$2.warn("增量模式跳过 web 部署");
|
|
504
907
|
} finally {
|
|
505
908
|
fs.rmSync(stagingDir, {
|
|
506
909
|
recursive: true,
|
|
@@ -508,21 +911,28 @@ async function runDeploy(dir, options) {
|
|
|
508
911
|
});
|
|
509
912
|
}
|
|
510
913
|
if (!shouldSkipDeployRuntime()) {
|
|
511
|
-
await configureNginxProxies(config.projectName, config.api, force, log$
|
|
914
|
+
await configureNginxProxies(config.projectName, config.api, force, log$2.info);
|
|
512
915
|
if (config.api?.length) await runCommand("pm2", ["save"], { ignoreExit: true });
|
|
513
916
|
}
|
|
917
|
+
if (!hasFullDeployRecord(projectDir)) {
|
|
918
|
+
markProjectFullDeployed(projectDir, config.projectName);
|
|
919
|
+
if (!incremental) log$2.info("已记录全量部署状态到 ~/.zpc/deploy-state.json,下次 deploy 将默认增量");
|
|
920
|
+
else if (resolvedFromConflicts) log$2.info("已根据现有部署目录建立部署记录,下次 deploy 将默认增量");
|
|
921
|
+
else if (zipIsIncremental) log$2.info("已记录部署状态,下次 deploy 将默认增量");
|
|
922
|
+
}
|
|
514
923
|
const apiCount = config.api?.length ?? 0;
|
|
515
924
|
const webCount = incremental ? 0 : config.web?.length ?? 0;
|
|
516
925
|
const prefix = `${config.projectName}/<name>`;
|
|
517
|
-
log$
|
|
926
|
+
log$2.success(`完成: api ${apiCount} 个 -> ${getApiRoot()}/${prefix},web ${webCount} 个 -> ${getWebRoot()}/${prefix}`);
|
|
518
927
|
}
|
|
519
928
|
function createDeployCommand() {
|
|
520
|
-
return new Command("deploy").description("按 zpc.config 将 dist.zip 部署到服务器").argument("[dir]", "项目目录,默认为当前目录").option("
|
|
929
|
+
return new Command("deploy").description("按 zpc.config 将 <projectName>-dist.zip 部署到服务器").argument("[dir]", "项目目录,默认为当前目录").option("--force", "允许抢占冲突的 nginx proxy").action(async (dir, options) => {
|
|
521
930
|
await runDeploy(dir, options);
|
|
522
931
|
});
|
|
523
932
|
}
|
|
524
933
|
//#endregion
|
|
525
934
|
//#region src/commands/init.ts
|
|
935
|
+
const log$1 = createLogger("init");
|
|
526
936
|
async function runInit(dir, options) {
|
|
527
937
|
const format = resolveFormat(options.template);
|
|
528
938
|
const root = path.resolve(dir ?? process.cwd());
|
|
@@ -536,10 +946,10 @@ async function runInit(dir, options) {
|
|
|
536
946
|
fs.mkdirSync(root, { recursive: true });
|
|
537
947
|
for (const file of existing) if (file !== dest) fs.rmSync(file);
|
|
538
948
|
fs.copyFileSync(resolveTemplateFile(format), dest);
|
|
539
|
-
|
|
949
|
+
log$1.success(`已写入 ${dest}`);
|
|
540
950
|
if (format === "json") {
|
|
541
951
|
fs.copyFileSync(resolveSchemaFile(), schemaDest);
|
|
542
|
-
|
|
952
|
+
log$1.success(`已写入 ${schemaDest}`);
|
|
543
953
|
} else if (fs.existsSync(schemaDest)) fs.rmSync(schemaDest);
|
|
544
954
|
}
|
|
545
955
|
function resolveFormat(template) {
|
|
@@ -553,14 +963,59 @@ function createInitCommand() {
|
|
|
553
963
|
});
|
|
554
964
|
}
|
|
555
965
|
//#endregion
|
|
556
|
-
//#region src/
|
|
557
|
-
function
|
|
558
|
-
|
|
966
|
+
//#region src/utils/incremental-mode.ts
|
|
967
|
+
function isDirectoryNonEmpty(dir) {
|
|
968
|
+
if (!fs.existsSync(dir)) return false;
|
|
969
|
+
try {
|
|
970
|
+
return fs.readdirSync(dir).length > 0;
|
|
971
|
+
} catch {
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
/** 配置中所有部署目录(api + web)里已存在且非空的目录。 */
|
|
976
|
+
function findNonemptyDeployDirs(config) {
|
|
977
|
+
const projectName = config.projectName;
|
|
978
|
+
if (!projectName) return [];
|
|
979
|
+
const found = [];
|
|
980
|
+
for (const item of config.api ?? []) {
|
|
981
|
+
const deployDir = path.join(getApiRoot(), projectName, item.name);
|
|
982
|
+
if (isDirectoryNonEmpty(deployDir)) found.push(deployDir);
|
|
983
|
+
}
|
|
984
|
+
for (const item of config.web ?? []) {
|
|
985
|
+
const deployDir = path.join(getWebRoot(), projectName, item.name);
|
|
986
|
+
if (isDirectoryNonEmpty(deployDir)) found.push(deployDir);
|
|
987
|
+
}
|
|
988
|
+
return found;
|
|
559
989
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
990
|
+
/**
|
|
991
|
+
* 全量 / 增量判定(pack 只读、deploy 读后写入):
|
|
992
|
+
* 1. ~/.zpc/deploy-state.json 有记录 → 增量
|
|
993
|
+
* 2. 无记录但 api/web 部署目录非空 → 交由调用方决策(不直接判定)
|
|
994
|
+
* 3. 无记录但 zip 含增量标记 → 增量
|
|
995
|
+
* 4. 否则全量
|
|
996
|
+
*/
|
|
997
|
+
function resolveIncrementalMode(projectRoot, config, options = {}) {
|
|
998
|
+
if (hasFullDeployRecord(projectRoot)) return {
|
|
999
|
+
incremental: true,
|
|
1000
|
+
existingDeployDirs: []
|
|
1001
|
+
};
|
|
1002
|
+
const existingDeployDirs = findNonemptyDeployDirs(config);
|
|
1003
|
+
if (existingDeployDirs.length > 0) return {
|
|
1004
|
+
incremental: false,
|
|
1005
|
+
existingDeployDirs
|
|
1006
|
+
};
|
|
1007
|
+
if (options.zipIsIncremental) return {
|
|
1008
|
+
incremental: true,
|
|
1009
|
+
existingDeployDirs: []
|
|
1010
|
+
};
|
|
1011
|
+
return {
|
|
1012
|
+
incremental: false,
|
|
1013
|
+
existingDeployDirs: []
|
|
1014
|
+
};
|
|
563
1015
|
}
|
|
1016
|
+
//#endregion
|
|
1017
|
+
//#region src/commands/pack.ts
|
|
1018
|
+
const log = createLogger("pack");
|
|
564
1019
|
function resolveProjects(root, config, incremental) {
|
|
565
1020
|
const projects = [];
|
|
566
1021
|
const kinds = incremental ? ["api"] : ["api", "web"];
|
|
@@ -596,18 +1051,45 @@ function resolveBuildEnv(project, config) {
|
|
|
596
1051
|
}
|
|
597
1052
|
return env;
|
|
598
1053
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1054
|
+
function shouldUseBuildTui(options) {
|
|
1055
|
+
return Boolean(process.stdout.isTTY) && options.tui !== false;
|
|
1056
|
+
}
|
|
1057
|
+
function toBuildJobSpecs(projects, config) {
|
|
1058
|
+
return projects.map((project) => ({
|
|
1059
|
+
id: `${project.kind}/${project.name}`,
|
|
1060
|
+
label: `${project.kind}/${project.name}`,
|
|
1061
|
+
cwd: project.absPath,
|
|
1062
|
+
env: resolveBuildEnv(project, config)
|
|
1063
|
+
}));
|
|
1064
|
+
}
|
|
1065
|
+
async function buildAllLegacy(projects, config, concurrency) {
|
|
1066
|
+
const limitText = concurrency !== void 0 ? `,最多同时 ${concurrency} 个` : "";
|
|
1067
|
+
log.info(`构建 ${projects.length} 个项目${limitText}`);
|
|
1068
|
+
const { runWithConcurrency } = await import("./build-jobs-m5jbiNTx.mjs");
|
|
1069
|
+
await runWithConcurrency(projects, concurrency ?? 0, async (project) => {
|
|
602
1070
|
const env = resolveBuildEnv(project, config);
|
|
603
|
-
if (project.kind === "web" && env.VITE_API_PROXY) log(`build ${project.kind}/${project.name} VITE_API_PROXY=${env.VITE_API_PROXY}`);
|
|
604
|
-
else log(`build ${project.kind}/${project.name} -> ${project.absPath}`);
|
|
1071
|
+
if (project.kind === "web" && env.VITE_API_PROXY) log.info(`build ${project.kind}/${project.name} VITE_API_PROXY=${env.VITE_API_PROXY}`);
|
|
1072
|
+
else log.info(`build ${project.kind}/${project.name} -> ${project.absPath}`);
|
|
605
1073
|
await runCommand("npm", ["run", "build"], {
|
|
606
1074
|
cwd: project.absPath,
|
|
607
1075
|
env,
|
|
608
1076
|
windowsCmd: true
|
|
609
1077
|
});
|
|
610
|
-
})
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
async function buildAllWithTui(projects, config, options) {
|
|
1081
|
+
const { runBuildDashboard } = await import("./build-dashboard-CqZVHPH7.mjs");
|
|
1082
|
+
await runBuildDashboard(toBuildJobSpecs(projects, config), {
|
|
1083
|
+
concurrency: options.concurrency,
|
|
1084
|
+
stayOnSuccess: options.stay
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
async function buildAll(projects, config, options) {
|
|
1088
|
+
if (shouldUseBuildTui(options)) {
|
|
1089
|
+
await buildAllWithTui(projects, config, options);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
await buildAllLegacy(projects, config, options.concurrency);
|
|
611
1093
|
}
|
|
612
1094
|
function copyRequiredFile(src, destDir, label) {
|
|
613
1095
|
if (!fs.existsSync(src)) throw new Error(`[pack] 缺少 ${label}: ${src}`);
|
|
@@ -619,7 +1101,7 @@ function stageApi(project, dest, incremental) {
|
|
|
619
1101
|
copyRequiredFile(path.join(project.absPath, "package.json"), dest, "package.json");
|
|
620
1102
|
copyRequiredFile(path.join(project.absPath, "bootstrap.js"), dest, "bootstrap.js");
|
|
621
1103
|
if (incremental) {
|
|
622
|
-
log(`api/${project.name} 增量:跳过 public`);
|
|
1104
|
+
log.warn(`api/${project.name} 增量:跳过 public`);
|
|
623
1105
|
return;
|
|
624
1106
|
}
|
|
625
1107
|
const publicDir = path.join(project.absPath, "public");
|
|
@@ -639,26 +1121,32 @@ function stageDist(stagingDir, root, projects, incremental) {
|
|
|
639
1121
|
const dest = path.join(stagingDir, project.kind, project.name);
|
|
640
1122
|
if (project.kind === "api") stageApi(project, dest, incremental);
|
|
641
1123
|
else fs.cpSync(project.distPath, dest, { recursive: true });
|
|
642
|
-
log(`收集 ${project.kind}/${project.name} -> ${path.relative(root, dest)}`);
|
|
1124
|
+
log.info(`收集 ${project.kind}/${project.name} -> ${path.relative(root, dest)}`);
|
|
643
1125
|
}
|
|
644
1126
|
}
|
|
645
1127
|
async function runPack(dir, options) {
|
|
646
1128
|
const root = path.resolve(dir ?? process.cwd());
|
|
647
1129
|
if (!fs.existsSync(root)) throw new Error(`[pack] 找不到路径: ${root}`);
|
|
648
1130
|
const projectRoot = fs.statSync(root).isDirectory() ? root : path.dirname(root);
|
|
649
|
-
const incremental = resolveIncremental(options);
|
|
650
1131
|
const stagingDir = path.join(projectRoot, ".packup-staging");
|
|
651
|
-
const zipPath = path.join(projectRoot, "dist.zip");
|
|
652
1132
|
const { file, config } = await loadConfig(projectRoot);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
log(
|
|
1133
|
+
const zipPath = distZipPath(projectRoot, config.projectName);
|
|
1134
|
+
const packMode = resolveIncrementalMode(projectRoot, config);
|
|
1135
|
+
const incremental = packMode.incremental || packMode.existingDeployDirs.length > 0;
|
|
1136
|
+
log.info(`项目目录: ${projectRoot}`);
|
|
1137
|
+
log.info(`配置: ${file}`);
|
|
1138
|
+
log.info(`项目: ${config.projectName}`);
|
|
1139
|
+
log.info(`模式: ${incremental ? "增量(仅 api,不含 public)" : "全量"}`);
|
|
1140
|
+
if (packMode.existingDeployDirs.length > 0) {
|
|
1141
|
+
const listed = packMode.existingDeployDirs.map((dir) => path.normalize(dir)).join(", ");
|
|
1142
|
+
log.warn(`无 ~/.zpc 部署记录,但检测到部署目录非空:${listed}。将按增量处理(api 保留 public,跳过 web);若确需全量请清空部署目录并删除 deploy-state 中对应记录`);
|
|
1143
|
+
}
|
|
1144
|
+
if (options.concurrency !== void 0) log.info(`并行上限: ${options.concurrency}`);
|
|
657
1145
|
const projects = resolveProjects(projectRoot, config, incremental);
|
|
658
|
-
await buildAll(projects, config);
|
|
1146
|
+
await buildAll(projects, config, options);
|
|
659
1147
|
stageDist(stagingDir, projectRoot, projects, incremental);
|
|
660
1148
|
try {
|
|
661
|
-
log(`压缩 ${path.relative(projectRoot, zipPath)}`);
|
|
1149
|
+
log.info(`压缩 ${path.relative(projectRoot, zipPath)}`);
|
|
662
1150
|
await zipDirectory(stagingDir, zipPath, projectRoot);
|
|
663
1151
|
} catch (err) {
|
|
664
1152
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -669,18 +1157,23 @@ async function runPack(dir, options) {
|
|
|
669
1157
|
force: true
|
|
670
1158
|
});
|
|
671
1159
|
}
|
|
672
|
-
log(`完成: ${zipPath}`);
|
|
673
|
-
if (
|
|
1160
|
+
log.success(`完成: ${zipPath}`);
|
|
1161
|
+
if (ensureZpcGitignore(projectRoot)) log.info("已更新 .gitignore,忽略 *-dist.zip");
|
|
1162
|
+
if (incremental) log.info("增量包已生成,部署时将自动识别为增量模式");
|
|
674
1163
|
}
|
|
675
1164
|
function createPackCommand() {
|
|
676
|
-
return new Command("pack").description("按 zpc.config 构建并打包 dist.zip").argument("[dir]", "项目目录,默认为当前目录").option("-
|
|
1165
|
+
return new Command("pack").description("按 zpc.config 构建并打包 <projectName>-dist.zip").argument("[dir]", "项目目录,默认为当前目录").option("--no-tui", "禁用构建 TUI,使用传统输出").option("--stay", "构建成功后保留 TUI,手动退出").option("-c, --concurrency <n>", "最大同时构建数(默认不限制)", (value) => {
|
|
1166
|
+
const parsed = Number(value);
|
|
1167
|
+
if (!Number.isInteger(parsed) || parsed < 1) throw new Error("--concurrency 必须是大于 0 的整数");
|
|
1168
|
+
return parsed;
|
|
1169
|
+
}).action(async (dir, options) => {
|
|
677
1170
|
await runPack(dir, options);
|
|
678
1171
|
});
|
|
679
1172
|
}
|
|
680
1173
|
//#endregion
|
|
681
1174
|
//#region src/utils/program.ts
|
|
682
1175
|
function createProgram() {
|
|
683
|
-
const program = new Command().name("zpc").description(description).version(version).showHelpAfterError().addCommand(createInitCommand()).addCommand(createPackCommand()).addCommand(createDeployCommand());
|
|
1176
|
+
const program = new Command().name("zpc").description(description).version(version).showHelpAfterError().addCommand(createInitCommand()).addCommand(createPackCommand()).addCommand(createDeployCommand()).addCommand(createCheckCommand());
|
|
684
1177
|
program.action(() => {
|
|
685
1178
|
program.outputHelp();
|
|
686
1179
|
});
|
|
@@ -690,8 +1183,7 @@ async function runCli(argv = process.argv) {
|
|
|
690
1183
|
try {
|
|
691
1184
|
await createProgram().parseAsync(argv);
|
|
692
1185
|
} catch (err) {
|
|
693
|
-
|
|
694
|
-
console.error(message);
|
|
1186
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
695
1187
|
process.exitCode = 1;
|
|
696
1188
|
}
|
|
697
1189
|
}
|
|
@@ -699,4 +1191,4 @@ async function runCli(argv = process.argv) {
|
|
|
699
1191
|
//#region src/cli.ts
|
|
700
1192
|
runCli();
|
|
701
1193
|
//#endregion
|
|
702
|
-
export {};
|
|
1194
|
+
export { runCommandCaptured as t };
|