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.
- package/README.md +2 -2
- package/package.json +3 -1
- package/src/cli/cleanup.js +179 -0
- package/src/cli/cleanup.test.js +269 -0
- package/src/cli/customCommand.js +94 -0
- package/src/cli/customCommand.test.js +143 -0
- package/src/cli/ui.js +59 -0
- package/src/cli/ui.test.js +115 -0
- package/src/config.js +62 -17
- package/src/gitCommit.js +73 -25
- package/src/ui/public/assets/AppVersionBadge-Ci_-H7TK.js +2 -0
- package/src/ui/public/assets/AppVersionBadge-Dj6H729N.css +1 -0
- package/src/ui/public/assets/BranchSelector-BIGlKQme.js +1 -0
- package/src/ui/public/assets/CommandConsole-DUu75Lvw.js +17 -0
- package/src/ui/public/assets/CommandConsole-Da3j09oc.css +1 -0
- package/src/ui/public/assets/CommitForm-asrs-955.js +6 -0
- package/src/ui/public/assets/CustomCommandManager-BBnTU2pp.js +1 -0
- package/src/ui/public/assets/EditorView-BYuHlMbn.js +0 -0
- package/src/ui/public/assets/{EditorView-Do_aRkqm.css → EditorView-BcZ8ToAk.css} +1 -1
- package/src/ui/public/assets/{FileDiffViewer-ASPmnTdG.css → FileDiffViewer-CLvy7mIG.css} +1 -1
- package/src/ui/public/assets/FileDiffViewer-vOvrYF89.js +13 -0
- package/src/ui/public/assets/{FlowExecutionViewer-ADwYz1XL.js → FlowExecutionViewer-DgQs18Ym.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CEM_0XAJ.js +21 -0
- package/src/ui/public/assets/GitStatus-DedZ2a63.js +6 -0
- package/src/ui/public/assets/ImagePreview-XjBK0T26.js +1 -0
- package/src/ui/public/assets/LogList-CUn9TvU1.js +7 -0
- package/src/ui/public/assets/ProjectStartupButton-DWfKVcJo.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-BVRJ8eAD.js +1 -0
- package/src/ui/public/assets/ResetToRemoteButton-CDhmy9Qj.js +1 -0
- package/src/ui/public/assets/SourceMapView-CUq_XXYS.js +3 -0
- package/src/ui/public/assets/SourceMapView-DjYKJZLj.css +1 -0
- package/src/ui/public/assets/TemplateManager-CogQCagT.js +1 -0
- package/src/ui/public/assets/UserInputNode-BcIaDC-e.js +1 -0
- package/src/ui/public/assets/WorkbenchView-CGclOlIQ.js +12 -0
- package/src/ui/public/assets/WorkbenchView-ir769eTr.css +1 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-DKIb-p1R.js → _plugin-vue_export-helper-BWJlNHzL.js} +3 -3
- package/src/ui/public/assets/element-plus-CRTdimvR.js +24 -0
- package/src/ui/public/assets/{flow-mindmap-mZisamFd.js → flow-mindmap-oBAz3_QK.js} +4 -4
- package/src/ui/public/assets/index-BkpNzYyv.js +7 -0
- package/src/ui/public/assets/index-CNhCnXkD.css +1 -0
- package/src/ui/public/assets/vendor-BxotT5qN.js +93 -0
- package/src/ui/public/assets/vue-flow-Bx03URoq.js +39 -0
- package/src/ui/public/index.html +6 -6
- package/src/ui/server/index.js +1 -0
- package/src/ui/server/routes/npm.js +161 -8
- package/src/ui/server/utils/instanceRegistry.js +11 -0
- package/src/ui/server/utils/startServerOnAvailablePort.js +13 -0
- package/src/utils/index.js +42 -10
- package/src/utils/parseCwdArg.test.js +89 -0
- package/src/utils/utils.test.js +200 -0
- package/src/ui/public/assets/AppVersionBadge-CTUXSSX1.css +0 -1
- package/src/ui/public/assets/AppVersionBadge-Dnl4p9oF.js +0 -2
- package/src/ui/public/assets/BranchSelector-C64gm1XB.js +0 -1
- package/src/ui/public/assets/CommandConsole-60KxfUE3.css +0 -1
- package/src/ui/public/assets/CommandConsole-CbufZ-aa.js +0 -17
- package/src/ui/public/assets/CommitForm-CVPUXS7N.js +0 -6
- package/src/ui/public/assets/CustomCommandManager-DYXfu8Gr.js +0 -1
- package/src/ui/public/assets/EditorView-DaLTdDZK.js +0 -0
- package/src/ui/public/assets/FileDiffViewer-BvL-9S74.js +0 -13
- package/src/ui/public/assets/FlowOrchestrationWorkspace-DQhf9Uyr.js +0 -21
- package/src/ui/public/assets/GitStatus-CjNzYRV2.js +0 -6
- package/src/ui/public/assets/ImagePreview-Bo8Y4HdH.js +0 -1
- package/src/ui/public/assets/LogList-vdQwp7e5.js +0 -7
- package/src/ui/public/assets/ProjectStartupButton-CIkYLyt7.js +0 -1
- package/src/ui/public/assets/RemoteRepoCard-CpyLlD_n.js +0 -1
- package/src/ui/public/assets/ResetToRemoteButton-BElORAbP.js +0 -1
- package/src/ui/public/assets/SourceMapView-BS35GVi7.js +0 -3
- package/src/ui/public/assets/SourceMapView-DTsSdcgG.css +0 -1
- package/src/ui/public/assets/TemplateManager-B5s2lBKa.js +0 -1
- package/src/ui/public/assets/UserInputNode-CKoZYrQX.js +0 -1
- package/src/ui/public/assets/WorkbenchView-DKDEzEIV.js +0 -12
- package/src/ui/public/assets/WorkbenchView-MepwSID3.css +0 -1
- package/src/ui/public/assets/__vite-optional-peer-dep_katex_markstream-vue-Bg9-FXxr.js +0 -1
- package/src/ui/public/assets/__vite-optional-peer-dep_mermaid_markstream-vue-cPZl55sA.js +0 -1
- package/src/ui/public/assets/__vite-optional-peer-dep_stream-markdown_markstream-vue-Dhgp-lRL.js +0 -1
- package/src/ui/public/assets/__vite-optional-peer-dep_stream-monaco_markstream-vue-Cr1d6De7.js +0 -1
- package/src/ui/public/assets/d2_markstream-vue-xMoF3M5G.js +0 -1
- package/src/ui/public/assets/element-plus-rcpA4EGF.js +0 -26
- package/src/ui/public/assets/index-BYAxMGne.css +0 -1
- package/src/ui/public/assets/index-DJIlYzzK.js +0 -7
- package/src/ui/public/assets/mhchem_markstream-vue-tp86PqxG.js +0 -1
- package/src/ui/public/assets/vendor-DoD2dmIW.js +0 -226
- 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
|
-
//
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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}`);
|
package/src/utils/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
432
|
-
const truncatedStderr = isStderrTruncated ? 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
|
-
|
|
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};
|