dsh-autorestart 0.7.0 → 0.8.1

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.
@@ -1,22 +1,37 @@
1
- <#
1
+ <#
2
2
  .SYNOPSIS
3
- dsh-autorestart 的急救开关:DSH 起不来的时候,不用 DSH 也能把本插件摘掉(或装回去)。
3
+ The dsh-autorestart emergency switch: take this plugin out (or put it back)
4
+ when DSH does not come up.
4
5
 
5
6
  .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)。
7
+ "I enabled the plugin and DSH does not start any more" is the hardest moment
8
+ for a plugin: fixing it needs the plugin itself. This script touches ONE
9
+ file - the package.json of the desktop profile - and ONE array in it:
10
+ `dsh.profile.bundles`.
11
+
12
+ - disable: take "dsh-autorestart" out of the bundles (it was not there
13
+ anyway -> report "already disabled", that is not a failure);
14
+ - enable : put "dsh-autorestart" back into the bundles (it was already
15
+ there -> report "already enabled");
16
+ - status : read only, prints whether it is in the bundles right now.
17
+
18
+ Two hard rules:
19
+
20
+ 1. the file is backed up as `package.json.bak-emergency-<yyyyMMdd-HHmmss>`
21
+ before anything is written;
22
+ 2. after writing, the JSON is parsed again and checked; if the check
23
+ fails, the backup is put back and the failure is reported honestly.
24
+
25
+ The indentation, the line endings and the trailing newline of the file are
26
+ kept as they are (the text is edited, the JSON is not re-serialized).
27
+
28
+ LANGUAGE: this file is pure ASCII on purpose. Windows PowerShell 5.1 reads a
29
+ UTF-8 file without a byte order mark as ANSI, so one Chinese character
30
+ inside the script turns into mojibake and the script dies with a ParserError
31
+ before it does anything. The Chinese wording lives in
32
+ scripts\rescue-strings.zh.json (UTF-8, no BOM) and is read at runtime; if
33
+ that file is missing or broken, every message falls back to English and the
34
+ script says so.
20
35
 
21
36
  .EXAMPLE
22
37
  powershell -NoProfile -ExecutionPolicy Bypass -File .\emergency-toggle.ps1 -Action status
@@ -25,29 +40,76 @@
25
40
  powershell -NoProfile -ExecutionPolicy Bypass -File .\emergency-toggle.ps1 -Action disable
26
41
 
27
42
  .NOTES
28
- Windows PowerShell 5.1 就能跑(没有用 6.0+ 的语法),双击的 .cmd 入口也是它。
43
+ Windows PowerShell 5.1 is enough (nothing from 6.0+ is used); the .cmd entry
44
+ point double-clicked by the user is that one too.
29
45
  #>
30
46
  [CmdletBinding()]
31
47
  param(
32
- # 要做什么。
48
+ # What to do.
33
49
  [ValidateSet('disable', 'enable', 'status')]
34
50
  [string] $Action = 'status',
35
51
 
36
- # 只打印“将会怎么写”,不落盘。
52
+ # Only print what would be written, change nothing.
37
53
  [switch] $DryRun,
38
54
 
39
- # 要操作的 package.json(测试和别的 profile 用)。
55
+ # The package.json to work on (for tests and for other profiles).
40
56
  [string] $PackageJson = (Join-Path $env:USERPROFILE '.dsh\profiles\desktop\package.json')
41
57
  )
42
58
 
43
59
  $ErrorActionPreference = 'Stop'
44
60
 
45
- # 本插件在 bundles 里的名字。
61
+ # The name of this plugin inside the bundles.
46
62
  $PackageName = 'dsh-autorestart'
47
63
 
48
- # 装回去时插在它后面(位置其实无所谓,但有个锚点更稳)。
64
+ # The anchor it is put back after (the position does not matter, but an anchor
65
+ # is steadier than the end of the array).
49
66
  $AnchorName = 'dsh-github-guard'
50
67
 
68
+ # ---------------------------------------------------------------------------
69
+ # The Chinese strings: read from rescue-strings.zh.json, fall back to English.
70
+ # ---------------------------------------------------------------------------
71
+
72
+ $script:Strings = $null
73
+ $script:StringsError = ''
74
+
75
+ function Initialize-Strings {
76
+ $path = Join-Path $PSScriptRoot 'rescue-strings.zh.json'
77
+ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
78
+ $script:StringsError = (T 'common.stringsMissingEn' 'the Chinese strings file is missing: {0} (falling back to English messages)') -f $path
79
+ return
80
+ }
81
+ try {
82
+ # Read as UTF-8 explicitly: Get-Content would use the console code page.
83
+ $text = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8)
84
+ $script:Strings = $text | ConvertFrom-Json
85
+ } catch {
86
+ $script:StringsError = (T 'common.stringsBrokenEn' 'the Chinese strings file could not be read: {0} (falling back to English messages)') -f $_.Exception.Message
87
+ $script:Strings = $null
88
+ }
89
+ }
90
+
91
+ # Look a string up. The English fallback is right here in the script, so a
92
+ # missing key (or a broken strings file) never stops the rescue.
93
+ function T {
94
+ param([string] $Key, [string] $Fallback)
95
+ if ($null -ne $script:Strings) {
96
+ $node = $script:Strings
97
+ foreach ($part in $Key.Split('.')) {
98
+ if ($null -eq $node) { break }
99
+ $property = $node.PSObject.Properties[$part]
100
+ if ($null -eq $property) { $node = $null; break }
101
+ $node = $property.Value
102
+ }
103
+ if ($null -ne $node) {
104
+ $text = [string] $node
105
+ if ($text -ne '') { return $text }
106
+ }
107
+ }
108
+ return $Fallback
109
+ }
110
+
111
+ Initialize-Strings
112
+
51
113
  function Say {
52
114
  param([string] $Text, [string] $Color = 'Gray')
53
115
  Write-Host $Text -ForegroundColor $Color
@@ -55,22 +117,22 @@ function Say {
55
117
 
56
118
  function Fail {
57
119
  param([string] $Text)
58
- Write-Host ('急救开关失败:' + $Text) -ForegroundColor Red
120
+ Write-Host ((T 'emergency.failed' 'the emergency switch failed: {0}') -f $Text) -ForegroundColor Red
59
121
  exit 1
60
122
  }
61
123
 
62
124
  # ---------------------------------------------------------------------------
63
- # 文本工具:只在 bundles 数组那一段里动刀,其余字节一个不碰。
125
+ # Text helpers: only the bundles array is touched, every other byte stays.
64
126
  # ---------------------------------------------------------------------------
65
127
 
66
- # bundles 数组的方括号位置。
128
+ # The square brackets of the bundles array.
67
129
  function Get-ArrayRange {
68
130
  param([string] $Text)
69
131
  $marker = '"bundles"'
70
132
  $markerIndex = $Text.IndexOf($marker)
71
- if ($markerIndex -lt 0) { throw 'package.json 里没有 bundles 这一项' }
133
+ if ($markerIndex -lt 0) { throw 'package.json has no bundles entry' }
72
134
  $open = $Text.IndexOf('[', $markerIndex)
73
- if ($open -lt 0) { throw 'bundles 不是数组' }
135
+ if ($open -lt 0) { throw 'bundles is not an array' }
74
136
  $depth = 0
75
137
  $inString = $false
76
138
  $escaped = $false
@@ -89,10 +151,11 @@ function Get-ArrayRange {
89
151
  if ($depth -eq 0) { return @($open, $i) }
90
152
  }
91
153
  }
92
- throw 'bundles 数组没有闭合'
154
+ throw 'the bundles array is not closed'
93
155
  }
94
156
 
95
- # 数组里的每一项:名字、缩进、有没有逗号、它在文本里的位置。
157
+ # Every item of the array: the name, the indentation, the comma, the position
158
+ # in the text.
96
159
  function Get-Items {
97
160
  param([string] $Text, [int] $Open, [int] $Close)
98
161
  $body = $Text.Substring($Open + 1, $Close - $Open - 1)
@@ -101,7 +164,7 @@ function Get-Items {
101
164
  foreach ($match in [regex]::Matches($body, $pattern)) {
102
165
  $lineStart = $Open + 1 + $match.Index
103
166
  $lineEnd = $lineStart + $match.Length
104
- # 这一行末尾的行尾符(\r\n 或 \n)算在行里。
167
+ # The line ending of that line (\r\n or \n) belongs to the line.
105
168
  if ($lineEnd -lt $Text.Length -and $Text[$lineEnd] -eq "`n") { $lineEnd++ }
106
169
  $nameGroup = $match.Groups['name']
107
170
  $commaGroup = $match.Groups['comma']
@@ -110,7 +173,8 @@ function Get-Items {
110
173
  Indent = $match.Groups['indent'].Value
111
174
  HasComma = ($commaGroup.Value -eq ',')
112
175
  CommaIndex = $commaGroup.Index
113
- # 名字后面那个右引号之后的位置(插逗号用)。
176
+ # The position right after the closing quote of the name (where the
177
+ # comma goes).
114
178
  ContentEnd = $nameGroup.Index + $nameGroup.Length + 1
115
179
  LineStart = $lineStart
116
180
  LineEnd = $lineEnd
@@ -119,7 +183,7 @@ function Get-Items {
119
183
  return $items
120
184
  }
121
185
 
122
- # 某一行的缩进(用来插出同样式子的新行)。
186
+ # The indentation of a line (so a new line can look the same).
123
187
  function Get-LineIndent {
124
188
  param([string] $Text, [int] $Index)
125
189
  $start = $Text.LastIndexOf("`n", [Math]::Max(0, $Index - 1))
@@ -133,7 +197,7 @@ function Get-LineIndent {
133
197
  return $indent
134
198
  }
135
199
 
136
- # 文件用的行尾符。
200
+ # The line ending the file uses.
137
201
  function Get-LineEnding {
138
202
  param([string] $Text)
139
203
  if ($Text.Contains("`r`n")) { return "`r`n" }
@@ -141,11 +205,15 @@ function Get-LineEnding {
141
205
  }
142
206
 
143
207
  # ---------------------------------------------------------------------------
144
- # 开始。
208
+ # Start.
145
209
  # ---------------------------------------------------------------------------
146
210
 
211
+ if ($script:StringsError -ne '') {
212
+ Write-Host ('WARNING: ' + $script:StringsError) -ForegroundColor Yellow
213
+ }
214
+
147
215
  if (-not (Test-Path -LiteralPath $PackageJson -PathType Leaf)) {
148
- Fail ('找不到 profile 的 package.json:' + $PackageJson)
216
+ Fail ((T 'emergency.notFound' 'the package.json of the profile cannot be found: {0}') -f $PackageJson)
149
217
  }
150
218
 
151
219
  $path = (Resolve-Path -LiteralPath $PackageJson).Path
@@ -156,7 +224,7 @@ $raw = [System.IO.File]::ReadAllText($path)
156
224
  try {
157
225
  $document = $raw | ConvertFrom-Json
158
226
  } catch {
159
- Fail ('这个 package.json 不是合法的 JSON:' + $_.Exception.Message)
227
+ Fail ((T 'emergency.badJson' 'this package.json is not valid JSON: {0}') -f $_.Exception.Message)
160
228
  }
161
229
 
162
230
  $bundles = @()
@@ -168,20 +236,20 @@ if ($null -ne $document -and $null -ne $document.dsh -and $null -ne $document.ds
168
236
 
169
237
  $present = ($bundles -contains $PackageName)
170
238
 
171
- Say ('文件:' + $path)
239
+ Say ((T 'emergency.file' 'file: {0}') -f $path)
172
240
 
173
241
  if ($Action -eq 'status') {
174
- if ($present) { Say ('已启用:' + $PackageName + ' 在 bundles 里。') 'Green' }
175
- else { Say ('已禁用:' + $PackageName + ' 不在 bundles 里。') 'Yellow' }
242
+ if ($present) { Say ((T 'emergency.enabled' 'enabled: {0} is in the bundles.') -f $PackageName) 'Green' }
243
+ else { Say ((T 'emergency.disabled' 'disabled: {0} is not in the bundles.') -f $PackageName) 'Yellow' }
176
244
  exit 0
177
245
  }
178
246
 
179
247
  if ($Action -eq 'enable' -and -not $hasBundles) {
180
- Fail '这个 profile 里没有 dsh.profile.bundles 列表,装不回去(请手动加一行 "dsh-autorestart")。'
248
+ Fail (T 'emergency.noBundles' 'this profile has no dsh.profile.bundles list, so the plugin cannot be put back (please add a line "dsh-autorestart" by hand).')
181
249
  }
182
250
 
183
251
  # ---------------------------------------------------------------------------
184
- # 算出新的文本。
252
+ # Work out the new text.
185
253
  # ---------------------------------------------------------------------------
186
254
 
187
255
  $new = $raw
@@ -191,14 +259,15 @@ $eol = Get-LineEnding $raw
191
259
 
192
260
  if ($Action -eq 'disable') {
193
261
  if (-not $present) {
194
- Say ('已经禁用:bundles 里本来就没有 ' + $PackageName + '(没有改动任何文件)。') 'Yellow'
262
+ Say ((T 'emergency.alreadyDisabled' 'already disabled: {0} was not in the bundles anyway (no file was changed).') -f $PackageName) 'Yellow'
195
263
  exit 0
196
264
  }
197
265
  $target = $items | Where-Object { $_.Name -eq $PackageName } | Select-Object -First 1
198
- if ($null -eq $target) { Fail ('在 bundles 里找不到 ' + $PackageName) }
266
+ if ($null -eq $target) { Fail ((T 'emergency.notInBundles' '{0} cannot be found in the bundles') -f $PackageName) }
199
267
  $shift = 0
200
268
  if (-not $target.HasComma) {
201
- # 它是数组里的最后一项:前一项的逗号要去掉,否则 JSON 非法。
269
+ # It is the last item of the array: the comma of the item before it has to
270
+ # go, otherwise the JSON is invalid.
202
271
  $previous = $items | Where-Object { $_.LineEnd -le $target.LineStart } | Select-Object -Last 1
203
272
  if ($null -ne $previous -and $previous.HasComma) {
204
273
  $new = $new.Remove($previous.CommaIndex, 1)
@@ -206,11 +275,11 @@ if ($Action -eq 'disable') {
206
275
  }
207
276
  }
208
277
  $new = $new.Remove($target.LineStart + $shift, $target.LineEnd - $target.LineStart)
209
- $summary = ('把 ' + $PackageName + ' 从 bundles 里拿掉')
278
+ $summary = (T 'emergency.summaryRemove' 'take {0} out of the bundles') -f $PackageName
210
279
  } else {
211
280
  # enable
212
281
  if ($present) {
213
- Say ('已经启用:' + $PackageName + ' 本来就在 bundles 里(没有改动任何文件)。') 'Yellow'
282
+ Say ((T 'emergency.alreadyEnabled' 'already enabled: {0} was in the bundles anyway (no file was changed).') -f $PackageName) 'Yellow'
214
283
  exit 0
215
284
  }
216
285
  $anchor = $items | Where-Object { $_.Name -eq $AnchorName } | Select-Object -First 1
@@ -221,7 +290,8 @@ if ($Action -eq 'disable') {
221
290
  $insertAt = $anchor.LineEnd
222
291
  $line = $anchor.Indent + '"' + $PackageName + '",'
223
292
  } else {
224
- # 锚点是最后一项:先给它补上逗号,再插新的一项(新的这项不带逗号)。
293
+ # The anchor is the last item: give it a comma first, then insert the new
294
+ # item (the new one has no comma).
225
295
  $new = $new.Insert($anchor.ContentEnd, ',')
226
296
  $insertAt = $anchor.LineEnd + 1
227
297
  $line = $anchor.Indent + '"' + $PackageName + '"'
@@ -237,39 +307,40 @@ if ($Action -eq 'disable') {
237
307
  }
238
308
  $line = $last.Indent + '"' + $PackageName + '"'
239
309
  } else {
240
- # 数组是空的。
310
+ # The array is empty.
241
311
  $insertAt = $range[1]
242
312
  $line = (Get-LineIndent $raw $range[1]) + ' "' + $PackageName + '"'
243
313
  }
244
314
  }
245
315
  $new = $new.Insert($insertAt, $line + $eol)
246
- $summary = ('把 ' + $PackageName + ' 加回 bundles')
316
+ $summary = (T 'emergency.summaryAdd' 'put {0} back into the bundles') -f $PackageName
247
317
  }
248
318
 
249
319
  if ($DryRun) {
250
- Say ('[试运行] 将会' + $summary + '。') 'Cyan'
251
- Say ('[试运行] 将会写入:' + $path) 'Cyan'
252
- Say ('[试运行] 将会先备份成:' + $path + '.bak-emergency-<时间戳>') 'Cyan'
253
- Say ('[试运行] 没有改动任何文件。') 'Cyan'
320
+ Say ((T 'emergency.dryRunWill' '[dry run] would {0}.') -f $summary) 'Cyan'
321
+ Say ((T 'emergency.dryRunWrite' '[dry run] would write: {0}') -f $path) 'Cyan'
322
+ Say ((T 'emergency.dryRunBackup' '[dry run] would back it up as: {0}.bak-emergency-<timestamp>') -f $path) 'Cyan'
323
+ Say (T 'emergency.dryRunNoChange' '[dry run] no file was changed.') 'Cyan'
254
324
  exit 0
255
325
  }
256
326
 
257
327
  # ---------------------------------------------------------------------------
258
- # 写:先备份,再写,然后重新解析校验;校验不过就回滚。
328
+ # Write: back up first, write, then parse and check again; on a failure, roll
329
+ # back.
259
330
  # ---------------------------------------------------------------------------
260
331
 
261
332
  $backup = $path + '.bak-emergency-' + (Get-Date -Format 'yyyyMMdd-HHmmss')
262
333
  try {
263
334
  Copy-Item -LiteralPath $path -Destination $backup -Force
264
335
  } catch {
265
- Fail ('备份失败(没有动原文件):' + $_.Exception.Message)
336
+ Fail ((T 'emergency.backupFailed' 'the backup failed (the original file was not touched): {0}') -f $_.Exception.Message)
266
337
  }
267
338
 
268
339
  try {
269
340
  $encoding = New-Object System.Text.UTF8Encoding($hasBom)
270
341
  [System.IO.File]::WriteAllText($path, $new, $encoding)
271
342
  } catch {
272
- Fail ('写入失败(备份在 ' + $backup + '):' + $_.Exception.Message)
343
+ Fail ((T 'emergency.writeFailed' 'the write failed (the backup is at {0}): {1}') -f $backup, $_.Exception.Message)
273
344
  }
274
345
 
275
346
  try {
@@ -280,23 +351,23 @@ try {
280
351
  $names = @($parsed.dsh.profile.bundles)
281
352
  }
282
353
  if ($Action -eq 'enable' -and -not ($names -contains $PackageName)) {
283
- throw '写完之后 bundles 里仍然没有 ' + $PackageName
354
+ throw ('after writing, the bundles still do not hold ' + $PackageName)
284
355
  }
285
356
  if ($Action -eq 'disable' -and ($names -contains $PackageName)) {
286
- throw '写完之后 bundles 里仍然有 ' + $PackageName
357
+ throw ('after writing, the bundles still hold ' + $PackageName)
287
358
  }
288
359
  } catch {
289
- # 校验不过:用备份把原文件放回去,然后如实报错。
360
+ # The check failed: put the original file back, then report honestly.
290
361
  try {
291
362
  Copy-Item -LiteralPath $backup -Destination $path -Force
292
- Say ('已经用备份回滚:' + $backup) 'Yellow'
363
+ Say ((T 'emergency.rolledBack' 'the backup was put back: {0}') -f $backup) 'Yellow'
293
364
  } catch {
294
- Fail ('校验失败,而且回滚也失败了(备份在 ' + $backup + '):' + $_.Exception.Message)
365
+ Fail ((T 'emergency.verifyFailedNoRollback' 'the check failed and the rollback failed too (the backup is at {0}): {1}') -f $backup, $_.Exception.Message)
295
366
  }
296
- Fail ('校验失败(已回滚):' + $_.Exception.Message)
367
+ Fail ((T 'emergency.verifyFailedRolledBack' 'the check failed (it was rolled back): {0}') -f $_.Exception.Message)
297
368
  }
298
369
 
299
- Say ('已经' + $summary + '。') 'Green'
300
- Say ('备份:' + $backup) 'Gray'
301
- Say ('请重启 DSH 才会生效。') 'Gray'
370
+ Say ((T 'emergency.done' 'done: {0}.') -f $summary) 'Green'
371
+ Say ((T 'emergency.backup' 'backup: {0}') -f $backup) 'Gray'
372
+ Say (T 'emergency.restartHint' 'restart DSH for it to take effect.') 'Gray'
302
373
  exit 0
@@ -0,0 +1,168 @@
1
+ {
2
+ "_meta": {
3
+ "purpose": "User-facing Chinese for the dsh-autorestart rescue kit.",
4
+ "encoding": "UTF-8 WITHOUT a byte order mark. The .ps1 files next to this one are pure ASCII, so this is the only place a non-ASCII byte may live. A BOM here would break JSON.parse.",
5
+ "placeholders": "Values may contain {0}, {1} ... placeholders that the scripts fill in with -f formatting.",
6
+ "fallback": "Every key has an English fallback inside the .ps1. If this file is missing or broken, the scripts say so in English and keep working in English."
7
+ },
8
+
9
+ "common": {
10
+ "stringsMissing": "中文文案文件不在:{0}(改用英文兜底)",
11
+ "stringsBroken": "中文文案文件读不出来:{0}(改用英文兜底)",
12
+ "stringsMissingEn": "the Chinese strings file is missing: {0} (falling back to English messages)",
13
+ "stringsBrokenEn": "the Chinese strings file could not be read: {0} (falling back to English messages)"
14
+ },
15
+
16
+ "restore": {
17
+ "title": "dsh-autorestart —— profile 快照还原器",
18
+ "failPrefix": "还原失败:",
19
+ "brokenKept": "你原来的 profile 完整地留在这里:{0}",
20
+ "brokenUntouched": "(这个目录没有被动过,里面还是你出事之前的样子。)",
21
+ "noProfileDir": "找不到要还原的 profile 目录(用 -ProfileDir 说清楚要还原哪一个)。",
22
+ "target": "目标 profile:{0}",
23
+ "targetSuffix": "(目标:{0})",
24
+ "onlyProfiles": "只允许还原 {0} 里的 profile(目标:{1})",
25
+ "missingDir": "目标 profile 目录不存在:{0}(还原需要那个目录已经在那儿;脚本不会为了还原去新建一个 profile 目录)",
26
+ "notProfile": "{0}(目标:{1})。拒绝动手 —— 免得把不是 profile 的目录弄坏。",
27
+ "notProfileSuffix": "。拒绝动手 —— 免得把不是 profile 的目录弄坏。",
28
+ "brokenNotice": "这个 profile 已经坏了:{0}",
29
+ "backupsDir": "快照目录:{0}",
30
+ "usingSnapshot": "要用快照:{0}({1})",
31
+ "snapshotMissing": "找不到这份快照:{0}",
32
+ "snapshotNoManifest": "这份快照没有 manifest.json,不是一份快照:{0}",
33
+ "snapshotSuffix": "{0}(快照:{1})",
34
+ "dryRunMove": "[试运行] 会先把现在的 profile 挪到:{0}",
35
+ "dryRunCopy": "[试运行] 会把 {0} 里的配置和插件复制回:{1}",
36
+ "dryRunVerify": "[试运行] 会回读校验 package.json / dsh.profile.bundles / {0} 的版本号。",
37
+ "dryRunNoChange": "[试运行] 没有改动任何文件。",
38
+ "moveFailed": "挪走现在的 profile 失败(没有动任何东西):{0}",
39
+ "moved": "原来那份已经挪到:{0}",
40
+ "noProfileBefore": "原来那个 profile 目录本来就不在,直接新建。",
41
+ "copyFailed": "把快照复制回去失败(原来那份还在:{0}):{1}",
42
+ "checkOk": " [好] {0} —— {1}",
43
+ "checkSkipped": " [跳过] {0} —— {1}",
44
+ "checkBad": " [坏] {0} —— {1}",
45
+ "verifyFailed": "还原后的 profile 没通过校验({0})。",
46
+ "noteWithPlugins": "(含插件)",
47
+ "noteNoPlugins": "(只有配置文件,没有插件)",
48
+ "done": "已还原到 {0} 的快照{1};现在可以重新打开 DSH。",
49
+ "brokenLeft": "原来那份留在了:{0}(确认没问题之后可以自己删掉它)",
50
+
51
+ "gateEmptyPath": "目标路径是空的",
52
+ "gateDriveRoot": "拒绝动盘符根目录",
53
+ "gateFsRoot": "拒绝动文件系统根目录",
54
+ "gateHome": "拒绝动用户主目录本身",
55
+ "gateSystemDir": "拒绝动系统目录:{0}",
56
+ "gateOtherUser": "拒绝动别人的用户目录",
57
+
58
+ "fingerprintNotProfile": "这里没有 package.json —— 不像 DSH profile",
59
+ "fingerprintProfile": "profile 的 package.json 里有 dsh.profile",
60
+ "fingerprintBrokenNoDsh": "profile 坏了(package.json 不再是 profile),但 profile 的文件还在",
61
+ "fingerprintBrokenNoPkg": "profile 坏了(package.json 没了),但 profile 的文件还在",
62
+ "fingerprintNotDshProfile": "package.json 里没有 dsh.profile,也没有别的东西能证明这是 profile",
63
+
64
+ "snapshotDirMissing": "找不到快照目录:{0}",
65
+ "snapshotNone": "这个目录里没有一份带 manifest 的快照:{0}",
66
+ "snapshotNoConfigPkg": "这份快照里没有 config\\package.json",
67
+ "snapshotBadConfigPkg": "这份快照的 package.json 不是合法的 JSON",
68
+ "snapshotNoDshProfile": "这份快照的 package.json 里没有 dsh.profile",
69
+ "snapshotIsProfile": "这份快照里装的是一个 DSH profile",
70
+ "snapshotUnknownTime": "时间不明",
71
+
72
+ "verifyPkgLabel": "package.json 能解析",
73
+ "verifyPkgMissing": "package.json 不存在",
74
+ "verifyPkgBad": "不是合法的 JSON",
75
+ "verifyBundlesLabel": "dsh.profile.bundles 有内容",
76
+ "verifyBundlesCount": "{0} 个",
77
+ "verifyBundlesEmpty": "没有 dsh.profile.bundles,或者它是空的",
78
+ "verifyPluginLabel": "{0} 的版本号读得到",
79
+ "verifyPluginNotDeclared": "这个 profile 没有声明要装 {0}(没什么可读)",
80
+ "verifyPluginMissing": "profile 里写着要装 {0},但 node_modules\\{0} 不在 —— 配置说一套、磁盘是另一套",
81
+ "verifyPluginNoVersion": "读不到版本号"
82
+ },
83
+
84
+ "drill": {
85
+ "dir": "演练目录:{0}",
86
+ "broke": "已经故意弄坏:package.json 写成非法 JSON,插件目录删掉了。",
87
+ "outputHeader": "--- 还原脚本的输出({0})---",
88
+ "outputEnd": "--- 输出结束 ---",
89
+ "assertions": "断言:",
90
+ "assertPass": " [通过] {0}",
91
+ "assertFail": " [失败] {0} —— {1}",
92
+ "assertFailPlain": " [失败] {0}",
93
+ "refusedNoResult": " [失败] {0} —— 没有拿到运行结果",
94
+ "refusedNotRefused": " [失败] {0} —— 退出码是 0,脚本根本没有拒绝",
95
+ "refusedNoReason": " [失败] {0} —— 退出码是 {1},但输出里没有拒绝原因(期望其中之一:{2})",
96
+ "refusedOk": " [通过] {0} —— 退出码 {1},输出里有拒绝原因「{2}」",
97
+ "cleanup": "清理临时目录:{0}",
98
+ "cleaned": "临时目录已清理干净。",
99
+ "notCleaned": "临时目录没有清理干净,请自己删掉:{0}",
100
+ "failed": "演练失败:{0} 条断言没过。",
101
+ "passed": "演练全部通过:故意弄坏 → 还原 → 校验 → 清理,一整圈跑通了。",
102
+ "error": "演练出错:{0}",
103
+ "noScript": "演练失败:找不到 {0}",
104
+ "dirOccupied": "演练失败:这个演练目录已经被占用了:{0}",
105
+
106
+ "assertDryRunExit": "试运行退出码是 0",
107
+ "assertDryRunUntouched": "试运行没有改动 package.json",
108
+ "assertDryRunNoBroken": "试运行没有留下 broken 目录",
109
+ "assertRestoreExit": "还原退出码是 0",
110
+ "assertChineseOutput": "还原脚本的输出里有中文(文案是从 JSON 读的)",
111
+ "assertParses": "还原后 package.json 能解析",
112
+ "assertUsedSnapshot": "用的是带 manifest 的那份快照(跳过了更新的诱饵)",
113
+ "assertBundles": "还原后 dsh.profile.bundles 有内容",
114
+ "assertVersionReadable": "还原后插件版本读得到",
115
+ "assertVersionSame": "插件版本是快照里的那一个",
116
+ "assertBrokenExists": "broken 证据目录存在",
117
+ "assertBrokenKeeps": "broken 目录里保留的还是坏掉的那份",
118
+ "assertNotProfileRefused": "不像 profile 的目录被拒绝(退出码 + 拒绝原因)",
119
+ "assertNotProfileUntouched": "被拒绝的时候没有动那个目录",
120
+ "assertDriveRootRefused": "盘符根目录被拒绝(退出码 + 拒绝原因)",
121
+ "assertSystemDirRefused": "系统目录 {0} 被拒绝(退出码 + 拒绝原因)",
122
+ "assertSystemDirIntact": "系统目录 {0} 原封不动(没被挪走)",
123
+ "assertMissingDirRefused": "目标 profile 目录不存在时被拒绝(退出码 + 拒绝原因)",
124
+ "assertMissingDirNotCreated": "目标目录不存在时没有把它创建出来",
125
+ "assertLyingRefused": "声明了插件却没装 → 还原必须失败(退出码 + 拒绝原因)",
126
+ "assertLyingBrokenHint": "声明了插件却没装 → 明确说 broken 目录还在",
127
+ "assertPs1Ascii": "restore-profile.ps1 是纯 ASCII(maxByte <= 127)",
128
+ "assertCmdAscii": "restore-profile.cmd 是纯 ASCII",
129
+ "assertOnlyTemp": "演练只在临时目录里动手(broken 目录在临时目录下面)",
130
+ "assertLockFile": "演练目录里有锁文件",
131
+
132
+ "keywords": {
133
+ "notProfile": ["不像", "不是 profile"],
134
+ "driveRoot": ["盘符", "根目录"],
135
+ "systemDir": ["系统目录", "不允许"],
136
+ "missingDir": ["不存在"],
137
+ "notValidated": ["没通过校验", "校验"],
138
+ "dryRun": ["试运行"]
139
+ }
140
+ },
141
+
142
+ "emergency": {
143
+ "failed": "急救开关失败:{0}",
144
+ "notFound": "找不到 profile 的 package.json:{0}",
145
+ "badJson": "这个 package.json 不是合法的 JSON:{0}",
146
+ "file": "文件:{0}",
147
+ "enabled": "已启用:{0} 在 bundles 里。",
148
+ "disabled": "已禁用:{0} 不在 bundles 里。",
149
+ "notInBundles": "在 bundles 里找不到 {0}",
150
+ "alreadyDisabled": "已经禁用:bundles 里本来就没有 {0}(没有改动任何文件)。",
151
+ "alreadyEnabled": "已经启用:{0} 本来就在 bundles 里(没有改动任何文件)。",
152
+ "noBundles": "这个 profile 里没有 dsh.profile.bundles 列表,装不回去(请手动加一行 \"dsh-autorestart\")。",
153
+ "dryRunWill": "[试运行] 将会{0}。",
154
+ "dryRunWrite": "[试运行] 将会写入:{0}",
155
+ "dryRunBackup": "[试运行] 将会先备份成:{0}.bak-emergency-<时间戳>",
156
+ "dryRunNoChange": "[试运行] 没有改动任何文件。",
157
+ "backupFailed": "备份失败(没有动原文件):{0}",
158
+ "writeFailed": "写入失败(备份在 {0}):{1}",
159
+ "rolledBack": "已经用备份回滚:{0}",
160
+ "verifyFailedNoRollback": "校验失败,而且回滚也失败了(备份在 {0}):{1}",
161
+ "verifyFailedRolledBack": "校验失败(已回滚):{0}",
162
+ "done": "已经{0}。",
163
+ "backup": "备份:{0}",
164
+ "restartHint": "请重启 DSH 才会生效。",
165
+ "summaryRemove": "把 dsh-autorestart 从 bundles 里拿掉",
166
+ "summaryAdd": "把 dsh-autorestart 加回 bundles"
167
+ }
168
+ }
@@ -0,0 +1,44 @@
1
+ @echo off
2
+ rem dsh-autorestart - restore the profile snapshot.
3
+ rem
4
+ rem Double-click this file when DSH does not start any more (a plugin upgrade
5
+ rem broke the profile, the configuration was damaged, node_modules is a mess):
6
+ rem it puts the newest snapshot that carries a manifest back over the profile.
7
+ rem
8
+ rem What it does, in this order:
9
+ rem 1. finds the newest snapshot with a manifest under <stateDir>\backups
10
+ rem 2. MOVES the current profile to <profile>\.broken-<timestamp> (nothing is
11
+ rem deleted, the evidence stays)
12
+ rem 3. copies the configuration files and the plugin tree back
13
+ rem 4. reads the result back: package.json, dsh.profile.bundles and the
14
+ rem version of dsh-autorestart have to be readable
15
+ rem
16
+ rem It never restarts DSH for you - you do that yourself.
17
+ rem
18
+ rem Optional arguments are passed straight to the PowerShell script, e.g.
19
+ rem restore-profile.cmd -DryRun
20
+ rem restore-profile.cmd -ProfileDir C:\temp\drill\profile -StateDir C:\temp\drill\state
21
+ rem
22
+ rem NOTE: this file is ASCII on purpose. cmd.exe reads a batch file with the
23
+ rem console code page, and a UTF-8 character in it would shift the rest of the
24
+ rem line and be run as a command. The Chinese explanation comes from the
25
+ rem PowerShell script below (and from scripts\README.md).
26
+ setlocal
27
+ set "SCRIPT=%~dp0restore-profile.ps1"
28
+ if not exist "%SCRIPT%" (
29
+ echo Cannot find "%SCRIPT%".
30
+ echo The rescue kit is incomplete - keep the plugin folder together.
31
+ pause
32
+ exit /b 1
33
+ )
34
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT%" %*
35
+ set "RC=%ERRORLEVEL%"
36
+ echo.
37
+ if "%RC%"=="0" (
38
+ echo Done. The result is above - open DSH again.
39
+ ) else (
40
+ echo The restore did NOT finish. Read the red lines above.
41
+ echo Your old profile was not deleted - it is kept as a ".broken-<timestamp>" folder.
42
+ )
43
+ pause
44
+ exit /b %RC%