dsh-crossterm 0.8.10 → 0.8.12
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 +40 -0
- package/README.md +16 -11
- package/lib/bus.js +1 -1
- package/lib/cloud/relay.js +36 -13
- package/lib/web.js +61 -24
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# dsh-crossterm 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.8.12 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
### 修复:更新链路在 pnpm 11 上必定失败;latest 被镜像滞后带偏
|
|
6
|
+
|
|
7
|
+
来源:跨设备邮件(YOCE_C 经「同端多会话2」转发,2026-10-20T06:20:28Z)—— 0.8.11 复测报告。
|
|
8
|
+
|
|
9
|
+
- **`--prefer-online` 是 pnpm 11 的非法选项**:0.8.11 把它拼进了 dsh / cli / pnpm 三级兜底,
|
|
10
|
+
B 机实测 `[ERROR] Unknown option: 'prefer-online'`,exit 1 ⇒ 三条路一起挂。
|
|
11
|
+
现在只给 pnpm 那步加它认识的 `--config.prefer-offline=false`;dsh / cli 两条路不加附加项。
|
|
12
|
+
- **24h 发布年龄策略**:新版本发布不到 24 小时会被 `minimumReleaseAge` 拒装
|
|
13
|
+
(`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`)。失败输出命中该关键字时,用
|
|
14
|
+
`--config.minimumReleaseAge=0` 重试一次,并把「已临时放宽」记进 `logs/update.log`。
|
|
15
|
+
- **`latestVersion()` 取最大版本**:原来「第一个能返回的 registry 就用」,而顺序是 npmmirror 优先,
|
|
16
|
+
镜像滞后时会把 0.8.11 读成 0.8.10(钉版本也会钉到旧号)。现在两源都问、取最大版本,
|
|
17
|
+
请求带 `cache-control: no-cache`。
|
|
18
|
+
- 探针 `probe-0811.mjs` 扩到 **25 项全 PASS**(新增「pnpm 合法选项」「不再出现 --prefer-online 参数」
|
|
19
|
+
「minimumReleaseAge 重试」「两源取最大」四项,替换掉原来断言 --prefer-online 的两项)。
|
|
20
|
+
|
|
21
|
+
## 0.8.11 — 2026-10-20
|
|
22
|
+
|
|
23
|
+
### 更新不再把版本装低(YOCE_C 实测事故)
|
|
24
|
+
|
|
25
|
+
来源:跨设备邮件(YOCE_C,2026-10-20T06:04:39Z)报告 —— 0.8.2 那台点「更新」被装成 **0.6.1**,还回 `ok:true updated:true`。
|
|
26
|
+
|
|
27
|
+
- **根因**:`updatePlugin()` 用 `pnpm add dsh-crossterm@latest`,pnpm 命中了缓存里的旧 packument
|
|
28
|
+
(`reused 2, downloaded 0, added 1`),装成 0.6.1;成功判定又是「装完版本 != 装前版本」,
|
|
29
|
+
于是降级也算成功。同类事故 10-19 09:51Z 也发生过(0.7.2 → 0.3.38)。
|
|
30
|
+
- **修法**:已知 `latest` 就钉版本号 `dsh-crossterm@<latest>`(拿不到才回退 `@latest`),
|
|
31
|
+
三级兜底(dsh → cli → pnpm)每步都加 `--prefer-online`;新增 `settle()`:
|
|
32
|
+
装完重读版本,`installed < before` 判为降级 → 按 `before` 精确回滚并记 `rollback`;
|
|
33
|
+
**成功条件是 `installed === latest`**,降级一律报错、日志写 `downgraded/rollback`。
|
|
34
|
+
- 路由返回体加 `downgraded`、`rollback`;文案分「已恢复到 X,请重试」「恢复失败请手动重装 X」。
|
|
35
|
+
|
|
36
|
+
### 同 id 邮件副本不再各自重投(F7 收尾)
|
|
37
|
+
|
|
38
|
+
- 以前只有「已投递成功」才按信封 id 归档重复副本;第一份投递失败时,同 id 的其他副本会各自重试,
|
|
39
|
+
最后可能投两次(YOCE_C 用 dedupe-a/dedupe-b 实测到)。
|
|
40
|
+
- 现在 `state.failedIds[信封id] = 首份文件名`:失败也记 id,同 id 的其他副本直接归档进 `done/`;
|
|
41
|
+
首份重投成功后清 `failedIds`。`state` 形状兼容旧文件(缺字段按空对象)。
|
|
42
|
+
|
|
3
43
|
## 0.8.10 — 2026-10-19
|
|
4
44
|
|
|
5
45
|
- **云端设备的显示规则调整**(User said (m11109):名称(权限)+在线状态不变,版本只在有差异时显示):
|
package/README.md
CHANGED
|
@@ -28,17 +28,22 @@ A 终端看不到 B 终端里有哪个会话、在干什么,也没法把一条
|
|
|
28
28
|
> `locatePkgJson()` 对子路径行返回 undefined),而插件页顶部标题与这一行共用同一份 `locale/zh.json` 的 meta,
|
|
29
29
|
> 所以它单独做入口(配置卡片 + 自动更新开关),标题带「(自更新)」;卡片头用 `shortTitle` 显示短名「跨端互通」。
|
|
30
30
|
|
|
31
|
-
**本版更新(0.8.
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
31
|
+
**本版更新(0.8.12)**
|
|
32
|
+
|
|
33
|
+
- **修「点更新必定失败」**:0.8.11 给三级兜底都加了 `--prefer-online`,而这是 pnpm 11 的**非法选项**
|
|
34
|
+
(`[ERROR] Unknown option`),三条路会一起挂。现在只给 pnpm 那步加它认识的
|
|
35
|
+
`--config.prefer-offline=false`。
|
|
36
|
+
- **新版本发布不到 24h 也能装**:撞 `minimumReleaseAge` 策略(`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`)时,
|
|
37
|
+
自动用 `--config.minimumReleaseAge=0` 重试一次,并在 `logs/update.log` 记一笔「临时放宽」。
|
|
38
|
+
- **npm 最新版读得准了**:原来「第一个能返回的镜像就用」(npmmirror 优先),镜像滞后时会把 0.8.12 读成旧号,
|
|
39
|
+
钉版本也钉错。现在两个源都问、取最大版本。
|
|
40
|
+
- 本机是 `link:` 源码版,点更新仍提示「改源码后重启」;以上对用 npm 安装的其他设备生效。
|
|
41
|
+
|
|
42
|
+
**上一版(0.8.11)**
|
|
43
|
+
|
|
44
|
+
- **更新不再把版本装低**:钉死 `dsh-crossterm@<npm 最新版>`(原来是 `@latest`,会命中 pnpm 缓存的旧 packument,
|
|
45
|
+
实测把 0.8.2 装成 0.6.1 还判成功);装完必须**等于**最新版才算成功,装低了自动回滚并报错。
|
|
46
|
+
- **同 id 邮件副本不再各自重投**:一份投递失败时,同 id 的其他副本直接归档。
|
|
42
47
|
|
|
43
48
|
**更早(0.8.8 / 0.8.7 / 0.8.6 / 0.8.5 / 0.8.2 / 0.8.0)**
|
|
44
49
|
|
package/lib/bus.js
CHANGED
|
@@ -23,7 +23,7 @@ import path from 'node:path'
|
|
|
23
23
|
import crypto from 'node:crypto'
|
|
24
24
|
import { rotateIfNeeded } from './logs.js'
|
|
25
25
|
|
|
26
|
-
export const VERSION = '0.8.
|
|
26
|
+
export const VERSION = '0.8.12'
|
|
27
27
|
|
|
28
28
|
/** DSH 的家目录:与宿主其它状态(sessions / profiles)同一个根,多终端天然共享 */
|
|
29
29
|
export function dshHome() {
|
package/lib/cloud/relay.js
CHANGED
|
@@ -13,9 +13,16 @@ const nowIso = () => new Date().toISOString()
|
|
|
13
13
|
function loadState(file) {
|
|
14
14
|
try {
|
|
15
15
|
const j = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
16
|
-
return {
|
|
16
|
+
return {
|
|
17
|
+
at: j.at || 0,
|
|
18
|
+
processed: j.processed || {},
|
|
19
|
+
seen: j.seen || {},
|
|
20
|
+
failed: j.failed || {},
|
|
21
|
+
failedIds: j.failedIds || {},
|
|
22
|
+
sent: j.sent || [],
|
|
23
|
+
}
|
|
17
24
|
} catch {
|
|
18
|
-
return { at: 0, processed: {}, seen: {}, failed: {}, sent: [] }
|
|
25
|
+
return { at: 0, processed: {}, seen: {}, failed: {}, failedIds: {}, sent: [] }
|
|
19
26
|
}
|
|
20
27
|
}
|
|
21
28
|
|
|
@@ -159,6 +166,21 @@ export function createRelay({ pan, getLayout, localDir, stateFile, deliver, getS
|
|
|
159
166
|
stats.pending = 0
|
|
160
167
|
const incomingDir = path.join(localDir, 'incoming')
|
|
161
168
|
fs.mkdirSync(incomingDir, { recursive: true })
|
|
169
|
+
// 0.8.11 F7 补完:同一个信封 id 出现多份文件时(哪怕第一份投递失败),
|
|
170
|
+
// 其余副本一律归档进 done/,不让它们各自重试、最后变成重复投递。
|
|
171
|
+
const archiveDuplicate = async (dupName, dupLocal, dupId, why) => {
|
|
172
|
+
try {
|
|
173
|
+
await pan.mkdir(layout.paths.doneDir)
|
|
174
|
+
await pan.put(dupLocal, `${layout.paths.doneDir}/${dupName}`)
|
|
175
|
+
await pan.rm(`${layout.paths.inboxDir}/${dupName}`)
|
|
176
|
+
state.seen[dupName] = dupId
|
|
177
|
+
delete state.failed[dupName]
|
|
178
|
+
saveState(stateFile, { ...state, at: Date.now() })
|
|
179
|
+
log(`中转站:${dupName} ${why},已归档不重投`)
|
|
180
|
+
} catch (e) {
|
|
181
|
+
log(`中转站:${dupName} 重复信封归档失败:${(e && e.message) || String(e)}`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
162
184
|
|
|
163
185
|
for (const f of files) {
|
|
164
186
|
const name = f.server_filename
|
|
@@ -191,17 +213,15 @@ export function createRelay({ pan, getLayout, localDir, stateFile, deliver, getS
|
|
|
191
213
|
const mailId = envelope.id || path.basename(name, '.json')
|
|
192
214
|
if (state.processed[mailId]) {
|
|
193
215
|
// 0.8.2 F7:同一个信封换了文件名又出现 —— 已经投过,把这个副本归档掉,不再投第二次
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
log(`中转站:${name} 重复信封归档失败:${(e && e.message) || String(e)}`)
|
|
204
|
-
}
|
|
216
|
+
await archiveDuplicate(name, localJson, mailId, `与已投递的 ${mailId} 同一个信封`)
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
// 0.8.11 F7 补完:这份信封的「第一份」如果已经投递失败在退避里,
|
|
220
|
+
// 同 id 的其他副本也不该各自重试 —— 归档掉,等第一份成功或进 dead/。
|
|
221
|
+
const failedFirst = state.failedIds && state.failedIds[mailId]
|
|
222
|
+
if (failedFirst && failedFirst !== name) {
|
|
223
|
+
stats.pending += 1
|
|
224
|
+
await archiveDuplicate(name, localJson, mailId, `与待重投的 ${failedFirst} 同一个信封`)
|
|
205
225
|
continue
|
|
206
226
|
}
|
|
207
227
|
// 0.8.0 F3:有账号密钥就要求签名对得上,否则拒收(防伪造发件人 / 提示注入)
|
|
@@ -214,6 +234,7 @@ export function createRelay({ pan, getLayout, localDir, stateFile, deliver, getS
|
|
|
214
234
|
const prevSig = state.failed[name] || {}
|
|
215
235
|
const sigTries = Number(prevSig.tries || 0) + 1
|
|
216
236
|
state.failed[name] = { at: nowIso(), mailId, reason: '签名无效', tries: sigTries, nextAt: Date.now() + 3600000 }
|
|
237
|
+
state.failedIds[mailId] = name
|
|
217
238
|
saveState(stateFile, { ...state, at: Date.now() })
|
|
218
239
|
continue
|
|
219
240
|
}
|
|
@@ -249,6 +270,7 @@ export function createRelay({ pan, getLayout, localDir, stateFile, deliver, getS
|
|
|
249
270
|
state.processed[mailId] = { at: nowIso(), name, mailId, archived, sessionId: result.sessionId || '', to: String(envelope.to) }
|
|
250
271
|
state.seen[name] = mailId
|
|
251
272
|
delete state.failed[name]
|
|
273
|
+
delete state.failedIds[mailId]
|
|
252
274
|
log(`中转站投递 ${mailId} → ${result.sessionId || String(envelope.to)}`)
|
|
253
275
|
} else {
|
|
254
276
|
stats.pending += 1
|
|
@@ -263,6 +285,7 @@ export function createRelay({ pan, getLayout, localDir, stateFile, deliver, getS
|
|
|
263
285
|
tries,
|
|
264
286
|
nextAt: Date.now() + delays[Math.min(tries - 1, delays.length - 1)],
|
|
265
287
|
}
|
|
288
|
+
state.failedIds[mailId] = name
|
|
266
289
|
log(`中转站留件 ${name}(第 ${tries} 次):${(result && result.reason) || '投递失败'}`)
|
|
267
290
|
if (tries >= 8) {
|
|
268
291
|
try {
|
package/lib/web.js
CHANGED
|
@@ -205,6 +205,10 @@ function compareVersion(a, b) {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
async function latestVersion() {
|
|
208
|
+
// 0.8.12:原来「第一个能返回的 registry 就用」+ 顺序是 npmmirror 优先 → 镜像滞后时
|
|
209
|
+
// 会把 0.8.11 读成 0.8.10,钉版本也钉到旧号(YOCE_C 2026-10-20 实测)。
|
|
210
|
+
// 现在两源都问,取最大的那个版本;单源失败不影响。
|
|
211
|
+
const found = []
|
|
208
212
|
let last = ''
|
|
209
213
|
for (const registry of NPM_REGISTRIES) {
|
|
210
214
|
const ctrl = new AbortController()
|
|
@@ -212,14 +216,17 @@ async function latestVersion() {
|
|
|
212
216
|
try {
|
|
213
217
|
const res = await fetch(`${registry}/${PKG}/latest`, {
|
|
214
218
|
signal: ctrl.signal,
|
|
215
|
-
headers: { accept: 'application/json' },
|
|
219
|
+
headers: { accept: 'application/json', 'cache-control': 'no-cache' },
|
|
216
220
|
})
|
|
217
221
|
if (!res.ok) {
|
|
218
222
|
last = `HTTP ${res.status}`
|
|
219
223
|
continue
|
|
220
224
|
}
|
|
221
225
|
const doc = await res.json()
|
|
222
|
-
if (doc && typeof doc.version === 'string')
|
|
226
|
+
if (doc && typeof doc.version === 'string') {
|
|
227
|
+
found.push(doc.version)
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
223
230
|
last = 'no version field'
|
|
224
231
|
} catch (error) {
|
|
225
232
|
last = String((error && error.message) || error)
|
|
@@ -227,6 +234,7 @@ async function latestVersion() {
|
|
|
227
234
|
clearTimeout(timer)
|
|
228
235
|
}
|
|
229
236
|
}
|
|
237
|
+
if (found.length > 0) return found.sort(compareVersion).pop()
|
|
230
238
|
throw new Error(last || 'unknown')
|
|
231
239
|
}
|
|
232
240
|
|
|
@@ -368,10 +376,15 @@ function appendUpdateLog(cfg, record) {
|
|
|
368
376
|
* 每条都记进 steps(命令、退出码、耗时、输出尾部),最后核对 node_modules 里的真实版本。
|
|
369
377
|
*/
|
|
370
378
|
async function updatePlugin(term, info, latest) {
|
|
371
|
-
|
|
379
|
+
// 0.8.11:不再用 @latest —— pnpm 可能拿缓存里的旧 packument,把机器「更新」成老版本
|
|
380
|
+
// (YOCE_C 实测 0.8.2 → 0.6.1,还判成功)。已知确切版本就钉版本号。
|
|
381
|
+
// 0.8.12:--prefer-online 是 pnpm 11 的非法选项([ERROR] Unknown option),
|
|
382
|
+
// 三级兜底会一起挂;pnpm 只认 --config.prefer-offline=false,所以改成只给 pnpm 步加这条。
|
|
383
|
+
const pin = latest ? `${PKG}@${latest}` : ''
|
|
384
|
+
const target = pin || `${PKG}@latest`
|
|
372
385
|
const before = installedVersion(info)
|
|
373
386
|
const steps = []
|
|
374
|
-
const
|
|
387
|
+
const record = async (label, cmd, args, opts) => {
|
|
375
388
|
const started = Date.now()
|
|
376
389
|
const res = await run(cmd, args, (opts && opts.cwd) || info.dir, opts)
|
|
377
390
|
steps.push({
|
|
@@ -383,17 +396,33 @@ async function updatePlugin(term, info, latest) {
|
|
|
383
396
|
})
|
|
384
397
|
return res
|
|
385
398
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
399
|
+
// 装完必须等于 latest 才算成功;装低了就按 before 精确回滚(同样钉版本号)
|
|
400
|
+
const settle = async (via, step) => {
|
|
401
|
+
let installed = installedVersion(info)
|
|
402
|
+
const downgraded = Boolean(before) && Boolean(installed) && compareVersion(installed, before) < 0
|
|
403
|
+
let rollback = null
|
|
404
|
+
if (downgraded && step) {
|
|
405
|
+
const args = step.args.map((a) => (a === target && pin ? `${PKG}@${before}` : a))
|
|
406
|
+
const res = await record(`rollback:${step.label}`, step.cmd, args, step.opts)
|
|
407
|
+
const restored = installedVersion(info)
|
|
408
|
+
rollback = { ok: res.code === 0 && restored === before, restored, code: res.code }
|
|
409
|
+
installed = restored
|
|
410
|
+
}
|
|
411
|
+
const exact = Boolean(installed) && Boolean(latest) && compareVersion(installed, latest) === 0
|
|
412
|
+
const ok = exact && !downgraded
|
|
413
|
+
const updated = ok && Boolean(before) && installed !== before
|
|
414
|
+
const out = { profile: info.name, ok, updated, via, before, installed, latest: latest || '', downgraded, rollback, steps }
|
|
415
|
+
appendUpdateLog(term.cfg, out)
|
|
416
|
+
return out
|
|
393
417
|
}
|
|
394
418
|
|
|
395
419
|
const { cmd: node, env } = nodeTool(term.cfg)
|
|
396
420
|
const cli = cliScript()
|
|
421
|
+
// pnpm 11:--prefer-online 不是合法选项;prefer-offline 只能通过 --config. 前缀关掉。
|
|
422
|
+
// 新版本发布不到 24h 还会撞 minimumReleaseAge 策略,失败时用 policyArgs 重试一次并记日志。
|
|
423
|
+
const pnpmFlags = ['--config.prefer-offline=false']
|
|
424
|
+
const policyArgs = ['--config.minimumReleaseAge=0']
|
|
425
|
+
const policyRe = /minimum[-_]?release[-_]?age/i
|
|
397
426
|
const plan = [{ label: 'dsh', cmd: 'dsh', args: ['plugin', '--profile', info.name, 'add', target], opts: {} }]
|
|
398
427
|
if (fs.existsSync(cli)) {
|
|
399
428
|
plan.push({
|
|
@@ -404,27 +433,28 @@ async function updatePlugin(term, info, latest) {
|
|
|
404
433
|
})
|
|
405
434
|
}
|
|
406
435
|
for (const pnpm of pnpmCandidates(term.cfg)) {
|
|
407
|
-
plan.push({ label: 'pnpm', cmd: node, args: [pnpm, 'add', target], opts: { shell: false, env } })
|
|
436
|
+
plan.push({ label: 'pnpm', cmd: node, args: [pnpm, 'add', target, ...pnpmFlags], opts: { shell: false, env } })
|
|
408
437
|
}
|
|
409
438
|
|
|
410
439
|
let last = null
|
|
411
|
-
for (const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (res.code
|
|
415
|
-
const
|
|
416
|
-
|
|
440
|
+
for (const base of plan) {
|
|
441
|
+
let step = base
|
|
442
|
+
let res = await record(step.label, step.cmd, step.args, step.opts)
|
|
443
|
+
if (res.code !== 0 && policyRe.test(res.out || '')) {
|
|
444
|
+
const args = [...step.args, ...policyArgs]
|
|
445
|
+
res = await record(`${step.label}+release-age`, step.cmd, args, step.opts)
|
|
446
|
+
step = { ...step, args }
|
|
417
447
|
}
|
|
448
|
+
last = res
|
|
449
|
+
if (res.code !== 0) continue
|
|
450
|
+
const result = await settle(step.label, step)
|
|
451
|
+
if (result.ok) return result
|
|
418
452
|
}
|
|
419
|
-
|
|
453
|
+
const settled = await settle('none', null)
|
|
420
454
|
return {
|
|
455
|
+
...settled,
|
|
421
456
|
ok: false,
|
|
422
457
|
updated: false,
|
|
423
|
-
via: 'none',
|
|
424
|
-
before,
|
|
425
|
-
installed: installedVersion(info),
|
|
426
|
-
latest: latest || '',
|
|
427
|
-
steps,
|
|
428
458
|
out: steps.map((s) => `[${s.label}] 退出码 ${s.code}\n${s.out}`).join('\n\n') || updateErrorText(last),
|
|
429
459
|
}
|
|
430
460
|
}
|
|
@@ -951,6 +981,11 @@ export function registerWeb(ctx, term, log) {
|
|
|
951
981
|
let message
|
|
952
982
|
if (result.ok && result.updated) message = `已更新到 ${installed},重启 DSH 后生效`
|
|
953
983
|
else if (result.ok) message = `已经是最新(${installed || latest}),不用更新`
|
|
984
|
+
else if (result.downgraded) {
|
|
985
|
+
message = result.rollback && result.rollback.ok
|
|
986
|
+
? `更新把版本装低了(原 ${result.before},装成更旧版本),已恢复到 ${installed};请重试`
|
|
987
|
+
: `更新把版本装低了(原 ${result.before},现在 ${installed}),恢复失败,请手动重装 ${result.before}`
|
|
988
|
+
} else if (installed && latest) message = `更新没成功(当前装着 ${installed},npm 最新 ${latest}):${result.out || '命令都返回失败'}`
|
|
954
989
|
else if (installed) message = `更新没成功(当前装着 ${installed}):${result.out || '命令都返回失败'}`
|
|
955
990
|
else message = `更新失败:${result.out || '未知原因'}`
|
|
956
991
|
json(res, 200, {
|
|
@@ -960,6 +995,8 @@ export function registerWeb(ctx, term, log) {
|
|
|
960
995
|
before: result.before || '',
|
|
961
996
|
installed,
|
|
962
997
|
latest,
|
|
998
|
+
downgraded: Boolean(result.downgraded),
|
|
999
|
+
rollback: result.rollback || null,
|
|
963
1000
|
needRestart: Boolean(result.ok && result.updated),
|
|
964
1001
|
steps,
|
|
965
1002
|
message,
|