zen-gitsync 2.16.48 → 2.16.50

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 (58) hide show
  1. package/README.md +8 -6
  2. package/package.json +1 -1
  3. package/src/ui/public/assets/{AgentView-BoEzb8Nb.js → AgentView-BgWYXDtl.js} +3 -3
  4. package/src/ui/public/assets/AppVersionBadge-BCIsBONZ.js +2 -0
  5. package/src/ui/public/assets/BranchSelector-BobZDQ3H.js +1 -0
  6. package/src/ui/public/assets/CommitForm-B5gIj9Ib.js +6 -0
  7. package/src/ui/public/assets/CommonDialog-CpMIq-Hr.js +1 -0
  8. package/src/ui/public/assets/EditorView-CH98t3V5.js +1 -0
  9. package/src/ui/public/assets/{FlowExecutionViewer-CLm6Q0o4.js → FlowExecutionViewer-DlsUDtqp.js} +1 -1
  10. package/src/ui/public/assets/FlowOrchestrationWorkspace-18v5NKYl.js +21 -0
  11. package/src/ui/public/assets/LogList-BYiEpIUO.js +7 -0
  12. package/src/ui/public/assets/MindmapView-CUC7icIE.js +2 -0
  13. package/src/ui/public/assets/MonitorView-ggIzE5af.js +1 -0
  14. package/src/ui/public/assets/ProjectStartupButton-BatNYldf.js +1 -0
  15. package/src/ui/public/assets/RemoteManagerDialog-Be7FyEZR.js +4 -0
  16. package/src/ui/public/assets/RemoteRepoCard-5kcQFW__.js +7 -0
  17. package/src/ui/public/assets/SourceMapView-C5bto8pg.js +3 -0
  18. package/src/ui/public/assets/SvgIcon-uPiOGYUG.js +1 -0
  19. package/src/ui/public/assets/UserInputNode-BaWZXnvN.js +1 -0
  20. package/src/ui/public/assets/WorkbenchView-CR9FKLxj.css +1 -0
  21. package/src/ui/public/assets/WorkbenchView-DYS6reGa.js +31 -0
  22. package/src/ui/public/assets/_plugin-vue_export-helper-SBtt0pJv.js +19 -0
  23. package/src/ui/public/assets/{configStore-Ceu0TxZJ.js → configStore-B93BSbxD.js} +1 -1
  24. package/src/ui/public/assets/{element-plus-C3mePHEv.js → element-plus-DhvzaBj9.js} +3 -3
  25. package/src/ui/public/assets/{flow-mindmap-BzX-PWap.js → flow-mindmap-iFPJqeGK.js} +12 -12
  26. package/src/ui/public/assets/index-CfUzUz7b.js +98 -0
  27. package/src/ui/public/assets/index-Ci3lbjk2.css +1 -0
  28. package/src/ui/public/assets/{office-docx-pY9m7PMg.js → office-docx-lTQk2xZI.js} +6 -6
  29. package/src/ui/public/assets/{office-excel-F21cTf8V.js → office-excel-DLpZO4xP.js} +1 -1
  30. package/src/ui/public/assets/{office-pptx-XHr0prIp.js → office-pptx-COhadcNP.js} +1 -1
  31. package/src/ui/public/assets/{toolsStore-CHBnAWFM.js → toolsStore-vZowzNpd.js} +1 -1
  32. package/src/ui/public/assets/{vendor-EBzJAuo1.js → vendor-CjAdbnlA.js} +42 -42
  33. package/src/ui/public/assets/vue-flow-Cn2Hs-io.js +39 -0
  34. package/src/ui/public/index.html +11 -11
  35. package/src/ui/server/routes/fs.js +24 -0
  36. package/src/ui/server/utils/directoryGitState.js +294 -0
  37. package/src/ui/server/utils/directoryGitState.test.js +296 -0
  38. package/src/ui/public/assets/AppVersionBadge-CsP7l4ne.js +0 -2
  39. package/src/ui/public/assets/BranchSelector-BYBycv-a.js +0 -1
  40. package/src/ui/public/assets/CommitForm-DzVZxIIr.js +0 -6
  41. package/src/ui/public/assets/CommonDialog-D2T9hYSC.js +0 -1
  42. package/src/ui/public/assets/EditorView-RfbE5b0z.js +0 -1
  43. package/src/ui/public/assets/FlowOrchestrationWorkspace-CVqtNs-K.js +0 -21
  44. package/src/ui/public/assets/LogList-CpWhG0bb.js +0 -7
  45. package/src/ui/public/assets/MindmapView-CIRBJmAL.js +0 -2
  46. package/src/ui/public/assets/MonitorView-DXSGBi3r.js +0 -1
  47. package/src/ui/public/assets/ProjectStartupButton-tD45E4sx.js +0 -1
  48. package/src/ui/public/assets/RemoteManagerDialog-B3hrAiEG.js +0 -4
  49. package/src/ui/public/assets/RemoteRepoCard-BEYVnotw.js +0 -7
  50. package/src/ui/public/assets/SourceMapView-C85LmOXa.js +0 -3
  51. package/src/ui/public/assets/SvgIcon-BuyQHvwg.js +0 -1
  52. package/src/ui/public/assets/UserInputNode-BYwFGCop.js +0 -1
  53. package/src/ui/public/assets/WorkbenchView-B2LoICiH.js +0 -28
  54. package/src/ui/public/assets/WorkbenchView-Bp2YaSGS.css +0 -1
  55. package/src/ui/public/assets/_plugin-vue_export-helper-CtcBeUZZ.js +0 -17
  56. package/src/ui/public/assets/index-BF9NCKhe.css +0 -1
  57. package/src/ui/public/assets/index-BrshD3VG.js +0 -92
  58. package/src/ui/public/assets/vue-flow-DB6byJul.js +0 -39
@@ -22,6 +22,7 @@ import os from 'os';
22
22
  import { spawn, exec, execSync } from 'child_process';
23
23
  import { ensureWithinCwd, normalizeProjectPath } from '../utils/pathGuard.js';
24
24
  import { asyncRoute, HttpError } from '../utils/asyncRoute.js';
25
+ import { probeDirectoryGitStates, normalizeDirKey } from '../utils/directoryGitState.js';
25
26
  import { invalidateCurrentProjectKey, invalidateRawConfigCache } from '../../../config.js';
26
27
  import { invalidateCwdCache } from '../../../utils/index.js';
27
28
 
@@ -83,6 +84,8 @@ export function registerFsRoutes({
83
84
  setProjectRoomId,
84
85
  setIsGitRepo
85
86
  }) {
87
+ // 单次 Git 状态探测的路径数上限(最近目录一般十几个,这里是防御性上限)
88
+ const MAX_GIT_PROBE_PATHS = 100;
86
89
  // ── 解析并校验 user 输入路径在当前项目 cwd 内(防 ../ 父目录逃逸、startsWith 假阳性、Windows 大小写)──
87
90
  const safePathInProject = async (userPath) => {
88
91
  const cwd = getCurrentProjectPath() || process.cwd()
@@ -380,6 +383,27 @@ export function registerFsRoutes({
380
383
  }
381
384
  }));
382
385
 
386
+ // 批量探测"最近目录"的 Git 状态(是不是仓库 / 有没有未提交改动)
387
+ //
388
+ // 列表卡片用它标 Git 徽标。安全上只接受**配置里的最近目录**:否则等于给前端开了
389
+ // 一个"任意路径 git 探测"的口子,可以拿来判断磁盘上任意目录存在与否以及仓库状态。
390
+ // body.paths 可选:不传就探测全部最近目录(面板场景),传了就只探这些。
391
+ app.post('/api/recent_directories/git-state', asyncRoute(async (req, res) => {
392
+ const recentDirs = (await configManager.getRecentDirectories()) || [];
393
+ const allowed = new Set(recentDirs.map(normalizeDirKey));
394
+
395
+ const requested = Array.isArray(req.body?.paths) ? req.body.paths : recentDirs;
396
+ const targets = [];
397
+ for (const p of requested.slice(0, MAX_GIT_PROBE_PATHS)) {
398
+ if (typeof p !== 'string' || !p.trim()) continue;
399
+ if (!allowed.has(normalizeDirKey(p))) continue; // 不在最近目录白名单里 → 忽略
400
+ targets.push(p);
401
+ }
402
+
403
+ const results = await probeDirectoryGitStates(targets);
404
+ res.json({ success: true, results });
405
+ }));
406
+
383
407
  // 在资源管理器/访达中打开当前目录
384
408
  app.post('/api/open_directory', asyncRoute(async (req, res) => {
385
409
  try {
@@ -0,0 +1,294 @@
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
+ // 批量探测目录的 Git 状态(是不是仓库 / 有没有未提交改动 / 与上游差多少)。
16
+ //
17
+ // 用途:「最近项目」「常用目录」列表要在卡片上标出 Git 状态,列表里可能有十几个目录,
18
+ // 因此这里做四件事:
19
+ // 1. 每个目录只跑**一次** git(status 成功即说明是仓库,失败即非仓库),
20
+ // 并用 --branch 顺带拿到分支与领先/落后,避免额外 3 次 spawn;
21
+ // 2. 并发上限 + 单目录超时,别让某个大仓库(或网络盘)拖死整个请求;
22
+ // 3. 结果短 TTL 缓存,弹窗反复打开不必重复扫盘;
23
+ // 4. 领先/落后只读**本地 remote-tracking 引用,不联网 fetch** —— 探测十几个目录
24
+ // 去联网是不可接受的;口径与应用里那条「你的分支落后」提示一致。
25
+ //
26
+ // 不用 src/utils/index.js 的 execGitCommand:那个函数固定跑在当前项目 cwd,
27
+ // 且会把每次调用写进命令历史和 socket 广播 —— 探测十几个无关目录不该污染这些。
28
+ import { execFile } from 'node:child_process';
29
+ import fs from 'node:fs/promises';
30
+
31
+ /** 单目录 git status 超时。大仓库 status 通常 <300ms,5s 已是宽容上限 */
32
+ export const DEFAULT_PROBE_TIMEOUT_MS = 5000;
33
+ /** 结果缓存 TTL。用户连续开几次弹窗时命中缓存,不必重复扫盘 */
34
+ export const DEFAULT_CACHE_TTL_MS = 15000;
35
+ /** 并发上限:并发开满会在慢盘/大仓库上互相抢 IO,4 是实测比较稳的值 */
36
+ export const DEFAULT_CONCURRENCY = 4;
37
+
38
+ const MAX_BUFFER = 8 * 1024 * 1024;
39
+ const cache = new Map(); // key: normalizeDirKey(path) → { ts, state }
40
+
41
+ /**
42
+ * 路径缓存键。Windows 上同一目录可能以 `D:/x`、`d:\x`、`d:\x\` 多种写法出现,
43
+ * 统一成小写 + 正斜杠 + 去尾斜杠,避免同一目录缓存成多份。
44
+ */
45
+ export function normalizeDirKey(dirPath) {
46
+ return String(dirPath || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
47
+ }
48
+
49
+ /** 空状态(非仓库 / 探测失败时复用),避免各处手写零值 */
50
+ function emptyState(extra = {}) {
51
+ return {
52
+ exists: true,
53
+ isGitRepo: false,
54
+ // 工作区计数
55
+ changed: 0, staged: 0, unstaged: 0, untracked: 0,
56
+ // 分支与上游跟踪
57
+ branch: null, upstream: null, hasUpstream: false, detached: false, ahead: 0, behind: 0,
58
+ ...extra,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * 解析 `git status --porcelain=v1` 输出 → 工作区计数。
64
+ * 纯函数,单测覆盖。
65
+ *
66
+ * 格式:`XY path`,X=暂存区状态,Y=工作区状态。
67
+ * - `## ...` → 分支头(见 parseBranchTracking),**不计入改动**
68
+ * - `??` → 未跟踪
69
+ * - `!!` → 被忽略(porcelain 默认不输出,防御性跳过)
70
+ * - 其余非空位 → 计入暂存/未暂存(冲突如 `UU` 会同时计入两边)
71
+ * `changed` 按**条目数**数,所以"已暂存又被修改"的文件只算 1 个,不重复计数。
72
+ */
73
+ export function parsePorcelainStatus(stdout) {
74
+ const result = { changed: 0, staged: 0, unstaged: 0, untracked: 0 };
75
+ for (const rawLine of String(stdout || '').split('\n')) {
76
+ const line = rawLine.replace(/\r$/, '');
77
+ // 必须显式跳过分支头:`## main...origin/main` 的前两个字符是 `#`,
78
+ // 既不是 `!!` 也不是 `??`,漏掉会把它算成 1 个"已暂存 + 未暂存"的文件。
79
+ if (line.startsWith('##')) continue;
80
+ if (line.length < 3) continue;
81
+ const x = line[0];
82
+ const y = line[1];
83
+ if (x === '!' && y === '!') continue;
84
+ if (x === '?' && y === '?') {
85
+ result.untracked += 1;
86
+ } else {
87
+ if (x !== ' ' && x !== '?') result.staged += 1;
88
+ if (y !== ' ' && y !== '?') result.unstaged += 1;
89
+ }
90
+ result.changed += 1;
91
+ }
92
+ return result;
93
+ }
94
+
95
+ /**
96
+ * 解析 `git status --porcelain=v1 --branch` 的 `##` 头 → 分支与领先/落后。
97
+ * 纯函数,单测覆盖。
98
+ *
99
+ * 见过的几种形状:
100
+ * `## main...origin/main [ahead 1, behind 2]` 有上游且有差异
101
+ * `## main...origin/main` 有上游且同步
102
+ * `## main...origin/main [gone]` 上游被删了(ahead/behind 无意义)
103
+ * `## main` 有分支但没设上游
104
+ * `## HEAD (no branch)` 分离 HEAD
105
+ * `## No commits yet on main` 空仓库(老 git 是 `## Initial commit on main`)
106
+ *
107
+ * 领先/落后取自**本地 remote-tracking 引用**,不联网 fetch —— 与应用里那条
108
+ * 「你的分支落后 'origin/develop' 17 个提交」同一口径(都基于上次 fetch 的结果)。
109
+ */
110
+ export function parseBranchTracking(stdout) {
111
+ const result = { branch: null, upstream: null, hasUpstream: false, detached: false, ahead: 0, behind: 0 };
112
+ const headLine = String(stdout || '')
113
+ .split('\n')
114
+ .map(l => l.replace(/\r$/, ''))
115
+ .find(l => l.startsWith('## '));
116
+ if (!headLine) return result;
117
+
118
+ let head = headLine.slice(3).trim();
119
+ // 跟踪信息固定在末尾方括号里,先摘掉再拆分支名(分支名里不可能出现 `[`)
120
+ const bracket = head.match(/\s+\[(.+)\]$/);
121
+ let tracking = '';
122
+ if (bracket) {
123
+ tracking = bracket[1];
124
+ head = head.slice(0, head.length - bracket[0].length).trim();
125
+ }
126
+
127
+ if (/^HEAD \(no branch\)/.test(head)) {
128
+ result.detached = true;
129
+ return result;
130
+ }
131
+
132
+ const fresh = head.match(/^(?:No commits yet on|Initial commit on)\s+(.+)$/);
133
+ if (fresh) {
134
+ result.branch = fresh[1].trim();
135
+ return result;
136
+ }
137
+
138
+ // git 的 ref 命名规则禁止 `..`,所以 `...` 只会是分隔符
139
+ const sep = head.indexOf('...');
140
+ if (sep === -1) {
141
+ result.branch = head;
142
+ } else {
143
+ result.branch = head.slice(0, sep);
144
+ result.upstream = head.slice(sep + 3);
145
+ // [gone] 表示上游引用已不存在,这时不算"有上游可比较"
146
+ result.hasUpstream = !/gone/.test(tracking);
147
+ }
148
+
149
+ const ahead = tracking.match(/ahead (\d+)/);
150
+ const behind = tracking.match(/behind (\d+)/);
151
+ if (ahead) result.ahead = Number(ahead[1]);
152
+ if (behind) result.behind = Number(behind[1]);
153
+
154
+ return result;
155
+ }
156
+
157
+ function execFileAsync(file, args, options) {
158
+ return new Promise((resolve, reject) => {
159
+ execFile(file, args, options, (error, stdout, stderr) => {
160
+ if (error) {
161
+ error.stdout = stdout;
162
+ error.stderr = stderr;
163
+ reject(error);
164
+ return;
165
+ }
166
+ resolve({ stdout, stderr });
167
+ });
168
+ });
169
+ }
170
+
171
+ /**
172
+ * 探测单个目录。
173
+ * 返回 isGitRepo:true|false|null —— null 表示"没探到"(超时/权限等),
174
+ * 前端对 null 不显示任何 Git 标记,避免把未知谎报成"非仓库"。
175
+ */
176
+ export async function probeDirectoryGitState(dirPath, { timeoutMs = DEFAULT_PROBE_TIMEOUT_MS } = {}) {
177
+ try {
178
+ const stat = await fs.stat(dirPath);
179
+ if (!stat.isDirectory()) return emptyState({ exists: false });
180
+ } catch {
181
+ return emptyState({ exists: false });
182
+ }
183
+
184
+ try {
185
+ const { stdout } = await execFileAsync(
186
+ 'git',
187
+ [
188
+ '-C', dirPath,
189
+ // --no-optional-locks:纯读探测不抢 index.lock,避免和用户正在跑的 git 操作打架
190
+ '--no-optional-locks',
191
+ 'status',
192
+ '--porcelain=v1',
193
+ // --branch 让 status 顺带输出 `## main...origin/main [ahead 1, behind 2]`,
194
+ // 领先/落后就白拿 —— 否则要再起 3 个进程(symbolic-ref + rev-parse @{u} + rev-list)。
195
+ // 只读本地 remote-tracking 引用,**不联网**。
196
+ '--branch',
197
+ // normal 会把未跟踪目录折叠成一条,大目录(node_modules 之类)下比 all 快很多,
198
+ // 而这里只需要"有没有 / 有几项"
199
+ '--untracked-files=normal',
200
+ ],
201
+ {
202
+ timeout: timeoutMs,
203
+ maxBuffer: MAX_BUFFER,
204
+ windowsHide: true,
205
+ env: {
206
+ ...process.env,
207
+ // 与 execGitCommand 保持一致:关闭路径转义,避免中文/空格路径变成 \xxx 八进制
208
+ GIT_CONFIG_PARAMETERS: "'core.quotepath=false'",
209
+ },
210
+ }
211
+ );
212
+ return {
213
+ exists: true,
214
+ isGitRepo: true,
215
+ ...parsePorcelainStatus(stdout),
216
+ ...parseBranchTracking(stdout),
217
+ };
218
+ } catch (error) {
219
+ const text = `${error?.stderr || ''}\n${error?.message || ''}`;
220
+ if (/not a git repository|not a git repo/i.test(text)) {
221
+ // 目录存在但不是仓库:明确回答"不是",不算错误
222
+ return emptyState();
223
+ }
224
+ // 超时 / 权限 / git 不存在等 —— 状态未知
225
+ return emptyState({ isGitRepo: null, error: String(error?.message || error).split('\n')[0] });
226
+ }
227
+ }
228
+
229
+ /** 以固定并发跑完所有任务,保持入参顺序输出 */
230
+ async function runWithConcurrency(items, limit, worker) {
231
+ const results = new Array(items.length);
232
+ let cursor = 0;
233
+ const size = Math.max(1, Math.min(limit, items.length));
234
+ await Promise.all(Array.from({ length: size }, async () => {
235
+ while (cursor < items.length) {
236
+ const index = cursor;
237
+ cursor += 1;
238
+ results[index] = await worker(items[index], index);
239
+ }
240
+ }));
241
+ return results;
242
+ }
243
+
244
+ /**
245
+ * 批量探测。返回 `{ [原始路径]: state }` —— 键用调用方传入的原始字符串,
246
+ * 前端可以直接 `states[item.path]` 取,不必关心后端的归一化规则。
247
+ *
248
+ * @param {string[]} paths
249
+ * @param {{ timeoutMs?: number, ttlMs?: number, concurrency?: number, useCache?: boolean }} [options]
250
+ */
251
+ export async function probeDirectoryGitStates(paths, options = {}) {
252
+ const {
253
+ timeoutMs = DEFAULT_PROBE_TIMEOUT_MS,
254
+ ttlMs = DEFAULT_CACHE_TTL_MS,
255
+ concurrency = DEFAULT_CONCURRENCY,
256
+ useCache = true,
257
+ } = options;
258
+
259
+ const list = Array.isArray(paths) ? paths.filter(p => typeof p === 'string' && p.trim()) : [];
260
+ const results = {};
261
+ const pending = [];
262
+ const now = Date.now();
263
+
264
+ for (const original of list) {
265
+ if (Object.prototype.hasOwnProperty.call(results, original)) continue; // 重复路径只探一次
266
+ const key = normalizeDirKey(original);
267
+ const hit = useCache ? cache.get(key) : undefined;
268
+ if (hit && now - hit.ts < ttlMs) {
269
+ results[original] = hit.state;
270
+ continue;
271
+ }
272
+ pending.push({ key, original });
273
+ }
274
+
275
+ const probed = await runWithConcurrency(pending, concurrency, async ({ key, original }) => {
276
+ const state = await probeDirectoryGitState(original, { timeoutMs });
277
+ return { key, original, state };
278
+ });
279
+
280
+ for (const { key, original, state } of probed) {
281
+ if (state.isGitRepo !== null) {
282
+ // 只有确定的结果才缓存:超时/未知下次还要再试
283
+ cache.set(key, { ts: Date.now(), state });
284
+ }
285
+ results[original] = state;
286
+ }
287
+
288
+ return results;
289
+ }
290
+
291
+ /** 仅供测试:清空缓存 */
292
+ export function clearGitStateCache() {
293
+ cache.clear();
294
+ }
@@ -0,0 +1,296 @@
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
+ // 目录 Git 状态探测 单元测试(node:test 内置)
16
+ import { test } from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+ import fs from 'node:fs/promises';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import { execFile } from 'node:child_process';
22
+ import { promisify } from 'node:util';
23
+ import {
24
+ parsePorcelainStatus,
25
+ parseBranchTracking,
26
+ normalizeDirKey,
27
+ probeDirectoryGitState,
28
+ probeDirectoryGitStates,
29
+ clearGitStateCache,
30
+ } from './directoryGitState.js';
31
+
32
+ const execFileAsync = promisify(execFile);
33
+ const git = (args, cwd) => execFileAsync('git', args, { cwd, windowsHide: true });
34
+ // 提交必须显式给身份并关掉签名,否则依赖开发机的全局 git 配置 → CI 上必挂
35
+ const gitCommit = (dir, msg) => git([
36
+ '-c', 'user.name=zen-test',
37
+ '-c', 'user.email=zen-test@example.invalid',
38
+ '-c', 'commit.gpgsign=false',
39
+ 'commit', '-q', '-m', msg,
40
+ ], dir);
41
+ // 本地路径当 remote 时统一成正斜杠,免得 Windows 反斜杠被当转义
42
+ const asRemote = p => p.replace(/\\/g, '/');
43
+
44
+ // ── 纯函数:porcelain 解析 ────────────────────────────────────────────────
45
+ test('parsePorcelainStatus: 空输入返回全零', () => {
46
+ assert.deepEqual(parsePorcelainStatus(''), { changed: 0, staged: 0, unstaged: 0, untracked: 0 });
47
+ assert.deepEqual(parsePorcelainStatus(undefined), { changed: 0, staged: 0, unstaged: 0, untracked: 0 });
48
+ });
49
+
50
+ test('parsePorcelainStatus: 分别统计已暂存 / 未暂存 / 未跟踪', () => {
51
+ const out = [
52
+ ' M src/a.js', // 工作区改动
53
+ 'M src/b.js', // 已暂存
54
+ '?? notes.txt', // 未跟踪
55
+ 'A src/c.js', // 新文件已暂存
56
+ ].join('\n');
57
+ const r = parsePorcelainStatus(out);
58
+ assert.equal(r.staged, 2);
59
+ assert.equal(r.unstaged, 1);
60
+ assert.equal(r.untracked, 1);
61
+ assert.equal(r.changed, 4);
62
+ });
63
+
64
+ test('parsePorcelainStatus: 同一文件既暂存又改动(MM)只算 1 项,但两边各计一次', () => {
65
+ const r = parsePorcelainStatus('MM src/both.js');
66
+ assert.equal(r.changed, 1, 'changed 按条目数,不能重复计数');
67
+ assert.equal(r.staged, 1);
68
+ assert.equal(r.unstaged, 1);
69
+ });
70
+
71
+ test('parsePorcelainStatus: 冲突(UU)两侧都计数,条目仍为 1', () => {
72
+ const r = parsePorcelainStatus('UU src/conflict.js');
73
+ assert.equal(r.changed, 1);
74
+ assert.equal(r.staged, 1);
75
+ assert.equal(r.unstaged, 1);
76
+ });
77
+
78
+ test('parsePorcelainStatus: 重命名算 1 项(已暂存)', () => {
79
+ const r = parsePorcelainStatus('R old.js -> new.js');
80
+ assert.equal(r.changed, 1);
81
+ assert.equal(r.staged, 1);
82
+ assert.equal(r.unstaged, 0);
83
+ });
84
+
85
+ test('parsePorcelainStatus: 忽略条目(!!)与 CRLF / 空行不计数', () => {
86
+ const out = '!! dist/bundle.js\r\n\r\n M a.js\r\n';
87
+ const r = parsePorcelainStatus(out);
88
+ assert.equal(r.changed, 1);
89
+ assert.equal(r.unstaged, 1);
90
+ });
91
+
92
+ // 回归:加了 --branch 之后 `## main...origin/main` 会出现在输出首行。
93
+ // 它的前两个字符是 `#`,既不是 `!!` 也不是 `??`,漏判就会被算成
94
+ // 1 个"已暂存 + 未暂存"的文件 —— 于是"干净仓库"凭空多出 1 项未提交。
95
+ test('parsePorcelainStatus: 分支头(##)不计入改动', () => {
96
+ const withHeader = parsePorcelainStatus('## main...origin/main [ahead 1, behind 2]\n M a.js');
97
+ assert.equal(withHeader.changed, 1, '只有真实改动那条才算');
98
+ assert.equal(withHeader.staged, 0);
99
+ assert.equal(withHeader.unstaged, 1);
100
+
101
+ const cleanWithHeader = parsePorcelainStatus('## main...origin/main');
102
+ assert.deepEqual(cleanWithHeader, { changed: 0, staged: 0, unstaged: 0, untracked: 0 });
103
+ });
104
+
105
+ // ── 纯函数:分支头解析 ────────────────────────────────────────────────────
106
+ test('parseBranchTracking: 有上游且有领先/落后', () => {
107
+ assert.deepEqual(parseBranchTracking('## main...origin/main [ahead 1, behind 2]'), {
108
+ branch: 'main', upstream: 'origin/main', hasUpstream: true, detached: false, ahead: 1, behind: 2,
109
+ });
110
+ assert.deepEqual(parseBranchTracking('## develop...origin/develop [behind 17]'), {
111
+ branch: 'develop', upstream: 'origin/develop', hasUpstream: true, detached: false, ahead: 0, behind: 17,
112
+ });
113
+ assert.deepEqual(parseBranchTracking('## main...origin/main [ahead 3]'), {
114
+ branch: 'main', upstream: 'origin/main', hasUpstream: true, detached: false, ahead: 3, behind: 0,
115
+ });
116
+ });
117
+
118
+ test('parseBranchTracking: 有上游但已同步 → 全零', () => {
119
+ const r = parseBranchTracking('## main...origin/main');
120
+ assert.equal(r.hasUpstream, true);
121
+ assert.equal(r.upstream, 'origin/main');
122
+ assert.equal(r.ahead, 0);
123
+ assert.equal(r.behind, 0);
124
+ });
125
+
126
+ test('parseBranchTracking: 没设上游 → hasUpstream=false 且不带上游名', () => {
127
+ const r = parseBranchTracking('## feature/foo-bar');
128
+ assert.equal(r.branch, 'feature/foo-bar', '带斜杠/短横线的分支名要完整保留');
129
+ assert.equal(r.upstream, null);
130
+ assert.equal(r.hasUpstream, false);
131
+ assert.equal(r.ahead, 0);
132
+ assert.equal(r.behind, 0);
133
+ });
134
+
135
+ test('parseBranchTracking: 上游被删([gone])不算有上游', () => {
136
+ const r = parseBranchTracking('## main...origin/main [gone]');
137
+ assert.equal(r.branch, 'main');
138
+ assert.equal(r.hasUpstream, false);
139
+ assert.equal(r.ahead, 0);
140
+ assert.equal(r.behind, 0);
141
+ });
142
+
143
+ test('parseBranchTracking: 分离 HEAD 与空仓库', () => {
144
+ const detached = parseBranchTracking('## HEAD (no branch)');
145
+ assert.equal(detached.detached, true);
146
+ assert.equal(detached.branch, null);
147
+ assert.equal(detached.hasUpstream, false);
148
+
149
+ assert.equal(parseBranchTracking('## No commits yet on main').branch, 'main');
150
+ assert.equal(parseBranchTracking('## Initial commit on trunk').branch, 'trunk');
151
+ });
152
+
153
+ test('parseBranchTracking: 无分支头 / 空输入 → 中性零值,不抛错', () => {
154
+ for (const input of ['', undefined, null, ' M a.js\n?? b.txt']) {
155
+ assert.deepEqual(parseBranchTracking(input), {
156
+ branch: null, upstream: null, hasUpstream: false, detached: false, ahead: 0, behind: 0,
157
+ });
158
+ }
159
+ });
160
+
161
+ test('normalizeDirKey: Windows 大小写 / 斜杠 / 尾斜杠写法归一', () => {
162
+ const a = normalizeDirKey('D:\\Workspace\\Proj\\');
163
+ const b = normalizeDirKey('d:/workspace/proj');
164
+ assert.equal(a, b);
165
+ });
166
+
167
+ // ── 真实文件系统 + git 进程 ────────────────────────────────────────────────
168
+ test('probeDirectoryGitState: 不存在的目录 → exists=false', async () => {
169
+ const missing = path.join(os.tmpdir(), 'zen-gitsync-not-exist-' + Date.now());
170
+ const state = await probeDirectoryGitState(missing);
171
+ assert.equal(state.exists, false);
172
+ assert.equal(state.isGitRepo, false);
173
+ });
174
+
175
+ test('probeDirectoryGitState: 普通目录 → exists=true 且 isGitRepo=false', async () => {
176
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-plain-'));
177
+ try {
178
+ const state = await probeDirectoryGitState(dir);
179
+ assert.equal(state.exists, true);
180
+ assert.equal(state.isGitRepo, false);
181
+ assert.equal(state.changed, 0);
182
+ } finally {
183
+ await fs.rm(dir, { recursive: true, force: true });
184
+ }
185
+ });
186
+
187
+ test('probeDirectoryGitState: 真实仓库 → 干净 0 项,加文件后计数递增', async () => {
188
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-repo-'));
189
+ try {
190
+ await git(['init', '-q'], dir);
191
+ const clean = await probeDirectoryGitState(dir);
192
+ assert.equal(clean.isGitRepo, true);
193
+ assert.equal(clean.changed, 0);
194
+
195
+ await fs.writeFile(path.join(dir, 'new-file.txt'), 'hello\n');
196
+ const untracked = await probeDirectoryGitState(dir);
197
+ assert.equal(untracked.isGitRepo, true);
198
+ assert.equal(untracked.changed, 1);
199
+ assert.equal(untracked.untracked, 1);
200
+
201
+ await git(['add', 'new-file.txt'], dir);
202
+ const staged = await probeDirectoryGitState(dir);
203
+ assert.equal(staged.staged, 1);
204
+ assert.equal(staged.changed, 1);
205
+ } finally {
206
+ await fs.rm(dir, { recursive: true, force: true });
207
+ }
208
+ });
209
+
210
+ // 端到端验证 --branch 真的生效、领先/落后数字是真的:
211
+ // 建一个裸仓库当远端,克隆两个副本,一个本地提交(领先)、另一个提交后推送再 fetch(落后)。
212
+ test('probeDirectoryGitState: 真实远端 → 读出上游分支与领先/落后', async () => {
213
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-track-'));
214
+ const originDir = path.join(root, 'origin.git');
215
+ const seedDir = path.join(root, 'seed');
216
+ const workDir = path.join(root, 'work');
217
+ const otherDir = path.join(root, 'other');
218
+ try {
219
+ // 1) 造一个已有 main 的裸远端(空裸仓库克隆出来没有上游,必须先有提交)
220
+ await git(['init', '-q', '--bare', '-b', 'main', originDir], root);
221
+ await git(['init', '-q', '-b', 'main', seedDir], root);
222
+ await fs.writeFile(path.join(seedDir, 'README.md'), 'seed\n');
223
+ await git(['add', '.'], seedDir);
224
+ await gitCommit(seedDir, 'init');
225
+ await git(['remote', 'add', 'origin', asRemote(originDir)], seedDir);
226
+ await git(['push', '-q', '-u', 'origin', 'main'], seedDir);
227
+
228
+ // 2) 克隆出来就自带上游,且与远端同步
229
+ await git(['clone', '-q', asRemote(originDir), workDir], root);
230
+ const synced = await probeDirectoryGitState(workDir);
231
+ assert.equal(synced.isGitRepo, true);
232
+ assert.equal(synced.hasUpstream, true);
233
+ assert.equal(synced.upstream, 'origin/main');
234
+ assert.equal(synced.branch, 'main');
235
+ assert.equal(synced.ahead, 0);
236
+ assert.equal(synced.behind, 0);
237
+ assert.equal(synced.changed, 0, '同步且干净时不能凭空多出改动(分支头误计的回归)');
238
+
239
+ // 3) 本地提交 → 领先 1
240
+ await fs.writeFile(path.join(workDir, 'local.txt'), 'local\n');
241
+ await git(['add', '.'], workDir);
242
+ await gitCommit(workDir, 'local work');
243
+ const ahead = await probeDirectoryGitState(workDir);
244
+ assert.equal(ahead.ahead, 1);
245
+ assert.equal(ahead.behind, 0);
246
+ assert.equal(ahead.changed, 0, '提交后工作区应该是干净的');
247
+
248
+ // 4) 另一个人推了新提交,再 fetch → 同时领先 1 / 落后 1
249
+ await git(['clone', '-q', asRemote(originDir), otherDir], root);
250
+ await fs.writeFile(path.join(otherDir, 'remote.txt'), 'remote\n');
251
+ await git(['add', '.'], otherDir);
252
+ await gitCommit(otherDir, 'remote work');
253
+ await git(['push', '-q'], otherDir);
254
+ await git(['fetch', '-q', 'origin'], workDir);
255
+
256
+ const diverged = await probeDirectoryGitState(workDir);
257
+ assert.equal(diverged.ahead, 1);
258
+ assert.equal(diverged.behind, 1);
259
+ assert.equal(diverged.hasUpstream, true);
260
+ } finally {
261
+ await fs.rm(root, { recursive: true, force: true });
262
+ }
263
+ });
264
+
265
+ test('probeDirectoryGitStates: 结果按调用方原始路径字符串为键,重复路径只探一次', async () => { clearGitStateCache();
266
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-batch-'));
267
+ try {
268
+ await git(['init', '-q'], dir);
269
+ const results = await probeDirectoryGitStates([dir, dir, 'not-a-real-path-xyz'], { useCache: false });
270
+ assert.deepEqual(Object.keys(results).sort(), [dir, 'not-a-real-path-xyz'].sort());
271
+ assert.equal(results[dir].isGitRepo, true);
272
+ assert.equal(results['not-a-real-path-xyz'].exists, false);
273
+ } finally {
274
+ await fs.rm(dir, { recursive: true, force: true });
275
+ }
276
+ });
277
+
278
+ test('probeDirectoryGitStates: 命中缓存时不重新扫盘,useCache:false 可绕过', async () => {
279
+ clearGitStateCache();
280
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-cache-'));
281
+ try {
282
+ await git(['init', '-q'], dir);
283
+ const first = await probeDirectoryGitStates([dir]);
284
+ assert.equal(first[dir].changed, 0);
285
+
286
+ // 缓存期内新增文件,结果应仍是缓存的旧值
287
+ await fs.writeFile(path.join(dir, 'later.txt'), 'x\n');
288
+ const cached = await probeDirectoryGitStates([dir]);
289
+ assert.equal(cached[dir].changed, 0, 'TTL 内应命中缓存');
290
+
291
+ const fresh = await probeDirectoryGitStates([dir], { useCache: false });
292
+ assert.equal(fresh[dir].changed, 1, 'useCache:false 必须重新探测');
293
+ } finally {
294
+ await fs.rm(dir, { recursive: true, force: true });
295
+ }
296
+ });
@@ -1,2 +0,0 @@
1
- import"./rolldown-runtime-BM3Ffeng.js";import{$n as e,An as t,Bn as n,Br as r,Cn as i,Dn as a,Et as o,G as s,Hn as c,Kr as l,Ln as u,M as d,On as f,Sn as p,U as m,Wn as h,bn as g,fr as _,i as v,nt as y,r as b,rn as x,tr as S,tt as C,xn as w,yn as T}from"./element-plus-C3mePHEv.js";import{n as E,t as D}from"./_plugin-vue_export-helper-CtcBeUZZ.js";async function O(e,t){let n=e.body.getReader(),r=new TextDecoder(`utf-8`),i=``;for(;;){let{done:e,value:a}=await n.read();if(e)break;i+=r.decode(a,{stream:!0});let o=i.split(`
2
- `);i=o.pop()??``;for(let e of o)e.trim()&&t(e)}i.trim()&&t(i)}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}await O(t,t=>{try{e(JSON.parse(t))}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)}if(!e.body)throw Error(`响应无 body`);return new Promise((t,n)=>{let r=!1,i=e=>{r||(r=!0,e())};O(e,e=>{try{let r=JSON.parse(e);r.type===`ready`&&typeof r.port==`number`&&r.port>0?i(()=>t(r.port)):r.type===`timeout`?i(()=>n(Error(`restart.timeout`))):r.type===`error`&&i(()=>n(Error(r.message||`restart.failed`)))}catch{}}).then(()=>{i(()=>n(Error(`restart.stream-closed`)))}).catch(()=>{})})}var M=`npm install -g zen-gitsync --registry https://registry.npmjs.org/`,N=t({__name:`UpgradeDialog`,props:{modelValue:{type:Boolean,required:!0},logs:{type:String,required:!0},status:{type:String,required:!0},countdown:{type:Number,required:!1,default:0}},emits:[`update:modelValue`,`retry`,`restart`,`cancel`],setup(t,{expose:n,emit:r}){n();let i=t,a=r,s=_(null);e(()=>i.logs,async()=>{await u(),s.value&&(s.value.scrollTop=s.value.scrollHeight)});function c(){i.status!==`running`&&(i.status===`success`&&i.countdown>0&&a(`cancel`),a(`update:modelValue`,!1))}function l(){a(`retry`)}function d(){a(`restart`)}function f(){a(`cancel`),a(`update:modelValue`,!1)}async function p(){try{await navigator.clipboard.writeText(M),v.success(E(`@F13B4:已复制升级命令`))}catch{v.warning(E(`@F13B4:复制失败,请手动复制`))}}let m={MANUAL_COMMAND:M,props:i,emit:a,logEl:s,close:c,onRetry:l,onRestart:d,onLater:f,onCopyCommand:p,get Loading(){return o},get CircleCheck(){return C},get CircleClose(){return y},get $t(){return E}};return Object.defineProperty(m,`__isScriptSetup`,{enumerable:!1,value:!0}),m}}),P={key:3},F={key:4},I={key:5},L={key:0,class:`upgrade-hint`},R={key:0,class:`upgrade-countdown`},z={key:1,class:`upgrade-manual`},B={class:`upgrade-manual__label`};function V(e,t,n,o,s,c){let u=x,_=m,v=d;return h(),w(v,{"model-value":n.modelValue,"onUpdate:modelValue":t[0]||=e=>o.emit(`update:modelValue`,e),title:o.$t(`@F13B4:升级`),width:`680px`,"close-on-click-modal":!1,"close-on-press-escape":!1,"show-close":n.status!==`running`,"append-to-body":``,onClose:o.close},{footer:S(()=>[n.status===`failed`?(h(),w(_,{key:0,type:`primary`,onClick:o.onRetry},{default:S(()=>[a(l(o.$t(`@F13B4:重试`)),1)]),_:1})):p(`v-if`,!0),n.status===`failed`?(h(),w(_,{key:1,onClick:o.onCopyCommand},{default:S(()=>[a(l(o.$t(`@F13B4:复制升级命令`)),1)]),_:1})):p(`v-if`,!0),n.status===`success`?(h(),w(_,{key:2,type:`primary`,onClick:o.onRestart},{default:S(()=>[a(l(o.$t(`@F13B4:立即重启并刷新`)),1)]),_:1})):p(`v-if`,!0),n.status===`success`?(h(),w(_,{key:3,onClick:o.onLater},{default:S(()=>[a(l(o.$t(`@F13B4:稍后手动重启`)),1)]),_:1})):p(`v-if`,!0),n.status===`running`?(h(),w(_,{key:4,disabled:``},{default:S(()=>[a(l(o.$t(`@F13B4:升级中`))+`... `,1)]),_:1})):p(`v-if`,!0)]),default:S(()=>[g(`div`,{class:r([`upgrade-status`,`is-${n.status}`])},[n.status===`running`?(h(),w(u,{key:0,class:`is-loading`},{default:S(()=>[f(o.Loading)]),_:1})):n.status===`success`?(h(),w(u,{key:1,color:`#67c23a`},{default:S(()=>[f(o.CircleCheck)]),_:1})):(h(),w(u,{key:2,color:`#f56c6c`},{default:S(()=>[f(o.CircleClose)]),_:1})),n.status===`running`?(h(),i(`span`,P,l(o.$t(`@F13B4:升级中`)),1)):n.status===`success`?(h(),i(`span`,F,l(o.$t(`@F13B4:升级完成`)),1)):(h(),i(`span`,I,l(o.$t(`@F13B4:升级失败`)),1))],2),n.status===`success`?(h(),i(`p`,L,[a(l(o.$t(`@F13B4:新版本已全局安装,需要重启服务才能生效`))+` `,1),n.countdown>0?(h(),i(`span`,R,l(o.$t(`@F13B4:{seconds} 秒后自动刷新`,{seconds:n.countdown})),1)):p(`v-if`,!0)])):p(`v-if`,!0),n.status===`failed`?(h(),i(`p`,z,[g(`span`,B,l(o.$t(`@F13B4:手动升级命令`)),1),g(`code`,{class:`upgrade-manual__cmd`},l(o.MANUAL_COMMAND))])):p(`v-if`,!0),g(`pre`,{ref:`logEl`,class:`upgrade-log`},l(n.logs||o.$t(`@F13B4:等待日志输出`)),513)]),_:1},8,[`model-value`,`title`,`show-close`])}var H=D(N,[[`render`,V],[`__scopeId`,`data-v-bc566bba`],[`__file`,`D:/xz_workspace/github_workspace/zen-gitsync/src/ui/client/src/components/UpgradeDialog.vue`]]),U=`https://github.com/xz333221/zen-gitsync/releases`,W=5,G=`app-version-check`,K=3600*1e3,q=t({__name:`AppVersionBadge`,setup(e,{expose:t}){t();let r=`2.16.48`,i=_(r),a=T(()=>i.value||r),o=_(null),s=_(!1),l=_(``),u=_(`running`),d=_(0),f=null,p=_(!1);function m(){try{let e=sessionStorage.getItem(G);if(!e)return null;let{at:t,data:n}=JSON.parse(e);return Date.now()-t>K?null:n}catch{return null}}function h(e){try{sessionStorage.setItem(G,JSON.stringify({at:Date.now(),data:e}))}catch{}}async function g(e=!1){let t=m();t&&(o.value=t);try{let t=await k();o.value=t,h(t),t.current&&(i.value=t.current),!e&&t.success&&t.hasUpdate&&v({type:`success`,message:E(`@F13B4:发现新版本 {version}`,{version:t.latest??``}),duration:4e3})}catch(t){e||v({type:`warning`,message:E(`@F13B4:检查更新失败 {error}`,{error:t?.message||``})})}}async function y(){if(o.value?.hasUpdate){try{await b.confirm(E(`@F13B4:确定要从 {current} 升级到 {latest} 吗?`,{current:o.value.current,latest:o.value.latest}),E(`@F13B4:升级`),{confirmButtonText:E(`@F13B4:确定升级`),cancelButtonText:E(`@F13B4:取消`),type:`info`})}catch{return}l.value=``,u.value=`running`,s.value=!0,await A(e=>{if(e.type===`done`){if(u.value=e.code===0?`success`:`failed`,e.code===0){try{sessionStorage.removeItem(G)}catch{}o.value?.latest&&(i.value=o.value.latest),x()}}else e.message&&(l.value+=e.message)})}}function x(){S(),d.value=W,f=setInterval(()=>{--d.value,d.value<=0&&(f&&=(clearInterval(f),null),w())},1e3)}function S(){f&&=(clearInterval(f),null),d.value=0}function C(){S()}async function w(){if(S(),!p.value){p.value=!0;try{let e=await j(),{pathname:t,search:n,hash:r}=window.location;window.location.href=`http://localhost:${e}${t}${n}${r}`}catch(e){let t=e?.message||``,n={"restart.timeout":`@F13B4:重启超时`,"restart.stream-closed":`@F13B4:重启失败,请刷新页面重试`,"restart.failed":`@F13B4:重启失败`}[t];v({type:`warning`,message:n?E(n):E(`@F13B4:重启失败 {error}`,{error:t})}),p.value=!1}}}n(()=>{S()}),c(()=>{g(!0)});let D={buildVersion:r,releasesUrl:U,runtimeVersion:i,displayVersion:a,latestInfo:o,upgradeDialogVisible:s,upgradeLogs:l,upgradeStatus:u,AUTO_REFRESH_SECONDS:W,countdown:d,get countdownTimer(){return f},set countdownTimer(e){f=e},restartInFlight:p,CACHE_KEY:G,CACHE_TTL_MS:K,readCache:m,writeCache:h,checkUpdate:g,handleUpgrade:y,startAutoRefreshCountdown:x,cancelAutoRefreshCountdown:S,handleCancelAutoRefresh:C,handleRestart:w,get $t(){return E},UpgradeDialog:H};return Object.defineProperty(D,`__isScriptSetup`,{enumerable:!1,value:!0}),D}}),J={class:`app-version-badge`},Y=[`aria-label`],X=[`aria-label`],Z=[`title`,`aria-label`],Q={class:`upgrade-btn__label`},$={class:`upgrade-btn__version`};function ee(e,t,n,r,a,o){let c=s;return h(),i(`div`,J,[p(` GitHub 链接 `),f(c,{content:r.$t(`@F13B4:在 GitHub 上查看源码`),placement:`top`,effect:`dark`,"show-after":300},{default:S(()=>[g(`a`,{href:`https://github.com/xz333221/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link`,"aria-label":r.$t(`@F13B4:在 GitHub 上查看源码`)},t[1]||=[g(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[g(`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,Y)]),_:1},8,[`content`]),p(` NPM 链接 `),f(c,{content:r.$t(`@F13B4:在 npm 上查看包`),placement:`top`,effect:`dark`,"show-after":300},{default:S(()=>[g(`a`,{href:`https://www.npmjs.com/package/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link badge-icon-link--npm`,"aria-label":r.$t(`@F13B4:在 npm 上查看包`)},t[2]||=[g(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[g(`path`,{d:`M2 2v20h20V2H2zm17.167 17.167H12.5v-10h-3.333v10H4.833V4.833h14.334v14.334z`})],-1)],8,X)]),_:1},8,[`content`]),p(` 版本号 `),f(c,{content:r.$t(`@F13B4:查看更新`),placement:`top`,effect:`dark`,"show-after":300},{default:S(()=>[g(`a`,{href:r.releasesUrl,target:`_blank`,rel:`noopener noreferrer`,class:`version-link`},`v`+l(r.displayVersion),1)]),_:1},8,[`content`]),r.latestInfo?.hasUpdate?(h(),i(`button`,{key:0,type:`button`,class:`upgrade-btn`,title:r.$t(`@F13B4:发现新版本 {version},点击升级`,{version:r.latestInfo.latest??``}),"aria-label":r.$t(`@F13B4:发现新版本 {version},点击升级`,{version:r.latestInfo.latest??``}),onClick:r.handleUpgrade},[t[3]||=g(`span`,{class:`upgrade-btn__dot`,"aria-hidden":`true`},null,-1),g(`span`,Q,l(r.$t(`@F13B4:升级`)),1),g(`span`,$,`v`+l(r.latestInfo.latest),1)],8,Z)):p(`v-if`,!0),f(r.UpgradeDialog,{modelValue:r.upgradeDialogVisible,"onUpdate:modelValue":t[0]||=e=>r.upgradeDialogVisible=e,logs:r.upgradeLogs,status:r.upgradeStatus,countdown:r.countdown,onRetry:r.handleUpgrade,onRestart:r.handleRestart,onCancel:r.handleCancelAutoRefresh},null,8,[`modelValue`,`logs`,`status`,`countdown`])])}var te=D(q,[[`render`,ee],[`__scopeId`,`data-v-7e0300ec`],[`__file`,`D:/xz_workspace/github_workspace/zen-gitsync/src/ui/client/src/components/AppVersionBadge.vue`]]);export{te as default};