dsh-recall-plugin 1.2.2 → 1.4.0
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 +109 -0
- package/README.en.md +124 -118
- package/README.md +125 -119
- package/cordis.patch.yml +19 -11
- package/lib/client.js +780 -456
- package/lib/config.js +36 -0
- package/lib/index.js +541 -164
- package/lib/maintenance.js +120 -107
- package/lib/scripts.posix.js +366 -290
- package/lib/scripts.pwsh.js +432 -334
- package/lib/snapshots.js +214 -192
- package/lib/store.js +342 -249
- package/package.json +52 -57
package/lib/scripts.pwsh.js
CHANGED
|
@@ -1,334 +1,432 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-recall-plugin — PowerShell 脚本模板(纯函数,无 ctx 依赖,win32 专用)
|
|
3
|
-
*
|
|
4
|
-
* 职责:集中拼装所有发给 shell 的 PowerShell 脚本文本。只做字符串构造,
|
|
5
|
-
* 不执行、无状态;执行侧(runShell)见 store.js,调用侧见
|
|
6
|
-
* snapshots.js / maintenance.js。集中在这里是为了:
|
|
7
|
-
* 1) PS 5.1 / pwsh 7 双版本兼容的坑只在一处处理;
|
|
8
|
-
* 2) 脚本片段(gitlink 清理、超大文件排除、用户排除同步)在多个
|
|
9
|
-
* 流程里逐字复用,散落各处必然改漏。
|
|
10
|
-
* POSIX(Linux/macOS)对应模板见 scripts.posix.js,两者导出同名接口,
|
|
11
|
-
* 由 store.js 按 process.platform 选择。
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
// 单引号字面量转义:PS 单引号串里只有 '' 表示一个单引号,且不展开变量,
|
|
15
|
-
// 是把 JS 值安全嵌进命令串的唯一可靠方式(杜绝 $、反引号注入)。
|
|
16
|
-
export function psq(value) {
|
|
17
|
-
return "'" + String(value).replace(/'/g, "''") + "'"
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// 统一 UTF-8 输出前导:中文等非 ASCII 机器的默认代码页(如 GBK)下,
|
|
21
|
-
// PowerShell 重定向 stdout 按 [Console]::OutputEncoding 编码,而 DSH 按
|
|
22
|
-
// UTF-8 解码——不强制时含中文的用户名/路径会变乱码。PS 5.1 / 7 均支持。
|
|
23
|
-
export const UTF8_PRELUDE = '$OutputEncoding = [Text.UTF8Encoding]::new($false)\ntry { [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false) } catch {}'
|
|
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
|
-
'& $git
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
'$
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
'$
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
'
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
'}
|
|
210
|
-
'$
|
|
211
|
-
'foreach ($r in @($
|
|
212
|
-
' if (-not $r) { continue }',
|
|
213
|
-
' $tab = $r.IndexOf("`t"); $path = $r.Substring($tab + 1)',
|
|
214
|
-
' $sha = ($r.Substring(0, $tab) -split " ")[
|
|
215
|
-
' $
|
|
216
|
-
'}',
|
|
217
|
-
'$
|
|
218
|
-
'foreach ($
|
|
219
|
-
' if (-not $
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* dsh-recall-plugin — PowerShell 脚本模板(纯函数,无 ctx 依赖,win32 专用)
|
|
3
|
+
*
|
|
4
|
+
* 职责:集中拼装所有发给 shell 的 PowerShell 脚本文本。只做字符串构造,
|
|
5
|
+
* 不执行、无状态;执行侧(runShell)见 store.js,调用侧见
|
|
6
|
+
* snapshots.js / maintenance.js。集中在这里是为了:
|
|
7
|
+
* 1) PS 5.1 / pwsh 7 双版本兼容的坑只在一处处理;
|
|
8
|
+
* 2) 脚本片段(gitlink 清理、超大文件排除、用户排除同步)在多个
|
|
9
|
+
* 流程里逐字复用,散落各处必然改漏。
|
|
10
|
+
* POSIX(Linux/macOS)对应模板见 scripts.posix.js,两者导出同名接口,
|
|
11
|
+
* 由 store.js 按 process.platform 选择。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// 单引号字面量转义:PS 单引号串里只有 '' 表示一个单引号,且不展开变量,
|
|
15
|
+
// 是把 JS 值安全嵌进命令串的唯一可靠方式(杜绝 $、反引号注入)。
|
|
16
|
+
export function psq(value) {
|
|
17
|
+
return "'" + String(value).replace(/'/g, "''") + "'"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 统一 UTF-8 输出前导:中文等非 ASCII 机器的默认代码页(如 GBK)下,
|
|
21
|
+
// PowerShell 重定向 stdout 按 [Console]::OutputEncoding 编码,而 DSH 按
|
|
22
|
+
// UTF-8 解码——不强制时含中文的用户名/路径会变乱码。PS 5.1 / 7 均支持。
|
|
23
|
+
export const UTF8_PRELUDE = '$OutputEncoding = [Text.UTF8Encoding]::new($false)\ntry { [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false) } catch {}'
|
|
24
|
+
|
|
25
|
+
// 超大文件跳过阈值:git 对象库对大文件极不友好,回退语义也不该被一个
|
|
26
|
+
// 200MB 的构建产物拖垮。默认值与 config.js 的 maxFileBytes 一致;实际
|
|
27
|
+
// 生效值以 store.maxFileBytes(用户 config 可调)经 oversizeBlock 注入为准。
|
|
28
|
+
export const MAX_FILE_BYTES = 104857600
|
|
29
|
+
|
|
30
|
+
// 去除 PS 5.1 Set-Content -Encoding utf8 写出的 BOM:JSON 解析前必须剥掉,
|
|
31
|
+
// 否则 JSON.parse 把 BOM 当正文首字符直接抛错。
|
|
32
|
+
export function stripBom(text) {
|
|
33
|
+
return text.replace(/^\uFEFF/, '')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 嵌套 git 仓库(工作区里的子项目自带 .git)会被 add -A 记成 gitlink(160000);
|
|
37
|
+
// gitlink 残留在 index 时 add -A 会 fatal "in unpopulated submodule",
|
|
38
|
+
// 且 gitlink 对文件回退毫无意义——所以 add 前后各清一次,子仓库内容不进快照。
|
|
39
|
+
// 依赖外层脚本已定义的 $git/$g;被 snapshot/diff/rollback 三处复用。
|
|
40
|
+
function dropGitlinksBlock() {
|
|
41
|
+
return [
|
|
42
|
+
"& $git --git-dir=$g ls-files --stage | Where-Object { $_ -like '160000*' } | ForEach-Object {",
|
|
43
|
+
" $p = ($_ -split \"`t\")[1]",
|
|
44
|
+
' & $git --literal-pathspecs --git-dir=$g update-index --force-remove -- $p',
|
|
45
|
+
'}'
|
|
46
|
+
].join('\n')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 剔除超大文件:扫描加 SilentlyContinue 是因为 EAP=Stop 下个别不可访问
|
|
50
|
+
// 子目录(杀软锁定、异常 ACL、损坏 junction)的非致命错误会被升级为终止,
|
|
51
|
+
// 整条快照作废;本扫描只用于排除超大文件,漏看个别文件是 fail-open,可接受。
|
|
52
|
+
// 阈值按调用注入(store.maxFileBytes,config 可调),不读模块常量。
|
|
53
|
+
// 依赖外层已定义的 $git/$g/$root。
|
|
54
|
+
function oversizeBlock(maxBytes) {
|
|
55
|
+
return [
|
|
56
|
+
'Get-ChildItem -LiteralPath $root -Recurse -File -Force -ErrorAction SilentlyContinue | Where-Object { $_.Length -gt ' + String(maxBytes || MAX_FILE_BYTES) + ' } | ForEach-Object {',
|
|
57
|
+
" $rel = $_.FullName.Substring($root.Length + 1).Replace('\\','/')",
|
|
58
|
+
' & $git --literal-pathspecs --git-dir=$g update-index --force-remove -- $rel',
|
|
59
|
+
'}'
|
|
60
|
+
].join('\n')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 用户自定义排除同步:把基础排除表与用户 exclude.txt 合并重写进 info/exclude,
|
|
64
|
+
// 再用 ls-files -i -c 找出「已被跟踪但命中排除」的条目从 index 清掉。
|
|
65
|
+
// - 只用 --exclude-from 指 info/exclude,不用 --exclude-standard:后者会
|
|
66
|
+
// 连带项目自己的 .gitignore 语义(后加 ignore 的已跟踪文件会被悄悄移出
|
|
67
|
+
// 快照),行为超出用户配置的本意。
|
|
68
|
+
// - 放在 add -A 之前:排除表先生效,新增的排除路径根本不会被暂存,
|
|
69
|
+
// 已跟踪的旧条目由 ls-files -i -c 补刀,两条路径一次覆盖。
|
|
70
|
+
// - 首行留空元素吸收 PS 5.1 utf8 BOM(BOM 粘在首行会废掉第一条模式),
|
|
71
|
+
// 与下方 ensureGitScript 的老技巧一致。
|
|
72
|
+
// - base 基础排除表按调用注入(config.baseExcludes 可调),不硬编码。
|
|
73
|
+
// - 依赖外层已定义的 $git/$g;被 ensureGit/snapshot/diff/rollback 复用,
|
|
74
|
+
// 因此 exclude.txt 的改动在下一次快照/diff/回退时即时生效,无需重启。
|
|
75
|
+
function excludeSyncBlock(excludeFile, base) {
|
|
76
|
+
const baseList = (Array.isArray(base) && base.length ? base : ['.git', 'node_modules/', '.dsh-recall-snapshots/']).map(psq).join(',')
|
|
77
|
+
return [
|
|
78
|
+
"$exFile = " + psq(excludeFile),
|
|
79
|
+
'$userPats = @()',
|
|
80
|
+
"if (Test-Path -LiteralPath $exFile) { $userPats = @(Get-Content -LiteralPath $exFile -Encoding UTF8 -ErrorAction SilentlyContinue | Where-Object { $t = $_.Trim(); $t -and -not $t.StartsWith('#') }) }",
|
|
81
|
+
"$lines = @('') + @(" + baseList + ") + $userPats",
|
|
82
|
+
"$exc = Join-Path $g 'info\\exclude'",
|
|
83
|
+
'Set-Content -LiteralPath $exc -Value $lines -Encoding utf8',
|
|
84
|
+
'& $git -c core.quotePath=false --literal-pathspecs --git-dir=$g ls-files -i -c --exclude-from=$exc | ForEach-Object {',
|
|
85
|
+
' if ($_) { & $git --literal-pathspecs --git-dir=$g update-index --force-remove -- $_ }',
|
|
86
|
+
'}'
|
|
87
|
+
].join('\n')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 解析 git 可执行文件路径:DSH 进程 PATH 可能不含 git,脚本里用绝对路径调用。
|
|
91
|
+
// 逐项判空再 Join-Path:个别 env 在特殊环境(32 位系统无 ProgramFiles(x86))
|
|
92
|
+
// 取到 null,EAP=Stop 下 Join-Path 抛错会让整个探测失败、误报 gitMissing。
|
|
93
|
+
export function resolveGitScript() {
|
|
94
|
+
return [
|
|
95
|
+
'$candidates = @()',
|
|
96
|
+
'$g = (Get-Command git -ErrorAction SilentlyContinue).Source',
|
|
97
|
+
'if ($g) { $candidates += $g }',
|
|
98
|
+
"if (${env:ProgramFiles}) { $candidates += (Join-Path ${env:ProgramFiles} 'Git\\cmd\\git.exe') }",
|
|
99
|
+
"if (${env:ProgramFiles(x86)}) { $candidates += (Join-Path ${env:ProgramFiles(x86)} 'Git\\cmd\\git.exe') }",
|
|
100
|
+
"if (${env:LocalAppData}) { $candidates += (Join-Path ${env:LocalAppData} 'Programs\\Git\\cmd\\git.exe') }",
|
|
101
|
+
"$g = $candidates | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Leaf) } | Select-Object -First 1",
|
|
102
|
+
'if ($g) { Write-Output $g }'
|
|
103
|
+
].join('\n')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 计算项目对应的 home 存储目录(DSH_HOME 优先,否则 ~/.dsh)。
|
|
107
|
+
// 哈希用 Create()+ComputeHash+BitConverter 而不是 HashData+ToHexString:
|
|
108
|
+
// 后两者是 .NET 5+(仅 PS 7)API,别人机器的 shell 若是 Windows PowerShell
|
|
109
|
+
// 5.1 会抛错,导致 home 存储永远降级到项目内;前者两个版本都可用。
|
|
110
|
+
export function homeDirScript(root, envHome) {
|
|
111
|
+
return [
|
|
112
|
+
'$r = ' + psq(root),
|
|
113
|
+
"$h = if ($env:DSH_HOME) { $env:DSH_HOME } elseif (" + psq(envHome) + ") { " + psq(envHome) + " } else { Join-Path $env:USERPROFILE \".dsh\" }",
|
|
114
|
+
'$sha = [Security.Cryptography.SHA256]::Create()',
|
|
115
|
+
"$hex = ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($r))) -replace '-','').ToLower()",
|
|
116
|
+
"Write-Output (Join-Path $h ('dsh-recall-snapshots\\' + $hex))"
|
|
117
|
+
].join('\n')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function mkdirScript(dir) {
|
|
121
|
+
return 'New-Item -ItemType Directory -Force -Path ' + psq(dir) + ' | Out-Null'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 旧版迁移:把降级时代落在项目内的影子仓库整体搬回 home 并删源目录
|
|
125
|
+
export function migrateScript(src, dst) {
|
|
126
|
+
return [
|
|
127
|
+
"$ErrorActionPreference = 'Stop'",
|
|
128
|
+
'$src = ' + psq(src),
|
|
129
|
+
'$dst = ' + psq(dst),
|
|
130
|
+
"if (Test-Path -LiteralPath (Join-Path $src 'git')) { Move-Item -LiteralPath (Join-Path $src 'git') -Destination (Join-Path $dst 'git') -Force }",
|
|
131
|
+
"if (Test-Path -LiteralPath (Join-Path $src 'index.json')) { Move-Item -LiteralPath (Join-Path $src 'index.json') -Destination (Join-Path $dst 'index.json') -Force }",
|
|
132
|
+
'Remove-Item -Recurse -Force -LiteralPath $src -ErrorAction SilentlyContinue',
|
|
133
|
+
"Write-Output 'MIGRATE_OK'"
|
|
134
|
+
].join('\n')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 建立影子仓库:普通 init(index 留在仓库内跨快照复用,git add 的 stat 缓存
|
|
138
|
+
// 让未变文件近乎零成本),core.longpaths 放开 Windows 深路径。
|
|
139
|
+
// autocrlf=false:按原始字节入快照(回退时逐字节还原),也避免用户全局
|
|
140
|
+
// autocrlf=true 时的 LF/CRLF stderr 警告;addEmbeddedRepo=false:嵌套仓库
|
|
141
|
+
// hint/warning 走 stderr,在 DSH shell(EAP=Stop)下会让整条脚本非零退出,
|
|
142
|
+
// 必须在仓库级配置里静默掉。
|
|
143
|
+
// 结尾回读 gc.stamp(maintenance.js 上次 gc 时间戳):让重启后的 gc 节流
|
|
144
|
+
// 不归零——没有它,天天重启 DSH 的用户每次开机第一条消息都会触发一次
|
|
145
|
+
// 全量 gc,纯浪费。
|
|
146
|
+
export function ensureGitScript(store, gitExe, base) {
|
|
147
|
+
return [
|
|
148
|
+
"$ErrorActionPreference = 'Stop'",
|
|
149
|
+
'$git = ' + psq(gitExe),
|
|
150
|
+
'$repo = ' + psq(store.repo),
|
|
151
|
+
'$g = ' + psq(store.git),
|
|
152
|
+
'if (-not (Test-Path -LiteralPath $g)) {',
|
|
153
|
+
' & $git init $repo | Out-Null',
|
|
154
|
+
'}',
|
|
155
|
+
'& $git --git-dir=$g config core.longpaths true',
|
|
156
|
+
'& $git --git-dir=$g config core.autocrlf false',
|
|
157
|
+
'& $git --git-dir=$g config advice.addEmbeddedRepo false',
|
|
158
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
159
|
+
"$stamp = Join-Path $g 'gc.stamp'",
|
|
160
|
+
"if (Test-Path -LiteralPath $stamp) { Write-Output ('GIT_OK ' + [String](Get-Content -LiteralPath $stamp -TotalCount 1 -ErrorAction SilentlyContinue)) } else { Write-Output 'GIT_OK' }"
|
|
161
|
+
].join('\n')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 快照:git add -A 增量同步 index(.gitignore/exclude 语义由 git 统一处理),
|
|
165
|
+
// write-tree 生成树、commit-tree 生成无父孤儿提交、tag 保对象可达。
|
|
166
|
+
// 不做 parent 链、不修剪:像 TraeWork 一样保留全量历史,tag 永远可查。
|
|
167
|
+
// tag -f:事件重放/重发会产生重复 messageId,裸 tag 对已存在 tag fatal
|
|
168
|
+
// 导致整条快照失败;commit-tree 每次生成新对象,-f 把 tag 指到最新提交,
|
|
169
|
+
// 与「同一条消息重快照取最新状态」的语义一致。
|
|
170
|
+
export function snapshotScript(root, store, gitExe, messageId, base) {
|
|
171
|
+
return [
|
|
172
|
+
"$ErrorActionPreference = 'Stop'",
|
|
173
|
+
'$git = ' + psq(gitExe),
|
|
174
|
+
'$g = ' + psq(store.git),
|
|
175
|
+
'$root = ' + psq(root),
|
|
176
|
+
dropGitlinksBlock(),
|
|
177
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
178
|
+
'& $git --git-dir=$g --work-tree=$root add -A',
|
|
179
|
+
dropGitlinksBlock(),
|
|
180
|
+
oversizeBlock(store.maxFileBytes),
|
|
181
|
+
'$tree = (& $git --git-dir=$g --work-tree=$root write-tree).Trim()',
|
|
182
|
+
"$commit = (& $git --git-dir=$g -c user.name=dsh-recall -c user.email=recall@dsh.local commit-tree $tree -m ('snapshot ' + " + psq(messageId) + ")).Trim()",
|
|
183
|
+
'& $git --git-dir=$g tag -f ' + psq('snap-' + messageId) + ' $commit | Out-Null',
|
|
184
|
+
"Write-Output 'SNAP_OK'"
|
|
185
|
+
].join('\n')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// diff:把当前状态 add 进 index 后用 ls-files --stage 取当前清单,
|
|
189
|
+
// 与目标 tag 的 ls-tree 对比——ignore/exclude 语义两侧一致,不会把
|
|
190
|
+
// node_modules 等误报为“新增”。
|
|
191
|
+
// 不用 -z:PowerShell 捕获原生命令输出会丢弃含 NUL 的行(实测整段变 null),
|
|
192
|
+
// 改用 core.quotePath=false 让非 ASCII 路径原样输出,逐行按 TAB 解析。
|
|
193
|
+
// 代价是文件名含换行的极端情况会解析错乱——概率可忽略,记录为已知限制。
|
|
194
|
+
// (UTF-8 输出编码由 runShell 注入的 UTF8_PRELUDE 统一保证,此处不再重复设置。)
|
|
195
|
+
export function diffScript(root, store, gitExe, tag, base) {
|
|
196
|
+
return [
|
|
197
|
+
"$ErrorActionPreference = 'Stop'",
|
|
198
|
+
'$git = ' + psq(gitExe),
|
|
199
|
+
'$g = ' + psq(store.git),
|
|
200
|
+
'$root = ' + psq(root),
|
|
201
|
+
dropGitlinksBlock(),
|
|
202
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
203
|
+
'& $git --git-dir=$g --work-tree=$root add -A',
|
|
204
|
+
dropGitlinksBlock(),
|
|
205
|
+
oversizeBlock(store.maxFileBytes),
|
|
206
|
+
'$curOut = & $git -c core.quotePath=false --git-dir=$g --work-tree=$root ls-files --stage',
|
|
207
|
+
// 旧 tag 的树里可能仍有 gitlink(修复前留下的),从目标侧一并剔除,
|
|
208
|
+
// 否则 diff 会报出“恢复 dsh-recall-plugin”这类幻影条目
|
|
209
|
+
"$targetOut = @(& $git -c core.quotePath=false --git-dir=$g ls-tree -r " + psq(tag) + " | Where-Object { -not $_.StartsWith('160000') })",
|
|
210
|
+
'$curMap = @{}',
|
|
211
|
+
'foreach ($r in @($curOut)) {',
|
|
212
|
+
' if (-not $r) { continue }',
|
|
213
|
+
' $tab = $r.IndexOf("`t"); $path = $r.Substring($tab + 1)',
|
|
214
|
+
' $sha = ($r.Substring(0, $tab) -split " ")[1]',
|
|
215
|
+
' $curMap[$path] = $sha',
|
|
216
|
+
'}',
|
|
217
|
+
'$targetMap = @{}',
|
|
218
|
+
'foreach ($r in @($targetOut)) {',
|
|
219
|
+
' if (-not $r) { continue }',
|
|
220
|
+
' $tab = $r.IndexOf("`t"); $path = $r.Substring($tab + 1)',
|
|
221
|
+
' $sha = ($r.Substring(0, $tab) -split " ")[2]',
|
|
222
|
+
' $targetMap[$path] = $sha',
|
|
223
|
+
'}',
|
|
224
|
+
'$result = @()',
|
|
225
|
+
'foreach ($k in $curMap.Keys) {',
|
|
226
|
+
' if (-not $targetMap.ContainsKey($k)) { $result += [pscustomobject]@{ rel = $k; kind = "added" } }',
|
|
227
|
+
' elseif ($targetMap[$k] -ne $curMap[$k]) { $result += [pscustomobject]@{ rel = $k; kind = "modified" } }',
|
|
228
|
+
'}',
|
|
229
|
+
'foreach ($k in $targetMap.Keys) {',
|
|
230
|
+
' if (-not $curMap.ContainsKey($k)) { $result += [pscustomobject]@{ rel = $k; kind = "restored" } }',
|
|
231
|
+
'}',
|
|
232
|
+
'$sorted = @($result | Sort-Object rel)',
|
|
233
|
+
'Write-Output (ConvertTo-Json -InputObject $sorted -Depth 3 -Compress)'
|
|
234
|
+
].join('\n')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 回退:恢复侧走 git archive --format=zip + Expand-Archive,删除侧按
|
|
238
|
+
// 清单移除「当前有、目标无」的文件。曾尝试 tar 优先(bsdtar 性能更好),
|
|
239
|
+
// 实测否决:System32\bsdtar 在 GBK 活动代码页(ACP=936)机器上把 tar
|
|
240
|
+
// 流里的 UTF-8 文件名按 ANSI 解码——中文文件名解包成「璇存槑.txt」式的
|
|
241
|
+
// 乱码新文件,原路径反而丢失;且 tar -m 才是必需的(stat 缓存碰撞),
|
|
242
|
+
// Expand-Archive 天然把 mtime 设为解包时刻,zip 链路反而更稳。
|
|
243
|
+
// 空树跳过 archive(空 zip 会让 Expand-Archive 报错),只执行删除。
|
|
244
|
+
// 回退后保留快照 tag 与索引:git delta 空间便宜,保留历史可再次
|
|
245
|
+
// 用该快照恢复(幂等),也避免误回退后无法找回。
|
|
246
|
+
export function rollbackScript(root, store, gitExe, tag, base) {
|
|
247
|
+
return [
|
|
248
|
+
"$ErrorActionPreference = 'Stop'",
|
|
249
|
+
'$git = ' + psq(gitExe),
|
|
250
|
+
'$g = ' + psq(store.git),
|
|
251
|
+
'$root = ' + psq(root),
|
|
252
|
+
dropGitlinksBlock(),
|
|
253
|
+
excludeSyncBlock(store.excludeFile, base),
|
|
254
|
+
'& $git --git-dir=$g --work-tree=$root add -A',
|
|
255
|
+
dropGitlinksBlock(),
|
|
256
|
+
oversizeBlock(store.maxFileBytes),
|
|
257
|
+
// 同 diffScript:-z 的 NUL 输出会被 PowerShell 捕获丢弃,改为逐行 + quotePath=false
|
|
258
|
+
'$curOut = & $git -c core.quotePath=false --git-dir=$g --work-tree=$root ls-files --stage',
|
|
259
|
+
"$targetOut = @(& $git -c core.quotePath=false --git-dir=$g ls-tree -r " + psq(tag) + " | Where-Object { -not $_.StartsWith('160000') })",
|
|
260
|
+
'$targetMap = @{}',
|
|
261
|
+
'foreach ($r in @($targetOut)) {',
|
|
262
|
+
' if (-not $r) { continue }',
|
|
263
|
+
' $tab = $r.IndexOf("`t"); $path = $r.Substring($tab + 1)',
|
|
264
|
+
' $targetMap[$path] = $true',
|
|
265
|
+
'}',
|
|
266
|
+
'$restored = $targetMap.Count',
|
|
267
|
+
'if ($restored -gt 0) {',
|
|
268
|
+
' $zip = ' + psq(store.dir + '\\restore-tmp.zip'),
|
|
269
|
+
' & $git --git-dir=$g archive --format=zip --output=$zip ' + psq(tag),
|
|
270
|
+
' Expand-Archive -LiteralPath $zip -DestinationPath $root -Force',
|
|
271
|
+
' Remove-Item -LiteralPath $zip -Force',
|
|
272
|
+
'}',
|
|
273
|
+
'$deleted = 0',
|
|
274
|
+
'foreach ($r in @($curOut)) {',
|
|
275
|
+
' if (-not $r) { continue }',
|
|
276
|
+
' $tab = $r.IndexOf("`t"); $path = $r.Substring($tab + 1)',
|
|
277
|
+
' if (-not $targetMap.ContainsKey($path)) {',
|
|
278
|
+
" $full = Join-Path $root ($path.Replace('/','\\'))",
|
|
279
|
+
' if (Test-Path -LiteralPath $full) { Remove-Item -LiteralPath $full -Force; $deleted++ }',
|
|
280
|
+
' }',
|
|
281
|
+
'}',
|
|
282
|
+
"Write-Output ('ROLLBACK_OK ' + $deleted + ' ' + $restored)"
|
|
283
|
+
].join('\n')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function listTagsScript(store, gitExe) {
|
|
287
|
+
return [
|
|
288
|
+
"$ErrorActionPreference = 'Stop'",
|
|
289
|
+
'$git = ' + psq(gitExe),
|
|
290
|
+
'$g = ' + psq(store.git),
|
|
291
|
+
'& $git --git-dir=$g tag -l "snap-*"'
|
|
292
|
+
].join('\n')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 定期 gc:全量保留策略下对象只增不减,且默认 loose 存储(每对象一个
|
|
296
|
+
// 小文件,NTFS 最小簇 4KB)非常浪费;gc 压 pack + 跨版本 delta 通常省一半
|
|
297
|
+
// 以上。--prune=now 让「会话删除联动清理」删掉的 tag 立即真正释放空间
|
|
298
|
+
// (默认 2 周宽限期内对象仍占盘)——安全前提是 gc 与快照在同一条串行
|
|
299
|
+
// 队列里执行(见 maintenance.js),不存在并发竞态。
|
|
300
|
+
// 结尾写 gc.stamp:跨重启的节流凭据(ensureGit 回读)。
|
|
301
|
+
export function gcScript(store, gitExe) {
|
|
302
|
+
return [
|
|
303
|
+
"$ErrorActionPreference = 'Stop'",
|
|
304
|
+
'$git = ' + psq(gitExe),
|
|
305
|
+
'$g = ' + psq(store.git),
|
|
306
|
+
'& $git --git-dir=$g gc --quiet --prune=now',
|
|
307
|
+
"Set-Content -LiteralPath (Join-Path $g 'gc.stamp') -Value ([DateTimeOffset]::UtcNow.ToUnixTimeSeconds().ToString()) -Encoding ascii",
|
|
308
|
+
"Write-Output 'GC_OK'"
|
|
309
|
+
].join('\n')
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 删除指定快照 tag(会话已删联动清理用)。best-effort:个别 tag 已不存在时
|
|
313
|
+
// git 非零退出,但其余 tag 已被删除——所以显式 exit 0 吞掉退出码,
|
|
314
|
+
// 残留的由下一次清理幂等地收尾;JS 侧无论脚本结果都会同步索引。
|
|
315
|
+
export function purgeTagsScript(store, gitExe, tags) {
|
|
316
|
+
return [
|
|
317
|
+
"$ErrorActionPreference = 'Stop'",
|
|
318
|
+
'$git = ' + psq(gitExe),
|
|
319
|
+
'$g = ' + psq(store.git),
|
|
320
|
+
'& $git --git-dir=$g tag -d ' + tags.map((t) => psq(t)).join(' '),
|
|
321
|
+
"Write-Output 'PURGE_DONE'",
|
|
322
|
+
'exit 0'
|
|
323
|
+
].join('\n')
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 任意长度文本写入(index.json / exclude.txt 共用):base64 以内联字面量
|
|
327
|
+
// 传递时受 Windows 命令行 32767 字符硬上限约束(DSH 的 pwsh 执行器把命令串
|
|
328
|
+
// 作为 -Command 的单个 argv 元素 spawn),快照攒到几百条就超限 spawn 失败
|
|
329
|
+
// ——按 20000 字符分块(分块循环在 store.js writeTextViaShell),首块
|
|
330
|
+
// Set-Content 覆盖、续块 Add-Content 追加。piece 为当前块的 base64 解码
|
|
331
|
+
// 表达式,first 决定覆盖还是追加。目录创建归调用方(writeExclude 的
|
|
332
|
+
// mkdirScript 兜底 / index.json 的父目录在建仓时已存在)。
|
|
333
|
+
export function fileWriteCmd(file, piece, first) {
|
|
334
|
+
if (first) {
|
|
335
|
+
return piece + 'Set-Content -LiteralPath ' + psq(file) + ' -Encoding utf8 -NoNewline'
|
|
336
|
+
}
|
|
337
|
+
return piece + 'Add-Content -LiteralPath ' + psq(file) + ' -Encoding utf8 -NoNewline'
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function indexReadCmd(dir) {
|
|
341
|
+
return 'Get-Content -LiteralPath ' + psq(dir + '\\index.json') + ' -Raw -ErrorAction SilentlyContinue'
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 旧版项目内 blobs 目录清理(仅 home 存储可用时调用,见 store.js cleanupLegacy)
|
|
345
|
+
export function legacyRmScript(path) {
|
|
346
|
+
return 'Remove-Item -Recurse -Force -LiteralPath ' + psq(path)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// exclude.txt 原文读取(设置页编辑用):-Raw 保留换行与空行结构,让用户
|
|
350
|
+
// 看到的就是落盘原文;文件不存在时 SilentlyContinue 输出空串,JS 侧按
|
|
351
|
+
// 「尚未配置」处理——设置页在快照存储刚建好、exclude.txt 还没写过时也会打开。
|
|
352
|
+
export function excludeReadCmd(file) {
|
|
353
|
+
return 'Get-Content -LiteralPath ' + psq(file) + ' -Raw -Encoding UTF8 -ErrorAction SilentlyContinue'
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 目录存在探测:输出定长 YES/NO 标记(与 posix 版逐字同语义),
|
|
357
|
+
// JS 侧统一按 'YES' 判定,不依赖退出码——runShell 对非零退出直接抛错。
|
|
358
|
+
export function dirExistsScript(dir) {
|
|
359
|
+
return "if (Test-Path -LiteralPath " + psq(dir) + " -PathType Container) { Write-Output 'YES' } else { Write-Output 'NO' }"
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 影子仓库磁盘占用(设置页快照管理卡片用):git 自带的 count-objects -v
|
|
363
|
+
// 输出含 size-pack(KiB 单位,pack 文件总大小),足够向用户展示量级,
|
|
364
|
+
// 不需要逐文件累加的慢扫描。
|
|
365
|
+
export function countObjectsScript(store, gitExe) {
|
|
366
|
+
return [
|
|
367
|
+
'$git = ' + psq(gitExe),
|
|
368
|
+
'$g = ' + psq(store.git),
|
|
369
|
+
'& $git --git-dir=$g count-objects -v'
|
|
370
|
+
].join('\n')
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 目录总大小(字节):降级存储时 git-dir 不是唯一占用(blobs 残留等),
|
|
374
|
+
// 直接量目录最诚实。SilentlyContinue 容忍个别不可读文件。
|
|
375
|
+
export function diskUsageScript(dir) {
|
|
376
|
+
return "(Get-ChildItem -LiteralPath " + psq(dir) + " -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum"
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 列目录下所有一级子目录全路径:manage/list 枚举 home 容器下的所有
|
|
380
|
+
// 哈希子目录用(每个子目录是一个工作区的 store)。SilentlyContinue
|
|
381
|
+
// 容忍个别不可读条目。输出每行一个全路径,JS 侧按换行拆分。
|
|
382
|
+
export function listSubdirsScript(dir) {
|
|
383
|
+
return "Get-ChildItem -LiteralPath " + psq(dir) + " -Directory -ErrorAction SilentlyContinue | ForEach-Object { Write-Output $_.FullName }"
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 批量 dump 全部 store 的元数据:一条 shell 拿「容器下所有子目录 +
|
|
387
|
+
// 额外降级目录」的 root.txt 与 index.json 全文。为什么批量:旧实现
|
|
388
|
+
// 每个目录 2 条 shell(读 index + 读 root.txt)串行跑,20 个目录就是
|
|
389
|
+
// 40 次 PowerShell 冷启动(每次 0.3-1s)——快照管理列表 20 秒级慢的
|
|
390
|
+
// 根因。输出定界格式(JS 侧 parseStoresDump 状态机解析):
|
|
391
|
+
// ==DIR <目录>
|
|
392
|
+
// ROOT <工作区路径或空>
|
|
393
|
+
// INDEXBEGIN / index.json 原文 / INDEXEND
|
|
394
|
+
// 标记行不会与内容混淆:root 路径不含换行(Windows 非法字符),JSON
|
|
395
|
+
// 单行以 [ 或 { 起头、内部路径同样不含换行。
|
|
396
|
+
export function storesDumpScript(container, extraDirs) {
|
|
397
|
+
const lines = [
|
|
398
|
+
"$ErrorActionPreference = 'Stop'",
|
|
399
|
+
'$dirs = @()'
|
|
400
|
+
]
|
|
401
|
+
if (container) {
|
|
402
|
+
lines.push('$base = ' + psq(container))
|
|
403
|
+
lines.push("$dirs += @(Get-ChildItem -LiteralPath $base -Directory -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName })")
|
|
404
|
+
}
|
|
405
|
+
for (const d of extraDirs || []) lines.push('$dirs += ' + psq(d))
|
|
406
|
+
lines.push(
|
|
407
|
+
'foreach ($d in $dirs) {',
|
|
408
|
+
' if (-not $d) { continue }',
|
|
409
|
+
' if (-not (Test-Path -LiteralPath $d -PathType Container)) { continue }',
|
|
410
|
+
' Write-Output ("==DIR " + $d)',
|
|
411
|
+
" $rt = Join-Path $d 'root.txt'",
|
|
412
|
+
" if (Test-Path -LiteralPath $rt -PathType Leaf) {",
|
|
413
|
+
" $rv = (Get-Content -LiteralPath $rt -Raw -Encoding UTF8 -ErrorAction SilentlyContinue)",
|
|
414
|
+
// 先规整成单行再拼接:root 是单行路径,但用户手改文件可能带 CRLF,
|
|
415
|
+
// 直接拼会把标记结构打乱
|
|
416
|
+
" if ($rv) { $rv = $rv.Trim() }",
|
|
417
|
+
" Write-Output ('ROOT ' + $rv)",
|
|
418
|
+
' } else {',
|
|
419
|
+
" Write-Output 'ROOT '",
|
|
420
|
+
' }',
|
|
421
|
+
" Write-Output 'INDEXBEGIN'",
|
|
422
|
+
" $ix = Join-Path $d 'index.json'",
|
|
423
|
+
" if (Test-Path -LiteralPath $ix -PathType Leaf) {",
|
|
424
|
+
' $j = Get-Content -LiteralPath $ix -Raw -Encoding UTF8 -ErrorAction SilentlyContinue',
|
|
425
|
+
' if ($j) { Write-Output $j.TrimEnd() }',
|
|
426
|
+
' }',
|
|
427
|
+
" Write-Output 'INDEXEND'",
|
|
428
|
+
'}',
|
|
429
|
+
'exit 0'
|
|
430
|
+
)
|
|
431
|
+
return lines.join('\n')
|
|
432
|
+
}
|