dsh-recall-plugin 2.3.1 → 2.3.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.
- package/CHANGELOG.md +48 -0
- package/README.en.md +4 -4
- package/README.md +8 -9
- package/lib/client.js +1689 -1484
- package/lib/config.js +53 -96
- package/lib/diagnostics.js +27 -52
- package/lib/dump-parse.js +79 -61
- package/lib/errors.js +41 -55
- package/lib/index.js +345 -446
- package/lib/maintenance.js +204 -275
- package/lib/routes-core.js +89 -140
- package/lib/routes-manage.js +571 -624
- package/lib/scripts.posix.js +452 -638
- package/lib/scripts.pwsh.js +537 -778
- package/lib/session-info.js +42 -57
- package/lib/snapshots.js +400 -588
- package/lib/store.js +364 -563
- package/package.json +7 -3
package/lib/store.js
CHANGED
|
@@ -1,563 +1,364 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return container
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// store 形态装配:exclude.txt 是用户自定义排除文件,home 存储时放在
|
|
369
|
-
// dsh-recall-snapshots 根(所有项目共享一份全局配置);降级存储时放
|
|
370
|
-
// store 目录内部——降级目录本身已被排除规则覆盖,不再往项目根塞文件。
|
|
371
|
-
// git init <dir> 会把真实 git-dir 建在 <dir>/.git,所以 repo 是仓库
|
|
372
|
-
// 工作目录、git 是真实 git-dir——冒烟测试踩过的坑。
|
|
373
|
-
// maxFileBytes 从 config 注入 store:脚本模板(snapshot/diff/rollback
|
|
374
|
-
// 的超大文件剔除)按调用时从 store 读取,用户改 config 后下一条命令
|
|
375
|
-
// 即生效,无需重启——因此用 getter 跟随 config 热更新,而不是创建时
|
|
376
|
-
// 快照(settings 卡片改 maxFileBytes 后 store 缓存不重建)。
|
|
377
|
-
function makeStore(dir, home) {
|
|
378
|
-
const excludeFile = home
|
|
379
|
-
? dir.slice(0, dir.lastIndexOf(SEP)) + SEP + 'exclude.txt'
|
|
380
|
-
: dir + SEP + 'exclude.txt'
|
|
381
|
-
return {
|
|
382
|
-
dir,
|
|
383
|
-
repo: dir + SEP + 'git',
|
|
384
|
-
git: dir + SEP + 'git' + SEP + '.git',
|
|
385
|
-
home,
|
|
386
|
-
excludeFile,
|
|
387
|
-
get maxFileBytes() { return config.maxFileBytes },
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// 将磁盘枚举出的 store 目录临时包装成 store 对象。全部删除必须覆盖
|
|
392
|
-
// `root.txt`/`index.json` 已失步的历史仓库:这时无法安全地用 root 调
|
|
393
|
-
// `resolveStore`(它可能新建另一个目录),所以直接以已枚举的 dir 为准。
|
|
394
|
-
// home 参数只影响 excludeFile;删除 tag/index 不依赖它,因而未知时用
|
|
395
|
-
// false 也安全。
|
|
396
|
-
function storeFromDir(dir, home) {
|
|
397
|
-
return makeStore(dir, Boolean(home))
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// store 级元数据 root.txt:内容为工作区绝对路径。store 目录名是 root 的
|
|
401
|
-
// 单向 SHA256,反解不了——「快照管理」跨工作区展示时靠它把哈希目录映射
|
|
402
|
-
// 回工作区名。best-effort(失败不阻断主流程),旧 store 在 resolveStore
|
|
403
|
-
// 再次被调用(重启后首个 init/快照/管理列表)时自然补写,存量自愈。
|
|
404
|
-
function persistRootHint(store, root) {
|
|
405
|
-
writeTextViaShell(store.dir + SEP + 'root.txt', root).catch(() => {})
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// 存储根:优先放 DSH home(保持项目目录干净)。shell 以宿主身份执行,
|
|
409
|
-
// 受限会话(workspace-write/read-only)也能写 home;只有 home 本身不可写
|
|
410
|
-
// (如 DSH_HOME 指向只读/网络盘)才降级到项目内(功能优先于干净)。
|
|
411
|
-
async function resolveStore(root) {
|
|
412
|
-
const cached = state.stores.get(root)
|
|
413
|
-
if (cached) return cached
|
|
414
|
-
let homeDir = null
|
|
415
|
-
try {
|
|
416
|
-
homeDir = await homeDirFor(root)
|
|
417
|
-
} catch (error) {
|
|
418
|
-
homeDir = null
|
|
419
|
-
}
|
|
420
|
-
if (homeDir) {
|
|
421
|
-
try {
|
|
422
|
-
await runShell(scripts.mkdirScript(homeDir), { stdoutMaxBytes: 4096 })
|
|
423
|
-
const store = makeStore(homeDir, true)
|
|
424
|
-
state.stores.set(root, store)
|
|
425
|
-
persistRootHint(store, root)
|
|
426
|
-
return store
|
|
427
|
-
} catch (error) {
|
|
428
|
-
recordError('recall home store unavailable, falling back to workspace: ' + String(error))
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
const fallback = root + SEP + '.dsh-recall-snapshots'
|
|
432
|
-
await runShell(scripts.mkdirScript(fallback), { stdoutMaxBytes: 4096 })
|
|
433
|
-
const store = makeStore(fallback, false)
|
|
434
|
-
state.stores.set(root, store)
|
|
435
|
-
persistRootHint(store, root)
|
|
436
|
-
return store
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// 旧版迁移:宿主身份执行前的版本在受限会话里会把影子仓库降级到项目内,
|
|
440
|
-
// 这里在下一条消息快照前把它整体迁回 home 并删除项目内目录,恢复
|
|
441
|
-
// 「项目目录干净」。失败节流 5 分钟,避免 home 不可写时每条消息白试。
|
|
442
|
-
async function tryUpgradeToHome(root) {
|
|
443
|
-
const store = state.stores.get(root)
|
|
444
|
-
if (!store || store.home) return store
|
|
445
|
-
const now = Date.now()
|
|
446
|
-
const last = state.homeRetryAt.get(root) || 0
|
|
447
|
-
if (now - last < HOME_RETRY_MS) return store
|
|
448
|
-
state.homeRetryAt.set(root, now)
|
|
449
|
-
let homeDir = null
|
|
450
|
-
try {
|
|
451
|
-
homeDir = await homeDirFor(root)
|
|
452
|
-
} catch (error) {
|
|
453
|
-
homeDir = null
|
|
454
|
-
}
|
|
455
|
-
if (!homeDir) return store
|
|
456
|
-
try {
|
|
457
|
-
await runShell(scripts.mkdirScript(homeDir), { stdoutMaxBytes: 4096 })
|
|
458
|
-
await runShell(scripts.migrateScript(store.dir, homeDir), { timeoutMs: 300000, stdoutMaxBytes: 4096 })
|
|
459
|
-
const upgraded = makeStore(homeDir, true)
|
|
460
|
-
state.stores.set(root, upgraded)
|
|
461
|
-
persistRootHint(upgraded, root)
|
|
462
|
-
state.gitReady.delete(store.git)
|
|
463
|
-
// 旧 store 的 gc 节流凭据随之作废,清掉避免新 store 误读
|
|
464
|
-
state.gcLastAt.delete(store.git)
|
|
465
|
-
state.gcCount.delete(store.git)
|
|
466
|
-
console.error('recall store upgraded to home:', root)
|
|
467
|
-
return upgraded
|
|
468
|
-
} catch (error) {
|
|
469
|
-
recordError('recall home upgrade failed: ' + String(error))
|
|
470
|
-
return store
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// 任意长度文本落盘(index.json / exclude.txt / lineage.json / root.txt
|
|
475
|
-
// 共用),原子写(H2):先写 <file>.tmp 再 rename 替换目标——写中途崩溃
|
|
476
|
-
// 最多留一个无害的 .tmp 残留(下次写覆盖),绝不会留下截断 JSON。
|
|
477
|
-
// PF-2:两平台统一「stdin 传全文 + 单进程落盘」(fileWriteStdinCmd,同名
|
|
478
|
-
// 导出纪律)。win32 曾按 base64 20000 字符分块——每块一条 PowerShell 进程,
|
|
479
|
-
// 索引几百条时 saveIndex = 6+ 条进程,而它在每条消息快照后、每次删除、
|
|
480
|
-
// 每次 init 都全量重写;POSIX 的 cat > tmp 内联命令一并迁进模板。stdin
|
|
481
|
-
// 不经命令行,argv 32767 上限与编码坑天然消失;pwsh 侧读取手法由探针
|
|
482
|
-
// 钉死(OpenStandardInput 字节流——Console.In 在 PS 5.1 按 GBK 解码必挂,
|
|
483
|
-
// 见 tests/probe/stdin-write.test.js 与 plan-performance.md 实施记录)。
|
|
484
|
-
// 空内容也落一次写(清空配置/空索引是合法状态),stdin 空串照常发送。
|
|
485
|
-
// rename 是同卷 O(1) 元数据操作,索引写频率为每消息一次,额外开销可忽略。
|
|
486
|
-
// rename 步的 ENOENT 容忍(WSL 双实例实弹发现):每实例写同一个
|
|
487
|
-
// <file>.tmp 路径,并发时一方 rename 把 tmp 消费掉,另一方 rename 报
|
|
488
|
-
// 「No such file / does not exist」。容忍是安全的,因为能走到 rename
|
|
489
|
-
// 的前提是写侧(stdin 写)已完整成功——任一写步失败都在写侧直接抛
|
|
490
|
-
// (POSIX set -e 对 cat 失败终止 / pwsh EAP=Stop 对 WriteAllText 抛),
|
|
491
|
-
// 进不到这里;所以此刻 tmp 消失只可能是同伴先把完整内容 rename 到了
|
|
492
|
-
// 目标——本侧写语义已被达成。Windows 侧「偶发一次 Move-Item:
|
|
493
|
-
// index.json.tmp does not exist」即同根。不进 recordError(用户错误列表
|
|
494
|
-
// 刷屏正是要消除的症状),console.error 留诊断痕迹;其余错误原样抛出。
|
|
495
|
-
async function renameTmpQuietly(tmp, file) {
|
|
496
|
-
try {
|
|
497
|
-
await runShell(scripts.renameFileCmd(tmp, file), { stdoutMaxBytes: 4096 })
|
|
498
|
-
} catch (error) {
|
|
499
|
-
const basename = tmp.slice(tmp.lastIndexOf(SEP) + 1)
|
|
500
|
-
if (isTmpConsumedError(error, basename)) {
|
|
501
|
-
console.error('recall writeTextViaShell: ' + basename + ' 已被并发写者 rename 消费,视同成功')
|
|
502
|
-
return
|
|
503
|
-
}
|
|
504
|
-
throw error
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
async function writeTextViaShell(file, text) {
|
|
509
|
-
const body = String(text == null ? '' : text)
|
|
510
|
-
const tmp = file + '.tmp'
|
|
511
|
-
await runShell(scripts.fileWriteStdinCmd(tmp), { stdin: body, stdoutMaxBytes: 4096 })
|
|
512
|
-
await renameTmpQuietly(tmp, file)
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// 建立影子仓库(幂等:gitReady 命中后直接跳过,省掉每条消息一次的
|
|
516
|
-
// config/exclude 重写)。同时回读 gc.stamp 种子化 gc 节流:让「上次 gc
|
|
517
|
-
// 时间」跨重启续存,避免天天重启的机器每开机都来一次全量 gc。
|
|
518
|
-
// 返回 {ok, error}(M1-D2):失败原因必须传出——captureSnapshot 要把它
|
|
519
|
-
// 分类成 snapFeedback 的可行动提示(此前吞成布尔,客户端空轮询 20 次、
|
|
520
|
-
// 用户零感知,issue #11 主线缺口);init/预热调用方忽略返回值,不受形状
|
|
521
|
-
// 变化影响。
|
|
522
|
-
async function ensureGit(root, store) {
|
|
523
|
-
if (state.gitReady.has(store.git)) return { ok: true }
|
|
524
|
-
const gitExe = await resolveGit()
|
|
525
|
-
if (!gitExe) {
|
|
526
|
-
// git 缺失分支:原先静默 return false,连 recordError 都没有(用户
|
|
527
|
-
// 重启也查不到原因的盲区)。进错误环靠上方尾部去重天然免刷屏。
|
|
528
|
-
const error = '未检测到 git CLI,快照不可用'
|
|
529
|
-
recordError('recall ensureGit: ' + error + ':请安装 git 或检查其是否在 PATH 中')
|
|
530
|
-
return { ok: false, error }
|
|
531
|
-
}
|
|
532
|
-
try {
|
|
533
|
-
const out = scripts.stripBom(await runShell(scripts.ensureGitScript(store, gitExe, config.baseExcludes), { stdoutMaxBytes: 4096 }))
|
|
534
|
-
state.gitReady.add(store.git)
|
|
535
|
-
const m = out.match(/GIT_OK\s+(\d+)/)
|
|
536
|
-
state.gcLastAt.set(store.git, m ? parseInt(m[1], 10) * 1000 : Date.now())
|
|
537
|
-
return { ok: true }
|
|
538
|
-
} catch (error) {
|
|
539
|
-
recordError('recall ensureGit failed: ' + String(error))
|
|
540
|
-
return { ok: false, error: String(error) }
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// 迁移收尾:删除旧版 blobs 格式的项目内 .dsh-recall-snapshots 目录,
|
|
545
|
-
// 仅在 home 存储可用时执行——降级场景下该目录就是新 store,不能删。
|
|
546
|
-
// PF-5 顺带:legacyCleaned 内存标记——该目录只存在于极早期版本用户的
|
|
547
|
-
// 降级工作区,探测成功一次后不可能再出现,同 root 多次 init 只付一条
|
|
548
|
-
// 进程;失败(环境性)也标记不重试:残留只占磁盘无功能影响,重置场景
|
|
549
|
-
// (DSH 重启)每进程一次可接受(文档 PF-5 顺带节的取舍)。
|
|
550
|
-
// pwsh 侧 legacyRmScript 已加 -ErrorAction SilentlyContinue:「目录本就
|
|
551
|
-
// 不存在」(常态)不再抛错中断,而是以成功返回被标记——否则常态下每次
|
|
552
|
-
// init 都白跑一条进程,标记就失去了意义。
|
|
553
|
-
const legacyCleaned = new Set()
|
|
554
|
-
function cleanupLegacy(root) {
|
|
555
|
-
const store = state.stores.get(root)
|
|
556
|
-
if (!store || !store.home) return
|
|
557
|
-
if (legacyCleaned.has(root)) return
|
|
558
|
-
legacyCleaned.add(root)
|
|
559
|
-
runShell(scripts.legacyRmScript(root + SEP + '.dsh-recall-snapshots'), { timeoutMs: 120000, stdoutMaxBytes: 4096 }).catch(() => {})
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
return { state, isWin, scripts, recordError, runShell, runShellMeta, writeTextViaShell, resolveRoot, resolveGit, homeDirFor, resolveHomeContainer, resolveStore, storeFromDir, tryUpgradeToHome, ensureGit, cleanupLegacy, cleanupAfterGitFailure }
|
|
563
|
-
}
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import * as pwshScripts from "./scripts.pwsh.js";
|
|
4
|
+
import * as posixScripts from "./scripts.posix.js";
|
|
5
|
+
import { classifyEnvError } from "./diagnostics.js";
|
|
6
|
+
const HOME_RETRY_MS = 3e5;
|
|
7
|
+
const ERROR_BUFFER_MAX = 20;
|
|
8
|
+
function selectPosixHomeBase({ probed, envHome, homedir }) {
|
|
9
|
+
if (probed) return { base: probed, third: false };
|
|
10
|
+
if (envHome) return { base: envHome, third: false };
|
|
11
|
+
return { base: homedir + "/.dsh", third: true };
|
|
12
|
+
}
|
|
13
|
+
async function resolvePosixHomeBase(deps, inputs) {
|
|
14
|
+
const { probed, envHome, homedir } = inputs;
|
|
15
|
+
const sel = selectPosixHomeBase({ probed, envHome, homedir });
|
|
16
|
+
if (!sel.third) return sel.base;
|
|
17
|
+
try {
|
|
18
|
+
const out = String(await deps.runShell(deps.scripts.legacyHomeMigrateScript(homedir), { timeoutMs: 3e5, stdoutMaxBytes: 4096 })).trim();
|
|
19
|
+
if (out === "MIGRATE_OK" || out === "OLD_ABSENT") return sel.base;
|
|
20
|
+
deps.recordError(
|
|
21
|
+
out === "BOTH_PRESENT" ? "recall home store \u65B0\u65E7\u5BB9\u5668\u5E76\u5B58\uFF08" + homedir + "/dsh-recall-snapshots \u4E0E " + homedir + "/.dsh/dsh-recall-snapshots\uFF09\uFF0C\u6CBF\u7528\u65E7\u4F4D\uFF0C\u672A\u505A\u4EFB\u4F55\u6539\u52A8" : "recall \u65E7\u5FEB\u7167\u5BB9\u5668\u8FC1\u79FB\u5931\u8D25\uFF08MIGRATE_FAIL\uFF09\uFF0C\u6CBF\u7528\u65E7\u4F4D " + homedir + "/dsh-recall-snapshots"
|
|
22
|
+
);
|
|
23
|
+
return homedir;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
deps.recordError("recall \u65E7\u5FEB\u7167\u5BB9\u5668\u8FC1\u79FB\u63A2\u6D4B\u5931\u8D25\uFF0C\u6CBF\u7528\u65E7\u4F4D: " + String(error));
|
|
26
|
+
return homedir;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function parseCleanupResult(out) {
|
|
30
|
+
const m = String(out || "").match(/CLEANUP_OTHER_INSTANCE\s+(\d+)/);
|
|
31
|
+
if (m) return { otherPid: parseInt(m[1], 10), skippedFresh: false };
|
|
32
|
+
if (String(out || "").indexOf("CLEANUP_SKIPPED_FRESH_LOCK") >= 0) return { otherPid: null, skippedFresh: true };
|
|
33
|
+
return { otherPid: null, skippedFresh: false };
|
|
34
|
+
}
|
|
35
|
+
function isTmpConsumedError(error, basename) {
|
|
36
|
+
const s = String(error || "");
|
|
37
|
+
if (!basename || s.indexOf(basename) < 0) return false;
|
|
38
|
+
return /No such file/i.test(s) || /does not exist/i.test(s) || /cannot find path/i.test(s);
|
|
39
|
+
}
|
|
40
|
+
function createRuntime(ctx, config) {
|
|
41
|
+
const shell = ctx.shell;
|
|
42
|
+
const sessions = ctx.sessions;
|
|
43
|
+
const isWin = process.platform === "win32";
|
|
44
|
+
const SEP = isWin ? "\\" : "/";
|
|
45
|
+
const scripts = isWin ? pwshScripts : posixScripts;
|
|
46
|
+
const state = {
|
|
47
|
+
roots: /* @__PURE__ */ new Map(),
|
|
48
|
+
stores: /* @__PURE__ */ new Map(),
|
|
49
|
+
snapshots: /* @__PURE__ */ new Map(),
|
|
50
|
+
queue: Promise.resolve(),
|
|
51
|
+
indexLoaded: /* @__PURE__ */ new Set(),
|
|
52
|
+
// PF-5 索引终态三/四档标记(rebuildOrphans 守卫的数据源):
|
|
53
|
+
// - indexHealthy:磁盘索引解析成功且在场(loadIndex 正常载入分支)——
|
|
54
|
+
// rebuildOrphans 对 healthy 且条目非空的 root 整体跳过(省 1+N 条进程)
|
|
55
|
+
// - indexTruncated:读截断(F-G3,内存是残缺视图)——rebuildOrphans
|
|
56
|
+
// 必须跳过:否则全部 tag 被判孤儿、用残缺孤儿集覆盖完好的大索引
|
|
57
|
+
// (feedback 全丢、数万条索引按 win32 分块写下是数百条进程的灾难)
|
|
58
|
+
// empty(无索引文件)/quarantined(损坏隔离)不标记 → rebuild 照跑,
|
|
59
|
+
// 自愈链路完整
|
|
60
|
+
indexHealthy: /* @__PURE__ */ new Set(),
|
|
61
|
+
indexTruncated: /* @__PURE__ */ new Set(),
|
|
62
|
+
gitReady: /* @__PURE__ */ new Set(),
|
|
63
|
+
cutSeqCache: /* @__PURE__ */ new Map(),
|
|
64
|
+
homeRetryAt: /* @__PURE__ */ new Map(),
|
|
65
|
+
gcLastAt: /* @__PURE__ */ new Map(),
|
|
66
|
+
gcCount: /* @__PURE__ */ new Map(),
|
|
67
|
+
gitExe: null,
|
|
68
|
+
posixHomeBase: null,
|
|
69
|
+
homeContainer: null,
|
|
70
|
+
errors: [],
|
|
71
|
+
// 逐消息的快照反馈(issue #7 失败可见性):失败 {failed,error} 或
|
|
72
|
+
// fail-open 跳过 {skipped:[...]},由 snapshot-info 端点下发给客户端
|
|
73
|
+
// 弹 toast。放共享 state 而非 snapshots.js 闭包:端点在 index.js,
|
|
74
|
+
// 与索引/根缓存同层取用。
|
|
75
|
+
snapFeedback: /* @__PURE__ */ new Map()
|
|
76
|
+
};
|
|
77
|
+
function recordError(text) {
|
|
78
|
+
const message = String(text);
|
|
79
|
+
const last = state.errors[state.errors.length - 1];
|
|
80
|
+
if (last && last.message === message) {
|
|
81
|
+
last.time = Date.now();
|
|
82
|
+
last.count += 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const rec = { time: Date.now(), message, count: 1, kind: classifyEnvError(message) };
|
|
86
|
+
state.errors.push(rec);
|
|
87
|
+
if (state.errors.length > ERROR_BUFFER_MAX) state.errors.splice(0, state.errors.length - ERROR_BUFFER_MAX);
|
|
88
|
+
console.error(message);
|
|
89
|
+
}
|
|
90
|
+
;
|
|
91
|
+
(function checkScriptParity() {
|
|
92
|
+
const SKIP = /* @__PURE__ */ new Set(["homeDirScript", "probeHomeScript", "legacyHomeMigrateScript"]);
|
|
93
|
+
const pwshKeys = Object.keys(pwshScripts).filter((k) => !SKIP.has(k) && typeof pwshScripts[k] === "function");
|
|
94
|
+
const posixKeys = Object.keys(posixScripts).filter((k) => !SKIP.has(k) && typeof posixScripts[k] === "function");
|
|
95
|
+
const missing = pwshKeys.filter((k) => posixKeys.indexOf(k) < 0);
|
|
96
|
+
if (missing.length) recordError("recall script parity: posix \u7F3A\u5C11\u5BFC\u51FA " + missing.join(", "));
|
|
97
|
+
})();
|
|
98
|
+
async function runShellMeta(command, opts) {
|
|
99
|
+
const sp = ctx.get("sandboxPolicy");
|
|
100
|
+
const spec = shell.resolve({
|
|
101
|
+
// 编码前导:pwsh 侧统一 UTF-8 输出(中文机器 GBK 代码页不再乱码);
|
|
102
|
+
// bash 侧 LC_ALL=C 确定序。各模板自带,这里统一前置注入。
|
|
103
|
+
command: scripts.UTF8_PRELUDE + "\n" + command,
|
|
104
|
+
timeoutMs: opts && opts.timeoutMs || 3e5,
|
|
105
|
+
stdoutMaxBytes: opts && opts.stdoutMaxBytes || 4194304,
|
|
106
|
+
// stdin 是官方 ShellExecRequest 契约字段(bash-local/pwsh 均实现),
|
|
107
|
+
// POSIX 侧用它传 index.json 全文,绕开 argv 长度上限
|
|
108
|
+
...opts && opts.stdin !== void 0 ? { stdin: opts.stdin } : {},
|
|
109
|
+
sandboxPolicy: { mode: "danger-full-access", workspaceRoot: sp && sp.workspaceRoot || process.cwd() }
|
|
110
|
+
});
|
|
111
|
+
const res = await shell.run(spec);
|
|
112
|
+
const out = res && res.stdout && res.stdout.text || "";
|
|
113
|
+
if (res && res.exitCode !== 0) {
|
|
114
|
+
await cleanupAfterGitFailure(command);
|
|
115
|
+
const err = (res && res.stderr && res.stderr.text || "").trim() || "exit " + String(res.exitCode);
|
|
116
|
+
throw new Error(err.slice(0, 1500));
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
text: out,
|
|
120
|
+
truncated: Boolean(res && res.stdout && res.stdout.truncated)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function runShell(command, opts) {
|
|
124
|
+
return (await runShellMeta(command, opts)).text;
|
|
125
|
+
}
|
|
126
|
+
function extractGitDir(command) {
|
|
127
|
+
const m = String(command).match(/(?:^|\n)[ \t]*(?:\$g|g)[ \t]*=[ \t]*'([^']+)/);
|
|
128
|
+
return m ? m[1] : null;
|
|
129
|
+
}
|
|
130
|
+
async function cleanupAfterGitFailure(command) {
|
|
131
|
+
if (!command || String(command).indexOf("RECALL_CLEANUP") >= 0) return;
|
|
132
|
+
const gitDir = extractGitDir(command);
|
|
133
|
+
if (!gitDir) return;
|
|
134
|
+
try {
|
|
135
|
+
const out = await runShell(scripts.killOrphansScript(gitDir), { timeoutMs: 6e4, stdoutMaxBytes: 4096 });
|
|
136
|
+
const r = parseCleanupResult(out);
|
|
137
|
+
if (r.otherPid !== null) recordError("recall \u68C0\u6D4B\u5230\u53E6\u4E00\u4E2A DSH \u5B9E\u4F8B\uFF08PID " + r.otherPid + "\uFF09\u6B63\u5728\u4F7F\u7528\u6B64\u5FEB\u7167\u5E93\uFF0C\u5931\u8D25\u6E05\u626B\u5DF2\u8BA9\u8DEF\uFF1A\u672A\u6740\u8FDB\u7A0B\u3001\u672A\u52A8\u9501");
|
|
138
|
+
else if (r.skippedFresh) recordError("recall \u68C0\u6D4B\u5230 5 \u5206\u949F\u5185\u7684\u65B0\u9501\u6587\u4EF6\uFF0C\u7591\u4F3C git \u64CD\u4F5C\u6B63\u5728\u8FDB\u884C\uFF0C\u5931\u8D25\u6E05\u626B\u5DF2\u8BA9\u8DEF\uFF08\u9501\u9648\u65E7\u540E\u4F1A\u81EA\u52A8\u6E05\u7406\uFF09");
|
|
139
|
+
} catch (error) {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function resolveRoot(sessionId) {
|
|
143
|
+
const key = sessionId ? String(sessionId) : "fallback";
|
|
144
|
+
const cached = state.roots.get(key);
|
|
145
|
+
if (cached) return cached;
|
|
146
|
+
let root = null;
|
|
147
|
+
let authoritative = false;
|
|
148
|
+
if (sessionId) {
|
|
149
|
+
const session = sessions.get(sessionId);
|
|
150
|
+
if (session && session.header && session.header.cwd) {
|
|
151
|
+
root = session.header.cwd;
|
|
152
|
+
authoritative = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!root && sessionId) {
|
|
156
|
+
try {
|
|
157
|
+
const query = ctx.get("sessionQuery");
|
|
158
|
+
if (query && typeof query.listSessions === "function") {
|
|
159
|
+
const records = await query.listSessions();
|
|
160
|
+
const rec = (records || []).find((r) => r && r.header && r.header.id === sessionId);
|
|
161
|
+
if (rec && rec.header && rec.header.cwd) {
|
|
162
|
+
root = rec.header.cwd;
|
|
163
|
+
authoritative = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!root) {
|
|
170
|
+
const sp = ctx.get("sandboxPolicy");
|
|
171
|
+
if (sp && sp.workspaceRoot) root = sp.workspaceRoot;
|
|
172
|
+
}
|
|
173
|
+
if (root) {
|
|
174
|
+
root = root.replace(/[\\/]+$/, "") || (isWin ? root : "/");
|
|
175
|
+
if (isWin && root.length === 2) root += "\\";
|
|
176
|
+
if (authoritative) state.roots.set(key, root);
|
|
177
|
+
}
|
|
178
|
+
return root;
|
|
179
|
+
}
|
|
180
|
+
async function resolveGit() {
|
|
181
|
+
if (state.gitExe !== null) return state.gitExe;
|
|
182
|
+
try {
|
|
183
|
+
const path = scripts.stripBom(await runShell(scripts.resolveGitScript(), { stdoutMaxBytes: 4096 })).trim();
|
|
184
|
+
state.gitExe = path || "";
|
|
185
|
+
} catch (error) {
|
|
186
|
+
state.gitExe = "";
|
|
187
|
+
}
|
|
188
|
+
return state.gitExe;
|
|
189
|
+
}
|
|
190
|
+
async function homeDirForWin(root) {
|
|
191
|
+
const envHome = process.env && process.env.DSH_HOME || "";
|
|
192
|
+
const text = scripts.stripBom(await runShell(scripts.homeDirScript(root, envHome), { stdoutMaxBytes: 4096 })).trim();
|
|
193
|
+
if (!text) return null;
|
|
194
|
+
if (/^\\\\/.test(text)) return "\\\\" + text.slice(2).replace(/\\{2,}/g, "\\");
|
|
195
|
+
return text.replace(/\\{2,}/g, "\\");
|
|
196
|
+
}
|
|
197
|
+
async function posixHomeBaseResolve() {
|
|
198
|
+
if (state.posixHomeBase === null) {
|
|
199
|
+
let probed = "";
|
|
200
|
+
try {
|
|
201
|
+
probed = (await runShell(scripts.probeHomeScript(), { stdoutMaxBytes: 4096 })).trim();
|
|
202
|
+
} catch (error) {
|
|
203
|
+
probed = "";
|
|
204
|
+
}
|
|
205
|
+
state.posixHomeBase = await resolvePosixHomeBase(
|
|
206
|
+
{ runShell, scripts, recordError },
|
|
207
|
+
{ probed, envHome: process.env && process.env.DSH_HOME || "", homedir: os.homedir() }
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return state.posixHomeBase;
|
|
211
|
+
}
|
|
212
|
+
async function homeDirForPosix(root) {
|
|
213
|
+
const base = await posixHomeBaseResolve();
|
|
214
|
+
const hash = crypto.createHash("sha256").update(root, "utf8").digest("hex");
|
|
215
|
+
return base.replace(/\/+$/, "") + "/dsh-recall-snapshots/" + hash;
|
|
216
|
+
}
|
|
217
|
+
async function homeDirFor(root) {
|
|
218
|
+
return isWin ? homeDirForWin(root) : homeDirForPosix(root);
|
|
219
|
+
}
|
|
220
|
+
async function resolveHomeContainer() {
|
|
221
|
+
if (state.homeContainer) return state.homeContainer;
|
|
222
|
+
let container = null;
|
|
223
|
+
try {
|
|
224
|
+
const probeRoot = Array.from(state.roots.values())[0] || process.cwd();
|
|
225
|
+
const homeDir = await homeDirFor(probeRoot);
|
|
226
|
+
if (homeDir) container = homeDir.slice(0, homeDir.length - 65);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
container = null;
|
|
229
|
+
}
|
|
230
|
+
if (container) state.homeContainer = container;
|
|
231
|
+
return container;
|
|
232
|
+
}
|
|
233
|
+
function makeStore(dir, home) {
|
|
234
|
+
const excludeFile = home ? dir.slice(0, dir.lastIndexOf(SEP)) + SEP + "exclude.txt" : dir + SEP + "exclude.txt";
|
|
235
|
+
return {
|
|
236
|
+
dir,
|
|
237
|
+
repo: dir + SEP + "git",
|
|
238
|
+
git: dir + SEP + "git" + SEP + ".git",
|
|
239
|
+
home,
|
|
240
|
+
excludeFile,
|
|
241
|
+
get maxFileBytes() {
|
|
242
|
+
return config.maxFileBytes;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function storeFromDir(dir, home) {
|
|
247
|
+
return makeStore(dir, Boolean(home));
|
|
248
|
+
}
|
|
249
|
+
function persistRootHint(store, root) {
|
|
250
|
+
writeTextViaShell(store.dir + SEP + "root.txt", root).catch(() => {
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async function resolveStore(root) {
|
|
254
|
+
const cached = state.stores.get(root);
|
|
255
|
+
if (cached) return cached;
|
|
256
|
+
let homeDir = null;
|
|
257
|
+
try {
|
|
258
|
+
homeDir = await homeDirFor(root);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
homeDir = null;
|
|
261
|
+
}
|
|
262
|
+
if (homeDir) {
|
|
263
|
+
try {
|
|
264
|
+
await runShell(scripts.mkdirScript(homeDir), { stdoutMaxBytes: 4096 });
|
|
265
|
+
const store2 = makeStore(homeDir, true);
|
|
266
|
+
state.stores.set(root, store2);
|
|
267
|
+
persistRootHint(store2, root);
|
|
268
|
+
return store2;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
recordError("recall home store unavailable, falling back to workspace: " + String(error));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const fallback = root + SEP + ".dsh-recall-snapshots";
|
|
274
|
+
await runShell(scripts.mkdirScript(fallback), { stdoutMaxBytes: 4096 });
|
|
275
|
+
const store = makeStore(fallback, false);
|
|
276
|
+
state.stores.set(root, store);
|
|
277
|
+
persistRootHint(store, root);
|
|
278
|
+
return store;
|
|
279
|
+
}
|
|
280
|
+
async function tryUpgradeToHome(root) {
|
|
281
|
+
const store = state.stores.get(root);
|
|
282
|
+
if (!store || store.home) return store || null;
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
const last = state.homeRetryAt.get(root) || 0;
|
|
285
|
+
if (now - last < HOME_RETRY_MS) return store;
|
|
286
|
+
state.homeRetryAt.set(root, now);
|
|
287
|
+
let homeDir = null;
|
|
288
|
+
try {
|
|
289
|
+
homeDir = await homeDirFor(root);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
homeDir = null;
|
|
292
|
+
}
|
|
293
|
+
if (!homeDir) return store;
|
|
294
|
+
try {
|
|
295
|
+
await runShell(scripts.mkdirScript(homeDir), { stdoutMaxBytes: 4096 });
|
|
296
|
+
await runShell(scripts.migrateScript(store.dir, homeDir), { timeoutMs: 3e5, stdoutMaxBytes: 4096 });
|
|
297
|
+
const upgraded = makeStore(homeDir, true);
|
|
298
|
+
state.stores.set(root, upgraded);
|
|
299
|
+
persistRootHint(upgraded, root);
|
|
300
|
+
state.gitReady.delete(store.git);
|
|
301
|
+
state.gcLastAt.delete(store.git);
|
|
302
|
+
state.gcCount.delete(store.git);
|
|
303
|
+
console.error("recall store upgraded to home:", root);
|
|
304
|
+
return upgraded;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
recordError("recall home upgrade failed: " + String(error));
|
|
307
|
+
return store;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function renameTmpQuietly(tmp, file) {
|
|
311
|
+
try {
|
|
312
|
+
await runShell(scripts.renameFileCmd(tmp, file), { stdoutMaxBytes: 4096 });
|
|
313
|
+
} catch (error) {
|
|
314
|
+
const basename = tmp.slice(tmp.lastIndexOf(SEP) + 1);
|
|
315
|
+
if (isTmpConsumedError(error, basename)) {
|
|
316
|
+
console.error("recall writeTextViaShell: " + basename + " \u5DF2\u88AB\u5E76\u53D1\u5199\u8005 rename \u6D88\u8D39\uFF0C\u89C6\u540C\u6210\u529F");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function writeTextViaShell(file, text) {
|
|
323
|
+
const body = String(text == null ? "" : text);
|
|
324
|
+
const tmp = file + ".tmp";
|
|
325
|
+
await runShell(scripts.fileWriteStdinCmd(tmp), { stdin: body, stdoutMaxBytes: 4096 });
|
|
326
|
+
await renameTmpQuietly(tmp, file);
|
|
327
|
+
}
|
|
328
|
+
async function ensureGit(root, store) {
|
|
329
|
+
if (state.gitReady.has(store.git)) return { ok: true };
|
|
330
|
+
const gitExe = await resolveGit();
|
|
331
|
+
if (!gitExe) {
|
|
332
|
+
const error = "\u672A\u68C0\u6D4B\u5230 git CLI\uFF0C\u5FEB\u7167\u4E0D\u53EF\u7528";
|
|
333
|
+
recordError("recall ensureGit: " + error + "\uFF1A\u8BF7\u5B89\u88C5 git \u6216\u68C0\u67E5\u5176\u662F\u5426\u5728 PATH \u4E2D");
|
|
334
|
+
return { ok: false, error };
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const out = scripts.stripBom(await runShell(scripts.ensureGitScript(store, gitExe, config.baseExcludes), { stdoutMaxBytes: 4096 }));
|
|
338
|
+
state.gitReady.add(store.git);
|
|
339
|
+
const m = out.match(/GIT_OK\s+(\d+)/);
|
|
340
|
+
state.gcLastAt.set(store.git, m ? parseInt(m[1], 10) * 1e3 : Date.now());
|
|
341
|
+
return { ok: true };
|
|
342
|
+
} catch (error) {
|
|
343
|
+
recordError("recall ensureGit failed: " + String(error));
|
|
344
|
+
return { ok: false, error: String(error) };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const legacyCleaned = /* @__PURE__ */ new Set();
|
|
348
|
+
function cleanupLegacy(root) {
|
|
349
|
+
const store = state.stores.get(root);
|
|
350
|
+
if (!store || !store.home) return;
|
|
351
|
+
if (legacyCleaned.has(root)) return;
|
|
352
|
+
legacyCleaned.add(root);
|
|
353
|
+
runShell(scripts.legacyRmScript(root + SEP + ".dsh-recall-snapshots"), { timeoutMs: 12e4, stdoutMaxBytes: 4096 }).catch(() => {
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return { state, isWin, scripts, recordError, runShell, runShellMeta, writeTextViaShell, resolveRoot, resolveGit, homeDirFor, resolveHomeContainer, resolveStore, storeFromDir, tryUpgradeToHome, ensureGit, cleanupLegacy, cleanupAfterGitFailure };
|
|
357
|
+
}
|
|
358
|
+
export {
|
|
359
|
+
createRuntime,
|
|
360
|
+
isTmpConsumedError,
|
|
361
|
+
parseCleanupResult,
|
|
362
|
+
resolvePosixHomeBase,
|
|
363
|
+
selectPosixHomeBase
|
|
364
|
+
};
|