zen-gitsync 2.11.39 → 2.12.3

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/LICENSE +190 -21
  2. package/README.md +695 -695
  3. package/index.js +25 -11
  4. package/package.json +2 -2
  5. package/scripts/convert-colors-to-vars.cjs +286 -272
  6. package/scripts/convert-fontsize-to-vars.cjs +221 -207
  7. package/scripts/convert-spacing-to-vars.cjs +256 -242
  8. package/scripts/convert-to-standard-vars.cjs +282 -268
  9. package/scripts/release.js +599 -585
  10. package/src/config.js +350 -336
  11. package/src/gitCommit.js +455 -440
  12. package/src/ui/public/assets/EditorView-CbqSI9nw.css +1 -0
  13. package/src/ui/public/assets/EditorView-GS5cmh99.js +21 -0
  14. package/src/ui/public/assets/SourceMapView-DyMK80hS.css +1 -0
  15. package/src/ui/public/assets/SourceMapView-_YRtzmZZ.js +3 -0
  16. package/src/ui/public/assets/index-ML5Y-5lO.css +1 -0
  17. package/src/ui/public/assets/index-yky0Sd13.js +73 -0
  18. package/src/ui/public/assets/{ts.worker-Dth06zuC.js → ts.worker-METxwbDZ.js} +1 -16
  19. package/src/ui/public/assets/{vendor-B1T2uxYO.js → vendor-DITsiaGj.js} +294 -287
  20. package/src/ui/public/assets/vendor-q83wvJns.css +1 -0
  21. package/src/ui/public/index.html +4 -4
  22. package/src/ui/server/.claude/codediff.txt +6 -0
  23. package/src/ui/server/index.js +410 -396
  24. package/src/ui/server/middleware/requestLogger.js +51 -37
  25. package/src/ui/server/routes/branchStatus.js +101 -87
  26. package/src/ui/server/routes/code.js +110 -96
  27. package/src/ui/server/routes/codeAnalysis.js +995 -981
  28. package/src/ui/server/routes/config.js +1172 -1158
  29. package/src/ui/server/routes/exec.js +272 -258
  30. package/src/ui/server/routes/fileOpen.js +279 -265
  31. package/src/ui/server/routes/fs.js +701 -699
  32. package/src/ui/server/routes/git/diff.js +352 -338
  33. package/src/ui/server/routes/git/diffUtils.js +128 -114
  34. package/src/ui/server/routes/git/stash.js +552 -538
  35. package/src/ui/server/routes/git/tags.js +172 -158
  36. package/src/ui/server/routes/git.js +190 -176
  37. package/src/ui/server/routes/gitOps.js +1179 -1165
  38. package/src/ui/server/routes/instances.js +38 -24
  39. package/src/ui/server/routes/npm.js +1023 -1009
  40. package/src/ui/server/routes/process.js +82 -68
  41. package/src/ui/server/routes/status.js +67 -53
  42. package/src/ui/server/routes/terminal.js +319 -305
  43. package/src/ui/server/socket/registerUiSocketHandlers.js +226 -212
  44. package/src/ui/server/utils/createSavePortToFile.js +46 -32
  45. package/src/ui/server/utils/instanceRegistry.js +270 -256
  46. package/src/ui/server/utils/pathGuard.js +155 -0
  47. package/src/ui/server/utils/pathGuard.test.js +138 -0
  48. package/src/ui/server/utils/randomStartPort.js +51 -37
  49. package/src/ui/server/utils/startServerOnAvailablePort.js +101 -87
  50. package/src/utils/index.js +1058 -1044
  51. package/src/ui/public/assets/devopicons-QN4QXivI.woff2 +0 -0
  52. package/src/ui/public/assets/file-icons-C0jOugUK.woff2 +0 -0
  53. package/src/ui/public/assets/fontawesome-B-jkhYfk.woff2 +0 -0
  54. package/src/ui/public/assets/index-BvVl-092.js +0 -95
  55. package/src/ui/public/assets/index-DXO3Lvqi.css +0 -1
  56. package/src/ui/public/assets/mfixx-CpAhKOZz.woff2 +0 -0
  57. package/src/ui/public/assets/octicons-CaZ_fok2.woff2 +0 -0
  58. package/src/ui/public/assets/vendor-hOO_r_AU.css +0 -1
@@ -1,256 +1,270 @@
1
- // 实例注册表工具
2
- // 维护 ~/.zen-gitsync-instances.json,记录所有正在运行的 GUI 实例
3
- // 多进程并发写采用 atomic temp+rename + 进程内串行化 Promise
4
- // stale 判定:PID 不存在 lastHeartbeat 超过阈值
5
-
6
- const STALE_MS = 30_000; // 心跳超时阈值(毫秒)
7
- const WATCH_DEBOUNCE_MS = 100; // fs.watch 防抖时间
8
- const REGISTRY_VERSION = 1;
9
-
10
- function isProcessAlive(pid) {
11
- if (!Number.isInteger(pid) || pid <= 0) return false;
12
- try {
13
- // 信号 0 仅做存活检查,不真正发送信号
14
- process.kill(pid, 0);
15
- return true;
16
- } catch (err) {
17
- if (err && (err.code === 'ESRCH' || err.code === 'ENOENT')) {
18
- return false;
19
- }
20
- // EPERM 等情况:进程存在但权限不足,按 alive 处理
21
- return true;
22
- }
23
- }
24
-
25
- // 解析项目名:优先 package.json.name,兜底为目录 basename
26
- async function resolveProjectName(projectPath, fsMod, pathMod) {
27
- if (!projectPath) return '';
28
- try {
29
- const pkgPath = pathMod.join(projectPath, 'package.json');
30
- const raw = await fsMod.readFile(pkgPath, 'utf-8');
31
- const pkg = JSON.parse(raw);
32
- if (pkg && typeof pkg.name === 'string' && pkg.name.trim()) {
33
- return pkg.name.trim();
34
- }
35
- } catch (_) {
36
- // 读失败或解析失败,兜底
37
- }
38
- return pathMod.basename(projectPath);
39
- }
40
-
41
- export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, registryPath }) {
42
- if (!fsMod || !pathMod || !osMod || !registryPath) {
43
- throw new Error('createInstanceRegistry: 必须提供 fs/path/os/registryPath');
44
- }
45
-
46
- // 进程内写串行化:所有 mutate 操作都 await 这条链
47
- let writeChain = Promise.resolve();
48
-
49
- function enqueueWrite(task) {
50
- const next = writeChain.then(task, task);
51
- // 不让单个失败阻塞后续操作
52
- writeChain = next.catch(() => {});
53
- return next;
54
- }
55
-
56
- async function readAll() {
57
- try {
58
- const raw = await fsMod.readFile(registryPath, 'utf-8');
59
- const parsed = JSON.parse(raw);
60
- if (parsed && typeof parsed === 'object' && parsed.instances && typeof parsed.instances === 'object') {
61
- return parsed;
62
- }
63
- return { version: REGISTRY_VERSION, instances: {} };
64
- } catch (err) {
65
- if (err && err.code === 'ENOENT') {
66
- return { version: REGISTRY_VERSION, instances: {} };
67
- }
68
- console.warn(`[instanceRegistry] 读取注册表失败,按空表处理: ${err?.message || err}`);
69
- return { version: REGISTRY_VERSION, instances: {} };
70
- }
71
- }
72
-
73
- async function writeAll(obj) {
74
- const payload = {
75
- version: REGISTRY_VERSION,
76
- ...obj,
77
- instances: obj.instances || {}
78
- };
79
- const tmpPath = `${registryPath}.tmp`;
80
- await fsMod.writeFile(tmpPath, JSON.stringify(payload, null, 2), 'utf-8');
81
- await fsMod.rename(tmpPath, registryPath);
82
- }
83
-
84
- // 同步裁剪:传入当前内存中的 instances 字典,返回裁剪后的新字典
85
- function pruneInPlace(instances) {
86
- const now = Date.now();
87
- const result = {};
88
- for (const [pidStr, entry] of Object.entries(instances)) {
89
- if (!entry || typeof entry !== 'object') continue;
90
- const pid = Number(entry.pid ?? Number(pidStr));
91
- if (!isProcessAlive(pid)) continue;
92
- if (typeof entry.lastHeartbeat === 'number' && now - entry.lastHeartbeat > STALE_MS) continue;
93
- result[pidStr] = entry;
94
- }
95
- return result;
96
- }
97
-
98
- // 公开 API
99
- async function register({ pid, port, projectPath, projectName, hostname } = {}) {
100
- if (!Number.isInteger(pid) || pid <= 0) throw new Error('register: pid 必填');
101
- if (!Number.isInteger(port) || port <= 0) throw new Error('register: port 必填');
102
- if (!projectPath) throw new Error('register: projectPath 必填');
103
-
104
- const resolvedName = projectName && String(projectName).trim()
105
- ? String(projectName).trim()
106
- : await resolveProjectName(projectPath, fsMod, pathMod);
107
-
108
- const entry = {
109
- pid,
110
- port,
111
- projectName: resolvedName,
112
- projectPath,
113
- startedAt: Date.now(),
114
- lastHeartbeat: Date.now(),
115
- hostname: hostname || osMod.hostname()
116
- };
117
-
118
- await enqueueWrite(async () => {
119
- const obj = await readAll();
120
- obj.instances[String(pid)] = entry;
121
- await writeAll(obj);
122
- });
123
- return entry;
124
- }
125
-
126
- async function unregister(pid) {
127
- if (!Number.isInteger(pid) || pid <= 0) return;
128
- await enqueueWrite(async () => {
129
- const obj = await readAll();
130
- if (obj.instances && obj.instances[String(pid)]) {
131
- delete obj.instances[String(pid)];
132
- await writeAll(obj);
133
- }
134
- });
135
- }
136
-
137
- async function heartbeat(pid, updates = {}) {
138
- if (!Number.isInteger(pid) || pid <= 0) return;
139
- await enqueueWrite(async () => {
140
- const obj = await readAll();
141
- const key = String(pid);
142
- const existing = obj.instances[key];
143
- if (!existing) {
144
- // 如果心跳时条目不存在(被裁剪或被外部清理),跳过;
145
- // 由调用方负责周期性 re-register
146
- return;
147
- }
148
- obj.instances[key] = {
149
- ...existing,
150
- ...(updates.projectPath ? { projectPath: updates.projectPath } : {}),
151
- ...(updates.projectName ? { projectName: updates.projectName } : {}),
152
- lastHeartbeat: Date.now()
153
- };
154
- await writeAll(obj);
155
- });
156
- }
157
-
158
- async function list({ pruneStale = true } = {}) {
159
- const obj = await readAll();
160
- let instances = obj.instances || {};
161
- if (pruneStale) {
162
- instances = pruneInPlace(instances);
163
- // 如果发生了裁剪,持久化回去
164
- const hasChange = Object.keys(instances).length !== Object.keys(obj.instances || {}).length;
165
- if (hasChange) {
166
- await enqueueWrite(async () => {
167
- const fresh = await readAll();
168
- fresh.instances = pruneInPlace(fresh.instances || {});
169
- await writeAll(fresh);
170
- });
171
- }
172
- }
173
- const arr = Object.values(instances);
174
- arr.sort((a, b) => (a.port || 0) - (b.port || 0));
175
- return arr;
176
- }
177
-
178
- // 监听注册表文件变化;callback 会在 debounce 后被调用,参数是最新 list
179
- // fsWatch 参数:node 'fs' 模块的 watch 函数(同步 + EventEmitter 形式)
180
- function watch(callback, fsWatch) {
181
- if (typeof callback !== 'function') {
182
- throw new Error('watch: callback 必填');
183
- }
184
- if (typeof fsWatch !== 'function') {
185
- console.warn('[instanceRegistry] 未提供 fs.watch,跨进程推送将不可用');
186
- return function noop() {};
187
- }
188
- let debounceTimer = null;
189
- let watcher = null;
190
- let pruneTimer = null;
191
- let closed = false;
192
-
193
- const fire = async () => {
194
- if (closed) return;
195
- try {
196
- const fresh = await list({ pruneStale: true });
197
- callback(fresh);
198
- } catch (e) {
199
- console.warn(`[instanceRegistry] watch 回调失败: ${e?.message || e}`);
200
- }
201
- };
202
-
203
- // 周期性 prune:即便没有其他进程写入注册表,也定期清理本地失效条目
204
- // (例如所有 server 都强 kill 后,文件里残留的僵尸条目会被自动清理)
205
- pruneTimer = setInterval(() => {
206
- if (closed) return;
207
- list({ pruneStale: true }).catch(() => {});
208
- }, STALE_MS / 3);
209
-
210
- try {
211
- watcher = fsWatch(registryPath, { persistent: false }, () => {
212
- if (closed) return;
213
- if (debounceTimer) clearTimeout(debounceTimer);
214
- debounceTimer = setTimeout(fire, WATCH_DEBOUNCE_MS);
215
- });
216
- if (watcher && typeof watcher.on === 'function') {
217
- watcher.on('error', (err) => {
218
- console.warn(`[instanceRegistry] fs.watch 出错: ${err?.message || err}`);
219
- });
220
- }
221
- } catch (err) {
222
- console.warn(`[instanceRegistry] 无法启动 fs.watch (${err?.message || err}),跨进程推送将不可用,请依赖轮询`);
223
- }
224
-
225
- return function closeWatcher() {
226
- closed = true;
227
- if (debounceTimer) {
228
- clearTimeout(debounceTimer);
229
- debounceTimer = null;
230
- }
231
- if (pruneTimer) {
232
- clearInterval(pruneTimer);
233
- pruneTimer = null;
234
- }
235
- if (watcher) {
236
- try { watcher.close(); } catch (_) {}
237
- watcher = null;
238
- }
239
- };
240
- }
241
-
242
- function close() {
243
- // 工厂内部无 timer,仅为 API 对称
244
- }
245
-
246
- return {
247
- register,
248
- unregister,
249
- heartbeat,
250
- list,
251
- watch,
252
- close,
253
- _resolveProjectName: (p) => resolveProjectName(p, fsMod, pathMod),
254
- _STALE_MS: STALE_MS
255
- };
256
- }
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
+ // 实例注册表工具
16
+ // 维护 ~/.zen-gitsync-instances.json,记录所有正在运行的 GUI 实例
17
+ // 多进程并发写采用 atomic temp+rename + 进程内串行化 Promise
18
+ // stale 判定:PID 不存在 或 lastHeartbeat 超过阈值
19
+
20
+ const STALE_MS = 30_000; // 心跳超时阈值(毫秒)
21
+ const WATCH_DEBOUNCE_MS = 100; // fs.watch 防抖时间
22
+ const REGISTRY_VERSION = 1;
23
+
24
+ function isProcessAlive(pid) {
25
+ if (!Number.isInteger(pid) || pid <= 0) return false;
26
+ try {
27
+ // 信号 0 仅做存活检查,不真正发送信号
28
+ process.kill(pid, 0);
29
+ return true;
30
+ } catch (err) {
31
+ if (err && (err.code === 'ESRCH' || err.code === 'ENOENT')) {
32
+ return false;
33
+ }
34
+ // EPERM 等情况:进程存在但权限不足,按 alive 处理
35
+ return true;
36
+ }
37
+ }
38
+
39
+ // 解析项目名:优先 package.json.name,兜底为目录 basename
40
+ async function resolveProjectName(projectPath, fsMod, pathMod) {
41
+ if (!projectPath) return '';
42
+ try {
43
+ const pkgPath = pathMod.join(projectPath, 'package.json');
44
+ const raw = await fsMod.readFile(pkgPath, 'utf-8');
45
+ const pkg = JSON.parse(raw);
46
+ if (pkg && typeof pkg.name === 'string' && pkg.name.trim()) {
47
+ return pkg.name.trim();
48
+ }
49
+ } catch (_) {
50
+ // 读失败或解析失败,兜底
51
+ }
52
+ return pathMod.basename(projectPath);
53
+ }
54
+
55
+ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, registryPath }) {
56
+ if (!fsMod || !pathMod || !osMod || !registryPath) {
57
+ throw new Error('createInstanceRegistry: 必须提供 fs/path/os/registryPath');
58
+ }
59
+
60
+ // 进程内写串行化:所有 mutate 操作都 await 这条链
61
+ let writeChain = Promise.resolve();
62
+
63
+ function enqueueWrite(task) {
64
+ const next = writeChain.then(task, task);
65
+ // 不让单个失败阻塞后续操作
66
+ writeChain = next.catch(() => {});
67
+ return next;
68
+ }
69
+
70
+ async function readAll() {
71
+ try {
72
+ const raw = await fsMod.readFile(registryPath, 'utf-8');
73
+ const parsed = JSON.parse(raw);
74
+ if (parsed && typeof parsed === 'object' && parsed.instances && typeof parsed.instances === 'object') {
75
+ return parsed;
76
+ }
77
+ return { version: REGISTRY_VERSION, instances: {} };
78
+ } catch (err) {
79
+ if (err && err.code === 'ENOENT') {
80
+ return { version: REGISTRY_VERSION, instances: {} };
81
+ }
82
+ console.warn(`[instanceRegistry] 读取注册表失败,按空表处理: ${err?.message || err}`);
83
+ return { version: REGISTRY_VERSION, instances: {} };
84
+ }
85
+ }
86
+
87
+ async function writeAll(obj) {
88
+ const payload = {
89
+ version: REGISTRY_VERSION,
90
+ ...obj,
91
+ instances: obj.instances || {}
92
+ };
93
+ const tmpPath = `${registryPath}.tmp`;
94
+ await fsMod.writeFile(tmpPath, JSON.stringify(payload, null, 2), 'utf-8');
95
+ await fsMod.rename(tmpPath, registryPath);
96
+ }
97
+
98
+ // 同步裁剪:传入当前内存中的 instances 字典,返回裁剪后的新字典
99
+ function pruneInPlace(instances) {
100
+ const now = Date.now();
101
+ const result = {};
102
+ for (const [pidStr, entry] of Object.entries(instances)) {
103
+ if (!entry || typeof entry !== 'object') continue;
104
+ const pid = Number(entry.pid ?? Number(pidStr));
105
+ if (!isProcessAlive(pid)) continue;
106
+ if (typeof entry.lastHeartbeat === 'number' && now - entry.lastHeartbeat > STALE_MS) continue;
107
+ result[pidStr] = entry;
108
+ }
109
+ return result;
110
+ }
111
+
112
+ // 公开 API
113
+ async function register({ pid, port, projectPath, projectName, hostname } = {}) {
114
+ if (!Number.isInteger(pid) || pid <= 0) throw new Error('register: pid 必填');
115
+ if (!Number.isInteger(port) || port <= 0) throw new Error('register: port 必填');
116
+ if (!projectPath) throw new Error('register: projectPath 必填');
117
+
118
+ const resolvedName = projectName && String(projectName).trim()
119
+ ? String(projectName).trim()
120
+ : await resolveProjectName(projectPath, fsMod, pathMod);
121
+
122
+ const entry = {
123
+ pid,
124
+ port,
125
+ projectName: resolvedName,
126
+ projectPath,
127
+ startedAt: Date.now(),
128
+ lastHeartbeat: Date.now(),
129
+ hostname: hostname || osMod.hostname()
130
+ };
131
+
132
+ await enqueueWrite(async () => {
133
+ const obj = await readAll();
134
+ obj.instances[String(pid)] = entry;
135
+ await writeAll(obj);
136
+ });
137
+ return entry;
138
+ }
139
+
140
+ async function unregister(pid) {
141
+ if (!Number.isInteger(pid) || pid <= 0) return;
142
+ await enqueueWrite(async () => {
143
+ const obj = await readAll();
144
+ if (obj.instances && obj.instances[String(pid)]) {
145
+ delete obj.instances[String(pid)];
146
+ await writeAll(obj);
147
+ }
148
+ });
149
+ }
150
+
151
+ async function heartbeat(pid, updates = {}) {
152
+ if (!Number.isInteger(pid) || pid <= 0) return;
153
+ await enqueueWrite(async () => {
154
+ const obj = await readAll();
155
+ const key = String(pid);
156
+ const existing = obj.instances[key];
157
+ if (!existing) {
158
+ // 如果心跳时条目不存在(被裁剪或被外部清理),跳过;
159
+ // 由调用方负责周期性 re-register
160
+ return;
161
+ }
162
+ obj.instances[key] = {
163
+ ...existing,
164
+ ...(updates.projectPath ? { projectPath: updates.projectPath } : {}),
165
+ ...(updates.projectName ? { projectName: updates.projectName } : {}),
166
+ lastHeartbeat: Date.now()
167
+ };
168
+ await writeAll(obj);
169
+ });
170
+ }
171
+
172
+ async function list({ pruneStale = true } = {}) {
173
+ const obj = await readAll();
174
+ let instances = obj.instances || {};
175
+ if (pruneStale) {
176
+ instances = pruneInPlace(instances);
177
+ // 如果发生了裁剪,持久化回去
178
+ const hasChange = Object.keys(instances).length !== Object.keys(obj.instances || {}).length;
179
+ if (hasChange) {
180
+ await enqueueWrite(async () => {
181
+ const fresh = await readAll();
182
+ fresh.instances = pruneInPlace(fresh.instances || {});
183
+ await writeAll(fresh);
184
+ });
185
+ }
186
+ }
187
+ const arr = Object.values(instances);
188
+ arr.sort((a, b) => (a.port || 0) - (b.port || 0));
189
+ return arr;
190
+ }
191
+
192
+ // 监听注册表文件变化;callback 会在 debounce 后被调用,参数是最新 list
193
+ // fsWatch 参数:node 'fs' 模块的 watch 函数(同步 + EventEmitter 形式)
194
+ function watch(callback, fsWatch) {
195
+ if (typeof callback !== 'function') {
196
+ throw new Error('watch: callback 必填');
197
+ }
198
+ if (typeof fsWatch !== 'function') {
199
+ console.warn('[instanceRegistry] 未提供 fs.watch,跨进程推送将不可用');
200
+ return function noop() {};
201
+ }
202
+ let debounceTimer = null;
203
+ let watcher = null;
204
+ let pruneTimer = null;
205
+ let closed = false;
206
+
207
+ const fire = async () => {
208
+ if (closed) return;
209
+ try {
210
+ const fresh = await list({ pruneStale: true });
211
+ callback(fresh);
212
+ } catch (e) {
213
+ console.warn(`[instanceRegistry] watch 回调失败: ${e?.message || e}`);
214
+ }
215
+ };
216
+
217
+ // 周期性 prune:即便没有其他进程写入注册表,也定期清理本地失效条目
218
+ // (例如所有 server 都强 kill 后,文件里残留的僵尸条目会被自动清理)
219
+ pruneTimer = setInterval(() => {
220
+ if (closed) return;
221
+ list({ pruneStale: true }).catch(() => {});
222
+ }, STALE_MS / 3);
223
+
224
+ try {
225
+ watcher = fsWatch(registryPath, { persistent: false }, () => {
226
+ if (closed) return;
227
+ if (debounceTimer) clearTimeout(debounceTimer);
228
+ debounceTimer = setTimeout(fire, WATCH_DEBOUNCE_MS);
229
+ });
230
+ if (watcher && typeof watcher.on === 'function') {
231
+ watcher.on('error', (err) => {
232
+ console.warn(`[instanceRegistry] fs.watch 出错: ${err?.message || err}`);
233
+ });
234
+ }
235
+ } catch (err) {
236
+ console.warn(`[instanceRegistry] 无法启动 fs.watch (${err?.message || err}),跨进程推送将不可用,请依赖轮询`);
237
+ }
238
+
239
+ return function closeWatcher() {
240
+ closed = true;
241
+ if (debounceTimer) {
242
+ clearTimeout(debounceTimer);
243
+ debounceTimer = null;
244
+ }
245
+ if (pruneTimer) {
246
+ clearInterval(pruneTimer);
247
+ pruneTimer = null;
248
+ }
249
+ if (watcher) {
250
+ try { watcher.close(); } catch (_) {}
251
+ watcher = null;
252
+ }
253
+ };
254
+ }
255
+
256
+ function close() {
257
+ // 工厂内部无 timer,仅为 API 对称
258
+ }
259
+
260
+ return {
261
+ register,
262
+ unregister,
263
+ heartbeat,
264
+ list,
265
+ watch,
266
+ close,
267
+ _resolveProjectName: (p) => resolveProjectName(p, fsMod, pathMod),
268
+ _STALE_MS: STALE_MS
269
+ };
270
+ }