@zhipu/zp-cli 0.0.1 → 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/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.1";
11
+ var version = "0.0.2";
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);
@@ -304,6 +438,139 @@ async function configureNginxProxies(projectName, apis, force, log) {
304
438
  log("nginx 已 reload");
305
439
  }
306
440
  //#endregion
441
+ //#region src/utils/deploy-state.ts
442
+ const DEPLOY_STATE_VERSION = 1;
443
+ const DEPLOY_STATE_FILE = "deploy-state.json";
444
+ function getZpcHome() {
445
+ return process.env.ZPC_HOME ?? path.join(os.homedir(), ".zpc");
446
+ }
447
+ function getDeployStatePath() {
448
+ return path.join(getZpcHome(), DEPLOY_STATE_FILE);
449
+ }
450
+ /** 以项目根目录绝对路径为键(统一分隔符,便于跨平台比对)。 */
451
+ function deployStateProjectKey(root) {
452
+ return path.resolve(root).replace(/\\/g, "/");
453
+ }
454
+ function emptyDeployState() {
455
+ return {
456
+ version: DEPLOY_STATE_VERSION,
457
+ projects: {}
458
+ };
459
+ }
460
+ function readDeployStateFile() {
461
+ const filePath = getDeployStatePath();
462
+ if (!fs.existsSync(filePath)) return emptyDeployState();
463
+ let parsed;
464
+ try {
465
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
466
+ } catch {
467
+ throw new Error(`[deploy] 无法解析 ${filePath},请检查 JSON 格式或删除后重试`);
468
+ }
469
+ if (typeof parsed !== "object" || parsed == null || parsed.version !== DEPLOY_STATE_VERSION || typeof parsed.projects !== "object" || parsed.projects == null) throw new Error(`[deploy] ${filePath} 格式无效,请删除后重试`);
470
+ return parsed;
471
+ }
472
+ function writeDeployStateFile(state) {
473
+ const home = getZpcHome();
474
+ fs.mkdirSync(home, { recursive: true });
475
+ fs.writeFileSync(getDeployStatePath(), `${JSON.stringify(state, null, 2)}\n`, "utf8");
476
+ }
477
+ function hasFullDeployRecord(root) {
478
+ const state = readDeployStateFile();
479
+ return deployStateProjectKey(root) in state.projects;
480
+ }
481
+ function markProjectFullDeployed(root, projectName) {
482
+ const state = readDeployStateFile();
483
+ const key = deployStateProjectKey(root);
484
+ state.projects[key] = {
485
+ projectName,
486
+ root: key,
487
+ fullDeployedAt: (/* @__PURE__ */ new Date()).toISOString()
488
+ };
489
+ writeDeployStateFile(state);
490
+ }
491
+ //#endregion
492
+ //#region src/utils/dist-zip.ts
493
+ const ZPC_GITIGNORE_HEADER = "# zpc files";
494
+ const ZPC_DIST_ZIP_IGNORE = "*-dist.zip";
495
+ function distZipFileName(projectName) {
496
+ return `${projectName}-dist.zip`;
497
+ }
498
+ function distZipPath(projectRoot, projectName) {
499
+ return path.join(projectRoot, distZipFileName(projectName));
500
+ }
501
+ function gitignoreHasDistZipRule(content) {
502
+ return content.split(/\r?\n/u).some((line) => line.trim() === ZPC_DIST_ZIP_IGNORE);
503
+ }
504
+ /** 若项目 .gitignore 缺少 *-dist.zip 规则则追加,返回是否新写入。 */
505
+ function ensureZpcGitignore(projectRoot) {
506
+ const gitignorePath = path.join(projectRoot, ".gitignore");
507
+ const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
508
+ if (gitignoreHasDistZipRule(existing)) return false;
509
+ const block = `${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}\n${ZPC_GITIGNORE_HEADER}\n${ZPC_DIST_ZIP_IGNORE}\n`;
510
+ fs.writeFileSync(gitignorePath, `${existing}${block}`, "utf8");
511
+ return true;
512
+ }
513
+ //#endregion
514
+ //#region src/utils/incremental-mode.ts
515
+ function isDirectoryNonEmpty(dir) {
516
+ if (!fs.existsSync(dir)) return false;
517
+ try {
518
+ return fs.readdirSync(dir).length > 0;
519
+ } catch {
520
+ return false;
521
+ }
522
+ }
523
+ /** 配置中 api 在部署根目录下已存在且非空的目录。 */
524
+ function findNonemptyApiDeployDirs(config) {
525
+ const projectName = config.projectName;
526
+ if (!projectName) return [];
527
+ const found = [];
528
+ for (const item of config.api ?? []) {
529
+ const deployDir = path.join(getApiRoot(), projectName, item.name);
530
+ if (isDirectoryNonEmpty(deployDir)) found.push(deployDir);
531
+ }
532
+ return found;
533
+ }
534
+ /**
535
+ * 全量 / 增量判定(pack 只读、deploy 读后写入):
536
+ * 1. ~/.zpc/deploy-state.json 有记录 → 增量
537
+ * 2. 无记录但 api 部署目录非空 → 增量并提示
538
+ * 3. 无记录但 zip 含增量标记 → 增量
539
+ * 4. 否则全量
540
+ */
541
+ function resolveIncrementalMode(projectRoot, config, options = {}) {
542
+ if (hasFullDeployRecord(projectRoot)) return { incremental: true };
543
+ const existingDeployDirs = findNonemptyApiDeployDirs(config);
544
+ if (existingDeployDirs.length > 0) return {
545
+ incremental: true,
546
+ existingDeployHint: `无 ~/.zpc 部署记录,但检测到 api 部署目录非空:${existingDeployDirs.map((dir) => path.normalize(dir)).join(", ")}。将按增量处理(api 保留 public,跳过 web);若确需全量请清空部署目录并删除 deploy-state 中对应记录`
547
+ };
548
+ if (options.zipIsIncremental) return { incremental: true };
549
+ return { incremental: false };
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)));
567
+ }
568
+ };
569
+ }
570
+ function logError(message) {
571
+ console.error(chalk.red(message));
572
+ }
573
+ //#endregion
307
574
  //#region src/utils/zip.ts
308
575
  /** ZIP End of Central Directory 签名 */
309
576
  const SIG_EOCD = 101010256;
@@ -419,27 +686,7 @@ async function zipDirectory(stagingDir, zipPath, cwd) {
419
686
  }
420
687
  //#endregion
421
688
  //#region src/commands/deploy.ts
422
- function log$1(message) {
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
- }
689
+ const log$2 = createLogger("deploy");
443
690
  function resolveProjectDir(dirArg) {
444
691
  let dir;
445
692
  if (dirArg) {
@@ -448,39 +695,40 @@ function resolveProjectDir(dirArg) {
448
695
  dir = fs.statSync(abs).isDirectory() ? abs : path.dirname(abs);
449
696
  } else if (process.env.DEPLOY_DIR) dir = path.resolve(process.env.DEPLOY_DIR);
450
697
  else dir = process.cwd();
451
- if (!hasProjectFiles(dir)) throw new Error(`[deploy] ${dir} 中需要同时有 dist.zip 和 zpc.config.*(配置请单独上传,不要打进压缩包)`);
698
+ if (!findConfigFile(dir)) throw new Error(`[deploy] ${dir} 中需要 zpc.config.*(配置请单独上传,不要打进压缩包)`);
452
699
  return dir;
453
700
  }
454
701
  async function deployKind(stagingDir, items, kind, destRoot, projectName, incremental) {
455
702
  const packed = new Set(listNamedDirs(path.join(stagingDir, kind)));
456
703
  const configured = items ?? [];
457
- for (const extra of packed) if (!configured.some((item) => item.name === extra)) log$1(`跳过压缩包中的 ${kind}/${extra}(未在配置中列出)`);
704
+ for (const extra of packed) if (!configured.some((item) => item.name === extra)) log$2.warn(`跳过压缩包中的 ${kind}/${extra}(未在配置中列出)`);
458
705
  for (const item of configured) {
459
706
  const src = path.join(stagingDir, kind, item.name);
460
707
  if (!fs.existsSync(src)) throw new Error(`[deploy] 配置中有 ${kind}/${item.name},但压缩包中没有对应目录`);
461
708
  const dest = path.join(destRoot, projectName, item.name);
462
709
  const pm2Name = `${projectName}-${item.name}`;
463
- if (kind === "api" && incremental && fs.existsSync(dest)) deployApiIncremental(src, dest, log$1);
710
+ if (kind === "api" && incremental && fs.existsSync(dest)) deployApiIncremental(src, dest, log$2.info);
464
711
  else {
465
- if (kind === "api" && incremental && !fs.existsSync(dest)) log$1(`${dest} 不存在,增量改为全量部署`);
466
- deployDir(src, dest, log$1);
712
+ if (kind === "api" && incremental && !fs.existsSync(dest)) log$2.warn(`${dest} 不存在,增量改为全量部署`);
713
+ deployDir(src, dest, log$2.info);
467
714
  }
468
- if (kind === "api" && !shouldSkipDeployRuntime()) await installAndReloadApi(pm2Name, dest, item, log$1);
715
+ if (kind === "api" && !shouldSkipDeployRuntime()) await installAndReloadApi(pm2Name, dest, item, log$2.info);
469
716
  }
470
717
  }
471
718
  async function runDeploy(dir, options) {
472
- const { wantIncremental, forceFull, force } = resolveFlags(options);
719
+ const force = Boolean(options.force) || process.env.DEPLOY_FORCE === "1" || process.env.DEPLOY_FORCE === "true";
473
720
  const projectDir = resolveProjectDir(dir);
474
721
  const stagingDir = path.join(projectDir, ".deploy-staging");
475
- const zipPath = path.join(projectDir, "dist.zip");
476
722
  const { file, config } = await loadConfig(projectDir);
477
723
  assertDeployConfig(config);
478
- log$1(`项目目录: ${projectDir}`);
479
- log$1(`压缩包: ${zipPath}`);
480
- log$1(`配置: ${file}`);
481
- log$1(`项目: ${config.projectName}`);
724
+ const zipPath = distZipPath(projectDir, config.projectName);
725
+ if (!fs.existsSync(zipPath)) throw new Error(`[deploy] 找不到 ${zipPath},请先在该目录执行 zpc pack(配置请单独上传,不要打进压缩包)`);
726
+ log$2.info(`项目目录: ${projectDir}`);
727
+ log$2.info(`压缩包: ${zipPath}`);
728
+ log$2.info(`配置: ${file}`);
729
+ log$2.info(`项目: ${config.projectName}`);
482
730
  try {
483
- log$1(`解压 ${zipPath}`);
731
+ log$2.info(`解压 ${zipPath}`);
484
732
  await extractArchive(zipPath, stagingDir);
485
733
  } catch (err) {
486
734
  fs.rmSync(stagingDir, {
@@ -490,17 +738,17 @@ async function runDeploy(dir, options) {
490
738
  const message = err instanceof Error ? err.message : String(err);
491
739
  throw new Error(`[deploy] 解压失败: ${message}`);
492
740
  }
493
- const packIsIncremental = fs.existsSync(path.join(stagingDir, INCREMENTAL_MARKER));
494
- let incremental = wantIncremental || packIsIncremental;
741
+ const zipIsIncremental = fs.existsSync(path.join(stagingDir, INCREMENTAL_MARKER));
742
+ const deployMode = resolveIncrementalMode(projectDir, config, { zipIsIncremental });
743
+ const incremental = deployMode.incremental;
495
744
  try {
496
- if (packIsIncremental && forceFull) throw new Error("[deploy] 当前 dist.zip 是增量包(含 .deploy-incremental),不能 --full,请使用 --incremental,或重新 pack 全量包");
497
- if (packIsIncremental && !wantIncremental) throw new Error("[deploy] 当前 dist.zip 是增量包(由 zpc pack -i 生成),请加上 --incremental / -i,以免全量覆盖清空 public");
498
- incremental = wantIncremental || packIsIncremental;
499
- log$1(`模式: ${incremental ? "增量(api 保留 public,跳过 web)" : "全量"}`);
500
- if (force) log$1("已启用 --force(允许抢占冲突的 nginx proxy)");
745
+ log$2.info(`模式: ${incremental ? "增量(api 保留 public,跳过 web)" : "全量"}`);
746
+ if (deployMode.existingDeployHint) log$2.warn(deployMode.existingDeployHint);
747
+ else if (!incremental) log$2.info("首次部署该项目,完成后将记录到 ~/.zpc/deploy-state.json");
748
+ if (force) log$2.warn("已启用 --force(允许抢占冲突的 nginx proxy)");
501
749
  await deployKind(stagingDir, config.api, "api", getApiRoot(), config.projectName, incremental);
502
750
  if (!incremental) await deployKind(stagingDir, config.web, "web", getWebRoot(), config.projectName, false);
503
- else if (config.web?.length) log$1("增量模式跳过 web 部署");
751
+ else if (config.web?.length) log$2.warn("增量模式跳过 web 部署");
504
752
  } finally {
505
753
  fs.rmSync(stagingDir, {
506
754
  recursive: true,
@@ -508,21 +756,28 @@ async function runDeploy(dir, options) {
508
756
  });
509
757
  }
510
758
  if (!shouldSkipDeployRuntime()) {
511
- await configureNginxProxies(config.projectName, config.api, force, log$1);
759
+ await configureNginxProxies(config.projectName, config.api, force, log$2.info);
512
760
  if (config.api?.length) await runCommand("pm2", ["save"], { ignoreExit: true });
513
761
  }
762
+ if (!hasFullDeployRecord(projectDir)) {
763
+ markProjectFullDeployed(projectDir, config.projectName);
764
+ if (!incremental) log$2.info("已记录全量部署状态到 ~/.zpc/deploy-state.json,下次 deploy 将默认增量");
765
+ else if (deployMode.existingDeployHint) log$2.info("已根据现有部署目录建立部署记录,下次 deploy 将默认增量");
766
+ else if (zipIsIncremental) log$2.info("已记录部署状态,下次 deploy 将默认增量");
767
+ }
514
768
  const apiCount = config.api?.length ?? 0;
515
769
  const webCount = incremental ? 0 : config.web?.length ?? 0;
516
770
  const prefix = `${config.projectName}/<name>`;
517
- log$1(`完成: api ${apiCount} 个 -> ${getApiRoot()}/${prefix},web ${webCount} 个 -> ${getWebRoot()}/${prefix}`);
771
+ log$2.success(`完成: api ${apiCount} 个 -> ${getApiRoot()}/${prefix},web ${webCount} 个 -> ${getWebRoot()}/${prefix}`);
518
772
  }
519
773
  function createDeployCommand() {
520
- return new Command("deploy").description("按 zpc.config 将 dist.zip 部署到服务器").argument("[dir]", "项目目录,默认为当前目录").option("-i, --incremental", "增量部署(api 保留 public,跳过 web)").option("--full", "全量部署").option("--force", "允许抢占冲突的 nginx proxy").action(async (dir, options) => {
774
+ return new Command("deploy").description("按 zpc.config 将 <projectName>-dist.zip 部署到服务器").argument("[dir]", "项目目录,默认为当前目录").option("--force", "允许抢占冲突的 nginx proxy").action(async (dir, options) => {
521
775
  await runDeploy(dir, options);
522
776
  });
523
777
  }
524
778
  //#endregion
525
779
  //#region src/commands/init.ts
780
+ const log$1 = createLogger("init");
526
781
  async function runInit(dir, options) {
527
782
  const format = resolveFormat(options.template);
528
783
  const root = path.resolve(dir ?? process.cwd());
@@ -536,10 +791,10 @@ async function runInit(dir, options) {
536
791
  fs.mkdirSync(root, { recursive: true });
537
792
  for (const file of existing) if (file !== dest) fs.rmSync(file);
538
793
  fs.copyFileSync(resolveTemplateFile(format), dest);
539
- console.log(`[init] 已写入 ${dest}`);
794
+ log$1.success(`已写入 ${dest}`);
540
795
  if (format === "json") {
541
796
  fs.copyFileSync(resolveSchemaFile(), schemaDest);
542
- console.log(`[init] 已写入 ${schemaDest}`);
797
+ log$1.success(`已写入 ${schemaDest}`);
543
798
  } else if (fs.existsSync(schemaDest)) fs.rmSync(schemaDest);
544
799
  }
545
800
  function resolveFormat(template) {
@@ -554,13 +809,7 @@ function createInitCommand() {
554
809
  }
555
810
  //#endregion
556
811
  //#region src/commands/pack.ts
557
- function log(message) {
558
- console.log(`[pack] ${message}`);
559
- }
560
- function resolveIncremental(options) {
561
- if (options.full) return false;
562
- return Boolean(options.incremental);
563
- }
812
+ const log = createLogger("pack");
564
813
  function resolveProjects(root, config, incremental) {
565
814
  const projects = [];
566
815
  const kinds = incremental ? ["api"] : ["api", "web"];
@@ -596,18 +845,45 @@ function resolveBuildEnv(project, config) {
596
845
  }
597
846
  return env;
598
847
  }
599
- async function buildAll(projects, config) {
600
- log(`并行构建 ${projects.length} 个项目`);
601
- await Promise.all(projects.map(async (project) => {
848
+ function shouldUseBuildTui(options) {
849
+ return Boolean(process.stdout.isTTY) && options.tui !== false;
850
+ }
851
+ function toBuildJobSpecs(projects, config) {
852
+ return projects.map((project) => ({
853
+ id: `${project.kind}/${project.name}`,
854
+ label: `${project.kind}/${project.name}`,
855
+ cwd: project.absPath,
856
+ env: resolveBuildEnv(project, config)
857
+ }));
858
+ }
859
+ async function buildAllLegacy(projects, config, concurrency) {
860
+ const limitText = concurrency !== void 0 ? `,最多同时 ${concurrency} 个` : "";
861
+ log.info(`构建 ${projects.length} 个项目${limitText}`);
862
+ const { runWithConcurrency } = await import("./build-jobs-m5jbiNTx.mjs");
863
+ await runWithConcurrency(projects, concurrency ?? 0, async (project) => {
602
864
  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}`);
865
+ if (project.kind === "web" && env.VITE_API_PROXY) log.info(`build ${project.kind}/${project.name} VITE_API_PROXY=${env.VITE_API_PROXY}`);
866
+ else log.info(`build ${project.kind}/${project.name} -> ${project.absPath}`);
605
867
  await runCommand("npm", ["run", "build"], {
606
868
  cwd: project.absPath,
607
869
  env,
608
870
  windowsCmd: true
609
871
  });
610
- }));
872
+ });
873
+ }
874
+ async function buildAllWithTui(projects, config, options) {
875
+ const { runBuildDashboard } = await import("./build-dashboard-CqZVHPH7.mjs");
876
+ await runBuildDashboard(toBuildJobSpecs(projects, config), {
877
+ concurrency: options.concurrency,
878
+ stayOnSuccess: options.stay
879
+ });
880
+ }
881
+ async function buildAll(projects, config, options) {
882
+ if (shouldUseBuildTui(options)) {
883
+ await buildAllWithTui(projects, config, options);
884
+ return;
885
+ }
886
+ await buildAllLegacy(projects, config, options.concurrency);
611
887
  }
612
888
  function copyRequiredFile(src, destDir, label) {
613
889
  if (!fs.existsSync(src)) throw new Error(`[pack] 缺少 ${label}: ${src}`);
@@ -619,7 +895,7 @@ function stageApi(project, dest, incremental) {
619
895
  copyRequiredFile(path.join(project.absPath, "package.json"), dest, "package.json");
620
896
  copyRequiredFile(path.join(project.absPath, "bootstrap.js"), dest, "bootstrap.js");
621
897
  if (incremental) {
622
- log(`api/${project.name} 增量:跳过 public`);
898
+ log.warn(`api/${project.name} 增量:跳过 public`);
623
899
  return;
624
900
  }
625
901
  const publicDir = path.join(project.absPath, "public");
@@ -639,26 +915,29 @@ function stageDist(stagingDir, root, projects, incremental) {
639
915
  const dest = path.join(stagingDir, project.kind, project.name);
640
916
  if (project.kind === "api") stageApi(project, dest, incremental);
641
917
  else fs.cpSync(project.distPath, dest, { recursive: true });
642
- log(`收集 ${project.kind}/${project.name} -> ${path.relative(root, dest)}`);
918
+ log.info(`收集 ${project.kind}/${project.name} -> ${path.relative(root, dest)}`);
643
919
  }
644
920
  }
645
921
  async function runPack(dir, options) {
646
922
  const root = path.resolve(dir ?? process.cwd());
647
923
  if (!fs.existsSync(root)) throw new Error(`[pack] 找不到路径: ${root}`);
648
924
  const projectRoot = fs.statSync(root).isDirectory() ? root : path.dirname(root);
649
- const incremental = resolveIncremental(options);
650
925
  const stagingDir = path.join(projectRoot, ".packup-staging");
651
- const zipPath = path.join(projectRoot, "dist.zip");
652
926
  const { file, config } = await loadConfig(projectRoot);
653
- log(`项目目录: ${projectRoot}`);
654
- log(`配置: ${file}`);
655
- log(`项目: ${config.projectName}`);
656
- log(`模式: ${incremental ? "增量(仅 api,不含 public)" : "全量"}`);
927
+ const zipPath = distZipPath(projectRoot, config.projectName);
928
+ const packMode = resolveIncrementalMode(projectRoot, config);
929
+ const incremental = packMode.incremental;
930
+ log.info(`项目目录: ${projectRoot}`);
931
+ log.info(`配置: ${file}`);
932
+ log.info(`项目: ${config.projectName}`);
933
+ log.info(`模式: ${incremental ? "增量(仅 api,不含 public)" : "全量"}`);
934
+ if (packMode.existingDeployHint) log.warn(packMode.existingDeployHint);
935
+ if (options.concurrency !== void 0) log.info(`并行上限: ${options.concurrency}`);
657
936
  const projects = resolveProjects(projectRoot, config, incremental);
658
- await buildAll(projects, config);
937
+ await buildAll(projects, config, options);
659
938
  stageDist(stagingDir, projectRoot, projects, incremental);
660
939
  try {
661
- log(`压缩 ${path.relative(projectRoot, zipPath)}`);
940
+ log.info(`压缩 ${path.relative(projectRoot, zipPath)}`);
662
941
  await zipDirectory(stagingDir, zipPath, projectRoot);
663
942
  } catch (err) {
664
943
  const message = err instanceof Error ? err.message : String(err);
@@ -669,11 +948,16 @@ async function runPack(dir, options) {
669
948
  force: true
670
949
  });
671
950
  }
672
- log(`完成: ${zipPath}`);
673
- if (incremental) log("请使用: zpc deploy --incremental");
951
+ log.success(`完成: ${zipPath}`);
952
+ if (ensureZpcGitignore(projectRoot)) log.info("已更新 .gitignore,忽略 *-dist.zip");
953
+ if (incremental) log.info("增量包已生成,部署时将自动识别为增量模式");
674
954
  }
675
955
  function createPackCommand() {
676
- return new Command("pack").description("按 zpc.config 构建并打包 dist.zip").argument("[dir]", "项目目录,默认为当前目录").option("-i, --incremental", "增量打包(仅 api,不含 public)").option("--full", "全量打包").action(async (dir, options) => {
956
+ return new Command("pack").description("按 zpc.config 构建并打包 <projectName>-dist.zip").argument("[dir]", "项目目录,默认为当前目录").option("--no-tui", "禁用构建 TUI,使用传统输出").option("--stay", "构建成功后保留 TUI,手动退出").option("-c, --concurrency <n>", "最大同时构建数(默认不限制)", (value) => {
957
+ const parsed = Number(value);
958
+ if (!Number.isInteger(parsed) || parsed < 1) throw new Error("--concurrency 必须是大于 0 的整数");
959
+ return parsed;
960
+ }).action(async (dir, options) => {
677
961
  await runPack(dir, options);
678
962
  });
679
963
  }
@@ -690,8 +974,7 @@ async function runCli(argv = process.argv) {
690
974
  try {
691
975
  await createProgram().parseAsync(argv);
692
976
  } catch (err) {
693
- const message = err instanceof Error ? err.message : String(err);
694
- console.error(message);
977
+ logError(err instanceof Error ? err.message : String(err));
695
978
  process.exitCode = 1;
696
979
  }
697
980
  }
@@ -699,4 +982,4 @@ async function runCli(argv = process.argv) {
699
982
  //#region src/cli.ts
700
983
  runCli();
701
984
  //#endregion
702
- export {};
985
+ export { runCommandCaptured as t };