zen-gitsync 2.17.1 → 2.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -1
- package/scripts/release.js +64 -0
- package/src/configSplit.js +708 -0
- package/src/dataDirMigration.js +342 -0
- package/src/fsAtomic.js +129 -0
- package/src/paths.js +79 -0
- package/src/ui/public/assets/{AppVersionBadge-Bw4-tHkL.js → AppVersionBadge-BUNlQAFq.js} +1 -1
- package/src/ui/public/assets/{BranchSelector-BZTHIIaF.js → BranchSelector-DzDm-rlz.js} +1 -1
- package/src/ui/public/assets/{CommitForm-DKedPnu-.js → CommitForm-CMEuo8S6.js} +1 -1
- package/src/ui/public/assets/{EditorView-C5O3ihKq.js → EditorView-CEG66-tP.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-SlVXZt60.js → FlowOrchestrationWorkspace-BuIdjN15.js} +1 -1
- package/src/ui/public/assets/{LogList-DlPJsPy_.js → LogList-DOkB_rda.js} +1 -1
- package/src/ui/public/assets/{MonitorView-GQ34uXPs.js → MonitorView-Bj1w16cJ.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-0UNF9CeP.js → ProjectStartupButton-DvatKLNn.js} +1 -1
- package/src/ui/public/assets/{RemoteManagerDialog-D51q3x7T.js → RemoteManagerDialog-D8x5aqN6.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-Bin9d3GZ.js → RemoteRepoCard-Bjyr6jDv.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-OxlKvnEc.js → SourceMapView-BYexJIGb.js} +1 -1
- package/src/ui/public/assets/{WorkbenchView-DyhIOjGD.js → WorkbenchView-C91Pm0rM.js} +1 -1
- package/src/ui/public/assets/{index-SjRTI8Of.js → index-BeNCRYqM.js} +3 -3
- package/src/ui/public/index.html +1 -1
|
@@ -0,0 +1,342 @@
|
|
|
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
|
+
// 把散落在用户主目录里的历史数据搬进统一数据目录 ~/.zen-gitsync/(见 src/paths.js)。
|
|
16
|
+
//
|
|
17
|
+
// 设计要点:
|
|
18
|
+
// 1. **幂等**:靠 DATA_DIR/.layout-migrated 标记跳过已完成的目录搬迁;主配置那一步
|
|
19
|
+
// 每次启动都跑一次(便宜,且要能应对"用户手动从备份恢复了旧文件")。
|
|
20
|
+
// 2. **永不抛错**:迁移是启动路径上的副作用,失败绝不能挡住应用启动 ——
|
|
21
|
+
// 旧配置读不到就回退到旧路径只读模式(configFallbackPath),下一轮再试。
|
|
22
|
+
// 3. **不直接删**:搬迁期间发现的纯垃圾(空目录、*.tmp 残渣)一律 rename 进
|
|
23
|
+
// DATA_DIR/_legacy-cleanup/<名字>.<时间戳>,留档可回溯,不做不可逆删除。
|
|
24
|
+
// 4. **不抢别人的活**:更早那版单文件心跳注册表(~/.zen-gitsync-instances.json)
|
|
25
|
+
// 的内容迁移归 instanceRegistry.migrateLegacy() 所有,这里只在它是"被误建成的
|
|
26
|
+
// 空目录"时顺手收走垃圾。
|
|
27
|
+
import fs from 'node:fs/promises';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import logger from './ui/server/utils/logger.js';
|
|
30
|
+
import {
|
|
31
|
+
DATA_DIR,
|
|
32
|
+
CONFIG_FILE,
|
|
33
|
+
CONFIG_BACKUP_FILE,
|
|
34
|
+
LEGACY_CONFIG_FILE,
|
|
35
|
+
LEGACY_CONFIG_BACKUP_FILE,
|
|
36
|
+
INSTANCES_DIR,
|
|
37
|
+
LEGACY_INSTANCES_DIR,
|
|
38
|
+
LEGACY_INSTANCES_FILE,
|
|
39
|
+
AI_IMAGES_DIR,
|
|
40
|
+
LEGACY_AI_IMAGES_DIR,
|
|
41
|
+
LEGACY_HOME_DIR,
|
|
42
|
+
LAYOUT_MIGRATION_MARKER,
|
|
43
|
+
MIGRATION_BACKUP_DIR,
|
|
44
|
+
} from './paths.js';
|
|
45
|
+
|
|
46
|
+
const LAYOUT_VERSION = 1;
|
|
47
|
+
|
|
48
|
+
// Windows 上 rename 会撞"目标被占用"(杀毒软件扫描、索引器、并发进程):毫秒级,重试即可
|
|
49
|
+
const BUSY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
50
|
+
async function renameWithRetry(src, dest, attempts = 6) {
|
|
51
|
+
let lastErr;
|
|
52
|
+
for (let i = 0; i < attempts; i++) {
|
|
53
|
+
try {
|
|
54
|
+
await fs.rename(src, dest);
|
|
55
|
+
return;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
lastErr = err;
|
|
58
|
+
if (!BUSY_CODES.has(err?.code)) throw err;
|
|
59
|
+
if (i < attempts - 1) await new Promise((r) => setTimeout(r, 15 * (i + 1)));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw lastErr;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function exists(p) {
|
|
66
|
+
try {
|
|
67
|
+
await fs.access(p);
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 'file' | 'dir' | null(不存在) */
|
|
75
|
+
async function statKind(p) {
|
|
76
|
+
try {
|
|
77
|
+
const st = await fs.lstat(p);
|
|
78
|
+
if (st.isDirectory()) return 'dir';
|
|
79
|
+
if (st.isFile()) return 'file';
|
|
80
|
+
return null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function ensureDir(p) {
|
|
87
|
+
try {
|
|
88
|
+
await fs.mkdir(p, { recursive: true });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (err?.code !== 'EEXIST') throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 移动文件:优先 rename(同盘原子),失败降级为 copy + unlink */
|
|
95
|
+
async function moveFile(src, dest, report) {
|
|
96
|
+
try {
|
|
97
|
+
await renameWithRetry(src, dest);
|
|
98
|
+
return true;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
try {
|
|
101
|
+
await fs.copyFile(src, dest);
|
|
102
|
+
await fs.unlink(src);
|
|
103
|
+
return true;
|
|
104
|
+
} catch (err2) {
|
|
105
|
+
report.errors.push(`搬移失败 ${src} → ${dest}: ${err?.code || err?.message} / ${err2?.code || err2?.message}`);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 把垃圾搬进留档目录(不删)。名字冲突时加时间戳后缀 */
|
|
112
|
+
async function moveAside(target, report) {
|
|
113
|
+
try {
|
|
114
|
+
await ensureDir(MIGRATION_BACKUP_DIR);
|
|
115
|
+
const base = path.basename(target);
|
|
116
|
+
let dest = path.join(MIGRATION_BACKUP_DIR, base);
|
|
117
|
+
if (await exists(dest)) dest = path.join(MIGRATION_BACKUP_DIR, `${base}.${Date.now()}`);
|
|
118
|
+
await renameWithRetry(target, dest);
|
|
119
|
+
report.cleaned.push(`${target} → ${dest}`);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
report.errors.push(`清理失败 ${target}: ${err?.code || err?.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 空目录/空文件才收走;非空则原样保留并记 skipped(可能还装着别人的数据) */
|
|
126
|
+
async function cleanIfEmptyDir(dir, report, note = '') {
|
|
127
|
+
let entries;
|
|
128
|
+
try {
|
|
129
|
+
entries = await fs.readdir(dir);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err?.code === 'ENOENT') return;
|
|
132
|
+
report.errors.push(`读取目录失败 ${dir}: ${err?.code || err?.message}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (entries.length > 0) {
|
|
136
|
+
report.skipped.push(`非空的旧目录,保留不动: ${dir}(${entries.length} 项)${note ? ' ' + note : ''}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
await moveAside(dir, report);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function writeMarker(report) {
|
|
143
|
+
try {
|
|
144
|
+
await fs.writeFile(
|
|
145
|
+
LAYOUT_MIGRATION_MARKER,
|
|
146
|
+
JSON.stringify({ version: LAYOUT_VERSION, at: new Date().toISOString(), report }, null, 2),
|
|
147
|
+
'utf-8'
|
|
148
|
+
);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
report.errors.push(`写迁移标记失败: ${err?.code || err?.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 主配置落位。返回"本次实际应该读写的配置文件路径"。
|
|
156
|
+
* 正常情况下就是 CONFIG_FILE;只有搬迁彻底失败(旧文件被占用/无权限)时才回退旧路径,
|
|
157
|
+
* 保证应用至少还能以只读方式跑起来。
|
|
158
|
+
*/
|
|
159
|
+
async function relocateConfig(report) {
|
|
160
|
+
const legacyKind = await statKind(LEGACY_CONFIG_FILE);
|
|
161
|
+
const targetKind = await statKind(CONFIG_FILE);
|
|
162
|
+
|
|
163
|
+
if (legacyKind === 'file') {
|
|
164
|
+
if (targetKind === 'file') {
|
|
165
|
+
// 两个都在:新路径是权威,旧文件原样留着(可能还有旧版本实例在读它),不删
|
|
166
|
+
report.skipped.push(`新旧配置同时存在,保留旧文件不处理: ${LEGACY_CONFIG_FILE}`);
|
|
167
|
+
} else {
|
|
168
|
+
if (targetKind === 'dir') {
|
|
169
|
+
// 配置路径被一个目录占着(与 .zen-gitsync-instances.json 同类的脏状态):
|
|
170
|
+
// 空的收走留档后继续落位,非空的只能回退(绝不贸然动别人的数据)
|
|
171
|
+
await cleanIfEmptyDir(CONFIG_FILE, report, '(占用配置路径的目录)');
|
|
172
|
+
if (await statKind(CONFIG_FILE) === 'dir') {
|
|
173
|
+
report.skipped.push(`配置路径被非空目录占用,无法落位: ${CONFIG_FILE}`);
|
|
174
|
+
return LEGACY_CONFIG_FILE;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const ok = await moveFile(LEGACY_CONFIG_FILE, CONFIG_FILE, report);
|
|
178
|
+
if (ok) report.moved.push(`${LEGACY_CONFIG_FILE} → ${CONFIG_FILE}`);
|
|
179
|
+
else if (await exists(LEGACY_CONFIG_FILE)) return LEGACY_CONFIG_FILE; // 只读回退
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 旧备份也一并收进来(仅当新备份还没有,避免覆盖用户更近的备份)
|
|
184
|
+
if (await exists(LEGACY_CONFIG_BACKUP_FILE) && !(await exists(CONFIG_BACKUP_FILE))) {
|
|
185
|
+
const ok = await moveFile(LEGACY_CONFIG_BACKUP_FILE, CONFIG_BACKUP_FILE, report);
|
|
186
|
+
if (ok) report.moved.push(`${LEGACY_CONFIG_BACKUP_FILE} → ${CONFIG_BACKUP_FILE}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return CONFIG_FILE;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 心跳注册表目录:逐文件合并(保留新目录里已有的 pid 文件),残渣留档 */
|
|
193
|
+
async function relocateInstances(report) {
|
|
194
|
+
if (!(await exists(LEGACY_INSTANCES_DIR))) return;
|
|
195
|
+
await ensureDir(INSTANCES_DIR);
|
|
196
|
+
|
|
197
|
+
let names = [];
|
|
198
|
+
try {
|
|
199
|
+
names = await fs.readdir(LEGACY_INSTANCES_DIR);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
report.errors.push(`读取旧心跳目录失败: ${err?.code || err?.message}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const name of names) {
|
|
206
|
+
const src = path.join(LEGACY_INSTANCES_DIR, name);
|
|
207
|
+
if (name.endsWith('.tmp')) {
|
|
208
|
+
await moveAside(src, report);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (name === '.migrated') {
|
|
212
|
+
// 心跳注册表自己的迁移标记:新目录里没有就带过去,否则它下次启动会重跑一遍旧文件迁移
|
|
213
|
+
const dest = path.join(INSTANCES_DIR, name);
|
|
214
|
+
if (!(await exists(dest))) await moveFile(src, dest, report);
|
|
215
|
+
else await moveAside(src, report);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const dest = path.join(INSTANCES_DIR, name);
|
|
219
|
+
if (await exists(dest)) {
|
|
220
|
+
report.skipped.push(`新心跳目录已有同名文件,旧文件留档: ${name}`);
|
|
221
|
+
await moveAside(src, report);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const ok = await moveFile(src, dest, report);
|
|
225
|
+
if (ok) report.moved.push(`${src} → ${dest}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await cleanIfEmptyDir(LEGACY_INSTANCES_DIR, report);
|
|
229
|
+
|
|
230
|
+
// 更早的单文件注册表:内容迁移归 instanceRegistry,这里只处理"被误建成目录"的垃圾。
|
|
231
|
+
// 本机就是这么个状态 —— 旧代码在 .zen-gitsync-instances.json 位置留下了空目录,
|
|
232
|
+
// instanceRegistry 对它 readFile 得 EISDIR、unlink 得 EPERM,两头静默失败 →
|
|
233
|
+
// 垃圾一直留着。空目录直接收走(非空则保留,见 cleanIfEmptyDir)。
|
|
234
|
+
if (await statKind(LEGACY_INSTANCES_FILE) === 'dir') {
|
|
235
|
+
await cleanIfEmptyDir(LEGACY_INSTANCES_FILE, report, '(旧单文件注册表被误建成的目录)');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** CLI 贴图目录;顺手把腾空的旧父目录 ~/.git-commit-tool/ 收走 */
|
|
240
|
+
async function relocateAiImages(report) {
|
|
241
|
+
if (await exists(LEGACY_AI_IMAGES_DIR)) {
|
|
242
|
+
await ensureDir(AI_IMAGES_DIR);
|
|
243
|
+
let names = [];
|
|
244
|
+
try {
|
|
245
|
+
names = await fs.readdir(LEGACY_AI_IMAGES_DIR);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
report.errors.push(`读取旧贴图目录失败: ${err?.code || err?.message}`);
|
|
248
|
+
names = [];
|
|
249
|
+
}
|
|
250
|
+
for (const name of names) {
|
|
251
|
+
const src = path.join(LEGACY_AI_IMAGES_DIR, name);
|
|
252
|
+
const dest = path.join(AI_IMAGES_DIR, name);
|
|
253
|
+
if (await exists(dest)) {
|
|
254
|
+
await moveAside(src, report);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const ok = await moveFile(src, dest, report);
|
|
258
|
+
if (ok) report.moved.push(`${src} → ${dest}`);
|
|
259
|
+
}
|
|
260
|
+
await cleanIfEmptyDir(LEGACY_AI_IMAGES_DIR, report);
|
|
261
|
+
}
|
|
262
|
+
// 贴图搬空后,旧的 ~/.git-commit-tool/ 父目录通常也就空了(cleanIfEmptyDir 自带"非空不动"保护)
|
|
263
|
+
if (await exists(LEGACY_HOME_DIR)) {
|
|
264
|
+
await cleanIfEmptyDir(LEGACY_HOME_DIR, report, '(贴图迁走后腾空的旧目录)');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 主目录下散落的、我们写出来的临时文件残渣 */
|
|
269
|
+
async function cleanLegacyTmpResidue(report) {
|
|
270
|
+
const home = path.dirname(LEGACY_CONFIG_FILE);
|
|
271
|
+
let names = [];
|
|
272
|
+
try {
|
|
273
|
+
names = await fs.readdir(home);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
report.errors.push(`扫描主目录失败: ${err?.code || err?.message}`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// 只认我们自己的命名模式,绝不碰用户其它 .tmp 文件
|
|
279
|
+
const mine = names.filter((n) => /^\.git-commit-tool\.json\..*\.tmp$/.test(n) || n === '.zen-gitsync-instances.json.tmp');
|
|
280
|
+
for (const n of mine) await moveAside(path.join(home, n), report);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function emptyReport() {
|
|
284
|
+
return { moved: [], cleaned: [], skipped: [], errors: [], alreadyMigrated: false, configPath: CONFIG_FILE };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 执行(或跳过)数据目录布局迁移。永不抛错。
|
|
289
|
+
* 调用点:server 启动、CLI 入口、以及 src/config.js 首次读配置时(惰性兜底)。
|
|
290
|
+
*/
|
|
291
|
+
export async function migrateDataDir({ force = false } = {}) {
|
|
292
|
+
const report = emptyReport();
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
await ensureDir(DATA_DIR);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
report.errors.push(`创建数据目录失败 ${DATA_DIR}: ${err?.code || err?.message}`);
|
|
298
|
+
return report; // 连目录都建不出来,后面的事都别做了(读配置会各自报错)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 主配置每次校验:便宜(2 次 stat),且能兜住"用户手动恢复了旧文件"
|
|
302
|
+
try {
|
|
303
|
+
report.configPath = await relocateConfig(report);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
report.errors.push(`主配置搬迁异常: ${err?.code || err?.message}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const migratedBefore = !force && (await exists(LAYOUT_MIGRATION_MARKER));
|
|
309
|
+
if (migratedBefore) {
|
|
310
|
+
report.alreadyMigrated = true;
|
|
311
|
+
return report;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
await relocateInstances(report);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
report.errors.push(`心跳目录搬迁异常: ${err?.code || err?.message}`);
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
await relocateAiImages(report);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
report.errors.push(`贴图目录搬迁异常: ${err?.code || err?.message}`);
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
await cleanLegacyTmpResidue(report);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
report.errors.push(`临时文件清理异常: ${err?.code || err?.message}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 有错就不写标记 → 下次启动重试;没错则标记,后续启动只做主配置校验
|
|
331
|
+
if (report.errors.length === 0) await writeMarker(report);
|
|
332
|
+
else logger.warn(`[dataDirMigration] 迁移存在 ${report.errors.length} 个错误,下次启动重试`);
|
|
333
|
+
|
|
334
|
+
if (report.moved.length || report.cleaned.length) {
|
|
335
|
+
logger.info(
|
|
336
|
+
`[dataDirMigration] 已搬移 ${report.moved.length} 项、留档 ${report.cleaned.length} 项到 ${DATA_DIR}`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return report;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export default { migrateDataDir };
|
package/src/fsAtomic.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
// 原子写盘工具:tmp + rename,带 Windows "目标被占用" 重试与降级覆盖。
|
|
16
|
+
//
|
|
17
|
+
// 为什么抽出来(2026-09-18 配置分文件):写入点从 1 个变成
|
|
18
|
+
// 1(config.json) + N(projects/*.json) + M(orchestration/*/*.json)
|
|
19
|
+
// 若各写一份重试逻辑,迟早有一边漏掉 EPERM/EACCES/EBUSY 重试 —— 那正是
|
|
20
|
+
// 之前 "并发读写配置偶发 500" 的成因,不能靠"记得两边都改"来维持。
|
|
21
|
+
import fs from 'node:fs/promises';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import chalk from 'chalk';
|
|
24
|
+
|
|
25
|
+
// Windows 上 rename / 覆盖写会撞上"目标此刻被别的句柄占着"(并发读者、杀毒软件
|
|
26
|
+
// 实时扫描、索引器),libuv 报 EPERM / EACCES / EBUSY。这类占用都是毫秒级的,
|
|
27
|
+
// 重试几次就能穿过,不该立刻降级 —— 实测并发读写时降级路径自己也会因目标被占用
|
|
28
|
+
// 抛 EBUSY,把"慢一点的成功写"变成前端看到的 500。
|
|
29
|
+
export const BUSY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
30
|
+
|
|
31
|
+
export async function retryOnBusy(fn, { attempts = 6, baseDelayMs = 15 } = {}) {
|
|
32
|
+
let lastErr;
|
|
33
|
+
for (let i = 0; i < attempts; i++) {
|
|
34
|
+
try {
|
|
35
|
+
return await fn();
|
|
36
|
+
} catch (err) {
|
|
37
|
+
lastErr = err;
|
|
38
|
+
// 只有"占用类"错误值得重试;ENOENT(源没了)、权限外的错误立刻上抛
|
|
39
|
+
if (!BUSY_CODES.has(err?.code)) throw err;
|
|
40
|
+
if (i < attempts - 1) await new Promise((r) => setTimeout(r, baseDelayMs * (i + 1)));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw lastErr;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// tmp 名带自增序号:同一毫秒内的两次写若共用同一个 tmp,先完成的 rename 会把
|
|
47
|
+
// tmp 搬走,后者的 rename 拿到 ENOENT/EPERM 而被迫降级覆盖写,原子性就丢了。
|
|
48
|
+
// 多进程之间靠 pid 天然不同,进程内靠这个序号。
|
|
49
|
+
let _tmpSeq = 0;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 原子地把 text 写到 filePath。
|
|
53
|
+
*
|
|
54
|
+
* - 先写 `<filePath>.<pid>.<ms>.<seq>.tmp` 再 rename(同目录同盘,rename 是原子的)。
|
|
55
|
+
* - rename 撞"占用"类错误先重试;重试仍失败才降级为直接覆盖写 —— 此时数据完整性
|
|
56
|
+
* 由 writeFile 保证,只是丢了 rename 的原子性(极端情况下可能留 truncate 到一半的
|
|
57
|
+
* 中间态,概率远低于整体写入失败)。
|
|
58
|
+
* - ⚠️ **降级写成功就算成功,不能把 rename 的错误再抛出去**:原实现 throw err,
|
|
59
|
+
* 于是数据其实已经落盘、调用方却收到异常,前端表现为"保存失败 500"。
|
|
60
|
+
* - 失败时清理孤儿 tmp,避免 `.zen-gitsync/` 里堆积 `*.tmp`。
|
|
61
|
+
*/
|
|
62
|
+
export async function atomicWriteText(filePath, text) {
|
|
63
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${++_tmpSeq}.tmp`;
|
|
64
|
+
try {
|
|
65
|
+
await fs.writeFile(tmpPath, text, 'utf-8');
|
|
66
|
+
try {
|
|
67
|
+
await retryOnBusy(() => fs.rename(tmpPath, filePath));
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn(chalk.yellow(
|
|
70
|
+
`[fsAtomic] 原子写降级为覆盖写(${path.basename(filePath)} rename 失败: ${err?.code || err?.message || err})`
|
|
71
|
+
));
|
|
72
|
+
try { await fs.unlink(tmpPath); } catch (_) { /* ignore */ }
|
|
73
|
+
await retryOnBusy(() => fs.writeFile(filePath, text, 'utf-8'));
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
try { await fs.unlink(tmpPath); } catch (_) { /* ignore */ }
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 安全的 unlink:ENOENT 不算错误(目标本来就不在) */
|
|
82
|
+
export async function unlinkIfExists(filePath) {
|
|
83
|
+
try {
|
|
84
|
+
await fs.unlink(filePath);
|
|
85
|
+
return true;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err?.code === 'ENOENT') return false;
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 安全的 mkdir -p。若目标存在但不是目录则抛 ENOTDIR(见下方注释) */
|
|
93
|
+
export async function ensureDir(dirPath) {
|
|
94
|
+
try {
|
|
95
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err?.code !== 'EEXIST') throw err;
|
|
98
|
+
}
|
|
99
|
+
// ⚠️ mkdir 在"目标已存在,但它是个**文件**"时同样报 EEXIST。只吞 EEXIST 就返回,
|
|
100
|
+
// 会让后续所有写入都撞 ENOTDIR —— 错误被推到很远的地方才暴露,而且中间可能已经
|
|
101
|
+
// 做了不可逆的破坏(实测:配置拆分因此把 config.json 里的内联 projects 摘掉了,
|
|
102
|
+
// 而分文件一个都没写成功 = 直接丢数据)。所以这里补一次类型校验,立刻抛。
|
|
103
|
+
const st = await fs.lstat(dirPath).catch(() => null);
|
|
104
|
+
if (!st || !st.isDirectory()) {
|
|
105
|
+
const err = new Error(`ensureDir: ${dirPath} 已存在但不是目录`);
|
|
106
|
+
err.code = 'ENOTDIR';
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 目录是否存在(且是目录) */
|
|
112
|
+
export async function dirExists(dirPath) {
|
|
113
|
+
try {
|
|
114
|
+
return (await fs.lstat(dirPath)).isDirectory();
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 文件是否存在(且是文件) */
|
|
121
|
+
export async function fileExists(filePath) {
|
|
122
|
+
try {
|
|
123
|
+
return (await fs.lstat(filePath)).isFile();
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default { atomicWriteText, retryOnBusy, unlinkIfExists, ensureDir, dirExists, fileExists, BUSY_CODES };
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
// zen-gitsync 全部本地数据路径的**唯一真相源**。
|
|
16
|
+
//
|
|
17
|
+
// 背景(2026-09-18):此前数据散在用户主目录下的三套命名前缀里 ——
|
|
18
|
+
// ~/.git-commit-tool.json 主配置(项目改名前 commit-tool 的遗留名)
|
|
19
|
+
// ~/.git-commit-tool.json.bak 写盘前的全量备份
|
|
20
|
+
// ~/.git-commit-tool/ai-images/ CLI 贴图(注意:又是个不带 .json 的名字)
|
|
21
|
+
// ~/.zen-gitsync-instances/ 实例心跳目录
|
|
22
|
+
// ~/.zen-gitsync-instances.json 心跳的旧版单文件(本机被误建成了目录,见 dataDirMigration)
|
|
23
|
+
// ~/.zen-gitsync/ 工作台数据(唯一收敛好的)
|
|
24
|
+
// 用户视角就是"不知道哪个是哪个的文件",而且同一个路径被 6 个模块各自拼了一遍。
|
|
25
|
+
// 现在统一:所有数据都收进 DATA_DIR,历史路径只保留"一次性迁移 + 只读回退"用途。
|
|
26
|
+
//
|
|
27
|
+
// ⚠️ 这里的常量在模块加载时由 os.homedir() 求值(与原有各模块一致)。
|
|
28
|
+
// 测试靠"import 之前改写 USERPROFILE/HOME"来沙箱隔离,所以不要在调用期重新求值 ——
|
|
29
|
+
// 那会让沙箱逻辑在部分模块失效。
|
|
30
|
+
import os from 'node:os';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
|
|
33
|
+
// 数据根目录:所有持久化数据都在这个目录下,不再往主目录散落文件
|
|
34
|
+
export const DATA_DIR = path.join(os.homedir(), '.zen-gitsync');
|
|
35
|
+
|
|
36
|
+
// ── 主配置 ────────────────────────────────────────────────────
|
|
37
|
+
export const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
|
|
38
|
+
export const CONFIG_BACKUP_FILE = path.join(DATA_DIR, 'config.json.bak');
|
|
39
|
+
// 旧路径(改名前的 g / commit-tool):仅用于一次性迁移与迁移失败时的只读回退
|
|
40
|
+
export const LEGACY_CONFIG_FILE = path.join(os.homedir(), '.git-commit-tool.json');
|
|
41
|
+
export const LEGACY_CONFIG_BACKUP_FILE = path.join(os.homedir(), '.git-commit-tool.json.bak');
|
|
42
|
+
|
|
43
|
+
// ── 配置分文件存储(2026-09-18 第二轮) ─────────────────────────
|
|
44
|
+
// config.json 里的 projects 占全文件 93%(605KB 中的 563KB),而其中 82% 是画布
|
|
45
|
+
// flowData。任何一次琐碎写入(改主题 / 拖布局比例 / 记最近目录)都要重写整份
|
|
46
|
+
// 605KB 并全量复制一份 .bak,于是拆成:
|
|
47
|
+
// config.json 全局(theme/locale/models/ui/recentDirectories)
|
|
48
|
+
// projects/<fileId>.json 单项目配置(不含 orchestrations)
|
|
49
|
+
// orchestration/<fileId>/<orchId>.json 每条画布一个文件
|
|
50
|
+
// 读写仍由 src/config.js + src/configSplit.js 包成"旧的单对象形状",调用点无感。
|
|
51
|
+
export const PROJECTS_DIR = path.join(DATA_DIR, 'projects');
|
|
52
|
+
export const ORCHESTRATION_DIR = path.join(DATA_DIR, 'orchestration');
|
|
53
|
+
// 分文件迁移标记:存在且 config.json 里没有内联 projects 才算已拆分。
|
|
54
|
+
// 两者都要看 —— 用户可能从备份恢复了旧的内联版本,那时需要重新拆一遍。
|
|
55
|
+
export const SPLIT_MIGRATION_MARKER = path.join(DATA_DIR, '.split-migrated');
|
|
56
|
+
|
|
57
|
+
// ── 实例注册表(每进程一个心跳文件) ────────────────────────────
|
|
58
|
+
export const INSTANCES_DIR = path.join(DATA_DIR, 'instances');
|
|
59
|
+
export const LEGACY_INSTANCES_DIR = path.join(os.homedir(), '.zen-gitsync-instances');
|
|
60
|
+
// 更早的单文件注册表。**它的内容迁移归 instanceRegistry.migrateLegacy() 所有**,
|
|
61
|
+
// 这里只登记路径,不参与搬迁。
|
|
62
|
+
export const LEGACY_INSTANCES_FILE = path.join(os.homedir(), '.zen-gitsync-instances.json');
|
|
63
|
+
|
|
64
|
+
// ── CLI 贴图 ─────────────────────────────────────────────────
|
|
65
|
+
export const AI_IMAGES_DIR = path.join(DATA_DIR, 'ai-images');
|
|
66
|
+
// 旧的是 ~/.git-commit-tool/ai-images(父目录名与主配置不同,少了个 .json)
|
|
67
|
+
export const LEGACY_HOME_DIR = path.join(os.homedir(), '.git-commit-tool');
|
|
68
|
+
export const LEGACY_AI_IMAGES_DIR = path.join(LEGACY_HOME_DIR, 'ai-images');
|
|
69
|
+
|
|
70
|
+
// ── 工作台 / 会话 / 缓存 ──────────────────────────────────────
|
|
71
|
+
export const WORKBENCH_IMAGES_DIR = path.join(DATA_DIR, 'workbench-images');
|
|
72
|
+
export const AGENT_SESSIONS_DIR = path.join(DATA_DIR, 'agent-sessions');
|
|
73
|
+
export const SPLIT_SESSIONS_DIR = path.join(DATA_DIR, 'ai-split-sessions');
|
|
74
|
+
export const AI_DIFF_SUMMARIES_DIR = path.join(DATA_DIR, 'ai-diff-summaries');
|
|
75
|
+
export const TMP_DIR = path.join(DATA_DIR, 'tmp');
|
|
76
|
+
|
|
77
|
+
// 布局迁移标记 + 迁移期发现的垃圾的留档目录(不直接删,便于回溯)
|
|
78
|
+
export const LAYOUT_MIGRATION_MARKER = path.join(DATA_DIR, '.layout-migrated');
|
|
79
|
+
export const MIGRATION_BACKUP_DIR = path.join(DATA_DIR, '_legacy-cleanup');
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import"./rolldown-runtime-BM3Ffeng.js";import{Cr as e,Dn as t,En as n,Fn as r,G as i,Gn as a,Hn as o,M as s,Mn as c,Nn as l,On as u,Tn as d,U as f,Wr as p,Yn as m,Yr as h,_r as g,i as _,ir as v,kt as y,nt as b,on as x,or as S,qn as C,r as w,rt as T,wn as E}from"./element-plus-H7kW-grr.js";import{n as D,t as O}from"./_plugin-vue_export-helper-BaSe6lmR.js";async function k(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 A(){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 j(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 k(t,t=>{try{e(JSON.parse(t))}catch{}})}async function M(){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())};k(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 N={key:3},P={key:4},F={key:5},I={key:0,class:`upgrade-hint`},L={key:0,class:`upgrade-countdown`},R={key:1,class:`upgrade-manual`},z={class:`upgrade-manual__label`},B=`npm install -g zen-gitsync --registry https://registry.npmjs.org/`,V=O(r({__name:`UpgradeDialog`,props:{modelValue:{type:Boolean},logs:{},status:{},countdown:{default:0}},emits:[`update:modelValue`,`retry`,`restart`,`cancel`],setup(r,{emit:i}){let a=r,C=i,w=g(null);v(()=>a.logs,async()=>{await o(),w.value&&(w.value.scrollTop=w.value.scrollHeight)});function E(){a.status!==`running`&&(a.status===`success`&&a.countdown>0&&C(`cancel`),C(`update:modelValue`,!1))}function O(){C(`retry`)}function k(){C(`restart`)}function A(){C(`cancel`),C(`update:modelValue`,!1)}async function j(){try{await navigator.clipboard.writeText(B),_.success(D(`@F13B4:已复制升级命令`))}catch{_.warning(D(`@F13B4:复制失败,请手动复制`))}}return(r,i)=>{let a=x,o=f,g=s;return m(),n(g,{"model-value":r.modelValue,"onUpdate:modelValue":i[0]||=e=>C(`update:modelValue`,e),title:e(D)(`@F13B4:升级`),width:`680px`,"close-on-click-modal":!1,"close-on-press-escape":!1,"show-close":r.status!==`running`,"append-to-body":``,onClose:E},{footer:S(()=>[r.status===`failed`?(m(),n(o,{key:0,type:`primary`,onClick:O},{default:S(()=>[c(h(e(D)(`@F13B4:重试`)),1)]),_:1})):t(``,!0),r.status===`failed`?(m(),n(o,{key:1,onClick:j},{default:S(()=>[c(h(e(D)(`@F13B4:复制升级命令`)),1)]),_:1})):t(``,!0),r.status===`success`?(m(),n(o,{key:2,type:`primary`,onClick:k},{default:S(()=>[c(h(e(D)(`@F13B4:立即重启并刷新`)),1)]),_:1})):t(``,!0),r.status===`success`?(m(),n(o,{key:3,onClick:A},{default:S(()=>[c(h(e(D)(`@F13B4:稍后手动重启`)),1)]),_:1})):t(``,!0),r.status===`running`?(m(),n(o,{key:4,disabled:``},{default:S(()=>[c(h(e(D)(`@F13B4:升级中`))+`... `,1)]),_:1})):t(``,!0)]),default:S(()=>[d(`div`,{class:p([`upgrade-status`,`is-${r.status}`])},[r.status===`running`?(m(),n(a,{key:0,class:`is-loading`},{default:S(()=>[l(e(y))]),_:1})):r.status===`success`?(m(),n(a,{key:1,color:`#67c23a`},{default:S(()=>[l(e(b))]),_:1})):(m(),n(a,{key:2,color:`#f56c6c`},{default:S(()=>[l(e(T))]),_:1})),r.status===`running`?(m(),u(`span`,N,h(e(D)(`@F13B4:升级中`)),1)):r.status===`success`?(m(),u(`span`,P,h(e(D)(`@F13B4:升级完成`)),1)):(m(),u(`span`,F,h(e(D)(`@F13B4:升级失败`)),1))],2),r.status===`success`?(m(),u(`p`,I,[c(h(e(D)(`@F13B4:新版本已全局安装,需要重启服务才能生效`))+` `,1),r.countdown>0?(m(),u(`span`,L,h(e(D)(`@F13B4:{seconds} 秒后自动刷新`,{seconds:r.countdown})),1)):t(``,!0)])):t(``,!0),r.status===`failed`?(m(),u(`p`,R,[d(`span`,z,h(e(D)(`@F13B4:手动升级命令`)),1),d(`code`,{class:`upgrade-manual__cmd`},h(B))])):t(``,!0),d(`pre`,{ref_key:`logEl`,ref:w,class:`upgrade-log`},h(r.logs||e(D)(`@F13B4:等待日志输出`)),513)]),_:1},8,[`model-value`,`title`,`show-close`])}}}),[[`__scopeId`,`data-v-40c190d7`]]),H={class:`app-version-badge`},U=[`aria-label`],W=[`aria-label`],G=[`title`,`aria-label`],K={class:`upgrade-btn__label`},q={class:`upgrade-btn__version`},J=`https://github.com/xz333221/zen-gitsync/releases`,Y=5,X=`app-version-check`,Z=3600*1e3,Q=O(r({__name:`AppVersionBadge`,setup(n){let r=`2.17.
|
|
2
|
+
`);i=o.pop()??``;for(let e of o)e.trim()&&t(e)}i.trim()&&t(i)}async function A(){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 j(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 k(t,t=>{try{e(JSON.parse(t))}catch{}})}async function M(){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())};k(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 N={key:3},P={key:4},F={key:5},I={key:0,class:`upgrade-hint`},L={key:0,class:`upgrade-countdown`},R={key:1,class:`upgrade-manual`},z={class:`upgrade-manual__label`},B=`npm install -g zen-gitsync --registry https://registry.npmjs.org/`,V=O(r({__name:`UpgradeDialog`,props:{modelValue:{type:Boolean},logs:{},status:{},countdown:{default:0}},emits:[`update:modelValue`,`retry`,`restart`,`cancel`],setup(r,{emit:i}){let a=r,C=i,w=g(null);v(()=>a.logs,async()=>{await o(),w.value&&(w.value.scrollTop=w.value.scrollHeight)});function E(){a.status!==`running`&&(a.status===`success`&&a.countdown>0&&C(`cancel`),C(`update:modelValue`,!1))}function O(){C(`retry`)}function k(){C(`restart`)}function A(){C(`cancel`),C(`update:modelValue`,!1)}async function j(){try{await navigator.clipboard.writeText(B),_.success(D(`@F13B4:已复制升级命令`))}catch{_.warning(D(`@F13B4:复制失败,请手动复制`))}}return(r,i)=>{let a=x,o=f,g=s;return m(),n(g,{"model-value":r.modelValue,"onUpdate:modelValue":i[0]||=e=>C(`update:modelValue`,e),title:e(D)(`@F13B4:升级`),width:`680px`,"close-on-click-modal":!1,"close-on-press-escape":!1,"show-close":r.status!==`running`,"append-to-body":``,onClose:E},{footer:S(()=>[r.status===`failed`?(m(),n(o,{key:0,type:`primary`,onClick:O},{default:S(()=>[c(h(e(D)(`@F13B4:重试`)),1)]),_:1})):t(``,!0),r.status===`failed`?(m(),n(o,{key:1,onClick:j},{default:S(()=>[c(h(e(D)(`@F13B4:复制升级命令`)),1)]),_:1})):t(``,!0),r.status===`success`?(m(),n(o,{key:2,type:`primary`,onClick:k},{default:S(()=>[c(h(e(D)(`@F13B4:立即重启并刷新`)),1)]),_:1})):t(``,!0),r.status===`success`?(m(),n(o,{key:3,onClick:A},{default:S(()=>[c(h(e(D)(`@F13B4:稍后手动重启`)),1)]),_:1})):t(``,!0),r.status===`running`?(m(),n(o,{key:4,disabled:``},{default:S(()=>[c(h(e(D)(`@F13B4:升级中`))+`... `,1)]),_:1})):t(``,!0)]),default:S(()=>[d(`div`,{class:p([`upgrade-status`,`is-${r.status}`])},[r.status===`running`?(m(),n(a,{key:0,class:`is-loading`},{default:S(()=>[l(e(y))]),_:1})):r.status===`success`?(m(),n(a,{key:1,color:`#67c23a`},{default:S(()=>[l(e(b))]),_:1})):(m(),n(a,{key:2,color:`#f56c6c`},{default:S(()=>[l(e(T))]),_:1})),r.status===`running`?(m(),u(`span`,N,h(e(D)(`@F13B4:升级中`)),1)):r.status===`success`?(m(),u(`span`,P,h(e(D)(`@F13B4:升级完成`)),1)):(m(),u(`span`,F,h(e(D)(`@F13B4:升级失败`)),1))],2),r.status===`success`?(m(),u(`p`,I,[c(h(e(D)(`@F13B4:新版本已全局安装,需要重启服务才能生效`))+` `,1),r.countdown>0?(m(),u(`span`,L,h(e(D)(`@F13B4:{seconds} 秒后自动刷新`,{seconds:r.countdown})),1)):t(``,!0)])):t(``,!0),r.status===`failed`?(m(),u(`p`,R,[d(`span`,z,h(e(D)(`@F13B4:手动升级命令`)),1),d(`code`,{class:`upgrade-manual__cmd`},h(B))])):t(``,!0),d(`pre`,{ref_key:`logEl`,ref:w,class:`upgrade-log`},h(r.logs||e(D)(`@F13B4:等待日志输出`)),513)]),_:1},8,[`model-value`,`title`,`show-close`])}}}),[[`__scopeId`,`data-v-40c190d7`]]),H={class:`app-version-badge`},U=[`aria-label`],W=[`aria-label`],G=[`title`,`aria-label`],K={class:`upgrade-btn__label`},q={class:`upgrade-btn__version`},J=`https://github.com/xz333221/zen-gitsync/releases`,Y=5,X=`app-version-check`,Z=3600*1e3,Q=O(r({__name:`AppVersionBadge`,setup(n){let r=`2.17.2`,o=g(r),s=E(()=>o.value||r),c=g(null),f=g(!1),p=g(``),v=g(`running`),y=g(0),b=null,x=g(!1);function T(){try{let e=sessionStorage.getItem(X);if(!e)return null;let{at:t,data:n}=JSON.parse(e);return Date.now()-t>Z?null:n}catch{return null}}function O(e){try{sessionStorage.setItem(X,JSON.stringify({at:Date.now(),data:e}))}catch{}}async function k(e=!1){let t=T();t&&(c.value=t);try{let t=await A();c.value=t,O(t),t.current&&(o.value=t.current),!e&&t.success&&t.hasUpdate&&_({type:`success`,message:D(`@F13B4:发现新版本 {version}`,{version:t.latest??``}),duration:4e3})}catch(t){e||_({type:`warning`,message:D(`@F13B4:检查更新失败 {error}`,{error:t?.message||``})})}}async function N(){if(c.value?.hasUpdate){try{await w.confirm(D(`@F13B4:确定要从 {current} 升级到 {latest} 吗?`,{current:c.value.current,latest:c.value.latest}),D(`@F13B4:升级`),{confirmButtonText:D(`@F13B4:确定升级`),cancelButtonText:D(`@F13B4:取消`),type:`info`})}catch{return}p.value=``,v.value=`running`,f.value=!0,await j(e=>{if(e.type===`done`){if(v.value=e.code===0?`success`:`failed`,e.code===0){try{sessionStorage.removeItem(X)}catch{}c.value?.latest&&(o.value=c.value.latest),P()}}else e.message&&(p.value+=e.message)})}}function P(){F(),y.value=Y,b=setInterval(()=>{--y.value,y.value<=0&&(b&&=(clearInterval(b),null),L())},1e3)}function F(){b&&=(clearInterval(b),null),y.value=0}function I(){F()}async function L(){if(F(),!x.value){x.value=!0;try{let e=await M(),{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];_({type:`warning`,message:n?D(n):D(`@F13B4:重启失败 {error}`,{error:t})}),x.value=!1}}}return a(()=>{F()}),C(()=>{k(!0)}),(n,r)=>{let a=i;return m(),u(`div`,H,[l(a,{content:e(D)(`@F13B4:在 GitHub 上查看源码`),placement:`top`,effect:`dark`,"show-after":300},{default:S(()=>[d(`a`,{href:`https://github.com/xz333221/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link`,"aria-label":e(D)(`@F13B4:在 GitHub 上查看源码`)},r[1]||=[d(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[d(`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,U)]),_:1},8,[`content`]),l(a,{content:e(D)(`@F13B4:在 npm 上查看包`),placement:`top`,effect:`dark`,"show-after":300},{default:S(()=>[d(`a`,{href:`https://www.npmjs.com/package/zen-gitsync`,target:`_blank`,rel:`noopener noreferrer`,class:`badge-icon-link badge-icon-link--npm`,"aria-label":e(D)(`@F13B4:在 npm 上查看包`)},r[2]||=[d(`svg`,{viewBox:`0 0 24 24`,width:`15`,height:`15`,"aria-hidden":`true`,fill:`currentColor`,xmlns:`http://www.w3.org/2000/svg`},[d(`path`,{d:`M2 2v20h20V2H2zm17.167 17.167H12.5v-10h-3.333v10H4.833V4.833h14.334v14.334z`})],-1)],8,W)]),_:1},8,[`content`]),l(a,{content:e(D)(`@F13B4:查看更新`),placement:`top`,effect:`dark`,"show-after":300},{default:S(()=>[d(`a`,{href:J,target:`_blank`,rel:`noopener noreferrer`,class:`version-link`},`v`+h(s.value),1)]),_:1},8,[`content`]),c.value?.hasUpdate?(m(),u(`button`,{key:0,type:`button`,class:`upgrade-btn`,title:e(D)(`@F13B4:发现新版本 {version},点击升级`,{version:c.value.latest??``}),"aria-label":e(D)(`@F13B4:发现新版本 {version},点击升级`,{version:c.value.latest??``}),onClick:N},[r[3]||=d(`span`,{class:`upgrade-btn__dot`,"aria-hidden":`true`},null,-1),d(`span`,K,h(e(D)(`@F13B4:升级`)),1),d(`span`,q,`v`+h(c.value.latest),1)],8,G)):t(``,!0),l(V,{modelValue:f.value,"onUpdate:modelValue":r[0]||=e=>f.value=e,logs:p.value,status:v.value,countdown:y.value,onRetry:N,onRestart:L,onCancel:I},null,8,[`modelValue`,`logs`,`status`,`countdown`])])}}}),[[`__scopeId`,`data-v-bc7c0d5c`]]);export{Q as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./rolldown-runtime-BM3Ffeng.js";import{Bt as e,Cr as t,Dn as n,E as r,En as i,Fn as a,Lt as o,Mt as ee,Nn as s,On as c,S as te,T as ne,Tn as l,Wr as u,Yn as d,Yr as f,Zn as p,_r as m,bn as h,on as g,or as _,q as re,tt as v,vn as ie,wn as y,x as b,y as x}from"./element-plus-H7kW-grr.js";import{n as S,t as C}from"./_plugin-vue_export-helper-BaSe6lmR.js";import{t as w}from"./SvgIcon-BpKq_phu.js";import{t as ae}from"./CommonDialog-OCekbVQM.js";import{E as oe,w as T}from"./index-
|
|
1
|
+
import"./rolldown-runtime-BM3Ffeng.js";import{Bt as e,Cr as t,Dn as n,E as r,En as i,Fn as a,Lt as o,Mt as ee,Nn as s,On as c,S as te,T as ne,Tn as l,Wr as u,Yn as d,Yr as f,Zn as p,_r as m,bn as h,on as g,or as _,q as re,tt as v,vn as ie,wn as y,x as b,y as x}from"./element-plus-H7kW-grr.js";import{n as S,t as C}from"./_plugin-vue_export-helper-BaSe6lmR.js";import{t as w}from"./SvgIcon-BpKq_phu.js";import{t as ae}from"./CommonDialog-OCekbVQM.js";import{E as oe,w as T}from"./index-BeNCRYqM.js";var E={key:0,class:`branch-info`},D={class:`branch-wrapper`},O={class:`branch-btn`},k={class:`branch-btn-name`},A={class:`branch-btn-caret`},j={class:`branch-popover-inner`},M={class:`branch-search`},N={class:`branch-list`},P=[`onClick`],F={key:1,class:`branch-item-check-placeholder`},I={class:`branch-item-name`},L={key:0,class:`branch-empty`},R={class:`branch-popover-footer`},z={class:`create-branch-content`},B={class:`form-label`},se={class:`form-label`},V={class:`dialog-footer`},H={class:`footer-actions`},U=[`disabled`],W=C(a({__name:`BranchSelector`,emits:[`branchChanged`],setup(a,{emit:C}){let W=T(),G=C,K=m(!1),q=m(``),J=y(()=>{let e=q.value.trim().toLowerCase();return e?W.allBranches.filter(t=>t.toLowerCase().includes(e)):W.allBranches});async function ce(e){if(e===W.currentBranch){K.value=!1;return}K.value=!1,await W.changeBranch(e)&&(G(`branchChanged`),W.refreshLog())}let Y=m(!1),X=m(``),Z=m(``);function le(){K.value=!1,Z.value=W.currentBranch,Y.value=!0}async function Q(){await W.createBranch(X.value,Z.value)&&(Y.value=!1,X.value=``,G(`branchChanged`))}let $=m(!1);async function ue(){if(!$.value){$.value=!0;try{await Promise.all([W.getCurrentBranch(!0),W.getBranchStatus(!0),W.getAllBranches()]),G(`branchChanged`),W.refreshLog()}finally{$.value=!1}}}return(a,m)=>{let y=g,C=re,T=x,G=r,de=b,fe=te,pe=ne;return t(W).currentBranch?(d(),c(`div`,E,[l(`div`,D,[s(T,{visible:K.value,"onUpdate:visible":m[1]||=e=>K.value=e,placement:`top-start`,width:260,trigger:`click`,"popper-class":`branch-popover`,"show-arrow":!1,onShow:m[2]||=e=>q.value=``},{reference:_(()=>[l(`button`,O,[s(w,{"icon-class":`git-branch`,"class-name":`branch-btn-icon`}),l(`span`,k,f(t(W).currentBranch),1),l(`span`,A,[s(y,null,{default:_(()=>m[7]||=[l(`svg`,{viewBox:`0 0 1024 1024`,xmlns:`http://www.w3.org/2000/svg`},[l(`path`,{fill:`currentColor`,d:`M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.776a29.12 29.12 0 0 0 0-41.6 30.592 30.592 0 0 0-42.752 0z`})],-1)]),_:1})])])]),default:_(()=>[l(`div`,j,[l(`div`,M,[s(C,{modelValue:q.value,"onUpdate:modelValue":m[0]||=e=>q.value=e,placeholder:t(S)(`@F13B4:搜索或签出分支`),size:`small`,clearable:``,autofocus:``},null,8,[`modelValue`,`placeholder`])]),l(`div`,N,[(d(!0),c(h,null,p(J.value,e=>(d(),c(`div`,{key:e,class:u([`branch-item`,{"is-active":e===t(W).currentBranch}]),onClick:t=>ce(e)},[e===t(W).currentBranch?(d(),i(y,{key:0,class:`branch-item-check`},{default:_(()=>[s(t(v))]),_:1})):(d(),c(`span`,F)),s(w,{"icon-class":`git-branch`,"class-name":`branch-item-icon`}),l(`span`,I,f(e),1)],10,P))),128)),J.value.length===0?(d(),c(`div`,L,f(t(S)(`@F13B4:没有匹配的分支`)),1)):n(``,!0)]),l(`div`,R,[l(`button`,{class:`branch-create-btn`,onClick:le},[s(y,null,{default:_(()=>[s(t(o))]),_:1}),l(`span`,null,f(t(S)(`@F13B4:创建新分支`)),1)])])])]),_:1},8,[`visible`]),s(oe,{tooltip:t(S)(`@CMDCON:刷新`),onClick:ue},{default:_(()=>[s(y,{class:u({"is-loading":$.value})},{default:_(()=>[s(t(e))]),_:1},8,[`class`])]),_:1},8,[`tooltip`])]),s(ae,{modelValue:Y.value,"onUpdate:modelValue":m[6]||=e=>Y.value=e,title:t(S)(`@F13B4:创建新分支`),size:`small`,"destroy-on-close":!0,"append-to-body":!0,"custom-class":`create-branch-dialog`},{footer:_(()=>[l(`div`,V,[l(`div`,H,[l(`button`,{type:`button`,class:`dialog-cancel-btn`,onClick:m[5]||=e=>Y.value=!1},f(t(S)(`@F13B4:取消`)),1),l(`button`,{type:`button`,class:`dialog-confirm-btn`,onClick:Q,disabled:t(W).isCreatingBranch},[t(W).isCreatingBranch?(d(),i(y,{key:1,class:`is-loading`},{default:_(()=>m[8]||=[l(`svg`,{viewBox:`0 0 1024 1024`,xmlns:`http://www.w3.org/2000/svg`},[l(`path`,{fill:`currentColor`,d:`M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32zm0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32zm448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32zm-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32z`})],-1)]),_:1})):(d(),i(y,{key:0},{default:_(()=>[s(t(v))]),_:1})),l(`span`,null,f(t(S)(`@F13B4:创建`)),1)],8,U)])])]),default:_(()=>[l(`div`,z,[s(pe,{model:{newBranchName:X.value,selectedBaseBranch:Z.value},"label-position":`top`},{default:_(()=>[s(G,null,{label:_(()=>[l(`div`,B,[s(y,{class:`label-icon`},{default:_(()=>[s(t(o))]),_:1}),l(`span`,null,f(t(S)(`@F13B4:新分支名称`)),1)])]),default:_(()=>[s(C,{modelValue:X.value,"onUpdate:modelValue":m[3]||=e=>X.value=e,placeholder:t(S)(`@F13B4:请输入新分支名称`),class:`modern-input`,size:`large`,onKeyup:ie(Q,[`enter`])},null,8,[`modelValue`,`placeholder`])]),_:1}),s(G,null,{label:_(()=>[l(`div`,se,[s(y,{class:`label-icon`},{default:_(()=>[s(t(ee))]),_:1}),l(`span`,null,f(t(S)(`@F13B4:基于分支`)),1)])]),default:_(()=>[s(fe,{modelValue:Z.value,"onUpdate:modelValue":m[4]||=e=>Z.value=e,placeholder:t(S)(`@F13B4:选择基础分支`),class:`modern-select`,size:`large`,style:{width:`100%`}},{default:_(()=>[(d(!0),c(h,null,p(t(W).allBranches,e=>(d(),i(de,{key:e,label:e,value:e},null,8,[`label`,`value`]))),128))]),_:1},8,[`modelValue`,`placeholder`])]),_:1})]),_:1},8,[`model`])])]),_:1},8,[`modelValue`,`title`])])):n(``,!0)}}}),[[`__scopeId`,`data-v-665171bc`]]);export{W as default};
|