vite-plugin-deploy-ftp 3.0.1 → 3.2.0

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/index.js CHANGED
@@ -1,15 +1,242 @@
1
1
  // src/index.ts
2
2
  import { checkbox, select } from "@inquirer/prompts";
3
3
  import { Client, FileType } from "basic-ftp";
4
- import chalk from "chalk";
4
+ import chalk5 from "chalk";
5
+ import cliProgress from "cli-progress";
5
6
  import dayjs from "dayjs";
6
- import fs from "fs";
7
+ import fs2 from "fs";
7
8
  import { stat } from "fs/promises";
9
+ import os2 from "os";
10
+ import path2 from "path";
11
+ import ora from "ora";
12
+ import { normalizePath as normalizePath2 } from "vite";
13
+
14
+ // src/utils/file.ts
15
+ import chalk from "chalk";
16
+ import fs from "fs";
8
17
  import os from "os";
9
18
  import path from "path";
10
- import ora from "ora";
11
19
  import { normalizePath } from "vite";
12
20
  import yazl from "yazl";
21
+ function getAllFiles(dirPath, arrayOfFiles = [], relativePath = "") {
22
+ const files = fs.readdirSync(dirPath);
23
+ files.forEach((file) => {
24
+ const fullPath = path.join(dirPath, file);
25
+ const relPath = path.join(relativePath, file);
26
+ if (fs.statSync(fullPath).isDirectory()) {
27
+ getAllFiles(fullPath, arrayOfFiles, relPath);
28
+ } else {
29
+ arrayOfFiles.push(normalizePath(relPath));
30
+ }
31
+ });
32
+ return arrayOfFiles;
33
+ }
34
+ function createTempDir(basePath) {
35
+ const tempBaseDir = os.tmpdir();
36
+ const tempPath = path.join(tempBaseDir, "vite-plugin-deploy-ftp", basePath);
37
+ if (!fs.existsSync(tempPath)) {
38
+ fs.mkdirSync(tempPath, { recursive: true });
39
+ }
40
+ return {
41
+ path: tempPath,
42
+ cleanup: () => {
43
+ try {
44
+ if (fs.existsSync(tempPath)) {
45
+ fs.rmSync(tempPath, { recursive: true, force: true });
46
+ }
47
+ } catch (error) {
48
+ console.warn(chalk.yellow(`\u26A0 \u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
49
+ }
50
+ }
51
+ };
52
+ }
53
+ async function createZipFile(sourceDir, outputPath) {
54
+ return new Promise((resolve, reject) => {
55
+ const output = fs.createWriteStream(outputPath);
56
+ const zipFile = new yazl.ZipFile();
57
+ const handleError = (error) => {
58
+ reject(error instanceof Error ? error : new Error(String(error)));
59
+ };
60
+ output.on("close", resolve);
61
+ output.on("error", handleError);
62
+ zipFile.outputStream.on("error", handleError);
63
+ zipFile.outputStream.pipe(output);
64
+ for (const relativePath of getAllFiles(sourceDir)) {
65
+ const filePath = path.join(sourceDir, relativePath);
66
+ zipFile.addFile(filePath, normalizePath(relativePath));
67
+ }
68
+ zipFile.end();
69
+ });
70
+ }
71
+
72
+ // src/utils/ftp.ts
73
+ import chalk2 from "chalk";
74
+ var sleep = async (ms) => {
75
+ await new Promise((resolve) => setTimeout(resolve, ms));
76
+ };
77
+ function validateFtpConfig(config) {
78
+ return !!(config.host && config.user && config.password);
79
+ }
80
+ async function connectWithRetry(client, config, maxRetries, retryDelay, silentLogs = false) {
81
+ let lastError;
82
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
83
+ try {
84
+ client.ftp.verbose = false;
85
+ await client.access({
86
+ ...config,
87
+ secure: true,
88
+ secureOptions: { rejectUnauthorized: false, timeout: 6e4 }
89
+ });
90
+ return;
91
+ } catch (error) {
92
+ lastError = error instanceof Error ? error : new Error(String(error));
93
+ if (attempt < maxRetries) {
94
+ if (!silentLogs) {
95
+ console.log(chalk2.yellow(`\u26A0 \u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
96
+ }
97
+ await sleep(retryDelay * attempt);
98
+ }
99
+ }
100
+ }
101
+ throw new Error(`\u274C FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
102
+ }
103
+
104
+ // src/utils/output.ts
105
+ import chalk3 from "chalk";
106
+ import cliTruncate from "cli-truncate";
107
+ import logSymbols from "log-symbols";
108
+ import stringWidth from "string-width";
109
+ var panelBorderColor = {
110
+ info: "cyan",
111
+ success: "green",
112
+ warning: "yellow",
113
+ danger: "red"
114
+ };
115
+ var getTerminalWidth = () => process.stdout?.columns || 100;
116
+ var getPanelInnerWidth = () => Math.max(46, Math.min(84, getTerminalWidth() - 4));
117
+ var padVisual = (text, width) => `${text}${" ".repeat(Math.max(0, width - stringWidth(text)))}`;
118
+ var fitVisual = (text, width) => {
119
+ if (width <= 0) return "";
120
+ return padVisual(cliTruncate(text, width, { position: "middle" }), width);
121
+ };
122
+ var truncateTerminalText = (text, reservedWidth = 26) => {
123
+ const maxWidth = Math.max(24, Math.min(88, getTerminalWidth() - reservedWidth));
124
+ return cliTruncate(text, maxWidth, { position: "middle" });
125
+ };
126
+ var renderPanel = (title, rows, tone = "info", footer) => {
127
+ const color = chalk3[panelBorderColor[tone]];
128
+ const innerWidth = getPanelInnerWidth();
129
+ const labelWidth = rows.length > 0 ? Math.max(...rows.map((row) => stringWidth(row.label))) : 0;
130
+ const contentLines = [chalk3.bold(cliTruncate(title, innerWidth, { position: "end" }))];
131
+ if (rows.length > 0) {
132
+ contentLines.push("");
133
+ for (const row of rows) {
134
+ const paddedLabel = padVisual(row.label, labelWidth);
135
+ const prefix = `${paddedLabel} `;
136
+ const availableValueWidth = Math.max(8, innerWidth - stringWidth(prefix));
137
+ contentLines.push(`${chalk3.gray(prefix)}${fitVisual(row.value, availableValueWidth)}`);
138
+ }
139
+ }
140
+ if (footer) {
141
+ contentLines.push("");
142
+ contentLines.push(chalk3.gray(cliTruncate(footer, innerWidth, { position: "middle" })));
143
+ }
144
+ const top = color(`\u256D${"\u2500".repeat(innerWidth + 2)}\u256E`);
145
+ const bottom = color(`\u2570${"\u2500".repeat(innerWidth + 2)}\u256F`);
146
+ const body = contentLines.map((line) => `${color("\u2502")} ${fitVisual(line, innerWidth)} ${color("\u2502")}`).join("\n");
147
+ return `${top}
148
+ ${body}
149
+ ${bottom}`;
150
+ };
151
+ var renderInlineStats = (items) => items.filter(Boolean).join(chalk3.gray(" \xB7 "));
152
+ var getPanelDot = (tone = "success") => {
153
+ switch (tone) {
154
+ case "info":
155
+ return chalk3.green("\u25CF");
156
+ case "success":
157
+ return chalk3.green("\u25CF");
158
+ case "warning":
159
+ return chalk3.yellow("\u25CF");
160
+ case "danger":
161
+ return chalk3.red("\u25CF");
162
+ }
163
+ };
164
+ var getLogSymbol = (tone) => {
165
+ switch (tone) {
166
+ case "success":
167
+ return logSymbols.success;
168
+ case "warning":
169
+ return logSymbols.warning;
170
+ case "danger":
171
+ return logSymbols.error;
172
+ }
173
+ };
174
+
175
+ // src/utils/path.ts
176
+ var normalizeSlash = (value) => value.replace(/\\/g, "/").trim();
177
+ var normalizePathSegments = (...values) => values.filter((value) => Boolean(value)).flatMap((value) => normalizeSlash(value).split("/")).filter(Boolean).join("/");
178
+ var normalizeFtpUploadPath = (targetPath) => {
179
+ const normalized = normalizePathSegments(targetPath);
180
+ return normalized ? `/${normalized}` : "/";
181
+ };
182
+ var normalizeRemotePath = (targetDir, relativeFilePath) => {
183
+ const normalizedTargetDir = normalizeFtpUploadPath(targetDir);
184
+ const normalizedRelativePath = normalizePathSegments(relativeFilePath);
185
+ if (!normalizedRelativePath) return normalizedTargetDir;
186
+ if (normalizedTargetDir === "/") return `/${normalizedRelativePath}`;
187
+ return `${normalizedTargetDir}/${normalizedRelativePath}`;
188
+ };
189
+ var splitUrlLikeBase = (value) => {
190
+ const normalized = normalizeSlash(value);
191
+ const protocolMatch = normalized.match(/^([a-zA-Z][a-zA-Z\d+.-]*:\/\/[^/]+)(.*)$/);
192
+ if (protocolMatch) {
193
+ return {
194
+ prefix: protocolMatch[1],
195
+ path: protocolMatch[2] || ""
196
+ };
197
+ }
198
+ const protocolRelativeMatch = normalized.match(/^(\/\/[^/]+)(.*)$/);
199
+ if (protocolRelativeMatch) {
200
+ return {
201
+ prefix: protocolRelativeMatch[1],
202
+ path: protocolRelativeMatch[2] || ""
203
+ };
204
+ }
205
+ if (normalized.startsWith("/")) {
206
+ return {
207
+ prefix: "/",
208
+ path: normalized
209
+ };
210
+ }
211
+ return {
212
+ prefix: "",
213
+ path: normalized
214
+ };
215
+ };
216
+ var normalizeUrlLikeBase = (base) => {
217
+ const { prefix, path: path3 } = splitUrlLikeBase(base);
218
+ const normalizedPath = normalizePathSegments(path3);
219
+ if (!prefix) return normalizedPath;
220
+ if (!normalizedPath) return prefix;
221
+ if (prefix === "/") return `/${normalizedPath}`;
222
+ return `${prefix}/${normalizedPath}`;
223
+ };
224
+ var normalizeSelectionPath = (value) => normalizePathSegments(value);
225
+ var joinUrlLikePath = (base, targetPath) => {
226
+ const normalizedBase = normalizeUrlLikeBase(base).replace(/\/+$/, "");
227
+ const normalizedTargetPath = normalizePathSegments(targetPath);
228
+ if (!normalizedTargetPath) return normalizedBase;
229
+ if (!normalizedBase) return `/${normalizedTargetPath}`;
230
+ return `${normalizedBase}/${normalizedTargetPath}`;
231
+ };
232
+ var resolveDisplayUrl = (alias, targetPath) => {
233
+ const normalizedTargetPath = normalizeFtpUploadPath(targetPath);
234
+ if (!alias) return normalizedTargetPath;
235
+ return joinUrlLikePath(alias, normalizedTargetPath);
236
+ };
237
+
238
+ // src/utils/progress.ts
239
+ import chalk4 from "chalk";
13
240
  var formatBytes = (bytes) => {
14
241
  if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
15
242
  const units = ["B", "KB", "MB", "GB", "TB"];
@@ -30,38 +257,39 @@ var formatDuration = (seconds) => {
30
257
  if (mins === 0) return `${secs}s`;
31
258
  return `${mins}m${String(secs).padStart(2, "0")}s`;
32
259
  };
33
- var trimMiddle = (text, maxLength) => {
34
- if (text.length <= maxLength) return text;
35
- if (maxLength <= 10) return text.slice(0, maxLength);
36
- const leftLength = Math.floor((maxLength - 3) / 2);
37
- const rightLength = maxLength - 3 - leftLength;
38
- return `${text.slice(0, leftLength)}...${text.slice(-rightLength)}`;
260
+
261
+ // src/index.ts
262
+ var backupArchivePattern = /^backup_\d{8}_\d{6}\.zip$/i;
263
+ var formatTimingDuration = (durationMs) => {
264
+ if (durationMs < 1e3) return `${durationMs}ms`;
265
+ const seconds = durationMs / 1e3;
266
+ return `${seconds.toFixed(seconds >= 10 ? 1 : 2)}s`;
39
267
  };
40
- var buildCapsuleBar = (ratio, width = 30) => {
41
- const safeRatio = Math.max(0, Math.min(1, ratio));
42
- if (width <= 0) return "";
43
- if (safeRatio >= 1) {
44
- return chalk.green("\u2588".repeat(width));
268
+ var renderBackupPanel = (summary) => {
269
+ const previewItems = summary.items.slice(0, 2);
270
+ const rows = [
271
+ { label: "\u7ED3\u679C:", value: chalk5.green(`${summary.items.length} \u4E2A\u5907\u4EFD\u6587\u4EF6`) },
272
+ ...previewItems.map((item, index) => ({
273
+ label: `\u6587\u4EF6 ${index + 1}:`,
274
+ value: chalk5.cyan(truncateTerminalText(item, 22))
275
+ }))
276
+ ];
277
+ if (summary.items.length > previewItems.length) {
278
+ rows.push({
279
+ label: "\u5176\u4F59:",
280
+ value: chalk5.gray(`\u8FD8\u6709 ${summary.items.length - previewItems.length} \u4E2A\u5907\u4EFD\u9879\u672A\u5C55\u5F00`)
281
+ });
45
282
  }
46
- const pointerIndex = Math.min(width - 1, Math.floor(width * safeRatio));
47
- const done = pointerIndex > 0 ? chalk.green("\u2588".repeat(pointerIndex)) : "";
48
- const pointer = chalk.cyanBright("\u25B8");
49
- const pending = pointerIndex < width - 1 ? chalk.gray("\u2591".repeat(width - pointerIndex - 1)) : "";
50
- return `${done}${pointer}${pending}`;
283
+ return renderPanel(`${getPanelDot("success")} ${summary.title}`, rows, "success");
51
284
  };
52
- var normalizeRemotePath = (targetDir, relativeFilePath) => {
53
- const joined = normalizePath(`${targetDir}/${relativeFilePath}`).replace(/\/{2,}/g, "/");
54
- if (targetDir.startsWith("/")) return joined.startsWith("/") ? joined : `/${joined}`;
55
- return joined.replace(/^\/+/, "");
56
- };
57
- var normalizeUploadPath = (targetPath) => {
58
- const normalized = normalizePath(targetPath).replace(/\/{2,}/g, "/").trim();
59
- if (!normalized || normalized === "." || normalized === "/") return "/";
60
- const withoutTrailingSlash = normalized.replace(/\/+$/, "");
61
- return withoutTrailingSlash.startsWith("/") ? withoutTrailingSlash : `/${withoutTrailingSlash}`;
62
- };
63
- var sleep = async (ms) => {
64
- await new Promise((resolve) => setTimeout(resolve, ms));
285
+ var renderDebugPanel = (entries) => {
286
+ const rows = entries.map((entry) => ({
287
+ label: `${entry.label}:`,
288
+ value: chalk5.cyan(
289
+ entry.detail ? `${formatTimingDuration(entry.durationMs)} \xB7 ${truncateTerminalText(entry.detail, 24)}` : formatTimingDuration(entry.durationMs)
290
+ )
291
+ }));
292
+ return renderPanel(`${getPanelDot("success")} \u8C03\u8BD5\u8017\u65F6`, rows, "info");
65
293
  };
66
294
  function vitePluginDeployFtp(option) {
67
295
  const safeOption = option || {};
@@ -71,6 +299,7 @@ function vitePluginDeployFtp(option) {
71
299
  singleBack = false,
72
300
  singleBackFiles = ["index.html"],
73
301
  showBackFile = false,
302
+ debug = false,
74
303
  maxRetries = 3,
75
304
  retryDelay = 1e3,
76
305
  autoUpload = false,
@@ -81,8 +310,8 @@ function vitePluginDeployFtp(option) {
81
310
  const isMultiFtp = "ftps" in safeOption;
82
311
  const ftpConfigs = isMultiFtp ? safeOption.ftps || [] : [{ ...safeOption, name: safeOption.name || safeOption.alias || safeOption.host }];
83
312
  const defaultFtp = isMultiFtp ? safeOption.defaultFtp : void 0;
84
- const normalizedUploadPath = normalizeUploadPath(uploadPath);
85
- let outDir = normalizePath(path.resolve("dist"));
313
+ const normalizedUploadPath = normalizeFtpUploadPath(uploadPath);
314
+ let outDir = normalizePath2(path2.resolve("dist"));
86
315
  let upload = false;
87
316
  let buildFailed = false;
88
317
  let resolvedConfig = null;
@@ -120,11 +349,15 @@ function vitePluginDeployFtp(option) {
120
349
  for (let attempt = 1; attempt <= context.maxRetries; attempt++) {
121
350
  try {
122
351
  await context.ensureConnected();
123
- const remoteDir = normalizePath(path.posix.dirname(task.remotePath));
352
+ const remoteDir = normalizePath2(path2.posix.dirname(task.remotePath));
124
353
  if (remoteDir && remoteDir !== ".") {
125
- await context.client.ensureDir(remoteDir);
354
+ await context.ensureRemoteDir(remoteDir);
355
+ }
356
+ const uploadStartedAt = Date.now();
357
+ await context.client.uploadFrom(task.filePath, path2.posix.basename(task.remotePath));
358
+ if (context.debugMetrics) {
359
+ context.debugMetrics.uploadMs += Date.now() - uploadStartedAt;
126
360
  }
127
- await context.client.uploadFrom(task.filePath, path.posix.basename(task.remotePath));
128
361
  return {
129
362
  success: true,
130
363
  file: task.filePath,
@@ -141,7 +374,7 @@ function vitePluginDeployFtp(option) {
141
374
  if (attempt === context.maxRetries) {
142
375
  if (!context.silentLogs) {
143
376
  console.log(
144
- `${chalk.red("\u2717")} ${task.filePath} => ${error instanceof Error ? error.message : String(error)}`
377
+ `${chalk5.red("\u2717")} ${task.filePath} => ${error instanceof Error ? error.message : String(error)}`
145
378
  );
146
379
  }
147
380
  return {
@@ -154,9 +387,7 @@ function vitePluginDeployFtp(option) {
154
387
  };
155
388
  }
156
389
  if (!context.silentLogs) {
157
- console.log(
158
- `${chalk.yellow("\u26A0")} ${task.filePath} \u4E0A\u4F20\u5931\u8D25\uFF0C\u6B63\u5728\u91CD\u8BD5 (${attempt}/${context.maxRetries})...`
159
- );
390
+ console.log(`${chalk5.yellow("\u26A0")} ${task.filePath} \u4E0A\u4F20\u5931\u8D25\uFF0C\u6B63\u5728\u91CD\u8BD5 (${attempt}/${context.maxRetries})...`);
160
391
  }
161
392
  await sleep(context.retryDelay * attempt);
162
393
  }
@@ -170,17 +401,21 @@ function vitePluginDeployFtp(option) {
170
401
  error: new Error("Max retries exceeded")
171
402
  };
172
403
  };
173
- const uploadFilesInBatches = async (connectConfig, files, targetDir, windowSize = concurrency) => {
404
+ const uploadFilesInBatches = async (context) => {
405
+ const { connectConfig, files, targetDir, windowSize = concurrency, reusableClient } = context;
174
406
  const results = [];
407
+ const debugEntries = [];
175
408
  const totalFiles = files.length;
176
409
  const tasks = [];
410
+ const taskGroups = [];
177
411
  let completed = 0;
178
412
  let failed = 0;
179
413
  let uploadedBytes = 0;
180
414
  let retries = 0;
415
+ const taskPrepareStartedAt = Date.now();
181
416
  const taskCandidates = await Promise.all(
182
417
  files.map(async (relativeFilePath) => {
183
- const filePath = normalizePath(path.resolve(outDir, relativeFilePath));
418
+ const filePath = normalizePath2(path2.resolve(outDir, relativeFilePath));
184
419
  const remotePath = normalizeRemotePath(targetDir, relativeFilePath);
185
420
  try {
186
421
  const fileStats = await stat(filePath);
@@ -206,68 +441,240 @@ function vitePluginDeployFtp(option) {
206
441
  });
207
442
  }
208
443
  }
444
+ debugEntries.push({
445
+ label: "\u751F\u6210\u4E0A\u4F20\u4EFB\u52A1",
446
+ durationMs: Date.now() - taskPrepareStartedAt,
447
+ detail: `${tasks.length} \u4E2A\u6587\u4EF6`
448
+ });
449
+ const normalizedTargetDir = normalizeFtpUploadPath(targetDir);
450
+ const groupStartedAt = Date.now();
451
+ const groupsByRelativeDir = /* @__PURE__ */ new Map();
452
+ for (const task of tasks) {
453
+ const remoteDir = normalizePath2(path2.posix.dirname(task.remotePath));
454
+ const normalizedRemoteDir = remoteDir && remoteDir !== "." ? remoteDir : normalizedTargetDir;
455
+ const relativeDir = normalizedRemoteDir === normalizedTargetDir ? "" : normalizedRemoteDir.slice(normalizedTargetDir.length).replace(/^\/+/, "");
456
+ const currentTasks = groupsByRelativeDir.get(relativeDir);
457
+ if (currentTasks) {
458
+ currentTasks.push(task);
459
+ } else {
460
+ groupsByRelativeDir.set(relativeDir, [task]);
461
+ }
462
+ }
463
+ for (const [relativeDir, groupedTasks] of groupsByRelativeDir) {
464
+ const remoteDir = relativeDir ? normalizeRemotePath(normalizedTargetDir, relativeDir) : normalizedTargetDir;
465
+ taskGroups.push({ relativeDir, remoteDir, tasks: groupedTasks });
466
+ }
467
+ taskGroups.sort((left, right) => left.remoteDir.localeCompare(right.remoteDir));
468
+ debugEntries.push({
469
+ label: "\u76EE\u5F55\u5206\u7EC4",
470
+ durationMs: Date.now() - groupStartedAt,
471
+ detail: `${taskGroups.length} \u7EC4`
472
+ });
209
473
  const totalBytes = tasks.reduce((sum, task) => sum + task.size, 0);
210
474
  const startAt = Date.now();
211
- const safeWindowSize = Math.max(1, Math.min(windowSize, tasks.length || 1));
212
- const activeFiles = /* @__PURE__ */ new Set();
475
+ const safeWindowSize = Math.max(1, Math.min(windowSize, taskGroups.length || 1));
476
+ const extraWorkerCount = reusableClient ? Math.max(0, safeWindowSize - 1) : safeWindowSize;
213
477
  const silentLogs = Boolean(useInteractiveOutput);
214
- const spinner = useInteractiveOutput ? ora({ text: "\u51C6\u5907\u4E0A\u4F20...", spinner: "dots12" }).start() : null;
215
- const reportEvery = Math.max(1, Math.ceil(totalFiles / 10));
478
+ const progressBar = useInteractiveOutput ? new cliProgress.SingleBar({
479
+ hideCursor: true,
480
+ clearOnComplete: true,
481
+ stopOnComplete: true,
482
+ barsize: 18,
483
+ barCompleteChar: "\u2588",
484
+ barIncompleteChar: "\u2591",
485
+ format: `${chalk5.gray("\u4E0A\u4F20")} ${chalk5.bold("{percentage}%")} ${chalk5.cyan("{bar}")} ${chalk5.gray("\xB7")} ${chalk5.magenta("{speed}/s")} ${chalk5.gray("\xB7")} ${chalk5.gray("{elapsed}")}s`
486
+ }) : null;
487
+ const reportEvery = Math.max(1, Math.ceil(totalFiles / 6));
216
488
  let lastReportedCompleted = -1;
489
+ const debugMetrics = {
490
+ connectMs: 0,
491
+ rootDirMs: 0,
492
+ switchDirMs: 0,
493
+ uploadMs: 0
494
+ };
495
+ if (progressBar) {
496
+ progressBar.start(totalFiles, 0, {
497
+ speed: formatBytes(0),
498
+ elapsed: "0"
499
+ });
500
+ }
217
501
  const updateProgress = () => {
218
- const progressRatio = totalFiles > 0 ? completed / totalFiles : 1;
219
- const percentage = Math.round(progressRatio * 100);
220
502
  const elapsedSeconds = (Date.now() - startAt) / 1e3;
221
503
  const speed = elapsedSeconds > 0 ? uploadedBytes / elapsedSeconds : 0;
222
- const etaSeconds = speed > 0 ? Math.max(0, (totalBytes - uploadedBytes) / speed) : 0;
223
- const activeList = Array.from(activeFiles);
224
- const currentFile = activeList.length > 0 ? trimMiddle(activeList[activeList.length - 1], 86) : "-";
225
- if (!spinner) {
504
+ if (!progressBar) {
505
+ const progressRatio = totalFiles > 0 ? completed / totalFiles : 1;
506
+ const percentage = Math.round(progressRatio * 100);
507
+ if (completed === 0 && totalFiles > 0) return;
226
508
  if (completed === lastReportedCompleted) return;
227
509
  if (completed === totalFiles || completed % reportEvery === 0) {
228
510
  console.log(
229
- `${chalk.gray("\u8FDB\u5EA6:")} ${completed}/${totalFiles} (${percentage}%) | ${chalk.gray("\u6570\u636E:")} ${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)} | ${chalk.gray("\u901F\u5EA6:")} ${formatBytes(speed)}/s`
511
+ `${chalk5.gray("\u4E0A\u4F20\u8FDB\u5EA6")} ${renderInlineStats([
512
+ chalk5.bold(`${completed}/${totalFiles}`),
513
+ `${percentage}%`,
514
+ `${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)}`,
515
+ `${formatBytes(speed)}/s`
516
+ ])}`
230
517
  );
231
518
  lastReportedCompleted = completed;
232
519
  }
233
520
  return;
234
521
  }
235
- const bar = buildCapsuleBar(progressRatio);
236
- const warnLine = retries > 0 || failed > 0 ? `
237
- ${chalk.yellow("\u91CD\u8BD5")}: ${retries} ${chalk.yellow("\u5931\u8D25")}: ${failed}` : "";
238
- spinner.text = [
239
- `${chalk.cyan("\u6B63\u5728\u4E0A\u4F20:")} ${chalk.white(currentFile)}`,
240
- `${bar} ${chalk.bold(`${percentage}%`)} ${chalk.gray(`(${completed}/${totalFiles})`)} ${chalk.gray("|")} ${chalk.blue(formatBytes(uploadedBytes))}/${chalk.blue(formatBytes(totalBytes))} ${chalk.gray("|")} ${chalk.magenta(`${formatBytes(speed)}/s`)} ${chalk.gray("|")} \u9884\u8BA1 ${chalk.yellow(formatDuration(etaSeconds))}`
241
- ].join("\n");
242
- spinner.text += warnLine;
522
+ progressBar.update(completed, {
523
+ speed: chalk5.magenta(formatBytes(speed)),
524
+ elapsed: formatDuration(elapsedSeconds).replace(/s$/, "")
525
+ });
243
526
  };
244
- const refreshTimer = spinner ? setInterval(updateProgress, 120) : null;
245
- let currentIndex = 0;
527
+ const refreshTimer = progressBar ? setInterval(updateProgress, 120) : null;
528
+ let currentGroupIndex = 0;
246
529
  const worker = async () => {
247
530
  const client = new Client();
248
531
  let connected = false;
532
+ let currentRelativeDir = "";
533
+ let rooted = false;
534
+ const ensuredRelativeDirs = /* @__PURE__ */ new Set();
249
535
  const ensureConnected = async () => {
250
536
  if (connected) return;
537
+ const connectStartedAt = Date.now();
251
538
  await connectWithRetry(client, connectConfig, maxRetries, retryDelay, true);
252
539
  connected = true;
540
+ rooted = false;
541
+ currentRelativeDir = "";
542
+ debugMetrics.connectMs += Date.now() - connectStartedAt;
543
+ };
544
+ const ensureRootDir = async () => {
545
+ if (rooted) return;
546
+ const rootStartedAt = Date.now();
547
+ await client.ensureDir(normalizedTargetDir);
548
+ rooted = true;
549
+ currentRelativeDir = "";
550
+ debugMetrics.rootDirMs += Date.now() - rootStartedAt;
551
+ };
552
+ const ensureRemoteDir = async (remoteDir) => {
553
+ await ensureRootDir();
554
+ const relativeDir = remoteDir === normalizedTargetDir ? "" : remoteDir.slice(normalizedTargetDir.length).replace(/^\/+/, "");
555
+ if (currentRelativeDir === relativeDir) return;
556
+ if (!relativeDir) {
557
+ const switchStartedAt2 = Date.now();
558
+ await client.cd(normalizedTargetDir);
559
+ currentRelativeDir = "";
560
+ debugMetrics.switchDirMs += Date.now() - switchStartedAt2;
561
+ return;
562
+ }
563
+ const switchStartedAt = Date.now();
564
+ await client.cd(normalizedTargetDir);
565
+ if (!ensuredRelativeDirs.has(relativeDir)) {
566
+ await client.ensureDir(relativeDir);
567
+ ensuredRelativeDirs.add(relativeDir);
568
+ } else {
569
+ await client.cd(relativeDir);
570
+ }
571
+ currentRelativeDir = relativeDir;
572
+ debugMetrics.switchDirMs += Date.now() - switchStartedAt;
253
573
  };
254
574
  const markDisconnected = () => {
255
575
  connected = false;
576
+ rooted = false;
577
+ currentRelativeDir = "";
578
+ ensuredRelativeDirs.clear();
256
579
  };
257
580
  try {
258
581
  while (true) {
259
- const index = currentIndex++;
260
- if (index >= tasks.length) return;
261
- const task = tasks[index];
262
- activeFiles.add(task.remotePath);
582
+ const groupIndex = currentGroupIndex++;
583
+ if (groupIndex >= taskGroups.length) return;
584
+ const taskGroup = taskGroups[groupIndex];
585
+ for (const task of taskGroup.tasks) {
586
+ updateProgress();
587
+ const result = await uploadFileWithRetry(task, {
588
+ client,
589
+ ensureConnected,
590
+ ensureRemoteDir,
591
+ markDisconnected,
592
+ silentLogs,
593
+ maxRetries,
594
+ retryDelay,
595
+ debugMetrics
596
+ });
597
+ completed++;
598
+ retries += result.retries;
599
+ if (result.success) {
600
+ uploadedBytes += result.size;
601
+ } else {
602
+ failed++;
603
+ }
604
+ results.push(result);
605
+ updateProgress();
606
+ }
607
+ }
608
+ } finally {
609
+ client.close();
610
+ }
611
+ };
612
+ const runReusableWorker = async (seed) => {
613
+ const client = seed.client;
614
+ let connected = true;
615
+ let currentRelativeDir = "";
616
+ let rooted = false;
617
+ const ensuredRelativeDirs = /* @__PURE__ */ new Set();
618
+ const ensureConnected = async () => {
619
+ if (connected) return;
620
+ const connectStartedAt = Date.now();
621
+ await connectWithRetry(client, connectConfig, maxRetries, retryDelay, true);
622
+ connected = true;
623
+ rooted = false;
624
+ currentRelativeDir = "";
625
+ debugMetrics.connectMs += Date.now() - connectStartedAt;
626
+ };
627
+ const ensureRootDir = async () => {
628
+ if (rooted) return;
629
+ const rootStartedAt = Date.now();
630
+ await client.ensureDir(normalizedTargetDir);
631
+ rooted = true;
632
+ currentRelativeDir = "";
633
+ debugMetrics.rootDirMs += Date.now() - rootStartedAt;
634
+ };
635
+ const ensureRemoteDir = async (remoteDir) => {
636
+ await ensureRootDir();
637
+ const relativeDir = remoteDir === normalizedTargetDir ? "" : remoteDir.slice(normalizedTargetDir.length).replace(/^\/+/, "");
638
+ if (currentRelativeDir === relativeDir) return;
639
+ if (!relativeDir) {
640
+ const switchStartedAt2 = Date.now();
641
+ await client.cd(normalizedTargetDir);
642
+ currentRelativeDir = "";
643
+ debugMetrics.switchDirMs += Date.now() - switchStartedAt2;
644
+ return;
645
+ }
646
+ const switchStartedAt = Date.now();
647
+ await client.cd(normalizedTargetDir);
648
+ if (!ensuredRelativeDirs.has(relativeDir)) {
649
+ await client.ensureDir(relativeDir);
650
+ ensuredRelativeDirs.add(relativeDir);
651
+ } else {
652
+ await client.cd(relativeDir);
653
+ }
654
+ currentRelativeDir = relativeDir;
655
+ debugMetrics.switchDirMs += Date.now() - switchStartedAt;
656
+ };
657
+ const markDisconnected = () => {
658
+ connected = false;
659
+ rooted = false;
660
+ currentRelativeDir = "";
661
+ ensuredRelativeDirs.clear();
662
+ };
663
+ while (true) {
664
+ const groupIndex = currentGroupIndex++;
665
+ if (groupIndex >= taskGroups.length) return;
666
+ const taskGroup = taskGroups[groupIndex];
667
+ for (const task of taskGroup.tasks) {
263
668
  updateProgress();
264
669
  const result = await uploadFileWithRetry(task, {
265
670
  client,
266
671
  ensureConnected,
672
+ ensureRemoteDir,
267
673
  markDisconnected,
268
674
  silentLogs,
269
675
  maxRetries,
270
- retryDelay
676
+ retryDelay,
677
+ debugMetrics
271
678
  });
272
679
  completed++;
273
680
  retries += result.retries;
@@ -277,79 +684,149 @@ ${chalk.yellow("\u91CD\u8BD5")}: ${retries} ${chalk.yellow("\u5931\u8D25")}: ${
277
684
  failed++;
278
685
  }
279
686
  results.push(result);
280
- activeFiles.delete(task.remotePath);
281
687
  updateProgress();
282
688
  }
283
- } finally {
284
- client.close();
285
689
  }
286
690
  };
287
691
  updateProgress();
288
692
  try {
289
- await Promise.all(Array.from({ length: safeWindowSize }, () => worker()));
693
+ const workers = Array.from({ length: extraWorkerCount }, () => worker());
694
+ if (reusableClient) {
695
+ workers.unshift(runReusableWorker(reusableClient));
696
+ }
697
+ await Promise.all(workers);
290
698
  } finally {
291
699
  if (refreshTimer) clearInterval(refreshTimer);
292
700
  }
293
- if (spinner) {
701
+ if (progressBar) {
294
702
  const elapsedSeconds = (Date.now() - startAt) / 1e3;
295
- const successCount = results.filter((item) => item.success).length;
296
703
  const speed = elapsedSeconds > 0 ? uploadedBytes / elapsedSeconds : 0;
297
- spinner.succeed(
298
- `${chalk.green("\u4E0A\u4F20\u6210\u529F")} ${successCount} \u4E2A\u6587\u4EF6\u3002
299
- ${buildCapsuleBar(1)} 100% (${totalFiles}/${totalFiles}) ${chalk.gray("|")} \u901F\u5EA6 ${chalk.magenta(`${formatBytes(speed)}/s`)} ${chalk.gray("|")} \u8017\u65F6 ${chalk.yellow(formatDuration(elapsedSeconds))}`
300
- );
704
+ progressBar.update(totalFiles, {
705
+ speed: chalk5.magenta(formatBytes(speed)),
706
+ elapsed: formatDuration(elapsedSeconds).replace(/s$/, "")
707
+ });
708
+ progressBar.stop();
301
709
  } else {
302
- console.log(`${chalk.green("\u2714")} \u6240\u6709\u6587\u4EF6\u4E0A\u4F20\u5B8C\u6210 (${totalFiles}/${totalFiles})`);
710
+ console.log(`${getLogSymbol("success")} \u6240\u6709\u6587\u4EF6\u4E0A\u4F20\u5B8C\u6210 (${totalFiles}/${totalFiles})`);
303
711
  }
304
- return results;
712
+ debugEntries.push(
713
+ {
714
+ label: "\u8FDE\u63A5\u670D\u52A1\u5668",
715
+ durationMs: debugMetrics.connectMs
716
+ },
717
+ {
718
+ label: "\u51C6\u5907\u6839\u76EE\u5F55",
719
+ durationMs: debugMetrics.rootDirMs
720
+ },
721
+ {
722
+ label: "\u5207\u6362\u5B50\u76EE\u5F55",
723
+ durationMs: debugMetrics.switchDirMs
724
+ },
725
+ {
726
+ label: "\u6587\u4EF6\u4F20\u8F93",
727
+ durationMs: debugMetrics.uploadMs,
728
+ detail: `${tasks.length} \u4E2A\u6587\u4EF6`
729
+ },
730
+ {
731
+ label: "\u4E0A\u4F20\u9636\u6BB5",
732
+ durationMs: Date.now() - startAt
733
+ }
734
+ );
735
+ return { results, debugEntries };
305
736
  };
306
737
  const deploySingleTarget = async (ftpConfig) => {
307
738
  const { host, port = 21, user, password, alias = "", name } = ftpConfig;
739
+ const normalizedAlias = alias ? normalizeUrlLikeBase(alias) : "";
308
740
  if (!host || !user || !password) {
309
- console.error(chalk.red(`\u274C FTP\u914D\u7F6E "${name || host || "\u672A\u77E5"}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570:`));
310
- if (!host) console.error(chalk.red(" - \u7F3A\u5C11 host"));
311
- if (!user) console.error(chalk.red(" - \u7F3A\u5C11 user"));
312
- if (!password) console.error(chalk.red(" - \u7F3A\u5C11 password"));
741
+ console.error(chalk5.red(`\u274C FTP\u914D\u7F6E "${name || host || "\u672A\u77E5"}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570:`));
742
+ if (!host) console.error(chalk5.red(" - \u7F3A\u5C11 host"));
743
+ if (!user) console.error(chalk5.red(" - \u7F3A\u5C11 user"));
744
+ if (!password) console.error(chalk5.red(" - \u7F3A\u5C11 password"));
313
745
  return { name: name || host || "unknown", totalFiles: 0, failedCount: 1 };
314
746
  }
747
+ const debugEntries = [];
748
+ const collectFilesStartedAt = Date.now();
315
749
  const allFiles = getAllFiles(outDir);
750
+ debugEntries.push({
751
+ label: "\u626B\u63CF\u672C\u5730\u6587\u4EF6",
752
+ durationMs: Date.now() - collectFilesStartedAt,
753
+ detail: `${allFiles.length} \u4E2A\u6587\u4EF6`
754
+ });
316
755
  const totalFiles = allFiles.length;
317
- const { protocol, baseUrl } = parseAlias(alias);
318
756
  const displayName = name || host;
319
757
  const startTime = Date.now();
320
758
  if (allFiles.length === 0) {
321
- console.log(`${chalk.yellow("\u26A0 \u6CA1\u6709\u627E\u5230\u9700\u8981\u4E0A\u4F20\u7684\u6587\u4EF6")}`);
759
+ console.log(`${getLogSymbol("warning")} \u6CA1\u6709\u627E\u5230\u9700\u8981\u4E0A\u4F20\u7684\u6587\u4EF6`);
322
760
  return { name: displayName, totalFiles: 0, failedCount: 0 };
323
761
  }
324
762
  clearScreen();
325
- console.log(chalk.cyan(`
326
- \u{1F680} FTP \u90E8\u7F72\u5F00\u59CB
327
- `));
328
- console.log(`${chalk.gray("Server:")} ${chalk.green(displayName)}`);
329
- console.log(`${chalk.gray("Host:")} ${chalk.green(host)}`);
330
- console.log(`${chalk.gray("Source:")} ${chalk.yellow(outDir)}`);
331
- console.log(`${chalk.gray("Target:")} ${chalk.yellow(normalizedUploadPath)}`);
332
- if (alias) console.log(`${chalk.gray("Alias:")} ${chalk.green(alias)}`);
333
- console.log(`${chalk.gray("Files:")} ${chalk.blue(totalFiles)}
334
- `);
763
+ console.log(
764
+ renderPanel(
765
+ `${getPanelDot("success")} \u51C6\u5907\u90E8\u7F72`,
766
+ [
767
+ {
768
+ label: "\u4F4D\u7F6E:",
769
+ value: chalk5.green(`${displayName} \xB7 ${port === 21 ? host : `${host}:${port}`}`)
770
+ },
771
+ {
772
+ label: "\u76EE\u6807:",
773
+ value: chalk5.yellow(
774
+ truncateTerminalText(
775
+ normalizedAlias ? `${normalizedUploadPath} \xB7 ${normalizedAlias}` : normalizedUploadPath,
776
+ 18
777
+ )
778
+ )
779
+ },
780
+ {
781
+ label: "\u6587\u4EF6:",
782
+ value: chalk5.blue(`${totalFiles} \u4E2A \xB7 ${truncateTerminalText(outDir, 30)}`)
783
+ }
784
+ ],
785
+ "info"
786
+ )
787
+ );
335
788
  const connectConfig = { host, port, user, password };
336
789
  const preflightClient = new Client();
337
790
  const preflightSpinner = useInteractiveOutput ? ora(`\u8FDE\u63A5\u5230 ${displayName}...`).start() : null;
338
791
  try {
792
+ const preflightConnectStartedAt = Date.now();
339
793
  await connectWithRetry(preflightClient, connectConfig, maxRetries, retryDelay, Boolean(preflightSpinner));
340
- if (preflightSpinner) preflightSpinner.succeed("\u8FDE\u63A5\u6210\u529F");
794
+ debugEntries.push({
795
+ label: "\u9884\u68C0\u8FDE\u63A5",
796
+ durationMs: Date.now() - preflightConnectStartedAt
797
+ });
798
+ if (preflightSpinner) preflightSpinner.stop();
799
+ const ensureTargetStartedAt = Date.now();
341
800
  await preflightClient.ensureDir(normalizedUploadPath);
801
+ debugEntries.push({
802
+ label: "\u786E\u8BA4\u76EE\u6807\u76EE\u5F55",
803
+ durationMs: Date.now() - ensureTargetStartedAt,
804
+ detail: normalizedUploadPath
805
+ });
806
+ const listRemoteStartedAt = Date.now();
342
807
  const fileList = await preflightClient.list();
808
+ debugEntries.push({
809
+ label: "\u8BFB\u53D6\u8FDC\u7AEF\u6587\u4EF6",
810
+ durationMs: Date.now() - listRemoteStartedAt,
811
+ detail: `${fileList.length} \u4E2A`
812
+ });
813
+ let backupSummary = null;
343
814
  if (fileList.length) {
344
815
  if (singleBack) {
345
- await createSingleBackup(
816
+ const backupStartedAt = Date.now();
817
+ backupSummary = await createSingleBackup(
346
818
  preflightClient,
347
819
  normalizedUploadPath,
348
- protocol,
349
- baseUrl,
820
+ normalizedAlias,
350
821
  singleBackFiles,
351
- showBackFile
822
+ showBackFile,
823
+ useInteractiveOutput
352
824
  );
825
+ debugEntries.push({
826
+ label: "\u6267\u884C\u5907\u4EFD",
827
+ durationMs: Date.now() - backupStartedAt,
828
+ detail: backupSummary ? `${backupSummary.items.length} \u4E2A\u5907\u4EFD\u6587\u4EF6` : "\u672A\u751F\u6210\u5907\u4EFD"
829
+ });
353
830
  } else {
354
831
  const shouldBackup = await select({
355
832
  message: `\u662F\u5426\u5907\u4EFD ${displayName} \u7684\u8FDC\u7A0B\u6587\u4EF6`,
@@ -357,72 +834,118 @@ ${buildCapsuleBar(1)} 100% (${totalFiles}/${totalFiles}) ${chalk.gray("|")} \u90
357
834
  default: "\u5426"
358
835
  });
359
836
  if (shouldBackup === "\u662F") {
360
- await createBackupFile(
837
+ const backupStartedAt = Date.now();
838
+ backupSummary = await createBackupFile(
361
839
  preflightClient,
362
840
  normalizedUploadPath,
363
- protocol,
364
- baseUrl,
365
- showBackFile
841
+ normalizedAlias,
842
+ showBackFile,
843
+ useInteractiveOutput
366
844
  );
845
+ debugEntries.push({
846
+ label: "\u6267\u884C\u5907\u4EFD",
847
+ durationMs: Date.now() - backupStartedAt,
848
+ detail: backupSummary ? `${backupSummary.items.length} \u4E2A\u5907\u4EFD\u6587\u4EF6` : "\u672A\u751F\u6210\u5907\u4EFD"
849
+ });
850
+ } else if (debug) {
851
+ debugEntries.push({
852
+ label: "\u6267\u884C\u5907\u4EFD",
853
+ durationMs: 0,
854
+ detail: "\u624B\u52A8\u8DF3\u8FC7"
855
+ });
367
856
  }
368
857
  }
858
+ } else if (debug) {
859
+ debugEntries.push({
860
+ label: "\u6267\u884C\u5907\u4EFD",
861
+ durationMs: 0,
862
+ detail: "\u8FDC\u7AEF\u4E3A\u7A7A\uFF0C\u8DF3\u8FC7"
863
+ });
864
+ }
865
+ if (backupSummary) {
866
+ console.log(renderBackupPanel(backupSummary));
369
867
  }
370
- const results = await uploadFilesInBatches(
868
+ const uploadExecution = await uploadFilesInBatches({
371
869
  connectConfig,
372
- allFiles,
373
- normalizedUploadPath,
374
- concurrency
375
- );
870
+ files: allFiles,
871
+ targetDir: normalizedUploadPath,
872
+ windowSize: concurrency,
873
+ reusableClient: { client: preflightClient }
874
+ });
875
+ const { results, debugEntries: uploadDebugEntries } = uploadExecution;
876
+ if (debug) {
877
+ debugEntries.push(...uploadDebugEntries);
878
+ }
376
879
  const successCount = results.filter((r) => r.success).length;
377
880
  const failedCount = results.length - successCount;
378
881
  const durationSeconds = (Date.now() - startTime) / 1e3;
379
- const duration = durationSeconds.toFixed(2);
380
882
  const uploadedBytes = results.reduce((sum, result) => result.success ? sum + result.size : sum, 0);
381
883
  const retryCount = results.reduce((sum, result) => sum + result.retries, 0);
382
884
  const avgSpeed = durationSeconds > 0 ? uploadedBytes / durationSeconds : 0;
885
+ const accessUrl = normalizedAlias ? resolveDisplayUrl(normalizedAlias, normalizedUploadPath) : "";
383
886
  clearScreen();
384
- console.log("\n" + chalk.gray("\u2500".repeat(40)) + "\n");
385
- if (failedCount === 0) {
386
- console.log(`${chalk.green("\u{1F389} \u90E8\u7F72\u6210\u529F!")}`);
387
- } else {
388
- console.log(`${chalk.yellow("\u26A0 \u90E8\u7F72\u5B8C\u6210\u4F46\u5B58\u5728\u9519\u8BEF")}`);
887
+ const resultRows = [
888
+ {
889
+ label: "\u7ED3\u679C:",
890
+ value: failedCount === 0 ? chalk5.green(`${successCount}/${results.length} \u5168\u90E8\u6210\u529F`) : chalk5.yellow(`\u6210\u529F ${successCount} \u4E2A\uFF0C\u5931\u8D25 ${failedCount} \u4E2A`)
891
+ },
892
+ {
893
+ label: "\u7EDF\u8BA1:",
894
+ value: renderInlineStats([
895
+ `${retryCount} \u6B21\u91CD\u8BD5`,
896
+ formatBytes(uploadedBytes),
897
+ `${formatBytes(avgSpeed)}/s`,
898
+ formatDuration(durationSeconds)
899
+ ])
900
+ }
901
+ ];
902
+ if (accessUrl) {
903
+ resultRows.push({ label: "\u8BBF\u95EE:", value: chalk5.cyan(truncateTerminalText(accessUrl, 20)) });
389
904
  }
390
- console.log(`
391
- ${chalk.gray("\u7EDF\u8BA1:")}`);
392
- console.log(` ${chalk.green("\u2714")} \u6210\u529F: ${chalk.bold(successCount)}`);
393
905
  if (failedCount > 0) {
394
- console.log(` ${chalk.red("\u2717")} \u5931\u8D25: ${chalk.bold(failedCount)}`);
395
- }
396
- console.log(` ${chalk.cyan("\u21C4")} \u91CD\u8BD5: ${chalk.bold(retryCount)}`);
397
- console.log(` ${chalk.blue("\u{1F4E6}")} \u6570\u636E: ${chalk.bold(formatBytes(uploadedBytes))}`);
398
- console.log(` ${chalk.magenta("\u26A1")} \u5E73\u5747\u901F\u5EA6: ${chalk.bold(`${formatBytes(avgSpeed)}/s`)}`);
399
- console.log(` ${chalk.blue("\u23F1")} \u8017\u65F6: ${chalk.bold(duration)}s`);
400
- if (baseUrl) {
401
- console.log(
402
- ` ${chalk.green("\u{1F517}")} \u8BBF\u95EE\u5730\u5740: ${chalk.bold(buildUrl(protocol, baseUrl, normalizedUploadPath))}`
906
+ const failedItems = results.filter((result) => !result.success).slice(0, 2);
907
+ resultRows.push(
908
+ ...failedItems.map((item, index) => ({
909
+ label: `\u5931\u8D25 ${index + 1}:`,
910
+ value: chalk5.red(
911
+ `${truncateTerminalText(item.name, 26)} \xB7 ${truncateTerminalText(item.error?.message || "unknown error", 22)}`
912
+ )
913
+ }))
403
914
  );
404
- }
405
- console.log("");
406
- if (failedCount > 0) {
407
- const failedItems = results.filter((result) => !result.success);
408
- const previewCount = Math.min(5, failedItems.length);
409
- console.log(chalk.red("\u5931\u8D25\u660E\u7EC6:"));
410
- for (let i = 0; i < previewCount; i++) {
411
- const item = failedItems[i];
412
- const reason = item.error?.message || "unknown error";
413
- console.log(` ${chalk.red("\u2022")} ${item.name} => ${reason}`);
414
- }
415
- if (failedItems.length > previewCount) {
416
- console.log(chalk.gray(` ... \u8FD8\u6709 ${failedItems.length - previewCount} \u4E2A\u5931\u8D25\u6587\u4EF6`));
915
+ if (failedCount > failedItems.length) {
916
+ resultRows.push({
917
+ label: "\u5176\u4F59:",
918
+ value: chalk5.gray(`\u8FD8\u6709 ${failedCount - failedItems.length} \u4E2A\u5931\u8D25\u9879\u672A\u5C55\u5F00`)
919
+ });
417
920
  }
418
- console.log("");
921
+ }
922
+ console.log(
923
+ renderPanel(
924
+ failedCount === 0 ? `${getPanelDot("success")} \u90E8\u7F72\u5B8C\u6210` : `${getPanelDot("warning")} \u90E8\u7F72\u5B8C\u6210`,
925
+ resultRows,
926
+ failedCount === 0 ? "success" : "warning"
927
+ )
928
+ );
929
+ if (debug) {
930
+ debugEntries.push({
931
+ label: "\u603B\u8017\u65F6",
932
+ durationMs: Date.now() - startTime
933
+ });
934
+ console.log(renderDebugPanel(debugEntries));
419
935
  }
420
936
  return { name: displayName, totalFiles: results.length, failedCount };
421
937
  } catch (error) {
422
- if (preflightSpinner) preflightSpinner.fail(`\u274C \u4E0A\u4F20\u5230 ${displayName} \u5931\u8D25`);
938
+ if (preflightSpinner) preflightSpinner.stop();
423
939
  console.log(`
424
- ${chalk.red("\u274C \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF:")} ${error}
940
+ ${getLogSymbol("danger")} \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF: ${error}
425
941
  `);
942
+ if (debug && debugEntries.length > 0) {
943
+ debugEntries.push({
944
+ label: "\u5931\u8D25\u524D\u8017\u65F6",
945
+ durationMs: Date.now() - startTime
946
+ });
947
+ console.log(renderDebugPanel(debugEntries));
948
+ }
426
949
  return {
427
950
  name: displayName,
428
951
  totalFiles,
@@ -447,28 +970,28 @@ ${chalk.red("\u274C \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF:")} $
447
970
  if (defaultFtp) {
448
971
  const defaultConfig = ftpConfigs.find((ftp) => ftp.name === defaultFtp);
449
972
  if (defaultConfig && validateFtpConfig(defaultConfig)) {
450
- console.log(chalk.blue(`\u4F7F\u7528\u9ED8\u8BA4FTP\u914D\u7F6E: ${defaultFtp}`));
973
+ console.log(chalk5.blue(`\u4F7F\u7528\u9ED8\u8BA4FTP\u914D\u7F6E: ${defaultFtp}`));
451
974
  selectedConfigs = [defaultConfig];
452
975
  } else if (defaultConfig) {
453
- console.log(chalk.yellow(`\u26A0 \u9ED8\u8BA4FTP\u914D\u7F6E "${defaultFtp}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5C06\u8FDB\u884C\u624B\u52A8\u9009\u62E9`));
976
+ console.log(chalk5.yellow(`\u26A0 \u9ED8\u8BA4FTP\u914D\u7F6E "${defaultFtp}" \u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5C06\u8FDB\u884C\u624B\u52A8\u9009\u62E9`));
454
977
  }
455
978
  }
456
979
  if (selectedConfigs.length === 0) {
457
980
  const validConfigs = ftpConfigs.filter(validateFtpConfig);
458
981
  const invalidConfigs = ftpConfigs.filter((config) => !validateFtpConfig(config));
459
982
  if (invalidConfigs.length > 0) {
460
- console.log(chalk.yellow("\n\u4EE5\u4E0BFTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5DF2\u4ECE\u9009\u62E9\u5217\u8868\u4E2D\u6392\u9664:"));
983
+ console.log(chalk5.yellow("\n\u4EE5\u4E0BFTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\uFF0C\u5DF2\u4ECE\u9009\u62E9\u5217\u8868\u4E2D\u6392\u9664:"));
461
984
  invalidConfigs.forEach((config) => {
462
985
  const missing = [];
463
986
  if (!config.host) missing.push("host");
464
987
  if (!config.user) missing.push("user");
465
988
  if (!config.password) missing.push("password");
466
- console.log(chalk.yellow(` - ${config.name || "\u672A\u547D\u540D"}: \u7F3A\u5C11 ${missing.join(", ")}`));
989
+ console.log(chalk5.yellow(` - ${config.name || "\u672A\u547D\u540D"}: \u7F3A\u5C11 ${missing.join(", ")}`));
467
990
  });
468
991
  console.log();
469
992
  }
470
993
  if (validConfigs.length === 0) {
471
- console.error(chalk.red("\u274C \u6CA1\u6709\u53EF\u7528\u7684\u6709\u6548FTP\u914D\u7F6E"));
994
+ console.error(chalk5.red("\u274C \u6CA1\u6709\u53EF\u7528\u7684\u6709\u6548FTP\u914D\u7F6E"));
472
995
  return [];
473
996
  }
474
997
  selectedConfigs = await checkbox({
@@ -489,7 +1012,7 @@ ${chalk.red("\u274C \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF:")} $
489
1012
  if (!singleConfig?.host) missing.push("host");
490
1013
  if (!singleConfig?.user) missing.push("user");
491
1014
  if (!singleConfig?.password) missing.push("password");
492
- console.error(chalk.red(`\u274C FTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: ${missing.join(", ")}`));
1015
+ console.error(chalk5.red(`\u274C FTP\u914D\u7F6E\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570: ${missing.join(", ")}`));
493
1016
  return [];
494
1017
  }
495
1018
  }
@@ -512,7 +1035,7 @@ ${chalk.red("\u274C \u4E0A\u4F20\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF:")} $
512
1035
  clearScreen();
513
1036
  const validationErrors = validateOptions();
514
1037
  if (validationErrors.length > 0) {
515
- console.log(`${chalk.red("\u2717 \u914D\u7F6E\u9519\u8BEF:")}
1038
+ console.log(`${chalk5.red("\u2717 \u914D\u7F6E\u9519\u8BEF:")}
516
1039
  ${validationErrors.map((err) => ` - ${err}`).join("\n")}`);
517
1040
  return;
518
1041
  }
@@ -521,7 +1044,7 @@ ${validationErrors.map((err) => ` - ${err}`).join("\n")}`);
521
1044
  },
522
1045
  configResolved(config) {
523
1046
  resolvedConfig = config;
524
- outDir = normalizePath(path.resolve(config.root, config.build.outDir));
1047
+ outDir = normalizePath2(path2.resolve(config.root, config.build.outDir));
525
1048
  },
526
1049
  closeBundle: {
527
1050
  sequential: true,
@@ -538,41 +1061,14 @@ ${validationErrors.map((err) => ` - ${err}`).join("\n")}`);
538
1061
  }
539
1062
  };
540
1063
  }
541
- function getAllFiles(dirPath, arrayOfFiles = [], relativePath = "") {
542
- const files = fs.readdirSync(dirPath);
543
- files.forEach((file) => {
544
- const fullPath = path.join(dirPath, file);
545
- const relPath = path.join(relativePath, file);
546
- if (fs.statSync(fullPath).isDirectory()) {
547
- getAllFiles(fullPath, arrayOfFiles, relPath);
548
- } else {
549
- arrayOfFiles.push(normalizePath(relPath));
550
- }
551
- });
552
- return arrayOfFiles;
553
- }
554
- function validateFtpConfig(config) {
555
- return !!(config.host && config.user && config.password);
556
- }
557
- function parseAlias(alias = "") {
558
- const [protocol = "", baseUrl = ""] = alias.split("://");
559
- return {
560
- protocol: protocol ? `${protocol}://` : "",
561
- baseUrl: baseUrl || ""
562
- };
563
- }
564
- function buildUrl(protocol, baseUrl, targetPath) {
565
- return protocol + normalizePath(baseUrl + targetPath);
566
- }
567
- var backupArchivePattern = /^backup_\d{8}_\d{6}\.zip$/i;
568
1064
  async function downloadRemoteFilesForBackup(client, remoteDir, localDir, downloadedFiles = []) {
569
- if (!fs.existsSync(localDir)) {
570
- fs.mkdirSync(localDir, { recursive: true });
1065
+ if (!fs2.existsSync(localDir)) {
1066
+ fs2.mkdirSync(localDir, { recursive: true });
571
1067
  }
572
1068
  const remoteEntries = await client.list(remoteDir);
573
1069
  for (const entry of remoteEntries) {
574
- const remotePath = normalizePath(`${remoteDir}/${entry.name}`);
575
- const localPath = path.join(localDir, entry.name);
1070
+ const remotePath = normalizeRemotePath(remoteDir, entry.name);
1071
+ const localPath = path2.join(localDir, entry.name);
576
1072
  if (entry.type === FileType.Directory) {
577
1073
  await downloadRemoteFilesForBackup(client, remotePath, localPath, downloadedFiles);
578
1074
  continue;
@@ -601,138 +1097,94 @@ async function downloadRemoteFilesForBackup(client, remoteDir, localDir, downloa
601
1097
  }
602
1098
  return downloadedFiles;
603
1099
  }
604
- async function connectWithRetry(client, config, maxRetries, retryDelay, silentLogs = false) {
605
- let lastError;
606
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
607
- try {
608
- client.ftp.verbose = false;
609
- await client.access({
610
- ...config,
611
- secure: true,
612
- secureOptions: { rejectUnauthorized: false, timeout: 6e4 }
613
- });
614
- return;
615
- } catch (error) {
616
- lastError = error instanceof Error ? error : new Error(String(error));
617
- if (attempt < maxRetries) {
618
- if (!silentLogs) {
619
- console.log(chalk.yellow(`\u26A0 \u8FDE\u63A5\u5931\u8D25\uFF0C${retryDelay}ms \u540E\u91CD\u8BD5 (${attempt}/${maxRetries})`));
620
- }
621
- await sleep(retryDelay * attempt);
622
- }
623
- }
624
- }
625
- throw new Error(`\u274C FTP \u8FDE\u63A5\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${maxRetries} \u6B21: ${lastError?.message}`);
626
- }
627
- function createTempDir(basePath) {
628
- const tempBaseDir = os.tmpdir();
629
- const tempPath = path.join(tempBaseDir, "vite-plugin-deploy-ftp", basePath);
630
- if (!fs.existsSync(tempPath)) {
631
- fs.mkdirSync(tempPath, { recursive: true });
632
- }
633
- return {
634
- path: tempPath,
635
- cleanup: () => {
636
- try {
637
- if (fs.existsSync(tempPath)) {
638
- fs.rmSync(tempPath, { recursive: true, force: true });
639
- }
640
- } catch (error) {
641
- console.warn(chalk.yellow(`\u26A0 \u6E05\u7406\u4E34\u65F6\u76EE\u5F55\u5931\u8D25: ${tempPath}`), error);
642
- }
643
- }
644
- };
645
- }
646
- async function createBackupFile(client, dir, protocol, baseUrl, showBackFile = false) {
647
- const backupSpinner = ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
1100
+ async function createBackupFile(client, dir, alias, showBackFile = false, useSpinner = true) {
1101
+ const targetUrl = resolveDisplayUrl(alias, dir);
1102
+ const backupSpinner = useSpinner ? ora(`\u521B\u5EFA\u5907\u4EFD\u6587\u4EF6\u4E2D ${chalk5.yellow(`==> ${targetUrl}`)}`).start() : null;
648
1103
  const fileName = `backup_${dayjs().format("YYYYMMDD_HHmmss")}.zip`;
649
1104
  const tempDir = createTempDir("backup-zip");
650
- const zipFilePath = path.join(os.tmpdir(), "vite-plugin-deploy-ftp", fileName);
1105
+ const zipFilePath = path2.join(os2.tmpdir(), "vite-plugin-deploy-ftp", fileName);
651
1106
  try {
652
- const zipDir = path.dirname(zipFilePath);
653
- if (!fs.existsSync(zipDir)) {
654
- fs.mkdirSync(zipDir, { recursive: true });
1107
+ const zipDir = path2.dirname(zipFilePath);
1108
+ if (!fs2.existsSync(zipDir)) {
1109
+ fs2.mkdirSync(zipDir, { recursive: true });
1110
+ }
1111
+ if (backupSpinner) {
1112
+ backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u4E2D ${chalk5.yellow(`==> ${targetUrl}`)}`;
655
1113
  }
656
- backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
657
1114
  const downloadedFiles = await downloadRemoteFilesForBackup(client, dir, tempDir.path);
658
1115
  if (downloadedFiles.length === 0) {
659
- backupSpinner.warn("\u672A\u627E\u5230\u53EF\u5907\u4EFD\u7684\u8FDC\u7A0B\u6587\u4EF6");
660
- return;
1116
+ if (backupSpinner) {
1117
+ backupSpinner.warn("\u672A\u627E\u5230\u53EF\u5907\u4EFD\u7684\u8FDC\u7A0B\u6587\u4EF6");
1118
+ }
1119
+ return null;
661
1120
  }
662
1121
  if (showBackFile) {
663
- console.log(chalk.cyan(`
1122
+ console.log(chalk5.cyan(`
664
1123
  \u5F00\u59CB\u5907\u4EFD\u8FDC\u7A0B\u6587\u4EF6\uFF0C\u5171 ${downloadedFiles.length} \u4E2A\u6587\u4EF6:`));
665
1124
  downloadedFiles.forEach((file) => {
666
- console.log(chalk.gray(` - ${file.remotePath} (${file.size} bytes)`));
1125
+ console.log(chalk5.gray(` - ${file.remotePath} (${file.size} bytes)`));
667
1126
  });
668
1127
  }
669
- backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`;
1128
+ if (backupSpinner) {
1129
+ backupSpinner.text = `\u4E0B\u8F7D\u8FDC\u7A0B\u6587\u4EF6\u6210\u529F ${chalk5.yellow(`==> ${targetUrl}`)}`;
1130
+ }
670
1131
  await createZipFile(tempDir.path, zipFilePath);
671
- backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${chalk.yellow(
672
- `==> ${buildUrl(protocol, baseUrl, `${dir}/${fileName}`)}`
673
- )}`;
674
- await client.uploadFrom(zipFilePath, normalizePath(`${dir}/${fileName}`));
675
- const backupUrl = buildUrl(protocol, baseUrl, `${dir}/${fileName}`);
676
- backupSpinner.succeed("\u5907\u4EFD\u5B8C\u6210");
677
- console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
678
- console.log(chalk.green(`${backupUrl}`));
679
- console.log();
1132
+ const backupRemotePath = normalizeRemotePath(dir, fileName);
1133
+ if (backupSpinner) {
1134
+ backupSpinner.text = `\u538B\u7F29\u5B8C\u6210, \u51C6\u5907\u4E0A\u4F20 ${chalk5.yellow(`==> ${resolveDisplayUrl(alias, backupRemotePath)}`)}`;
1135
+ }
1136
+ await client.uploadFrom(zipFilePath, backupRemotePath);
1137
+ const backupUrl = resolveDisplayUrl(alias, backupRemotePath);
1138
+ backupSpinner?.stop();
1139
+ return {
1140
+ title: "\u5907\u4EFD\u5B8C\u6210",
1141
+ items: [backupUrl]
1142
+ };
680
1143
  } catch (error) {
681
- backupSpinner.fail("\u5907\u4EFD\u5931\u8D25");
1144
+ if (backupSpinner) {
1145
+ backupSpinner.fail("\u5907\u4EFD\u5931\u8D25");
1146
+ }
682
1147
  throw error;
683
1148
  } finally {
684
1149
  tempDir.cleanup();
685
1150
  try {
686
- if (fs.existsSync(zipFilePath)) {
687
- fs.rmSync(zipFilePath);
1151
+ if (fs2.existsSync(zipFilePath)) {
1152
+ fs2.rmSync(zipFilePath);
688
1153
  }
689
1154
  } catch (error) {
690
- console.warn(chalk.yellow("\u26A0 \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
1155
+ console.warn(chalk5.yellow("\u26A0 \u6E05\u7406zip\u6587\u4EF6\u5931\u8D25"), error);
691
1156
  }
692
1157
  }
693
1158
  }
694
- async function createZipFile(sourceDir, outputPath) {
695
- return new Promise((resolve, reject) => {
696
- const output = fs.createWriteStream(outputPath);
697
- const zipFile = new yazl.ZipFile();
698
- const handleError = (error) => {
699
- reject(error instanceof Error ? error : new Error(String(error)));
700
- };
701
- output.on("close", resolve);
702
- output.on("error", handleError);
703
- zipFile.outputStream.on("error", handleError);
704
- zipFile.outputStream.pipe(output);
705
- for (const relativePath of getAllFiles(sourceDir)) {
706
- const filePath = path.join(sourceDir, relativePath);
707
- zipFile.addFile(filePath, normalizePath(relativePath));
708
- }
709
- zipFile.end();
710
- });
711
- }
712
- async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFiles, showBackFile = false) {
1159
+ async function createSingleBackup(client, dir, alias, singleBackFiles, showBackFile = false, useSpinner = true) {
713
1160
  const timestamp = dayjs().format("YYYYMMDD_HHmmss");
714
- const backupSpinner = ora(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${chalk.yellow(`==> ${buildUrl(protocol, baseUrl, dir)}`)}`).start();
1161
+ const backupSpinner = useSpinner ? ora(`\u5907\u4EFD\u6307\u5B9A\u6587\u4EF6\u4E2D ${chalk5.yellow(`==> ${resolveDisplayUrl(alias, dir)}`)}`).start() : null;
715
1162
  const tempDir = createTempDir("single-backup");
716
1163
  let backupProgressSpinner;
717
1164
  try {
718
1165
  const remoteFiles = await client.list(dir);
719
- const backupTasks = singleBackFiles.map((fileName) => {
720
- const remoteFile = remoteFiles.find((f) => f.name === fileName);
1166
+ const normalizedSingleBackFiles = singleBackFiles.map((fileName) => normalizeSelectionPath(fileName)).filter(Boolean);
1167
+ const backupTasks = normalizedSingleBackFiles.map((fileName) => {
1168
+ const remoteFile = remoteFiles.find((file) => file.name === fileName);
721
1169
  return remoteFile ? { fileName, exists: true } : { fileName, exists: false };
722
1170
  }).filter((task) => task.exists);
723
1171
  if (backupTasks.length === 0) {
724
- backupSpinner.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
725
- return;
1172
+ if (backupSpinner) {
1173
+ backupSpinner.warn("\u672A\u627E\u5230\u9700\u8981\u5907\u4EFD\u7684\u6587\u4EF6");
1174
+ }
1175
+ return null;
726
1176
  }
727
- backupSpinner.stop();
1177
+ backupSpinner?.stop();
728
1178
  if (showBackFile) {
729
- console.log(chalk.cyan(`
1179
+ console.log(chalk5.cyan(`
730
1180
  \u5F00\u59CB\u5355\u6587\u4EF6\u5907\u4EFD\uFF0C\u5171 ${backupTasks.length} \u4E2A\u6587\u4EF6:`));
731
1181
  backupTasks.forEach((task) => {
732
- console.log(chalk.gray(` - ${task.fileName}`));
1182
+ console.log(chalk5.gray(` - ${task.fileName}`));
733
1183
  });
734
1184
  }
735
- backupProgressSpinner = ora("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
1185
+ if (useSpinner) {
1186
+ backupProgressSpinner = ora("\u6B63\u5728\u5907\u4EFD\u6587\u4EF6...").start();
1187
+ }
736
1188
  const concurrencyLimit = 3;
737
1189
  let backedUpCount = 0;
738
1190
  const backedUpFiles = [];
@@ -740,18 +1192,19 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
740
1192
  const batch = backupTasks.slice(i, i + concurrencyLimit);
741
1193
  const promises = batch.map(async ({ fileName }) => {
742
1194
  try {
743
- const localTempPath = path.join(tempDir.path, fileName);
1195
+ const localTempPath = path2.join(tempDir.path, fileName);
744
1196
  const extIndex = fileName.lastIndexOf(".");
745
1197
  const name = extIndex > -1 ? fileName.slice(0, extIndex) : fileName;
746
1198
  const ext = extIndex > -1 ? fileName.slice(extIndex) : "";
747
1199
  const backupFileName = `${name}.${timestamp}${ext}`;
748
- const backupRemotePath = normalizePath(`${dir}/${backupFileName}`);
749
- await client.downloadTo(localTempPath, normalizePath(`${dir}/${fileName}`));
1200
+ const sourceRemotePath = normalizeRemotePath(dir, fileName);
1201
+ const backupRemotePath = normalizeRemotePath(dir, backupFileName);
1202
+ await client.downloadTo(localTempPath, sourceRemotePath);
750
1203
  await client.uploadFrom(localTempPath, backupRemotePath);
751
- backedUpFiles.push(buildUrl(protocol, baseUrl, backupRemotePath));
1204
+ backedUpFiles.push(resolveDisplayUrl(alias, backupRemotePath));
752
1205
  return true;
753
1206
  } catch (error) {
754
- console.warn(chalk.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
1207
+ console.warn(chalk5.yellow(`\u5907\u4EFD\u6587\u4EF6 ${fileName} \u5931\u8D25:`), error instanceof Error ? error.message : error);
755
1208
  return false;
756
1209
  }
757
1210
  });
@@ -759,19 +1212,21 @@ async function createSingleBackup(client, dir, protocol, baseUrl, singleBackFile
759
1212
  backedUpCount += results.filter(Boolean).length;
760
1213
  }
761
1214
  if (backedUpCount > 0) {
762
- backupProgressSpinner.succeed("\u5907\u4EFD\u5B8C\u6210");
763
- console.log(chalk.cyan("\n\u5907\u4EFD\u6587\u4EF6:"));
764
- backedUpFiles.forEach((url) => {
765
- console.log(chalk.green(`\u{1F517} ${url}`));
766
- });
767
- console.log();
1215
+ backupProgressSpinner?.stop();
1216
+ return {
1217
+ title: "\u5907\u4EFD\u5B8C\u6210",
1218
+ items: backedUpFiles
1219
+ };
768
1220
  } else {
769
- backupProgressSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
1221
+ if (backupProgressSpinner) {
1222
+ backupProgressSpinner.fail("\u6240\u6709\u6587\u4EF6\u5907\u4EFD\u5931\u8D25");
1223
+ }
1224
+ return null;
770
1225
  }
771
1226
  } catch (error) {
772
1227
  if (backupProgressSpinner) {
773
1228
  backupProgressSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
774
- } else {
1229
+ } else if (backupSpinner) {
775
1230
  backupSpinner.fail("\u5907\u4EFD\u8FC7\u7A0B\u4E2D\u53D1\u751F\u9519\u8BEF");
776
1231
  }
777
1232
  throw error;