@zhipu/zp-cli 0.0.2 → 0.0.4
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 +11 -0
- package/dist/cli.mjs +301 -85
- package/dist/{config-BZc9pg7J.mjs → config-DzcldbBT.mjs} +13 -4
- package/dist/index.d.mts +11 -3
- package/dist/index.mjs +1 -1
- package/package.json +25 -1
- package/src/templates/zpc.config.schema.json +9 -0
package/README.md
CHANGED
|
@@ -16,10 +16,21 @@ zpc pack
|
|
|
16
16
|
# 在服务器上:目录内需有 <projectName>-dist.zip + zpc.config.*(配置不要打进压缩包)
|
|
17
17
|
zpc deploy
|
|
18
18
|
zpc deploy --force # 允许抢占冲突的 nginx proxy
|
|
19
|
+
|
|
20
|
+
# 盘点部署目录与 nginx 配置,标注已记录 / 未记录 / 记录但缺失
|
|
21
|
+
zpc check
|
|
19
22
|
```
|
|
20
23
|
|
|
24
|
+
`zpc deploy` 若发现无 `~/.zpc` 部署记录、但 api/web 部署目录非空或已有本项目 nginx 配置,会交互询问:
|
|
25
|
+
|
|
26
|
+
1. 清空目标文件夹并覆盖(全量,旧目录备份为带时间戳的 `.bak-*`)
|
|
27
|
+
2. 使用增量部署(api 保留 `excludeOnIncremental` 指定项,默认 `public`;web 全量覆盖)
|
|
28
|
+
3. 取消部署(默认;非交互环境同样默认取消)
|
|
29
|
+
|
|
21
30
|
可选环境变量:`DEPLOY_DIR`、`DEPLOY_FORCE`;`ZPC_HOME` 覆盖 `~/.zpc`(部署状态记录);测试可用 `ZPC_API_ROOT` / `ZPC_WEB_ROOT` / `ZPC_NGINX_DEFAULT_D` / `ZPC_DEPLOY_SKIP_RUNTIME`。
|
|
22
31
|
|
|
32
|
+
`api.excludeOnIncremental`:增量部署时排除、不覆盖的目录或文件名列表,默认 `["public"]`;仅对 api 生效,指定项在增量部署后保留服务器上的既有内容。
|
|
33
|
+
|
|
23
34
|
## 部署约定
|
|
24
35
|
|
|
25
36
|
`zpc.config` 中的 `projectName` 即下文 `<system-tag>`。
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
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-DzcldbBT.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import fs, { mkdtempSync, writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -8,7 +8,7 @@ import os, { tmpdir } from "node:os";
|
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import zlib from "node:zlib";
|
|
10
10
|
//#region package.json
|
|
11
|
-
var version = "0.0.
|
|
11
|
+
var version = "0.0.4";
|
|
12
12
|
var description = "内部单命令部署工具";
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/utils/deploy-paths.ts
|
|
@@ -321,17 +321,20 @@ function listNamedDirs(parent) {
|
|
|
321
321
|
if (!fs.existsSync(parent)) return [];
|
|
322
322
|
return fs.readdirSync(parent, { withFileTypes: true }).filter((d) => d.isDirectory() && isSafeDirName(d.name) && !d.name.startsWith(".")).map((d) => d.name);
|
|
323
323
|
}
|
|
324
|
-
|
|
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
|
+
*/
|
|
325
333
|
function deployDir(src, dest, log) {
|
|
326
334
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
fs.rmSync(bak, {
|
|
331
|
-
recursive: true,
|
|
332
|
-
force: true
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
+
const stamp = backupTimestamp();
|
|
336
|
+
let bak = `${dest}.bak-${stamp}`;
|
|
337
|
+
for (let i = 1; fs.existsSync(bak); i++) bak = `${dest}.bak-${stamp}-${i}`;
|
|
335
338
|
if (fs.existsSync(dest)) {
|
|
336
339
|
log(`备份 ${dest} -> ${bak}`);
|
|
337
340
|
fs.renameSync(dest, bak);
|
|
@@ -348,8 +351,8 @@ function deployDir(src, dest, log) {
|
|
|
348
351
|
}
|
|
349
352
|
}
|
|
350
353
|
const API_INCREMENTAL_FILES = ["package.json", "bootstrap.js"];
|
|
351
|
-
/** api 增量部署:只替换 dist 与根部 package.json / bootstrap.js
|
|
352
|
-
function deployApiIncremental(src, dest, log) {
|
|
354
|
+
/** api 增量部署:只替换 dist 与根部 package.json / bootstrap.js,不覆盖 excludeOnIncremental 指定项。 */
|
|
355
|
+
function deployApiIncremental(src, dest, exclude, log) {
|
|
353
356
|
const distSrc = path.join(src, "dist");
|
|
354
357
|
if (!fs.existsSync(distSrc)) throw new Error(`[deploy] ${src} 缺少 dist,无法增量部署`);
|
|
355
358
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -375,8 +378,15 @@ function deployApiIncremental(src, dest, log) {
|
|
|
375
378
|
if (!fs.existsSync(from)) throw new Error(`[deploy] ${src} 缺少 ${file},无法增量部署`);
|
|
376
379
|
fs.cpSync(from, path.join(dest, file));
|
|
377
380
|
}
|
|
378
|
-
|
|
379
|
-
|
|
381
|
+
for (const entry of exclude) {
|
|
382
|
+
const destEntry = path.join(dest, entry);
|
|
383
|
+
if (fs.existsSync(destEntry)) continue;
|
|
384
|
+
const srcEntry = path.join(src, entry);
|
|
385
|
+
if (fs.existsSync(srcEntry) && !fs.statSync(srcEntry).isDirectory()) continue;
|
|
386
|
+
fs.mkdirSync(destEntry, { recursive: true });
|
|
387
|
+
}
|
|
388
|
+
const skipText = exclude.length ? `,跳过 ${exclude.join("、")}` : "";
|
|
389
|
+
log(`增量部署 ${src} -> ${dest}(更新 dist、${API_INCREMENTAL_FILES.join("、")}${skipText})`);
|
|
380
390
|
}
|
|
381
391
|
async function installAndReloadApi(name, dest, item, log) {
|
|
382
392
|
if (!fs.existsSync(path.join(dest, "package.json"))) throw new Error(`[deploy] ${dest} 缺少 package.json,无法 pnpm i`);
|
|
@@ -488,6 +498,115 @@ function markProjectFullDeployed(root, projectName) {
|
|
|
488
498
|
};
|
|
489
499
|
writeDeployStateFile(state);
|
|
490
500
|
}
|
|
501
|
+
/** 返回 ~/.zpc/deploy-state.json 中已记录的所有 projectName(去重、排序)。 */
|
|
502
|
+
function listRecordedProjectNames() {
|
|
503
|
+
const state = readDeployStateFile();
|
|
504
|
+
return [...new Set(Object.values(state.projects).map((p) => p.projectName))].sort();
|
|
505
|
+
}
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/utils/check-state.ts
|
|
508
|
+
/** 从 nginx 配置的 `owned-by: projectName/name` 头解析出 projectName。 */
|
|
509
|
+
function nginxProjectName(text) {
|
|
510
|
+
const owned = parseNginxOwnedBy(text);
|
|
511
|
+
if (!owned) return null;
|
|
512
|
+
const idx = owned.indexOf("/");
|
|
513
|
+
return idx >= 0 ? owned.slice(0, idx) : owned;
|
|
514
|
+
}
|
|
515
|
+
/** 盘点部署痕迹(api/web 目录 + nginx 配置)与部署记录,合并成按系统分组的结果。 */
|
|
516
|
+
function collectSystemChecks() {
|
|
517
|
+
const recorded = new Set(listRecordedProjectNames());
|
|
518
|
+
const apiByProject = /* @__PURE__ */ new Map();
|
|
519
|
+
for (const projectName of listNamedDirs(getApiRoot())) apiByProject.set(projectName, listNamedDirs(path.join(getApiRoot(), projectName)));
|
|
520
|
+
const webByProject = /* @__PURE__ */ new Map();
|
|
521
|
+
for (const projectName of listNamedDirs(getWebRoot())) webByProject.set(projectName, listNamedDirs(path.join(getWebRoot(), projectName)));
|
|
522
|
+
const nginxByProject = /* @__PURE__ */ new Map();
|
|
523
|
+
const unownedNginxConfs = [];
|
|
524
|
+
const nginxDir = getNginxDefaultD();
|
|
525
|
+
if (fs.existsSync(nginxDir)) for (const name of fs.readdirSync(nginxDir)) {
|
|
526
|
+
if (!name.endsWith(".conf")) continue;
|
|
527
|
+
const full = path.join(nginxDir, name);
|
|
528
|
+
let text = "";
|
|
529
|
+
try {
|
|
530
|
+
text = fs.readFileSync(full, "utf8");
|
|
531
|
+
} catch {}
|
|
532
|
+
const projectName = nginxProjectName(text);
|
|
533
|
+
if (projectName) {
|
|
534
|
+
const arr = nginxByProject.get(projectName) ?? [];
|
|
535
|
+
arr.push(full);
|
|
536
|
+
nginxByProject.set(projectName, arr);
|
|
537
|
+
} else unownedNginxConfs.push(full);
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
systems: [.../* @__PURE__ */ new Set([
|
|
541
|
+
...recorded,
|
|
542
|
+
...apiByProject.keys(),
|
|
543
|
+
...webByProject.keys(),
|
|
544
|
+
...nginxByProject.keys()
|
|
545
|
+
])].sort().map((projectName) => ({
|
|
546
|
+
projectName,
|
|
547
|
+
recorded: recorded.has(projectName),
|
|
548
|
+
apiDirs: apiByProject.get(projectName) ?? [],
|
|
549
|
+
webDirs: webByProject.get(projectName) ?? [],
|
|
550
|
+
nginxConfs: nginxByProject.get(projectName) ?? []
|
|
551
|
+
})),
|
|
552
|
+
unownedNginxConfs
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
//#endregion
|
|
556
|
+
//#region src/utils/log.ts
|
|
557
|
+
function tagged(tag, message) {
|
|
558
|
+
return `${chalk.cyan(`[${tag}]`)} ${message}`;
|
|
559
|
+
}
|
|
560
|
+
/** 带命令前缀的彩色日志:info 默认、success 绿、warn 黄。 */
|
|
561
|
+
function createLogger(tag) {
|
|
562
|
+
return {
|
|
563
|
+
info: (message) => {
|
|
564
|
+
console.log(tagged(tag, message));
|
|
565
|
+
},
|
|
566
|
+
success: (message) => {
|
|
567
|
+
console.log(tagged(tag, chalk.green(message)));
|
|
568
|
+
},
|
|
569
|
+
warn: (message) => {
|
|
570
|
+
console.log(tagged(tag, chalk.yellow(message)));
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function logError(message) {
|
|
575
|
+
console.error(chalk.red(message));
|
|
576
|
+
}
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/commands/check.ts
|
|
579
|
+
const log$3 = createLogger("check");
|
|
580
|
+
function systemStatus(system) {
|
|
581
|
+
if (!system.recorded) return "未记录";
|
|
582
|
+
return system.apiDirs.length > 0 || system.webDirs.length > 0 || system.nginxConfs.length > 0 ? "已记录" : "记录但缺失";
|
|
583
|
+
}
|
|
584
|
+
function runCheck() {
|
|
585
|
+
const { systems, unownedNginxConfs } = collectSystemChecks();
|
|
586
|
+
if (systems.length === 0 && unownedNginxConfs.length === 0) {
|
|
587
|
+
log$3.info("未发现任何部署记录或部署痕迹");
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
for (const system of systems) {
|
|
591
|
+
const status = systemStatus(system);
|
|
592
|
+
const head = `[${status}] ${system.projectName}`;
|
|
593
|
+
if (status === "已记录") log$3.info(head);
|
|
594
|
+
else log$3.warn(head);
|
|
595
|
+
if (!(system.apiDirs.length > 0 || system.webDirs.length > 0 || system.nginxConfs.length > 0)) continue;
|
|
596
|
+
if (system.apiDirs.length > 0) log$3.info(` api: ${system.apiDirs.join(", ")}`);
|
|
597
|
+
if (system.webDirs.length > 0) log$3.info(` web: ${system.webDirs.join(", ")}`);
|
|
598
|
+
if (system.nginxConfs.length > 0) log$3.info(` nginx: ${system.nginxConfs.join(", ")}`);
|
|
599
|
+
}
|
|
600
|
+
if (unownedNginxConfs.length > 0) {
|
|
601
|
+
log$3.warn(`[未归属 nginx 配置] ${unownedNginxConfs.length} 个`);
|
|
602
|
+
for (const conf of unownedNginxConfs) log$3.warn(` ${conf}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function createCheckCommand() {
|
|
606
|
+
return new Command("check").description("盘点部署目录与 nginx 配置,标注已记录 / 未记录 / 记录但缺失").action(async () => {
|
|
607
|
+
runCheck();
|
|
608
|
+
});
|
|
609
|
+
}
|
|
491
610
|
//#endregion
|
|
492
611
|
//#region src/utils/dist-zip.ts
|
|
493
612
|
const ZPC_GITIGNORE_HEADER = "# zpc files";
|
|
@@ -511,8 +630,8 @@ function ensureZpcGitignore(projectRoot) {
|
|
|
511
630
|
return true;
|
|
512
631
|
}
|
|
513
632
|
//#endregion
|
|
514
|
-
//#region src/utils/
|
|
515
|
-
function isDirectoryNonEmpty(dir) {
|
|
633
|
+
//#region src/utils/deploy-conflict.ts
|
|
634
|
+
function isDirectoryNonEmpty$1(dir) {
|
|
516
635
|
if (!fs.existsSync(dir)) return false;
|
|
517
636
|
try {
|
|
518
637
|
return fs.readdirSync(dir).length > 0;
|
|
@@ -520,55 +639,80 @@ function isDirectoryNonEmpty(dir) {
|
|
|
520
639
|
return false;
|
|
521
640
|
}
|
|
522
641
|
}
|
|
523
|
-
/**
|
|
524
|
-
function
|
|
642
|
+
/** 收集「无 ~/.zpc 记录」时应提示用户的部署痕迹:api/web 目录非空、已存在/冲突的 nginx 配置。 */
|
|
643
|
+
function collectDeployConflicts(config) {
|
|
525
644
|
const projectName = config.projectName;
|
|
526
|
-
|
|
527
|
-
|
|
645
|
+
const conflicts = [];
|
|
646
|
+
if (!projectName) return conflicts;
|
|
528
647
|
for (const item of config.api ?? []) {
|
|
529
|
-
const
|
|
530
|
-
if (isDirectoryNonEmpty(
|
|
648
|
+
const dir = path.join(getApiRoot(), projectName, item.name);
|
|
649
|
+
if (isDirectoryNonEmpty$1(dir)) conflicts.push({
|
|
650
|
+
kind: "api-dir",
|
|
651
|
+
message: `api 部署目录非空: ${dir}`
|
|
652
|
+
});
|
|
531
653
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
//#endregion
|
|
552
|
-
//#region src/utils/log.ts
|
|
553
|
-
function tagged(tag, message) {
|
|
554
|
-
return `${chalk.cyan(`[${tag}]`)} ${message}`;
|
|
555
|
-
}
|
|
556
|
-
/** 带命令前缀的彩色日志:info 默认、success 绿、warn 黄。 */
|
|
557
|
-
function createLogger(tag) {
|
|
558
|
-
return {
|
|
559
|
-
info: (message) => {
|
|
560
|
-
console.log(tagged(tag, message));
|
|
561
|
-
},
|
|
562
|
-
success: (message) => {
|
|
563
|
-
console.log(tagged(tag, chalk.green(message)));
|
|
564
|
-
},
|
|
565
|
-
warn: (message) => {
|
|
566
|
-
console.log(tagged(tag, chalk.yellow(message)));
|
|
654
|
+
for (const item of config.web ?? []) {
|
|
655
|
+
const dir = path.join(getWebRoot(), projectName, item.name);
|
|
656
|
+
if (isDirectoryNonEmpty$1(dir)) conflicts.push({
|
|
657
|
+
kind: "web-dir",
|
|
658
|
+
message: `web 部署目录非空: ${dir}`
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
const defaultD = getNginxDefaultD();
|
|
662
|
+
if (!fs.existsSync(defaultD)) return conflicts;
|
|
663
|
+
const proxies = (config.api ?? []).map((item) => item.proxy).filter((p) => !!p);
|
|
664
|
+
for (const name of fs.readdirSync(defaultD)) {
|
|
665
|
+
if (!name.endsWith(".conf")) continue;
|
|
666
|
+
const full = path.join(defaultD, name);
|
|
667
|
+
let text;
|
|
668
|
+
try {
|
|
669
|
+
text = fs.readFileSync(full, "utf8");
|
|
670
|
+
} catch {
|
|
671
|
+
continue;
|
|
567
672
|
}
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
|
|
673
|
+
const owned = parseNginxOwnedBy(text);
|
|
674
|
+
if (owned?.startsWith(`${projectName}/`) || name.startsWith(`${projectName}-`)) {
|
|
675
|
+
conflicts.push({
|
|
676
|
+
kind: "nginx",
|
|
677
|
+
message: `已存在本项目 nginx 配置: ${full}`
|
|
678
|
+
});
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
for (const proxy of proxies) if (confDefinesProxy(text, proxy)) {
|
|
682
|
+
conflicts.push({
|
|
683
|
+
kind: "nginx",
|
|
684
|
+
message: `nginx 配置 ${full} 已定义 proxy ${nginxLocation(proxy)}(归属 ${owned ?? "未知"})`
|
|
685
|
+
});
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return conflicts;
|
|
690
|
+
}
|
|
691
|
+
function isInteractive() {
|
|
692
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
693
|
+
}
|
|
694
|
+
/** 交互询问部署方式;非 TTY 环境直接返回取消(默认)。 */
|
|
695
|
+
async function promptDeployResolution() {
|
|
696
|
+
if (!isInteractive()) return "cancel";
|
|
697
|
+
const { select } = await import("@inquirer/prompts");
|
|
698
|
+
return await select({
|
|
699
|
+
message: "检测到已有部署痕迹(无 ~/.zpc 记录),请选择部署方式:",
|
|
700
|
+
choices: [
|
|
701
|
+
{
|
|
702
|
+
name: "清空目标文件夹并覆盖(全量,旧目录将备份)",
|
|
703
|
+
value: "full"
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
name: "使用增量部署(api 保留排除项,web 全量覆盖)",
|
|
707
|
+
value: "incremental"
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
name: "取消部署",
|
|
711
|
+
value: "cancel"
|
|
712
|
+
}
|
|
713
|
+
],
|
|
714
|
+
default: "cancel"
|
|
715
|
+
});
|
|
572
716
|
}
|
|
573
717
|
//#endregion
|
|
574
718
|
//#region src/utils/zip.ts
|
|
@@ -707,15 +851,16 @@ async function deployKind(stagingDir, items, kind, destRoot, projectName, increm
|
|
|
707
851
|
if (!fs.existsSync(src)) throw new Error(`[deploy] 配置中有 ${kind}/${item.name},但压缩包中没有对应目录`);
|
|
708
852
|
const dest = path.join(destRoot, projectName, item.name);
|
|
709
853
|
const pm2Name = `${projectName}-${item.name}`;
|
|
710
|
-
|
|
854
|
+
const apiItem = kind === "api" ? item : null;
|
|
855
|
+
if (apiItem && incremental && fs.existsSync(dest)) deployApiIncremental(src, dest, apiItem.excludeOnIncremental ?? [], log$2.info);
|
|
711
856
|
else {
|
|
712
|
-
if (
|
|
857
|
+
if (apiItem && incremental && !fs.existsSync(dest)) log$2.warn(`${dest} 不存在,增量改为全量部署`);
|
|
713
858
|
deployDir(src, dest, log$2.info);
|
|
714
859
|
}
|
|
715
|
-
if (
|
|
860
|
+
if (apiItem && !shouldSkipDeployRuntime()) await installAndReloadApi(pm2Name, dest, apiItem, log$2.info);
|
|
716
861
|
}
|
|
717
862
|
}
|
|
718
|
-
async function runDeploy(dir, options) {
|
|
863
|
+
async function runDeploy(dir, options, prompt = promptDeployResolution) {
|
|
719
864
|
const force = Boolean(options.force) || process.env.DEPLOY_FORCE === "1" || process.env.DEPLOY_FORCE === "true";
|
|
720
865
|
const projectDir = resolveProjectDir(dir);
|
|
721
866
|
const stagingDir = path.join(projectDir, ".deploy-staging");
|
|
@@ -739,16 +884,33 @@ async function runDeploy(dir, options) {
|
|
|
739
884
|
throw new Error(`[deploy] 解压失败: ${message}`);
|
|
740
885
|
}
|
|
741
886
|
const zipIsIncremental = fs.existsSync(path.join(stagingDir, INCREMENTAL_MARKER));
|
|
742
|
-
const
|
|
743
|
-
|
|
887
|
+
const hadRecord = hasFullDeployRecord(projectDir);
|
|
888
|
+
let incremental;
|
|
889
|
+
let resolvedFromConflicts = false;
|
|
890
|
+
if (hadRecord) incremental = true;
|
|
891
|
+
else {
|
|
892
|
+
const conflicts = collectDeployConflicts(config);
|
|
893
|
+
if (conflicts.length > 0) {
|
|
894
|
+
for (const conflict of conflicts) log$2.warn(conflict.message);
|
|
895
|
+
const choice = await prompt();
|
|
896
|
+
if (choice === "cancel") {
|
|
897
|
+
log$2.warn("已取消部署");
|
|
898
|
+
fs.rmSync(stagingDir, {
|
|
899
|
+
recursive: true,
|
|
900
|
+
force: true
|
|
901
|
+
});
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
incremental = choice === "incremental";
|
|
905
|
+
resolvedFromConflicts = true;
|
|
906
|
+
} else incremental = zipIsIncremental;
|
|
907
|
+
}
|
|
744
908
|
try {
|
|
745
|
-
log$2.info(`模式: ${incremental ? "增量(api
|
|
746
|
-
if (
|
|
747
|
-
else if (!incremental) log$2.info("首次部署该项目,完成后将记录到 ~/.zpc/deploy-state.json");
|
|
909
|
+
log$2.info(`模式: ${incremental ? "增量(api 保留排除项,web 全量覆盖)" : "全量"}`);
|
|
910
|
+
if (!incremental && !hadRecord) log$2.info("首次部署该项目,完成后将记录到 ~/.zpc/deploy-state.json");
|
|
748
911
|
if (force) log$2.warn("已启用 --force(允许抢占冲突的 nginx proxy)");
|
|
749
912
|
await deployKind(stagingDir, config.api, "api", getApiRoot(), config.projectName, incremental);
|
|
750
|
-
|
|
751
|
-
else if (config.web?.length) log$2.warn("增量模式跳过 web 部署");
|
|
913
|
+
await deployKind(stagingDir, config.web, "web", getWebRoot(), config.projectName, false);
|
|
752
914
|
} finally {
|
|
753
915
|
fs.rmSync(stagingDir, {
|
|
754
916
|
recursive: true,
|
|
@@ -762,11 +924,11 @@ async function runDeploy(dir, options) {
|
|
|
762
924
|
if (!hasFullDeployRecord(projectDir)) {
|
|
763
925
|
markProjectFullDeployed(projectDir, config.projectName);
|
|
764
926
|
if (!incremental) log$2.info("已记录全量部署状态到 ~/.zpc/deploy-state.json,下次 deploy 将默认增量");
|
|
765
|
-
else if (
|
|
927
|
+
else if (resolvedFromConflicts) log$2.info("已根据现有部署目录建立部署记录,下次 deploy 将默认增量");
|
|
766
928
|
else if (zipIsIncremental) log$2.info("已记录部署状态,下次 deploy 将默认增量");
|
|
767
929
|
}
|
|
768
930
|
const apiCount = config.api?.length ?? 0;
|
|
769
|
-
const webCount =
|
|
931
|
+
const webCount = config.web?.length ?? 0;
|
|
770
932
|
const prefix = `${config.projectName}/<name>`;
|
|
771
933
|
log$2.success(`完成: api ${apiCount} 个 -> ${getApiRoot()}/${prefix},web ${webCount} 个 -> ${getWebRoot()}/${prefix}`);
|
|
772
934
|
}
|
|
@@ -808,12 +970,62 @@ function createInitCommand() {
|
|
|
808
970
|
});
|
|
809
971
|
}
|
|
810
972
|
//#endregion
|
|
973
|
+
//#region src/utils/incremental-mode.ts
|
|
974
|
+
function isDirectoryNonEmpty(dir) {
|
|
975
|
+
if (!fs.existsSync(dir)) return false;
|
|
976
|
+
try {
|
|
977
|
+
return fs.readdirSync(dir).length > 0;
|
|
978
|
+
} catch {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/** 配置中所有部署目录(api + web)里已存在且非空的目录。 */
|
|
983
|
+
function findNonemptyDeployDirs(config) {
|
|
984
|
+
const projectName = config.projectName;
|
|
985
|
+
if (!projectName) return [];
|
|
986
|
+
const found = [];
|
|
987
|
+
for (const item of config.api ?? []) {
|
|
988
|
+
const deployDir = path.join(getApiRoot(), projectName, item.name);
|
|
989
|
+
if (isDirectoryNonEmpty(deployDir)) found.push(deployDir);
|
|
990
|
+
}
|
|
991
|
+
for (const item of config.web ?? []) {
|
|
992
|
+
const deployDir = path.join(getWebRoot(), projectName, item.name);
|
|
993
|
+
if (isDirectoryNonEmpty(deployDir)) found.push(deployDir);
|
|
994
|
+
}
|
|
995
|
+
return found;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* 全量 / 增量判定(pack 只读、deploy 读后写入):
|
|
999
|
+
* 1. ~/.zpc/deploy-state.json 有记录 → 增量
|
|
1000
|
+
* 2. 无记录但 api/web 部署目录非空 → 交由调用方决策(不直接判定)
|
|
1001
|
+
* 3. 无记录但 zip 含增量标记 → 增量
|
|
1002
|
+
* 4. 否则全量
|
|
1003
|
+
*/
|
|
1004
|
+
function resolveIncrementalMode(projectRoot, config, options = {}) {
|
|
1005
|
+
if (hasFullDeployRecord(projectRoot)) return {
|
|
1006
|
+
incremental: true,
|
|
1007
|
+
existingDeployDirs: []
|
|
1008
|
+
};
|
|
1009
|
+
const existingDeployDirs = findNonemptyDeployDirs(config);
|
|
1010
|
+
if (existingDeployDirs.length > 0) return {
|
|
1011
|
+
incremental: false,
|
|
1012
|
+
existingDeployDirs
|
|
1013
|
+
};
|
|
1014
|
+
if (options.zipIsIncremental) return {
|
|
1015
|
+
incremental: true,
|
|
1016
|
+
existingDeployDirs: []
|
|
1017
|
+
};
|
|
1018
|
+
return {
|
|
1019
|
+
incremental: false,
|
|
1020
|
+
existingDeployDirs: []
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
//#endregion
|
|
811
1024
|
//#region src/commands/pack.ts
|
|
812
1025
|
const log = createLogger("pack");
|
|
813
|
-
function resolveProjects(root, config
|
|
1026
|
+
function resolveProjects(root, config) {
|
|
814
1027
|
const projects = [];
|
|
815
|
-
const
|
|
816
|
-
for (const kind of kinds) {
|
|
1028
|
+
for (const kind of ["api", "web"]) {
|
|
817
1029
|
const items = config[kind];
|
|
818
1030
|
if (items == null) continue;
|
|
819
1031
|
for (const item of items) {
|
|
@@ -829,7 +1041,7 @@ function resolveProjects(root, config, incremental) {
|
|
|
829
1041
|
});
|
|
830
1042
|
}
|
|
831
1043
|
}
|
|
832
|
-
if (projects.length === 0) throw new Error(
|
|
1044
|
+
if (projects.length === 0) throw new Error("[pack] 配置中没有 api / web 项目");
|
|
833
1045
|
return projects;
|
|
834
1046
|
}
|
|
835
1047
|
function resolveBuildEnv(project, config) {
|
|
@@ -895,7 +1107,8 @@ function stageApi(project, dest, incremental) {
|
|
|
895
1107
|
copyRequiredFile(path.join(project.absPath, "package.json"), dest, "package.json");
|
|
896
1108
|
copyRequiredFile(path.join(project.absPath, "bootstrap.js"), dest, "bootstrap.js");
|
|
897
1109
|
if (incremental) {
|
|
898
|
-
|
|
1110
|
+
const excluded = project.excludeOnIncremental ?? [];
|
|
1111
|
+
log.warn(excluded.length ? `api/${project.name} 增量:跳过 ${excluded.join("、")}` : `api/${project.name} 增量:不排除任何目录/文件`);
|
|
899
1112
|
return;
|
|
900
1113
|
}
|
|
901
1114
|
const publicDir = path.join(project.absPath, "public");
|
|
@@ -926,14 +1139,17 @@ async function runPack(dir, options) {
|
|
|
926
1139
|
const { file, config } = await loadConfig(projectRoot);
|
|
927
1140
|
const zipPath = distZipPath(projectRoot, config.projectName);
|
|
928
1141
|
const packMode = resolveIncrementalMode(projectRoot, config);
|
|
929
|
-
const incremental = packMode.incremental;
|
|
1142
|
+
const incremental = packMode.incremental || packMode.existingDeployDirs.length > 0;
|
|
930
1143
|
log.info(`项目目录: ${projectRoot}`);
|
|
931
1144
|
log.info(`配置: ${file}`);
|
|
932
1145
|
log.info(`项目: ${config.projectName}`);
|
|
933
|
-
log.info(`模式: ${incremental ? "
|
|
934
|
-
if (packMode.
|
|
1146
|
+
log.info(`模式: ${incremental ? "增量(api 不含排除项,web 全量)" : "全量"}`);
|
|
1147
|
+
if (packMode.existingDeployDirs.length > 0) {
|
|
1148
|
+
const listed = packMode.existingDeployDirs.map((dir) => path.normalize(dir)).join(", ");
|
|
1149
|
+
log.warn(`无 ~/.zpc 部署记录,但检测到部署目录非空:${listed}。将按增量处理(api 保留排除项,web 全量覆盖);若确需全量请清空部署目录并删除 deploy-state 中对应记录`);
|
|
1150
|
+
}
|
|
935
1151
|
if (options.concurrency !== void 0) log.info(`并行上限: ${options.concurrency}`);
|
|
936
|
-
const projects = resolveProjects(projectRoot, config
|
|
1152
|
+
const projects = resolveProjects(projectRoot, config);
|
|
937
1153
|
await buildAll(projects, config, options);
|
|
938
1154
|
stageDist(stagingDir, projectRoot, projects, incremental);
|
|
939
1155
|
try {
|
|
@@ -964,7 +1180,7 @@ function createPackCommand() {
|
|
|
964
1180
|
//#endregion
|
|
965
1181
|
//#region src/utils/program.ts
|
|
966
1182
|
function createProgram() {
|
|
967
|
-
const program = new Command().name("zpc").description(description).version(version).showHelpAfterError().addCommand(createInitCommand()).addCommand(createPackCommand()).addCommand(createDeployCommand());
|
|
1183
|
+
const program = new Command().name("zpc").description(description).version(version).showHelpAfterError().addCommand(createInitCommand()).addCommand(createPackCommand()).addCommand(createDeployCommand()).addCommand(createCheckCommand());
|
|
968
1184
|
program.action(() => {
|
|
969
1185
|
program.outputHelp();
|
|
970
1186
|
});
|
|
@@ -13,6 +13,8 @@ const CONFIG_FORMATS = [
|
|
|
13
13
|
"cjs",
|
|
14
14
|
"json"
|
|
15
15
|
];
|
|
16
|
+
/** 增量部署默认排除、不覆盖的目录/文件名。 */
|
|
17
|
+
const DEFAULT_EXCLUDE_ON_INCREMENTAL = ["public"];
|
|
16
18
|
function defineConfig(config) {
|
|
17
19
|
return config;
|
|
18
20
|
}
|
|
@@ -66,9 +68,9 @@ function validateConfig(config, source) {
|
|
|
66
68
|
function optionalAppList(value, kind, source) {
|
|
67
69
|
if (value == null) return [];
|
|
68
70
|
if (!Array.isArray(value)) throw new Error(`[config] ${source} 中 ${kind} 必须是数组`);
|
|
69
|
-
return value.map((item, index) => parseAppConfig(item, `${source} ${kind}[${index}]`));
|
|
71
|
+
return value.map((item, index) => parseAppConfig(item, kind, `${source} ${kind}[${index}]`));
|
|
70
72
|
}
|
|
71
|
-
function parseAppConfig(item, label) {
|
|
73
|
+
function parseAppConfig(item, kind, label) {
|
|
72
74
|
if (!isPlainObject(item)) throw new Error(`[config] ${label} 必须是对象`);
|
|
73
75
|
if (typeof item.path !== "string" || item.path.trim() === "") throw new Error(`[config] ${label} 缺少 path`);
|
|
74
76
|
if (!isSafeName(item.name)) throw new Error(`[config] ${label} 缺少合法 name`);
|
|
@@ -81,8 +83,15 @@ function parseAppConfig(item, label) {
|
|
|
81
83
|
parsed.proxy = item.proxy;
|
|
82
84
|
}
|
|
83
85
|
if (item.buildEnv != null) parsed.buildEnv = parseEnvMap(item.buildEnv, `${label} buildEnv`);
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
+
if (kind === "web") return parsed;
|
|
87
|
+
const api = { ...parsed };
|
|
88
|
+
if (item.env != null) api.env = parseEnvMap(item.env, `${label} env`);
|
|
89
|
+
api.excludeOnIncremental = item.excludeOnIncremental != null ? parseExcludeOnIncremental(item.excludeOnIncremental, `${label} excludeOnIncremental`) : [...DEFAULT_EXCLUDE_ON_INCREMENTAL];
|
|
90
|
+
return api;
|
|
91
|
+
}
|
|
92
|
+
function parseExcludeOnIncremental(value, label) {
|
|
93
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || !isSafeName(entry))) throw new Error(`[config] ${label} 必须是字符串数组(目录/文件名,不含路径分隔符)`);
|
|
94
|
+
return value;
|
|
86
95
|
}
|
|
87
96
|
function parseEnvMap(value, label) {
|
|
88
97
|
if (!isPlainObject(value)) throw new Error(`[config] ${label} 必须是对象`);
|
package/dist/index.d.mts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
declare const CONFIG_FORMATS: readonly ["ts", "mts", "cts", "js", "mjs", "cjs", "json"];
|
|
3
3
|
type ConfigFormat = (typeof CONFIG_FORMATS)[number];
|
|
4
4
|
type ZpcEnvMap = Record<string, string | number | boolean>;
|
|
5
|
+
/** api / web 应用配置公共字段 */
|
|
5
6
|
interface ZpcAppConfig {
|
|
6
7
|
/** 相对项目根的源码目录 */
|
|
7
8
|
path: string;
|
|
@@ -11,14 +12,21 @@ interface ZpcAppConfig {
|
|
|
11
12
|
proxy?: string;
|
|
12
13
|
/** 仅构建期注入 */
|
|
13
14
|
buildEnv?: ZpcEnvMap;
|
|
15
|
+
}
|
|
16
|
+
/** api 应用配置(额外含运行时 env 与增量部署排除项) */
|
|
17
|
+
interface ZpcApiConfig extends ZpcAppConfig {
|
|
14
18
|
/** 仅运行时注入(pm2) */
|
|
15
19
|
env?: ZpcEnvMap;
|
|
20
|
+
/** 增量部署时排除、不覆盖的目录/文件名;默认 ["public"] */
|
|
21
|
+
excludeOnIncremental?: string[];
|
|
16
22
|
}
|
|
23
|
+
/** web 应用配置(静态站点,仅构建期 buildEnv,无运行时 env) */
|
|
24
|
+
interface ZpcWebConfig extends ZpcAppConfig {}
|
|
17
25
|
interface ZpcConfig {
|
|
18
26
|
projectName: string;
|
|
19
|
-
api?:
|
|
20
|
-
web?:
|
|
27
|
+
api?: ZpcApiConfig[];
|
|
28
|
+
web?: ZpcWebConfig[];
|
|
21
29
|
}
|
|
22
30
|
declare function defineConfig(config: ZpcConfig): ZpcConfig;
|
|
23
31
|
//#endregion
|
|
24
|
-
export { type ConfigFormat, type ZpcAppConfig, type ZpcConfig, type ZpcEnvMap, defineConfig };
|
|
32
|
+
export { type ConfigFormat, type ZpcApiConfig, type ZpcAppConfig, type ZpcConfig, type ZpcEnvMap, type ZpcWebConfig, defineConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as defineConfig } from "./config-
|
|
1
|
+
import { i as defineConfig } from "./config-DzcldbBT.mjs";
|
|
2
2
|
export { defineConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhipu/zp-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "内部单命令部署工具",
|
|
5
5
|
"bin": {
|
|
6
6
|
"zpc": "./dist/cli.mjs"
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@commander-js/extra-typings": "^15.0.0",
|
|
22
|
+
"@inquirer/prompts": "^8.6.0",
|
|
22
23
|
"@vue-tui/components": "^0.3.0",
|
|
23
24
|
"@vue-tui/runtime": "^0.3.0",
|
|
24
25
|
"chalk": "^6.0.0",
|
|
@@ -55,6 +56,21 @@
|
|
|
55
56
|
"@alcalzone/ansi-tokenize": "0.3.0",
|
|
56
57
|
"@babel/parser": "7.29.8",
|
|
57
58
|
"@commander-js/extra-typings": "15.0.0",
|
|
59
|
+
"@inquirer/ansi": "2.0.7",
|
|
60
|
+
"@inquirer/checkbox": "5.2.2",
|
|
61
|
+
"@inquirer/confirm": "6.2.0",
|
|
62
|
+
"@inquirer/core": "12.0.0",
|
|
63
|
+
"@inquirer/editor": "5.3.0",
|
|
64
|
+
"@inquirer/expand": "5.1.2",
|
|
65
|
+
"@inquirer/external-editor": "3.0.4",
|
|
66
|
+
"@inquirer/figures": "2.0.8",
|
|
67
|
+
"@inquirer/input": "5.1.3",
|
|
68
|
+
"@inquirer/number": "4.2.0",
|
|
69
|
+
"@inquirer/password": "5.1.2",
|
|
70
|
+
"@inquirer/prompts": "8.6.0",
|
|
71
|
+
"@inquirer/rawlist": "5.3.2",
|
|
72
|
+
"@inquirer/search": "4.3.0",
|
|
73
|
+
"@inquirer/select": "5.2.2",
|
|
58
74
|
"@vue-tui/components": "0.3.0",
|
|
59
75
|
"@vue-tui/runtime": "0.3.0",
|
|
60
76
|
"@vue/compiler-core": "3.5.41",
|
|
@@ -69,16 +85,24 @@
|
|
|
69
85
|
"5.6.2",
|
|
70
86
|
"6.0.0"
|
|
71
87
|
],
|
|
88
|
+
"chardet": "2.2.0",
|
|
72
89
|
"cli-boxes": "3.0.0",
|
|
73
90
|
"cli-truncate": "6.1.1",
|
|
91
|
+
"cli-width": "4.1.0",
|
|
74
92
|
"commander": "15.0.0",
|
|
75
93
|
"entities": "7.0.1",
|
|
76
94
|
"environment": "1.1.0",
|
|
77
95
|
"estree-walker": "2.0.2",
|
|
96
|
+
"fast-string-truncated-width": "3.0.3",
|
|
97
|
+
"fast-string-width": "3.0.2",
|
|
98
|
+
"fast-wrap-ansi": "0.2.2",
|
|
78
99
|
"get-east-asian-width": "1.6.0",
|
|
100
|
+
"iconv-lite": "0.7.3",
|
|
79
101
|
"is-fullwidth-code-point": "5.1.0",
|
|
80
102
|
"jiti": "2.7.0",
|
|
103
|
+
"mute-stream": "3.0.0",
|
|
81
104
|
"patch-console": "2.0.0",
|
|
105
|
+
"safer-buffer": "2.1.2",
|
|
82
106
|
"signal-exit": "4.1.0",
|
|
83
107
|
"slice-ansi": "9.0.0",
|
|
84
108
|
"source-map-js": "1.2.1",
|
|
@@ -54,6 +54,15 @@
|
|
|
54
54
|
"env": {
|
|
55
55
|
"$ref": "#/definitions/envMap",
|
|
56
56
|
"description": "仅运行时注入(pm2)"
|
|
57
|
+
},
|
|
58
|
+
"excludeOnIncremental": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"minLength": 1
|
|
63
|
+
},
|
|
64
|
+
"default": ["public"],
|
|
65
|
+
"description": "增量部署时排除、不覆盖的目录/文件名;仅 api 生效,默认 [\"public\"]"
|
|
57
66
|
}
|
|
58
67
|
}
|
|
59
68
|
},
|