zen-gitsync 2.13.29 → 2.13.32

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.
Files changed (83) hide show
  1. package/README.md +2 -2
  2. package/package.json +3 -1
  3. package/src/cli/cleanup.js +179 -0
  4. package/src/cli/cleanup.test.js +269 -0
  5. package/src/cli/customCommand.js +94 -0
  6. package/src/cli/customCommand.test.js +143 -0
  7. package/src/cli/ui.js +59 -0
  8. package/src/cli/ui.test.js +115 -0
  9. package/src/config.js +62 -17
  10. package/src/gitCommit.js +73 -25
  11. package/src/ui/public/assets/AppVersionBadge-Ci_-H7TK.js +2 -0
  12. package/src/ui/public/assets/AppVersionBadge-Dj6H729N.css +1 -0
  13. package/src/ui/public/assets/BranchSelector-BIGlKQme.js +1 -0
  14. package/src/ui/public/assets/CommandConsole-DUu75Lvw.js +17 -0
  15. package/src/ui/public/assets/CommandConsole-Da3j09oc.css +1 -0
  16. package/src/ui/public/assets/CommitForm-asrs-955.js +6 -0
  17. package/src/ui/public/assets/CustomCommandManager-BBnTU2pp.js +1 -0
  18. package/src/ui/public/assets/EditorView-BYuHlMbn.js +0 -0
  19. package/src/ui/public/assets/{EditorView-Do_aRkqm.css → EditorView-BcZ8ToAk.css} +1 -1
  20. package/src/ui/public/assets/{FileDiffViewer-ASPmnTdG.css → FileDiffViewer-CLvy7mIG.css} +1 -1
  21. package/src/ui/public/assets/FileDiffViewer-vOvrYF89.js +13 -0
  22. package/src/ui/public/assets/{FlowExecutionViewer-ADwYz1XL.js → FlowExecutionViewer-DgQs18Ym.js} +1 -1
  23. package/src/ui/public/assets/FlowOrchestrationWorkspace-CEM_0XAJ.js +21 -0
  24. package/src/ui/public/assets/GitStatus-DedZ2a63.js +6 -0
  25. package/src/ui/public/assets/ImagePreview-XjBK0T26.js +1 -0
  26. package/src/ui/public/assets/LogList-CUn9TvU1.js +7 -0
  27. package/src/ui/public/assets/ProjectStartupButton-DWfKVcJo.js +1 -0
  28. package/src/ui/public/assets/RemoteRepoCard-BVRJ8eAD.js +1 -0
  29. package/src/ui/public/assets/ResetToRemoteButton-CDhmy9Qj.js +1 -0
  30. package/src/ui/public/assets/SourceMapView-CUq_XXYS.js +3 -0
  31. package/src/ui/public/assets/SourceMapView-DjYKJZLj.css +1 -0
  32. package/src/ui/public/assets/TemplateManager-CogQCagT.js +1 -0
  33. package/src/ui/public/assets/UserInputNode-BcIaDC-e.js +1 -0
  34. package/src/ui/public/assets/WorkbenchView-CGclOlIQ.js +12 -0
  35. package/src/ui/public/assets/WorkbenchView-ir769eTr.css +1 -0
  36. package/src/ui/public/assets/{_plugin-vue_export-helper-DKIb-p1R.js → _plugin-vue_export-helper-BWJlNHzL.js} +3 -3
  37. package/src/ui/public/assets/element-plus-CRTdimvR.js +24 -0
  38. package/src/ui/public/assets/{flow-mindmap-mZisamFd.js → flow-mindmap-oBAz3_QK.js} +4 -4
  39. package/src/ui/public/assets/index-BkpNzYyv.js +7 -0
  40. package/src/ui/public/assets/index-CNhCnXkD.css +1 -0
  41. package/src/ui/public/assets/vendor-BxotT5qN.js +93 -0
  42. package/src/ui/public/assets/vue-flow-Bx03URoq.js +39 -0
  43. package/src/ui/public/index.html +6 -6
  44. package/src/ui/server/index.js +1 -0
  45. package/src/ui/server/routes/npm.js +161 -8
  46. package/src/ui/server/utils/instanceRegistry.js +11 -0
  47. package/src/ui/server/utils/startServerOnAvailablePort.js +13 -0
  48. package/src/utils/index.js +42 -10
  49. package/src/utils/parseCwdArg.test.js +89 -0
  50. package/src/utils/utils.test.js +200 -0
  51. package/src/ui/public/assets/AppVersionBadge-CTUXSSX1.css +0 -1
  52. package/src/ui/public/assets/AppVersionBadge-Dnl4p9oF.js +0 -2
  53. package/src/ui/public/assets/BranchSelector-C64gm1XB.js +0 -1
  54. package/src/ui/public/assets/CommandConsole-60KxfUE3.css +0 -1
  55. package/src/ui/public/assets/CommandConsole-CbufZ-aa.js +0 -17
  56. package/src/ui/public/assets/CommitForm-CVPUXS7N.js +0 -6
  57. package/src/ui/public/assets/CustomCommandManager-DYXfu8Gr.js +0 -1
  58. package/src/ui/public/assets/EditorView-DaLTdDZK.js +0 -0
  59. package/src/ui/public/assets/FileDiffViewer-BvL-9S74.js +0 -13
  60. package/src/ui/public/assets/FlowOrchestrationWorkspace-DQhf9Uyr.js +0 -21
  61. package/src/ui/public/assets/GitStatus-CjNzYRV2.js +0 -6
  62. package/src/ui/public/assets/ImagePreview-Bo8Y4HdH.js +0 -1
  63. package/src/ui/public/assets/LogList-vdQwp7e5.js +0 -7
  64. package/src/ui/public/assets/ProjectStartupButton-CIkYLyt7.js +0 -1
  65. package/src/ui/public/assets/RemoteRepoCard-CpyLlD_n.js +0 -1
  66. package/src/ui/public/assets/ResetToRemoteButton-BElORAbP.js +0 -1
  67. package/src/ui/public/assets/SourceMapView-BS35GVi7.js +0 -3
  68. package/src/ui/public/assets/SourceMapView-DTsSdcgG.css +0 -1
  69. package/src/ui/public/assets/TemplateManager-B5s2lBKa.js +0 -1
  70. package/src/ui/public/assets/UserInputNode-CKoZYrQX.js +0 -1
  71. package/src/ui/public/assets/WorkbenchView-DKDEzEIV.js +0 -12
  72. package/src/ui/public/assets/WorkbenchView-MepwSID3.css +0 -1
  73. package/src/ui/public/assets/__vite-optional-peer-dep_katex_markstream-vue-Bg9-FXxr.js +0 -1
  74. package/src/ui/public/assets/__vite-optional-peer-dep_mermaid_markstream-vue-cPZl55sA.js +0 -1
  75. package/src/ui/public/assets/__vite-optional-peer-dep_stream-markdown_markstream-vue-Dhgp-lRL.js +0 -1
  76. package/src/ui/public/assets/__vite-optional-peer-dep_stream-monaco_markstream-vue-Cr1d6De7.js +0 -1
  77. package/src/ui/public/assets/d2_markstream-vue-xMoF3M5G.js +0 -1
  78. package/src/ui/public/assets/element-plus-rcpA4EGF.js +0 -26
  79. package/src/ui/public/assets/index-BYAxMGne.css +0 -1
  80. package/src/ui/public/assets/index-DJIlYzzK.js +0 -7
  81. package/src/ui/public/assets/mhchem_markstream-vue-tp86PqxG.js +0 -1
  82. package/src/ui/public/assets/vendor-DoD2dmIW.js +0 -226
  83. package/src/ui/public/assets/vue-flow-CfamgGcL.js +0 -39
@@ -16,9 +16,11 @@ import express from 'express';
16
16
  import fs from 'fs/promises';
17
17
  import fsSync from 'fs';
18
18
  import path from 'path';
19
+ import os from 'os';
19
20
  import { exec, spawn } from 'child_process';
20
21
  import https from 'https';
21
22
  import { fileURLToPath } from 'url';
23
+ import { getRegistryPath } from '../utils/instanceRegistry.js';
22
24
 
23
25
  const __filename = fileURLToPath(import.meta.url);
24
26
  const __dirname = path.dirname(__filename);
@@ -29,9 +31,36 @@ export function registerNpmRoutes({
29
31
  }) {
30
32
  // 标记是否有升级任务在跑(防并发)
31
33
  let activeUpgrade = false;
34
+ // 标记是否有重启任务在跑(防并发)
35
+ let activeRestart = false;
36
+ // 自拉起重启相关常量
37
+ const RESTART_TIMEOUT_MS = 8000; // 子进程必须在此时间内注册到 instanceRegistry
38
+ const REGISTRY_POLL_MS = 250; // 轮询注册表文件间隔
39
+ const EXIT_DELAY_MS = 100; // 子进程就绪后给前端的最后缓冲时间
32
40
 
33
41
  // ========== 应用自升级相关 API ==========
34
42
 
43
+ // 解析"自拉起"要 spawn 的入口绝对路径。
44
+ // 优先 process.argv[1](开发态 server.js / 生产态 src/gitCommit.js 都是绝对路径),
45
+ // 但 launch.json 的 backend 是 `node -e "...process.argv = [...]; require('./server.js')"`,
46
+ // 这种篡改出来的 argv[1] 是相对路径 "server.js",不能直接 spawn。
47
+ // 回退: 用 __dirname 推导 src/ui/server/index.js 的同级 server.js(仓库根)
48
+ function resolveRestartEntry() {
49
+ const arg = process.argv[1]
50
+ if (arg && path.isAbsolute(arg) && fsSync.existsSync(arg)) {
51
+ return { entry: arg, args: process.argv.slice(2) }
52
+ }
53
+ // 回退到仓库根的 server.js(开发态入口),保留原 PORT 等环境变量
54
+ const fallback = path.resolve(__dirname, '../../../..', 'server.js')
55
+ if (fsSync.existsSync(fallback)) {
56
+ // 透传用户原始 argv[2..](--no-open 等);如果原 argv[1] 是 "server.js" 这类相对路径,
57
+ // 用户大概率没传任何 args,所以空数组也合理
58
+ const originalArgs = (arg && !path.isAbsolute(arg)) ? process.argv.slice(2) : []
59
+ return { entry: fallback, args: originalArgs }
60
+ }
61
+ return null
62
+ }
63
+
35
64
  // 当前安装的版本(从外层 package.json 读取)
36
65
  function getCurrentVersion() {
37
66
  try {
@@ -191,15 +220,139 @@ export function registerNpmRoutes({
191
220
  })
192
221
 
193
222
 
194
- // POST /api/app-restart - 优雅退出当前 Node 进程,由外层 launcher/desktop 自动拉起
223
+ // 同步读取 instanceRegistry 文件并返回 Map<pid, entry>
224
+ // npm.js 在自拉起重启场景下需要轮询此文件判断子进程是否已绑定端口
225
+ function readRegistrySync(p) {
226
+ const m = new Map();
227
+ try {
228
+ const raw = fsSync.readFileSync(p, 'utf-8');
229
+ const obj = JSON.parse(raw);
230
+ const instances = (obj && obj.instances) || {};
231
+ for (const [pidStr, entry] of Object.entries(instances)) {
232
+ const pid = Number(pidStr);
233
+ if (Number.isFinite(pid) && entry && typeof entry === 'object') {
234
+ m.set(pid, entry);
235
+ }
236
+ }
237
+ } catch (_) {
238
+ // 文件不存在或 JSON 损坏 → 返回空 Map,调用方按"还没注册"处理
239
+ }
240
+ return m;
241
+ }
242
+
243
+ // POST /api/app-restart - 自拉起重启:spawn 当前进程入口 + 轮询 instanceRegistry
244
+ // 拿到子进程端口后通过 NDJSON 流告知前端;旧进程再退出
195
245
  app.post('/api/app-restart', (_req, res) => {
196
- res.json({ success: true, message: '正在重启服务…' })
197
- // 给响应一点时间落到客户端再退出
198
- setTimeout(() => {
199
- console.log('[app-restart] 收到重启指令,退出当前进程')
200
- process.exit(0)
201
- }, 300)
202
- })
246
+ if (activeRestart) {
247
+ res.status(409).json({ success: false, error: '已有重启任务在进行中' });
248
+ return;
249
+ }
250
+ activeRestart = true;
251
+
252
+ res.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8');
253
+ res.setHeader('Cache-Control', 'no-cache');
254
+ res.setHeader('Connection', 'keep-alive');
255
+ res.flushHeaders?.();
256
+
257
+ const send = (obj) => { try { res.write(JSON.stringify(obj) + '\n') } catch (_) {} };
258
+ const finish = () => { try { res.end() } catch (_) {} };
259
+ const release = () => { activeRestart = false };
260
+
261
+ // 1. 记录当前所有 instance,后续用"出现的新 pid"判定子进程
262
+ const registryPath = getRegistryPath();
263
+ const before = readRegistrySync(registryPath);
264
+ const start = Date.now();
265
+ const spawnAt = Date.now();
266
+
267
+ // 2. 准备子进程 ready 信号文件 —— startServerOnAvailablePort 在 listen 成功后会写入
268
+ // (instanceRegistry 在 EPERM 环境下不可用,这是回退通道)
269
+ const notifyPath = path.join(
270
+ os.tmpdir(),
271
+ `zen-gitsync-restart-${process.pid}-${Date.now()}.port`
272
+ );
273
+ try { fsSync.unlinkSync(notifyPath) } catch (_) {} // 清掉旧同名文件
274
+
275
+ const entryInfo = resolveRestartEntry();
276
+ if (!entryInfo) {
277
+ send({ type: 'error', message: '无法解析重启入口(process.argv[1] 既不是绝对路径,仓库根也没有 server.js)' });
278
+ finish();
279
+ release();
280
+ return;
281
+ }
282
+
283
+ let child;
284
+ try {
285
+ // spawn 当前进程当时是怎么被 node 启的同一个入口(开发态 server.js / 生产态 gitCommit.js)
286
+ // detached: true + stdio: 'ignore' + child.unref() → Windows 上父进程 exit 不会带走子进程
287
+ child = spawn(process.execPath, [entryInfo.entry, ...entryInfo.args], {
288
+ detached: true,
289
+ stdio: 'ignore',
290
+ windowsHide: true,
291
+ env: { ...process.env, ZEN_RESTART_NOTIFY_PATH: notifyPath }
292
+ });
293
+ child.unref();
294
+ } catch (err) {
295
+ send({ type: 'error', message: `拉起子进程失败: ${err?.message || err}` });
296
+ try { fsSync.unlinkSync(notifyPath) } catch (_) {}
297
+ finish();
298
+ release();
299
+ return;
300
+ }
301
+
302
+ send({ type: 'stdout', message: `[restart] 已在 PID ${child.pid} 拉起子进程 (${entryInfo.entry}),等待端口就绪…\n` });
303
+
304
+ const poll = setInterval(() => {
305
+ if (Date.now() - start > RESTART_TIMEOUT_MS) {
306
+ clearInterval(poll);
307
+ try { child.kill('SIGTERM') } catch (_) {}
308
+ send({ type: 'error', message: `子进程 ${RESTART_TIMEOUT_MS / 1000}s 内未就绪(未读到 ${notifyPath}),已 kill` });
309
+ send({ type: 'timeout' });
310
+ finish();
311
+ try { fsSync.unlinkSync(notifyPath) } catch (_) {}
312
+ // 关键:超时分支旧进程不退出,用户仍可继续手动刷新页面
313
+ release();
314
+ return;
315
+ }
316
+
317
+ // 优先判定:子进程是否写了 notify 文件(最可靠,不走 instanceRegistry)
318
+ let notifiedPort = null;
319
+ try {
320
+ const raw = fsSync.readFileSync(notifyPath, 'utf8').trim();
321
+ const p = Number(raw);
322
+ if (Number.isInteger(p) && p > 0 && p < 65536) notifiedPort = p;
323
+ } catch (_) {}
324
+
325
+ // 兜底:instanceRegistry 里出现的新 pid + port(用于没有 savePort 的环境)
326
+ let registryPort = null;
327
+ if (!notifiedPort) {
328
+ const after = readRegistrySync(registryPath);
329
+ for (const [pid, entry] of after) {
330
+ if (
331
+ !before.has(pid) &&
332
+ Number.isInteger(entry.port) && entry.port > 0 &&
333
+ (entry.lastHeartbeat ?? entry.startedAt ?? 0) >= spawnAt
334
+ ) {
335
+ registryPort = entry.port;
336
+ break;
337
+ }
338
+ }
339
+ }
340
+
341
+ const newPort = notifiedPort ?? registryPort;
342
+ if (newPort) {
343
+ clearInterval(poll);
344
+ // 给 EXIT_DELAY_MS 让新进程稳定,再通知前端 + 旧进程退出
345
+ setTimeout(() => {
346
+ send({ type: 'ready', port: newPort });
347
+ finish();
348
+ try { fsSync.unlinkSync(notifyPath) } catch (_) {}
349
+ console.log('[app-restart] 子进程已就绪,退出当前进程');
350
+ process.exit(0);
351
+ }, EXIT_DELAY_MS);
352
+ return;
353
+ }
354
+ }, REGISTRY_POLL_MS);
355
+ });
203
356
 
204
357
 
205
358
  // 读取 package.json 文件内容
@@ -17,10 +17,21 @@
17
17
  // 多进程并发写采用 atomic temp+rename + 进程内串行化 Promise 链
18
18
  // stale 判定:PID 不存在 或 lastHeartbeat 超过阈值
19
19
 
20
+ import nodePath from 'node:path';
21
+ import nodeOs from 'node:os';
22
+
20
23
  const STALE_MS = 30_000; // 心跳超时阈值(毫秒)
21
24
  const WATCH_DEBOUNCE_MS = 100; // fs.watch 防抖时间
22
25
  const REGISTRY_VERSION = 1;
23
26
 
27
+ /**
28
+ * 默认注册表文件路径(与 createInstanceRegistry({ registryPath }) 的约定一致)
29
+ * 用于:npm.js 的 /api/app-restart 自拉起时,父进程轮询此路径判断子进程是否就绪。
30
+ */
31
+ export function getRegistryPath() {
32
+ return nodePath.join(nodeOs.homedir(), '.zen-gitsync-instances.json');
33
+ }
34
+
24
35
  function isProcessAlive(pid) {
25
36
  if (!Number.isInteger(pid) || pid <= 0) return false;
26
37
  try {
@@ -20,6 +20,7 @@ export async function startServerOnAvailablePort({
20
20
  noOpen,
21
21
  isGitRepo,
22
22
  savePortToFile,
23
+ fsSync,
23
24
  maxTries = 100,
24
25
  callbackExecutedRef
25
26
  }) {
@@ -74,6 +75,18 @@ export async function startServerOnAvailablePort({
74
75
 
75
76
  savePortToFile(currentPort);
76
77
 
78
+ // 自拉起重启通知:父进程 spawn 时设置了 ZEN_RESTART_NOTIFY_PATH
79
+ // 子进程成功 bind 端口后,把端口写到该文件作为"已就绪"信号
80
+ // (instanceRegistry 在 EPERM 环境下不可用,这是回退通道)
81
+ const notifyPath = process.env.ZEN_RESTART_NOTIFY_PATH;
82
+ if (notifyPath && fsSync) {
83
+ try {
84
+ fsSync.writeFileSync(notifyPath, String(currentPort), 'utf8');
85
+ } catch (_) {
86
+ // 通知写失败不影响主流程
87
+ }
88
+ }
89
+
77
90
  if (!noOpen) {
78
91
  setTimeout(() => {
79
92
  open(`http://localhost:${currentPort}`);
@@ -241,7 +241,13 @@ function execSyncGitCommand(command, options = {}) {
241
241
  // Add a command history array to store commands and their results
242
242
  const commandHistory = [];
243
243
  const MAX_HISTORY_SIZE = 100;
244
- const MAX_OUTPUT_LENGTH = 5000; // Limit the output length to avoid memory issues
244
+ // 输出截断上限 单点常量,避免在 execGitCommand / addCommandToHistory /
245
+ // 其他未来 history 写入点之间漂移导致截断行为不一致。
246
+ // 截断提示也集中在这里,所有调用方用同一个文案,便于后续 i18n 替换。
247
+ const MAX_OUTPUT_LENGTH = 5000;
248
+ const TRUNCATED_SUFFIX_STDOUT = '\n... (output truncated)';
249
+ const TRUNCATED_SUFFIX_STDERR = '\n... (error output truncated)';
250
+ const TRUNCATED_SUFFIX_MANUAL = '...[truncated]';
245
251
 
246
252
  // 添加一个变量来保存Socket.io实例
247
253
  let ioInstance = null;
@@ -303,14 +309,14 @@ function execGitCommand(command, options = {}) {
303
309
  let truncatedStderr = stderr;
304
310
  let isStdoutTruncated = false;
305
311
  let isStderrTruncated = false;
306
-
312
+
307
313
  if (stdout && stdout.length > MAX_OUTPUT_LENGTH) {
308
- truncatedStdout = stdout.substring(0, MAX_OUTPUT_LENGTH) + '\n... (output truncated)';
314
+ truncatedStdout = truncateForHistory(stdout, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_STDOUT);
309
315
  isStdoutTruncated = true;
310
316
  }
311
-
317
+
312
318
  if (stderr && stderr.length > MAX_OUTPUT_LENGTH) {
313
- truncatedStderr = stderr.substring(0, MAX_OUTPUT_LENGTH) + '\n... (error output truncated)';
319
+ truncatedStderr = truncateForHistory(stderr, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_STDERR);
314
320
  isStderrTruncated = true;
315
321
  }
316
322
 
@@ -416,8 +422,6 @@ function getCommandHistory() {
416
422
 
417
423
  // Function to manually add command to history (for commands not using execGitCommand)
418
424
  function addCommandToHistory(command, stdout = '', stderr = '', error = null, executionTime = 0) {
419
- const MAX_OUTPUT_LENGTH = 5000;
420
-
421
425
  // 防御:历史里的 command 必须始终是字符串,防止前端 item.command.trim() 抛错
422
426
  if (Array.isArray(command)) {
423
427
  command = command.join(' ')
@@ -428,8 +432,8 @@ function addCommandToHistory(command, stdout = '', stderr = '', error = null, ex
428
432
  // Truncate outputs if too long
429
433
  const isStdoutTruncated = stdout.length > MAX_OUTPUT_LENGTH;
430
434
  const isStderrTruncated = stderr.length > MAX_OUTPUT_LENGTH;
431
- const truncatedStdout = isStdoutTruncated ? stdout.substring(0, MAX_OUTPUT_LENGTH) + '...[truncated]' : stdout;
432
- const truncatedStderr = isStderrTruncated ? stderr.substring(0, MAX_OUTPUT_LENGTH) + '...[truncated]' : stderr;
435
+ const truncatedStdout = isStdoutTruncated ? truncateForHistory(stdout, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_MANUAL) : stdout;
436
+ const truncatedStderr = isStderrTruncated ? truncateForHistory(stderr, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_MANUAL) : stderr;
433
437
 
434
438
  const historyItem = {
435
439
  command,
@@ -619,11 +623,38 @@ async function printGitLog() {
619
623
  }
620
624
 
621
625
  function exec_exit(exit) {
622
- if (exit) {
626
+ // 仅当显式传 true 时退出;空值/0/'false'/'no'/其他非真值都按"不退出"处理。
627
+ // 之前的 truthy 判断会把字符串 'false' 误判为 true(非空字符串都是 truthy),
628
+ // 调用方传 '--exit=false' 等解析出来的字符串时会错误退出。
629
+ if (exit === true) {
623
630
  process.exit()
624
631
  }
625
632
  }
626
633
 
634
+ /**
635
+ * 按 char 边界安全截断,避免切断 UTF-16 代理对(emoji / 辅助平面 CJK)。
636
+ * 原 substring(0, N) 在 N-1 是 high surrogate 时会把半个字符拼到末尾,
637
+ * 前端 history 渲染可能显示 '�'。本函数检测这种情况并向前回退 1 个码元。
638
+ *
639
+ * @param {string} str
640
+ * @param {number} limit - 最大字符数(<=0 视为"不截断",原样返回)
641
+ * @param {string} suffix - 截断提示后缀
642
+ * @returns {string}
643
+ */
644
+ function truncateForHistory(str, limit, suffix) {
645
+ if (typeof str !== 'string') return str;
646
+ if (!Number.isFinite(limit) || limit <= 0 || str.length <= limit) return str;
647
+ // 码元 = UTF-16 code unit;length 已经是码元数,但 surrogate pair 占 2 个码元。
648
+ const cutAt = limit;
649
+ const code = str.charCodeAt(cutAt - 1);
650
+ // 0xD800–0xDBFF = high surrogate,后面必须跟 low surrogate 才完整;
651
+ // 如果在 limit 处正好停在 high surrogate,回退一格避免半个 emoji/罕见汉字。
652
+ if (code >= 0xD800 && code <= 0xDBFF) {
653
+ return str.substring(0, cutAt - 1) + suffix;
654
+ }
655
+ return str.substring(0, cutAt) + suffix;
656
+ }
657
+
627
658
  function judgeUnmerged(statusOutput) {
628
659
  const hasUnmerged = statusOutput.includes('You have unmerged paths');
629
660
  if (hasUnmerged) {
@@ -1091,6 +1122,7 @@ export {
1091
1122
  coloredLog, errorLog, execSyncGitCommand,
1092
1123
  execGitCommand, getCommandHistory, addCommandToHistory, // Add command history exports
1093
1124
  clearCommandHistory,
1125
+ truncateForHistory, // 暴露便于测试与外部复用(单测会 import 这个验证 surrogate-pair 回退)
1094
1126
  checkAndClearGitLock,
1095
1127
  registerSocketIO, // 导出注册Socket.io的函数
1096
1128
  getCwd, parseCwdArg, judgePlatform, showHelp, judgeLog, printGitLog,
@@ -83,4 +83,93 @@ test('--cwd 优先 --path 之前出现', () => {
83
83
  parseCwdArg(['--cwd=/c', '--path=/p']),
84
84
  '/c'
85
85
  )
86
+ })
87
+
88
+ // ========== 边界用例扩展(2026-06-26 增补) ==========
89
+
90
+ test('带空格的路径(带引号包裹)', () => {
91
+ // POSIX/macOS 常见: --path="/Users/me/My Project"
92
+ // 解析行为:eqIdx 取第一个 `=`,整段 `--path=` 之后保留引号本身,
93
+ // 由下游 getCwd → process.chdir 或 exec cwd 自行去引号(shell 词法在 child_process 那层)
94
+ assert.equal(
95
+ parseCwdArg(['node', 'g.js', '--path="/Users/me/My Project"']),
96
+ '"/Users/me/My Project"'
97
+ )
98
+ })
99
+
100
+ test('带空格的路径(无引号 + 空格分隔形式)', () => {
101
+ // --path <value> 形式,value 含空格
102
+ assert.equal(
103
+ parseCwdArg(['node', 'g.js', '--path', '/Users/me/My Project']),
104
+ '/Users/me/My Project'
105
+ )
106
+ })
107
+
108
+ test('相对路径 ./ 与 ../', () => {
109
+ // 用户在子目录下用 --path=./repo 触发相对路径解析,
110
+ // parseCwdArg 只做字符串切片,不解析;是否 resolve 由 getCwd 决定
111
+ assert.equal(parseCwdArg(['--path=./repo']), './repo')
112
+ assert.equal(parseCwdArg(['--path=../sibling']), '../sibling')
113
+ assert.equal(parseCwdArg(['--path', './inner']), './inner')
114
+ })
115
+
116
+ test('Windows 盘符 C:\\Project 不被特殊处理', () => {
117
+ // 解析器对盘符大小写不敏感,字符串原样返回;
118
+ // 真正的 case-insensitive 去重在 normalizeProjectPath(config.js)层做
119
+ assert.equal(
120
+ parseCwdArg(['node', 'g.js', '--path=C:\\Project']),
121
+ 'C:\\Project'
122
+ )
123
+ assert.equal(
124
+ parseCwdArg(['node', 'g.js', '--path=c:\\project']),
125
+ 'c:\\project'
126
+ )
127
+ assert.equal(
128
+ parseCwdArg(['node', 'g.js', '--path=D:/work']),
129
+ 'D:/work'
130
+ )
131
+ })
132
+
133
+ test('value 中嵌入等号不被截断', () => {
134
+ // --path=/a=b → indexOf 取第一个 `=`,value 保留嵌入等号
135
+ // 例: --path=/opt/some=weird-name
136
+ assert.equal(
137
+ parseCwdArg(['--path=/opt/some=weird-name']),
138
+ '/opt/some=weird-name'
139
+ )
140
+ // 多重等号同理
141
+ assert.equal(
142
+ parseCwdArg(['--path=x=y=z']),
143
+ 'x=y=z'
144
+ )
145
+ })
146
+
147
+ test('--path= 仅在 argv 末尾也安全返回 null', () => {
148
+ // 防御:argv 解析不抛
149
+ assert.equal(parseCwdArg(['--path=']), null)
150
+ })
151
+
152
+ test('argv 中混入无关 unicode 不影响解析', () => {
153
+ // 中间混 emoji / CJK,只关心 --path 的值
154
+ assert.equal(
155
+ parseCwdArg(['node', 'g.js', '🚀', '--path=/repo', '中文']),
156
+ '/repo'
157
+ )
158
+ })
159
+
160
+ test('--path 等号紧贴无空格', () => {
161
+ // --path=/repo(无空格) vs --path /repo(空格)
162
+ // 行为应一致
163
+ assert.equal(
164
+ parseCwdArg(['--path=/repo']),
165
+ parseCwdArg(['--path', '/repo'])
166
+ )
167
+ })
168
+
169
+ test('重复 --path 取首个出现', () => {
170
+ // 已经测过,这里再确认带空格的重复
171
+ assert.equal(
172
+ parseCwdArg(['--path=/first with space', '--path=/second']),
173
+ '/first with space'
174
+ )
86
175
  })
@@ -0,0 +1,200 @@
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ // utils/index.js 中暴露的工具函数测试(node:test 内置)。
16
+ // 覆盖 truncateForHistory(surrogate-pair 安全截断)+ exec_exit(字符串 'false' 回归)。
17
+ import { test } from 'node:test'
18
+ import assert from 'node:assert/strict'
19
+ import { truncateForHistory, exec_exit } from './index.js'
20
+
21
+ // ========== truncateForHistory ==========
22
+
23
+ test('truncateForHistory: 短字符串原样返回', () => {
24
+ assert.equal(truncateForHistory('hello', 100, '...[t]'), 'hello')
25
+ })
26
+
27
+ test('truncateForHistory: 长度等于 limit 原样返回', () => {
28
+ // 边界条件:不超 limit 就保持原状
29
+ assert.equal(truncateForHistory('hello', 5, '...[t]'), 'hello')
30
+ })
31
+
32
+ test('truncateForHistory: 长 ASCII 字符串按 limit 截断 + suffix', () => {
33
+ const out = truncateForHistory('abcdefghijklmnop', 10, '...[t]')
34
+ assert.equal(out, 'abcdefghij...[t]')
35
+ })
36
+
37
+ test('truncateForHistory: 不会切断 UTF-16 代理对(emoji)', () => {
38
+ // 🦄 = U+1F984,UTF-16 编码是 0xD83E 0xDD84(一对 surrogate)。
39
+ // 如果 limit=1,substring(0,1) 会拿到 0xD83E(孤立 high surrogate),
40
+ // 截断函数应识别并回退 1 位,返回 suffix。
41
+ const out = truncateForHistory('🦄hello', 1, '...[t]')
42
+ // 不应包含未配对的 high surrogate
43
+ assert.ok(!out.startsWith('\uD83E'), '不应以孤立 high surrogate 开头')
44
+ assert.ok(out.endsWith('...[t]'), '应以 suffix 收尾')
45
+ })
46
+
47
+ test('truncateForHistory: 完整 emoji 不会被切坏', () => {
48
+ // 🦄🐢🐲 = 3 个 supplementary plane 字符,每个 2 码元,共 6 码元
49
+ // limit=7 → cutAt=7,正好停在 'r' 上,完整保留 3 emoji
50
+ const out = truncateForHistory('🦄🐢🐲rest', 7, '...[t]')
51
+ // 高低位 surrogate 总是成对出现 → 数 codepoint 数应等于 emoji 数
52
+ // 不应出现奇数个 surrogate(否则有孤立的)
53
+ const codeUnits = [...out].map(c => c.codePointAt(0))
54
+ const highSurrogates = codeUnits.filter(c => c >= 0xD800 && c <= 0xDBFF).length
55
+ const lowSurrogates = codeUnits.filter(c => c >= 0xDC00 && c <= 0xDFFF).length
56
+ assert.equal(highSurrogates, lowSurrogates, 'high/low surrogate 必须成对')
57
+ assert.ok(out.startsWith('🦄🐢🐲'), '前 3 个 emoji 应完整保留')
58
+ assert.ok(out.endsWith('...[t]'))
59
+ })
60
+
61
+ test('truncateForHistory: 截断位置恰好在 emoji 边界时回退', () => {
62
+ // helper: 数 surrogate 是否成对(无孤立)
63
+ const checkPaired = (s) => {
64
+ const codeUnits = [...s].map(c => c.codePointAt(0))
65
+ const high = codeUnits.filter(c => c >= 0xD800 && c <= 0xDBFF).length
66
+ const low = codeUnits.filter(c => c >= 0xDC00 && c <= 0xDFFF).length
67
+ return high === low
68
+ }
69
+ // 🦄 = 2 码元,limit=2 想保留前 2 码元 → 完整 emoji → 不回退
70
+ const out1 = truncateForHistory('🦄hello', 2, '...[t]')
71
+ assert.ok(out1.startsWith('🦄'), 'limit=2 应保留完整 emoji')
72
+ assert.ok(checkPaired(out1), '应无孤立 surrogate')
73
+ // limit=3 想保留前 3 码元 = 高+低+'h',emoji 已完整,直接切
74
+ const out2 = truncateForHistory('🦄hello', 3, '...[t]')
75
+ assert.ok(out2.startsWith('🦄h'), 'limit=3 应保留 emoji + 后续字符')
76
+ assert.ok(checkPaired(out2), '应无孤立 surrogate')
77
+ })
78
+
79
+ test('truncateForHistory: 中日韩 BMP 字符不被切断(单码元)', () => {
80
+ // 中文 = BMP 字符,每个 1 码元,substring 不会切坏
81
+ // '你好世界你好世界' 共 10 字符,substring(0, 6) = '你好世界你好'
82
+ const out = truncateForHistory('你好世界你好世界', 6, '...[t]')
83
+ assert.equal(out, '你好世界你好' + '...[t]')
84
+ })
85
+
86
+ test('truncateForHistory: 非字符串输入安全返回原值', () => {
87
+ // 防御:execGitCommand 失败时 stdout/stderr 可能 undefined
88
+ assert.equal(truncateForHistory(undefined, 100, '...[t]'), undefined)
89
+ assert.equal(truncateForHistory(null, 100, '...[t]'), null)
90
+ assert.equal(truncateForHistory(123, 100, '...[t]'), 123)
91
+ })
92
+
93
+ test('truncateForHistory: limit=0 / 负数返回原值(防御退化)', () => {
94
+ // limit<=0 没有合理的"保留 N 码元"语义,直接原样返回,避免误返回空+suffix
95
+ assert.equal(truncateForHistory('abc', 0, '...[t]'), 'abc')
96
+ assert.equal(truncateForHistory('abc', -1, '...[t]'), 'abc')
97
+ })
98
+
99
+ // ========== exec_exit ==========
100
+
101
+ test('exec_exit: 显式 true 触发 process.exit', () => {
102
+ const original = process.exit
103
+ let exitCode = null
104
+ process.exit = (code) => {
105
+ exitCode = code
106
+ // 不真退出,抛个特殊标记让测试断言
107
+ const e = new Error('__test_exit_called__')
108
+ e.__isExitStub = true
109
+ throw e
110
+ }
111
+ try {
112
+ try {
113
+ exec_exit(true)
114
+ } catch (e) {
115
+ assert.ok(e.__isExitStub, '应调用 process.exit')
116
+ }
117
+ assert.equal(exitCode, undefined, 'process.exit 默认无参')
118
+ } finally {
119
+ process.exit = original
120
+ }
121
+ })
122
+
123
+ test('exec_exit: false 不退出', () => {
124
+ const original = process.exit
125
+ let called = false
126
+ process.exit = () => {
127
+ called = true
128
+ const e = new Error('__test_exit_called__')
129
+ e.__isExitStub = true
130
+ throw e
131
+ }
132
+ try {
133
+ exec_exit(false)
134
+ assert.equal(called, false, 'false 不应触发 exit')
135
+ } finally {
136
+ process.exit = original
137
+ }
138
+ })
139
+
140
+ test('exec_exit: 字符串 "false" 不退出(回归)', () => {
141
+ // 修复前:if (exit) 会把 'false' 当 true → 错误退出
142
+ // 修复后:if (exit === true) 只接受 boolean true
143
+ const original = process.exit
144
+ let called = false
145
+ process.exit = () => {
146
+ called = true
147
+ const e = new Error('__test_exit_called__')
148
+ e.__isExitStub = true
149
+ throw e
150
+ }
151
+ try {
152
+ exec_exit('false')
153
+ assert.equal(called, false, '字符串 "false" 不应触发 exit')
154
+ } finally {
155
+ process.exit = original
156
+ }
157
+ })
158
+
159
+ test('exec_exit: 字符串 "true" 也不退出(严格 boolean 契约)', () => {
160
+ // 修复策略:严格 === true,任何非 boolean true 都不退,
161
+ // 包括 "true" / 1 / {} 等 truthy 值 — 防止调用方传字符串/数字的歧义
162
+ const original = process.exit
163
+ let called = false
164
+ process.exit = () => {
165
+ called = true
166
+ const e = new Error('__test_exit_called__')
167
+ e.__isExitStub = true
168
+ throw e
169
+ }
170
+ try {
171
+ exec_exit('true')
172
+ assert.equal(called, false, '字符串 "true" 不应触发 exit')
173
+ exec_exit(1)
174
+ assert.equal(called, false, '数字 1 不应触发 exit')
175
+ exec_exit({})
176
+ assert.equal(called, false, '空对象不应触发 exit')
177
+ } finally {
178
+ process.exit = original
179
+ }
180
+ })
181
+
182
+ test('exec_exit: undefined / null 不退出', () => {
183
+ const original = process.exit
184
+ let called = false
185
+ process.exit = () => {
186
+ called = true
187
+ const e = new Error('__test_exit_called__')
188
+ e.__isExitStub = true
189
+ throw e
190
+ }
191
+ try {
192
+ exec_exit(undefined)
193
+ exec_exit(null)
194
+ exec_exit(0)
195
+ exec_exit('')
196
+ assert.equal(called, false, 'falsy 值不应触发 exit')
197
+ } finally {
198
+ process.exit = original
199
+ }
200
+ })
@@ -1 +0,0 @@
1
- .upgrade-status[data-v-3de8edac]{align-items:center;gap:var(--spacing-base);margin-bottom:var(--spacing-md);font-size:14px;font-weight:500;display:flex}.upgrade-status.is-running[data-v-3de8edac]{color:var(--el-color-primary)}.upgrade-status.is-success[data-v-3de8edac]{color:var(--el-color-success)}.upgrade-status.is-failed[data-v-3de8edac]{color:var(--el-color-danger)}.upgrade-status .is-loading[data-v-3de8edac]{animation:2s linear infinite rotating-3de8edac}.upgrade-log[data-v-3de8edac]{color:#d4d4d4;white-space:pre-wrap;word-break:break-all;background:#1e1e1e;border-radius:4px;height:320px;margin:0;padding:12px;font-family:JetBrains Mono,Cascadia Code,Consolas,monospace;font-size:12px;line-height:1.6;overflow:auto}.upgrade-hint[data-v-3de8edac]{margin:0 0 var(--spacing-md) 0;border-left:3px solid var(--el-color-success);color:var(--el-color-success);background:#67c23a14;border-radius:4px;padding:8px 12px;font-size:13px}.upgrade-countdown[data-v-3de8edac]{background:var(--el-color-success);color:#fff;border-radius:10px;margin-left:8px;padding:1px 8px;font-size:12px;font-weight:600;animation:1s ease-in-out infinite pulse-3de8edac;display:inline-block}@keyframes pulse-3de8edac{0%,to{opacity:1}50%{opacity:.55}}@keyframes rotating-3de8edac{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.app-version-badge[data-v-c04e50c5]{align-items:center;gap:var(--spacing-base);margin-left:auto;display:inline-flex}.badge-icon-link[data-v-c04e50c5]{width:22px;height:22px;color:var(--text-tertiary);opacity:.7;border-radius:4px;justify-content:center;align-items:center;text-decoration:none;transition:color .18s,opacity .18s,background .18s;display:inline-flex}.badge-icon-link[data-v-c04e50c5]:hover{opacity:1;color:var(--text-secondary);background:var(--bg-hover,#0000000f)}.badge-icon-link--npm[data-v-c04e50c5]:hover{color:#cb3837}.badge-icon-link[data-v-c04e50c5]:first-child:hover{color:var(--text-primary)}.version-link[data-v-c04e50c5]{color:#6c757d;border-radius:3px;padding:2px 6px;font-family:monospace;font-size:11px;text-decoration:none;transition:color .2s,background-color .2s}.version-link[data-v-c04e50c5]:hover{color:var(--el-color-primary);background-color:#0000000a}.upgrade-btn[data-v-c04e50c5]{appearance:none;cursor:pointer;color:#fff;background:linear-gradient(135deg, var(--color-primary) 0%, #3a8ee6 100%);height:22px;box-shadow:var(--shadow-sm);transition:transform var(--transition-fast) var(--ease-custom), box-shadow var(--transition-fast) var(--ease-custom), filter var(--transition-fast) var(--ease-custom);white-space:nowrap;-webkit-user-select:none;user-select:none;border:0;border-radius:11px;outline:none;align-items:center;gap:5px;margin:0;padding:0 10px 0 8px;font-family:inherit;font-size:11px;font-weight:600;line-height:1;display:inline-flex}.upgrade-btn[data-v-c04e50c5]:hover{filter:brightness(1.08);box-shadow:var(--shadow-md);transform:translateY(-1px)}.upgrade-btn[data-v-c04e50c5]:active{filter:brightness(.96);box-shadow:var(--shadow-sm);transform:translateY(0)}.upgrade-btn[data-v-c04e50c5]:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.upgrade-btn__dot[data-v-c04e50c5]{background:#fff;border-radius:50%;flex-shrink:0;width:6px;height:6px;animation:1.6s ease-in-out infinite upgrade-pulse-c04e50c5;box-shadow:0 0 0 2px #ffffff59}.upgrade-btn__label[data-v-c04e50c5]{letter-spacing:.5px}.upgrade-btn__version[data-v-c04e50c5]{background:#ffffff38;border-radius:6px;padding:1px 5px;font-family:monospace;font-size:10px;font-weight:500;line-height:1.2}@keyframes upgrade-pulse-c04e50c5{0%,to{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(.8)}}
@@ -1,2 +0,0 @@
1
- import"./rolldown-runtime-CMxvf4Kt.js";import{B as e,Cn as t,D as n,Fn as r,G as i,Jn as a,Mn as o,Pn as s,Rn as c,Sn as l,Tn as u,Tt as d,Xn as f,Y as p,_n as m,_r as h,a as g,gn as _,hn as v,i as y,mn as b,mr as x,nt as S,pr as C,rt as w,sr as T,vn as E}from"./element-plus-rcpA4EGF.js";import{n as D,t as O}from"./_plugin-vue_export-helper-DKIb-p1R.js";async function k(){let e=await fetch(`/api/app-version`);return e.ok?e.json():{success:!1,current:``,latest:null,hasUpdate:!1,error:`HTTP ${e.status}`}}async function A(e){let t=await fetch(`/api/app-upgrade`,{method:`POST`});if(!t.ok){let n=`HTTP ${t.status}`;try{let e=await t.json();e?.error&&(n=e.error)}catch{}e({type:`error`,message:n}),e({type:`done`,code:t.status});return}if(!t.body){e({type:`error`,message:`响应无 body`}),e({type:`done`,code:-1});return}let n=t.body.getReader(),r=new TextDecoder(`utf-8`),i=``;for(;;){let{done:t,value:a}=await n.read();if(t)break;i+=r.decode(a,{stream:!0});let o=i.split(`
2
- `);i=o.pop()??``;for(let t of o)if(t.trim())try{e(JSON.parse(t))}catch{}}if(i.trim())try{e(JSON.parse(i))}catch{}}async function j(){let e=await fetch(`/api/app-restart`,{method:`POST`});if(!e.ok){let t=`HTTP ${e.status}`;try{let n=await e.json();n?.error&&(t=n.error)}catch{}throw Error(t)}}var M={key:3},N={key:4},P={key:5},F={key:0,class:`upgrade-hint`},I={key:0,class:`upgrade-countdown`},L=O(u({__name:`UpgradeDialog`,props:{modelValue:{type:Boolean},logs:{},status:{},countdown:{default:0}},emits:[`update:modelValue`,`retry`,`restart`,`cancel`],setup(r,{emit:i}){let s=r,u=i,g=T(null);a(()=>s.logs,async()=>{await o(),g.value&&(g.value.scrollTop=g.value.scrollHeight)});function y(){s.status!==`running`&&(s.status===`success`&&s.countdown>0&&u(`cancel`),u(`update:modelValue`,!1))}function b(){u(`retry`)}function O(){u(`restart`)}function k(){u(`cancel`),u(`update:modelValue`,!1)}return(i,a)=>{let o=p,s=e,T=n;return c(),_(T,{"model-value":r.modelValue,"onUpdate:modelValue":a[0]||=e=>u(`update:modelValue`,e),title:C(D)(`@F13B4:升级`),width:`680px`,"close-on-click-modal":!1,"close-on-press-escape":!1,"show-close":r.status!==`running`,"append-to-body":``,onClose:y},{footer:f(()=>[r.status===`failed`?(c(),_(s,{key:0,type:`primary`,onClick:b},{default:f(()=>[l(h(C(D)(`@F13B4:重试`)),1)]),_:1})):m(``,!0),r.status===`success`?(c(),_(s,{key:1,type:`primary`,onClick:O},{default:f(()=>[l(h(C(D)(`@F13B4:立即重启并刷新`)),1)]),_:1})):m(``,!0),r.status===`success`?(c(),_(s,{key:2,onClick:k},{default:f(()=>[l(h(C(D)(`@F13B4:稍后手动重启`)),1)]),_:1})):m(``,!0),r.status===`running`?(c(),_(s,{key:3,disabled:``},{default:f(()=>[l(h(C(D)(`@F13B4:升级中`))+`... `,1)]),_:1})):m(``,!0)]),default:f(()=>[v(`div`,{class:x([`upgrade-status`,`is-${r.status}`])},[r.status===`running`?(c(),_(o,{key:0,class:`is-loading`},{default:f(()=>[t(C(d))]),_:1})):r.status===`success`?(c(),_(o,{key:1,color:`#67c23a`},{default:f(()=>[t(C(S))]),_:1})):(c(),_(o,{key:2,color:`#f56c6c`},{default:f(()=>[t(C(w))]),_:1})),r.status===`running`?(c(),E(`span`,M,h(C(D)(`@F13B4:升级中`)),1)):r.status===`success`?(c(),E(`span`,N,h(C(D)(`@F13B4:升级完成`)),1)):(c(),E(`span`,P,h(C(D)(`@F13B4:升级失败`)),1))],2),r.status===`success`?(c(),E(`p`,F,[l(h(C(D)(`@F13B4:新版本已全局安装,需要重启服务才能生效`))+` `,1),r.countdown>0?(c(),E(`span`,I,h(C(D)(`@F13B4:{seconds} 秒后自动刷新`,{seconds:r.countdown})),1)):m(``,!0)])):m(``,!0),v(`pre`,{ref_key:`logEl`,ref:g,class:`upgrade-log`},h(r.logs||C(D)(`@F13B4:等待日志输出`)),513)]),_:1},8,[`model-value`,`title`,`show-close`])}}}),[[`__scopeId`,`data-v-3de8edac`]]),R={class:`app-version-badge`},z=[`aria-label`],B=[`aria-label`],V=[`title`,`aria-label`],H={class:`upgrade-btn__label`},U={class:`upgrade-btn__version`},W=`https://github.com/xz333221/zen-gitsync/releases`,G=5,K=`app-version-check`,q=3600*1e3,J=O(u({__name:`AppVersionBadge`,setup(e){let n=`2.13.29`,a=T(n),o=b(()=>a.value||n),l=T(null),u=T(!1),d=T(``),p=T(`running`),_=T(0),x=null;function S(){try{let e=sessionStorage.getItem(K);if(!e)return null;let{at:t,data:n}=JSON.parse(e);return Date.now()-t>q?null:n}catch{return null}}function w(e){try{sessionStorage.setItem(K,JSON.stringify({at:Date.now(),data:e}))}catch{}}async function O(e=!1){let t=S();t&&(l.value=t);try{let t=await k();l.value=t,w(t),t.current&&(a.value=t.current),!e&&t.success&&t.hasUpdate&&g({type:`success`,message:D(`@F13B4:发现新版本 {version}`,{version:t.latest??``}),duration:4e3})}catch(t){e||g({type:`warning`,message:D(`@F13B4:检查更新失败 {error}`,{error:t?.message||``})})}}async function M(){if(l.value?.hasUpdate){try{await y.confirm(D(`@F13B4:确定要从 {current} 升级到 {latest} 吗?`,{current:l.value.current,latest:l.value.latest}),D(`@F13B4:升级`),{confirmButtonText:D(`@F13B4:确定升级`),cancelButtonText:D(`@F13B4:取消`),type:`info`})}catch{return}d.value=``,p.value=`running`,u.value=!0,await A(e=>{if(e.type===`done`){if(p.value=e.code===0?`success`:`failed`,e.code===0){try{sessionStorage.removeItem(K)}catch{}l.value?.latest&&(a.value=l.value.latest),N()}}else e.message&&(d.value+=e.message)})}}function N(){P(),_.value=G,x=setInterval(()=>{--_.value,_.value<=0&&(x&&=(clearInterval(x),null),I())},1e3)}function P(){x&&=(clearInterval(x),null),_.value=0}function F(){P()}async function I(){P();try{await j(),setTimeout(()=>{window.location.reload()},600)}catch(e){g({type:`warning`,message:D(`@F13B4:重启失败 {error}`,{error:e?.message||``})})}}return s(()=>{P()}),r(()=>{O(!0)}),(e,n)=>{let r=i;return c(),E(`div`,R,[t(r,{content:C(D)(`@F13B4:在 GitHub 上查看源码`),placement:`top`,effect:`dark`,"show-after":300},{default:f(()=>[v(`a`,{href:`https://github.com/xz333221/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link`,"aria-label":C(D)(`@F13B4:在 GitHub 上查看源码`)},[...n[1]||=[v(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[v(`path`,{d:`M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z`})],-1)]],8,z)]),_:1},8,[`content`]),t(r,{content:C(D)(`@F13B4:在 npm 上查看包`),placement:`top`,effect:`dark`,"show-after":300},{default:f(()=>[v(`a`,{href:`https://www.npmjs.com/package/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link badge-icon-link--npm`,"aria-label":C(D)(`@F13B4:在 npm 上查看包`)},[...n[2]||=[v(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[v(`path`,{d:`M2 2v20h20V2H2zm17.167 17.167H12.5v-10h-3.333v10H4.833V4.833h14.334v14.334z`})],-1)]],8,B)]),_:1},8,[`content`]),t(r,{content:C(D)(`@F13B4:查看更新`),placement:`top`,effect:`dark`,"show-after":300},{default:f(()=>[v(`a`,{href:W,target:`_blank`,rel:`noopener noreferrer`,class:`version-link`},`v`+h(o.value),1)]),_:1},8,[`content`]),l.value?.hasUpdate?(c(),E(`button`,{key:0,type:`button`,class:`upgrade-btn`,title:C(D)(`@F13B4:发现新版本 {version},点击升级`,{version:l.value.latest??``}),"aria-label":C(D)(`@F13B4:发现新版本 {version},点击升级`,{version:l.value.latest??``}),onClick:M},[n[3]||=v(`span`,{class:`upgrade-btn__dot`,"aria-hidden":`true`},null,-1),v(`span`,H,h(C(D)(`@F13B4:升级`)),1),v(`span`,U,`v`+h(l.value.latest),1)],8,V)):m(``,!0),t(L,{modelValue:u.value,"onUpdate:modelValue":n[0]||=e=>u.value=e,logs:d.value,status:p.value,countdown:_.value,onRetry:M,onRestart:I,onCancel:F},null,8,[`modelValue`,`logs`,`status`,`countdown`])])}}}),[[`__scopeId`,`data-v-c04e50c5`]]);export{J as default};