dsh-autorestart 0.6.2 → 0.7.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.
@@ -0,0 +1,302 @@
1
+ <#
2
+ .SYNOPSIS
3
+ dsh-autorestart 的急救开关:DSH 起不来的时候,不用 DSH 也能把本插件摘掉(或装回去)。
4
+
5
+ .DESCRIPTION
6
+ “刚打开插件,DSH 就起不来了” 是插件最难受的时刻:修它需要它自己。
7
+ 这个脚本只动一个文件 —— 桌面版 profile 的 package.json —— 而且只动一个数组:
8
+ `dsh.profile.bundles`。
9
+
10
+ - disable:从 bundles 里拿掉 "dsh-autorestart"(本来就不在 → 报告“已经禁用”,不算失败);
11
+ - enable :把 "dsh-autorestart" 加回 bundles(已经在 → 报告“已经启用”);
12
+ - status :只读,打印现在在不在 bundles 里。
13
+
14
+ 两条硬规矩:
15
+
16
+ 1. 写之前先备份成 `package.json.bak-emergency-<yyyyMMdd-HHmmss>`;
17
+ 2. 写完之后重新解析一遍 JSON 校验;校验不过就用备份回滚,并且如实报错。
18
+
19
+ 文件的缩进、行尾和末尾换行都原样保留(改的是文本,不是重新序列化整个 JSON)。
20
+
21
+ .EXAMPLE
22
+ powershell -NoProfile -ExecutionPolicy Bypass -File .\emergency-toggle.ps1 -Action status
23
+
24
+ .EXAMPLE
25
+ powershell -NoProfile -ExecutionPolicy Bypass -File .\emergency-toggle.ps1 -Action disable
26
+
27
+ .NOTES
28
+ Windows PowerShell 5.1 就能跑(没有用 6.0+ 的语法),双击的 .cmd 入口也是它。
29
+ #>
30
+ [CmdletBinding()]
31
+ param(
32
+ # 要做什么。
33
+ [ValidateSet('disable', 'enable', 'status')]
34
+ [string] $Action = 'status',
35
+
36
+ # 只打印“将会怎么写”,不落盘。
37
+ [switch] $DryRun,
38
+
39
+ # 要操作的 package.json(测试和别的 profile 用)。
40
+ [string] $PackageJson = (Join-Path $env:USERPROFILE '.dsh\profiles\desktop\package.json')
41
+ )
42
+
43
+ $ErrorActionPreference = 'Stop'
44
+
45
+ # 本插件在 bundles 里的名字。
46
+ $PackageName = 'dsh-autorestart'
47
+
48
+ # 装回去时插在它后面(位置其实无所谓,但有个锚点更稳)。
49
+ $AnchorName = 'dsh-github-guard'
50
+
51
+ function Say {
52
+ param([string] $Text, [string] $Color = 'Gray')
53
+ Write-Host $Text -ForegroundColor $Color
54
+ }
55
+
56
+ function Fail {
57
+ param([string] $Text)
58
+ Write-Host ('急救开关失败:' + $Text) -ForegroundColor Red
59
+ exit 1
60
+ }
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # 文本工具:只在 bundles 数组那一段里动刀,其余字节一个不碰。
64
+ # ---------------------------------------------------------------------------
65
+
66
+ # bundles 数组的方括号位置。
67
+ function Get-ArrayRange {
68
+ param([string] $Text)
69
+ $marker = '"bundles"'
70
+ $markerIndex = $Text.IndexOf($marker)
71
+ if ($markerIndex -lt 0) { throw 'package.json 里没有 bundles 这一项' }
72
+ $open = $Text.IndexOf('[', $markerIndex)
73
+ if ($open -lt 0) { throw 'bundles 不是数组' }
74
+ $depth = 0
75
+ $inString = $false
76
+ $escaped = $false
77
+ for ($i = $open; $i -lt $Text.Length; $i++) {
78
+ $c = $Text[$i]
79
+ if ($inString) {
80
+ if ($escaped) { $escaped = $false; continue }
81
+ if ($c -eq '\') { $escaped = $true; continue }
82
+ if ($c -eq '"') { $inString = $false }
83
+ continue
84
+ }
85
+ if ($c -eq '"') { $inString = $true; continue }
86
+ if ($c -eq '[') { $depth++; continue }
87
+ if ($c -eq ']') {
88
+ $depth--
89
+ if ($depth -eq 0) { return @($open, $i) }
90
+ }
91
+ }
92
+ throw 'bundles 数组没有闭合'
93
+ }
94
+
95
+ # 数组里的每一项:名字、缩进、有没有逗号、它在文本里的位置。
96
+ function Get-Items {
97
+ param([string] $Text, [int] $Open, [int] $Close)
98
+ $body = $Text.Substring($Open + 1, $Close - $Open - 1)
99
+ $items = @()
100
+ $pattern = '(?m)^(?<indent>[ \t]*)"(?<name>[^"]*)"(?<comma>,?)[ \t]*\r?$'
101
+ foreach ($match in [regex]::Matches($body, $pattern)) {
102
+ $lineStart = $Open + 1 + $match.Index
103
+ $lineEnd = $lineStart + $match.Length
104
+ # 这一行末尾的行尾符(\r\n 或 \n)算在行里。
105
+ if ($lineEnd -lt $Text.Length -and $Text[$lineEnd] -eq "`n") { $lineEnd++ }
106
+ $nameGroup = $match.Groups['name']
107
+ $commaGroup = $match.Groups['comma']
108
+ $items += [pscustomobject]@{
109
+ Name = $nameGroup.Value
110
+ Indent = $match.Groups['indent'].Value
111
+ HasComma = ($commaGroup.Value -eq ',')
112
+ CommaIndex = $commaGroup.Index
113
+ # 名字后面那个右引号之后的位置(插逗号用)。
114
+ ContentEnd = $nameGroup.Index + $nameGroup.Length + 1
115
+ LineStart = $lineStart
116
+ LineEnd = $lineEnd
117
+ }
118
+ }
119
+ return $items
120
+ }
121
+
122
+ # 某一行的缩进(用来插出同样式子的新行)。
123
+ function Get-LineIndent {
124
+ param([string] $Text, [int] $Index)
125
+ $start = $Text.LastIndexOf("`n", [Math]::Max(0, $Index - 1))
126
+ if ($start -lt 0) { $start = 0 } else { $start++ }
127
+ $indent = ''
128
+ for ($i = $start; $i -lt $Text.Length; $i++) {
129
+ $c = $Text[$i]
130
+ if ($c -eq ' ' -or $c -eq "`t") { $indent += $c; continue }
131
+ break
132
+ }
133
+ return $indent
134
+ }
135
+
136
+ # 文件用的行尾符。
137
+ function Get-LineEnding {
138
+ param([string] $Text)
139
+ if ($Text.Contains("`r`n")) { return "`r`n" }
140
+ return "`n"
141
+ }
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # 开始。
145
+ # ---------------------------------------------------------------------------
146
+
147
+ if (-not (Test-Path -LiteralPath $PackageJson -PathType Leaf)) {
148
+ Fail ('找不到 profile 的 package.json:' + $PackageJson)
149
+ }
150
+
151
+ $path = (Resolve-Path -LiteralPath $PackageJson).Path
152
+ $bytes = [System.IO.File]::ReadAllBytes($path)
153
+ $hasBom = ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF)
154
+ $raw = [System.IO.File]::ReadAllText($path)
155
+
156
+ try {
157
+ $document = $raw | ConvertFrom-Json
158
+ } catch {
159
+ Fail ('这个 package.json 不是合法的 JSON:' + $_.Exception.Message)
160
+ }
161
+
162
+ $bundles = @()
163
+ $hasBundles = $false
164
+ if ($null -ne $document -and $null -ne $document.dsh -and $null -ne $document.dsh.profile -and $null -ne $document.dsh.profile.bundles) {
165
+ $bundles = @($document.dsh.profile.bundles)
166
+ $hasBundles = $true
167
+ }
168
+
169
+ $present = ($bundles -contains $PackageName)
170
+
171
+ Say ('文件:' + $path)
172
+
173
+ if ($Action -eq 'status') {
174
+ if ($present) { Say ('已启用:' + $PackageName + ' 在 bundles 里。') 'Green' }
175
+ else { Say ('已禁用:' + $PackageName + ' 不在 bundles 里。') 'Yellow' }
176
+ exit 0
177
+ }
178
+
179
+ if ($Action -eq 'enable' -and -not $hasBundles) {
180
+ Fail '这个 profile 里没有 dsh.profile.bundles 列表,装不回去(请手动加一行 "dsh-autorestart")。'
181
+ }
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # 算出新的文本。
185
+ # ---------------------------------------------------------------------------
186
+
187
+ $new = $raw
188
+ $range = Get-ArrayRange $raw
189
+ $items = Get-Items $raw $range[0] $range[1]
190
+ $eol = Get-LineEnding $raw
191
+
192
+ if ($Action -eq 'disable') {
193
+ if (-not $present) {
194
+ Say ('已经禁用:bundles 里本来就没有 ' + $PackageName + '(没有改动任何文件)。') 'Yellow'
195
+ exit 0
196
+ }
197
+ $target = $items | Where-Object { $_.Name -eq $PackageName } | Select-Object -First 1
198
+ if ($null -eq $target) { Fail ('在 bundles 里找不到 ' + $PackageName) }
199
+ $shift = 0
200
+ if (-not $target.HasComma) {
201
+ # 它是数组里的最后一项:前一项的逗号要去掉,否则 JSON 非法。
202
+ $previous = $items | Where-Object { $_.LineEnd -le $target.LineStart } | Select-Object -Last 1
203
+ if ($null -ne $previous -and $previous.HasComma) {
204
+ $new = $new.Remove($previous.CommaIndex, 1)
205
+ $shift = -1
206
+ }
207
+ }
208
+ $new = $new.Remove($target.LineStart + $shift, $target.LineEnd - $target.LineStart)
209
+ $summary = ('把 ' + $PackageName + ' 从 bundles 里拿掉')
210
+ } else {
211
+ # enable
212
+ if ($present) {
213
+ Say ('已经启用:' + $PackageName + ' 本来就在 bundles 里(没有改动任何文件)。') 'Yellow'
214
+ exit 0
215
+ }
216
+ $anchor = $items | Where-Object { $_.Name -eq $AnchorName } | Select-Object -First 1
217
+ $line = ''
218
+ $insertAt = 0
219
+ if ($null -ne $anchor) {
220
+ if ($anchor.HasComma) {
221
+ $insertAt = $anchor.LineEnd
222
+ $line = $anchor.Indent + '"' + $PackageName + '",'
223
+ } else {
224
+ # 锚点是最后一项:先给它补上逗号,再插新的一项(新的这项不带逗号)。
225
+ $new = $new.Insert($anchor.ContentEnd, ',')
226
+ $insertAt = $anchor.LineEnd + 1
227
+ $line = $anchor.Indent + '"' + $PackageName + '"'
228
+ }
229
+ } else {
230
+ $last = $items | Select-Object -Last 1
231
+ if ($null -ne $last) {
232
+ if ($last.HasComma) {
233
+ $insertAt = $last.LineEnd
234
+ } else {
235
+ $new = $new.Insert($last.ContentEnd, ',')
236
+ $insertAt = $last.LineEnd + 1
237
+ }
238
+ $line = $last.Indent + '"' + $PackageName + '"'
239
+ } else {
240
+ # 数组是空的。
241
+ $insertAt = $range[1]
242
+ $line = (Get-LineIndent $raw $range[1]) + ' "' + $PackageName + '"'
243
+ }
244
+ }
245
+ $new = $new.Insert($insertAt, $line + $eol)
246
+ $summary = ('把 ' + $PackageName + ' 加回 bundles')
247
+ }
248
+
249
+ if ($DryRun) {
250
+ Say ('[试运行] 将会' + $summary + '。') 'Cyan'
251
+ Say ('[试运行] 将会写入:' + $path) 'Cyan'
252
+ Say ('[试运行] 将会先备份成:' + $path + '.bak-emergency-<时间戳>') 'Cyan'
253
+ Say ('[试运行] 没有改动任何文件。') 'Cyan'
254
+ exit 0
255
+ }
256
+
257
+ # ---------------------------------------------------------------------------
258
+ # 写:先备份,再写,然后重新解析校验;校验不过就回滚。
259
+ # ---------------------------------------------------------------------------
260
+
261
+ $backup = $path + '.bak-emergency-' + (Get-Date -Format 'yyyyMMdd-HHmmss')
262
+ try {
263
+ Copy-Item -LiteralPath $path -Destination $backup -Force
264
+ } catch {
265
+ Fail ('备份失败(没有动原文件):' + $_.Exception.Message)
266
+ }
267
+
268
+ try {
269
+ $encoding = New-Object System.Text.UTF8Encoding($hasBom)
270
+ [System.IO.File]::WriteAllText($path, $new, $encoding)
271
+ } catch {
272
+ Fail ('写入失败(备份在 ' + $backup + '):' + $_.Exception.Message)
273
+ }
274
+
275
+ try {
276
+ $written = [System.IO.File]::ReadAllText($path)
277
+ $parsed = $written | ConvertFrom-Json
278
+ $names = @()
279
+ if ($null -ne $parsed -and $null -ne $parsed.dsh -and $null -ne $parsed.dsh.profile -and $null -ne $parsed.dsh.profile.bundles) {
280
+ $names = @($parsed.dsh.profile.bundles)
281
+ }
282
+ if ($Action -eq 'enable' -and -not ($names -contains $PackageName)) {
283
+ throw '写完之后 bundles 里仍然没有 ' + $PackageName
284
+ }
285
+ if ($Action -eq 'disable' -and ($names -contains $PackageName)) {
286
+ throw '写完之后 bundles 里仍然有 ' + $PackageName
287
+ }
288
+ } catch {
289
+ # 校验不过:用备份把原文件放回去,然后如实报错。
290
+ try {
291
+ Copy-Item -LiteralPath $backup -Destination $path -Force
292
+ Say ('已经用备份回滚:' + $backup) 'Yellow'
293
+ } catch {
294
+ Fail ('校验失败,而且回滚也失败了(备份在 ' + $backup + '):' + $_.Exception.Message)
295
+ }
296
+ Fail ('校验失败(已回滚):' + $_.Exception.Message)
297
+ }
298
+
299
+ Say ('已经' + $summary + '。') 'Green'
300
+ Say ('备份:' + $backup) 'Gray'
301
+ Say ('请重启 DSH 才会生效。') 'Gray'
302
+ exit 0
@@ -0,0 +1,46 @@
1
+ # dsh-autorestart — run the plugin's node:test suite with the DSH bundled Node.
2
+ #
3
+ # There is no `node` on PATH in the DSH desktop environment, so this script uses
4
+ # the runtime that ships with the app. Usage (from anywhere):
5
+ #
6
+ # pwsh -NoProfile -File plugins/dsh-autorestart/scripts/run-tests.ps1
7
+ #
8
+ # Optional args:
9
+ # -NodeExe <path> override the bundled node binary
10
+ # -TestGlob <glob> test file glob (default: <plugin>/test/*.test.mjs)
11
+ #
12
+ # NOTE: Node >= 24 treats positional --test arguments as glob patterns, so pass
13
+ # a glob (or explicit files), not a directory. `node --test <dir>` fails with
14
+ # MODULE_NOT_FOUND on v24.
15
+
16
+ param(
17
+ [string]$NodeExe = 'C:\Users\Administrator\AppData\Local\Programs\DeepSeek Harness\resources\runtime\primary-runtime\dependencies\node\bin\node.exe',
18
+ [string]$TestGlob = ''
19
+ )
20
+
21
+ $ErrorActionPreference = 'Stop'
22
+ $pluginDir = Split-Path -Parent $PSScriptRoot
23
+
24
+ if (-not (Test-Path -LiteralPath $NodeExe)) {
25
+ throw "Bundled Node not found: $NodeExe"
26
+ }
27
+
28
+ $pattern = if ($TestGlob) { $TestGlob } else { Join-Path $pluginDir 'test\*.test.mjs' }
29
+
30
+ Write-Host 'dsh-autorestart tests'
31
+ Write-Host " node : $NodeExe"
32
+ Write-Host " tests: $pattern"
33
+ Write-Host ''
34
+
35
+ & $NodeExe --test $pattern
36
+ $exitCode = $LASTEXITCODE
37
+
38
+ if ($exitCode -ne 0) {
39
+ Write-Host ''
40
+ Write-Host "TESTS FAILED (exit code $exitCode)" -ForegroundColor Red
41
+ exit $exitCode
42
+ }
43
+
44
+ Write-Host ''
45
+ Write-Host 'TESTS PASSED' -ForegroundColor Green
46
+ exit 0