tensorgrid-ui 1.0.0 → 1.0.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.
- package/lib/client.js +42 -21
- package/lib/update-check.js +128 -114
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -183,14 +183,19 @@ window.__ModuleLoader__.load({
|
|
|
183
183
|
'update.working': 'Working…',
|
|
184
184
|
'update.checking': 'Checking…',
|
|
185
185
|
'update.upToDate': 'Latest version installed',
|
|
186
|
-
'update.available': '
|
|
187
|
-
'update.installed': 'Installed {
|
|
186
|
+
'update.available': 'Version {version} is available',
|
|
187
|
+
'update.installed': 'Installed {version}, verified on dsh {dsh}',
|
|
188
188
|
'update.doneReload': 'Updated. Reload the page.',
|
|
189
189
|
'update.doneRestart': 'Updated. Restart the application.',
|
|
190
190
|
'update.failed': 'Update failed',
|
|
191
|
+
'update.problem.noStamp': 'No installation record — reinstall',
|
|
192
|
+
'update.problem.noPackage': 'Installed package not found — reinstall',
|
|
193
|
+
'update.problem.registryStatus': 'The registry refused the request',
|
|
194
|
+
'update.problem.registryUnreachable': 'Could not reach the registry',
|
|
195
|
+
'update.problem.badVersion': 'The registry returned no valid version',
|
|
196
|
+
'update.problem.noProfile': 'The installation record has no profile — reinstall',
|
|
197
|
+
'update.problem.installFailed': 'Installing from the registry failed',
|
|
191
198
|
'update.dshMismatch': 'Warning: dsh {actual} is installed, but this version was verified on {expected}',
|
|
192
|
-
'update.ahead': 'Installed version is {count} commits ahead of the released one',
|
|
193
|
-
'update.unknownHistory': 'Installed version is missing from history — reinstall',
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
const ZH_DICT = {
|
|
@@ -208,14 +213,19 @@ window.__ModuleLoader__.load({
|
|
|
208
213
|
'update.working': '处理中…',
|
|
209
214
|
'update.checking': '检查中…',
|
|
210
215
|
'update.upToDate': '已是最新版本',
|
|
211
|
-
'update.available': '
|
|
212
|
-
'update.installed': '已安装 {
|
|
216
|
+
'update.available': '有新版本 {version}',
|
|
217
|
+
'update.installed': '已安装 {version},在 dsh {dsh} 上验证',
|
|
213
218
|
'update.doneReload': '已更新。请刷新页面。',
|
|
214
219
|
'update.doneRestart': '已更新。请重启应用。',
|
|
215
220
|
'update.failed': '更新失败',
|
|
221
|
+
'update.problem.noStamp': '没有安装记录 — 请重新安装',
|
|
222
|
+
'update.problem.noPackage': '未找到已安装的包 — 请重新安装',
|
|
223
|
+
'update.problem.registryStatus': '注册表拒绝了请求',
|
|
224
|
+
'update.problem.registryUnreachable': '无法连接到注册表',
|
|
225
|
+
'update.problem.badVersion': '注册表未返回有效版本',
|
|
226
|
+
'update.problem.noProfile': '安装记录中没有配置文件 — 请重新安装',
|
|
227
|
+
'update.problem.installFailed': '从注册表安装失败',
|
|
216
228
|
'update.dshMismatch': '注意:已安装 dsh {actual},但此版本在 {expected} 上验证',
|
|
217
|
-
'update.ahead': '已安装版本比已发布版本新 {count} 次提交',
|
|
218
|
-
'update.unknownHistory': '已安装版本不在历史记录中 — 请重新安装',
|
|
219
229
|
}
|
|
220
230
|
|
|
221
231
|
// ── RU-PACK:BEGIN — генерируется tools/build-ru.mjs, руками не править ──
|
|
@@ -1530,14 +1540,19 @@ window.__ModuleLoader__.load({
|
|
|
1530
1540
|
'update.working': 'Работаю…',
|
|
1531
1541
|
'update.checking': 'Проверяю…',
|
|
1532
1542
|
'update.upToDate': 'Установлена последняя версия',
|
|
1533
|
-
'update.available': 'Доступна
|
|
1534
|
-
'update.installed': '
|
|
1543
|
+
'update.available': 'Доступна версия {version}',
|
|
1544
|
+
'update.installed': 'Установлена {version}, проверено на dsh {dsh}',
|
|
1535
1545
|
'update.doneReload': 'Обновлено. Обновите страницу.',
|
|
1536
1546
|
'update.doneRestart': 'Обновлено. Перезапустите приложение.',
|
|
1537
1547
|
'update.failed': 'Обновить не удалось',
|
|
1548
|
+
'update.problem.noStamp': 'Нет отметки об установке — переустановите',
|
|
1549
|
+
'update.problem.noPackage': 'Установленный пакет не найден — переустановите',
|
|
1550
|
+
'update.problem.registryStatus': 'Реестр отклонил запрос',
|
|
1551
|
+
'update.problem.registryUnreachable': 'Не удалось связаться с реестром',
|
|
1552
|
+
'update.problem.badVersion': 'Реестр не сообщил корректную версию',
|
|
1553
|
+
'update.problem.noProfile': 'В отметке об установке нет профиля — переустановите',
|
|
1554
|
+
'update.problem.installFailed': 'Не удалось установить из реестра',
|
|
1538
1555
|
'update.dshMismatch': 'Внимание: установлен dsh {actual}, а эта версия проверялась на {expected}',
|
|
1539
|
-
'update.ahead': 'Установленная версия на {count} коммитов новее выпущенной',
|
|
1540
|
-
'update.unknownHistory': 'Установленной версии нет в истории — переустановите',
|
|
1541
1556
|
}
|
|
1542
1557
|
|
|
1543
1558
|
// ── Стили ────────────────────────────────────────────────────────────
|
|
@@ -1922,20 +1937,26 @@ window.__ModuleLoader__.load({
|
|
|
1922
1937
|
}
|
|
1923
1938
|
}, [])
|
|
1924
1939
|
|
|
1940
|
+
// Host сообщает и код проблемы, и её текст. Код переводится, текст
|
|
1941
|
+
// остаётся запасным вариантом: сообщение с Host всегда по-русски, и
|
|
1942
|
+
// без кода англоязычный пользователь увидел бы кириллицу.
|
|
1943
|
+
const problemText = (value) => {
|
|
1944
|
+
if (!value.problemCode) return value.problem
|
|
1945
|
+
const key = 'update.problem.' + value.problemCode
|
|
1946
|
+
const translated = t(key)
|
|
1947
|
+
return translated === key ? value.problem : translated
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1925
1950
|
let summary = t('update.checking')
|
|
1926
1951
|
if (state !== null) {
|
|
1927
|
-
if (state.problem) summary = state
|
|
1928
|
-
else if (state.
|
|
1929
|
-
else if (state.updateAvailable) summary = t('update.available').replace('{count}', String(state.behind))
|
|
1930
|
-
// Установленное опережает выпущенное — так выглядит машина
|
|
1931
|
-
// разработчика, который поставил себе ещё не запушенное.
|
|
1932
|
-
else if (state.ahead > 0) summary = t('update.ahead').replace('{count}', String(state.ahead))
|
|
1952
|
+
if (state.problem) summary = problemText(state)
|
|
1953
|
+
else if (state.updateAvailable) summary = t('update.available').replace('{version}', String(state.latestVersion))
|
|
1933
1954
|
else summary = t('update.upToDate')
|
|
1934
1955
|
}
|
|
1935
1956
|
|
|
1936
|
-
const version = state === null || !state.
|
|
1957
|
+
const version = state === null || !state.installedVersion
|
|
1937
1958
|
? ''
|
|
1938
|
-
: t('update.installed').replace('{
|
|
1959
|
+
: t('update.installed').replace('{version}', state.installedVersion).replace('{dsh}', state.dshExpected || '?')
|
|
1939
1960
|
|
|
1940
1961
|
// Расхождение версий dsh — самый тихий способ всё сломать: пакет
|
|
1941
1962
|
// опирается на контракты, а проверяли его на другой версии. Пользователь
|
|
@@ -1963,7 +1984,7 @@ window.__ModuleLoader__.load({
|
|
|
1963
1984
|
className: 'dsx-setting__hint dsx-setting__hint--strong',
|
|
1964
1985
|
children: result.ok
|
|
1965
1986
|
? (result.needsRestart ? t('update.doneRestart') : t('update.doneReload'))
|
|
1966
|
-
: (result
|
|
1987
|
+
: (problemText(result) || t('update.failed')),
|
|
1967
1988
|
}),
|
|
1968
1989
|
],
|
|
1969
1990
|
}),
|
package/lib/update-check.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Проверка и применение обновлений — Host-сторона.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* поломка обнаружится у пользователя.
|
|
4
|
+
* Источник истины — реестр npm, а не git. Прежняя версия сравнивала
|
|
5
|
+
* коммиты, и это требовало от каждой машины установленного git и доступа к
|
|
6
|
+
* приватному репозиторию. Реестр отдаёт версию анонимно обычным HTTP.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Обновления приходят с двух сторон: от разработчика (новая версия пакета)
|
|
9
|
+
* и от DeepSeek (новая версия dsh). Здесь показывается только первое —
|
|
10
|
+
* dsh пользователь обновлять сам не должен, иначе контракты разъедутся, а
|
|
11
|
+
* проверку никто не прогонял.
|
|
12
12
|
*
|
|
13
13
|
* Импортируются только встроенные модули Node: строка композиции грузится
|
|
14
14
|
* раньше, чем о пакете что-либо известно, и неразрешимый импорт уронил бы
|
|
@@ -16,14 +16,29 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { execFile } from 'node:child_process'
|
|
18
18
|
import { readFile } from 'node:fs/promises'
|
|
19
|
-
import { existsSync } from 'node:fs'
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
20
|
+
import { createHash } from 'node:crypto'
|
|
20
21
|
import { dirname, join } from 'node:path'
|
|
21
22
|
import { fileURLToPath } from 'node:url'
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
const PACKAGE_NAME = 'tensorgrid-ui'
|
|
25
|
+
const REGISTRY = 'https://registry.npmjs.org'
|
|
26
|
+
|
|
27
|
+
/** Версия из реестра попадает в командную строку — пускаем только безопасное. */
|
|
28
|
+
const SAFE_VERSION = /^[0-9A-Za-z.\-+]{1,64}$/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Запуск команды через оболочку на Windows.
|
|
32
|
+
*
|
|
33
|
+
* Напрямую `dsh` из Node не запускается: там это `dsh.cmd`, а execFile без
|
|
34
|
+
* оболочки исполняемым его не считает.
|
|
35
|
+
*/
|
|
36
|
+
function run(command, args, cwd, timeout = 60000) {
|
|
25
37
|
return new Promise((resolve) => {
|
|
26
|
-
|
|
38
|
+
const isWindows = process.platform === 'win32'
|
|
39
|
+
const file = isWindows ? 'cmd' : command
|
|
40
|
+
const argv = isWindows ? ['/c', command, ...args] : args
|
|
41
|
+
execFile(file, argv, { cwd, timeout, windowsHide: true }, (error, stdout, stderr) => {
|
|
27
42
|
resolve({
|
|
28
43
|
ok: !error,
|
|
29
44
|
stdout: String(stdout ?? '').trim(),
|
|
@@ -35,13 +50,13 @@ function run(command, args, cwd, timeout = 20000) {
|
|
|
35
50
|
}
|
|
36
51
|
|
|
37
52
|
/**
|
|
38
|
-
* Каталог профиля: отсюда
|
|
39
|
-
* `<профиль>/
|
|
40
|
-
* установке — так путь не зависит от
|
|
53
|
+
* Каталог профиля: отсюда модуль лежит в
|
|
54
|
+
* `<профиль>/node_modules/<пакет>/lib/`. Поднимаемся, пока не найдём
|
|
55
|
+
* отметку об установке — так путь не зависит от глубины вложения.
|
|
41
56
|
*/
|
|
42
57
|
function findProfileRoot(startDir) {
|
|
43
58
|
let dir = startDir
|
|
44
|
-
for (let depth = 0; depth <
|
|
59
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
45
60
|
if (existsSync(join(dir, 'tensorgrid-installed.json'))) return dir
|
|
46
61
|
const parent = dirname(dir)
|
|
47
62
|
if (parent === dir) break
|
|
@@ -50,13 +65,12 @@ function findProfileRoot(startDir) {
|
|
|
50
65
|
return null
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
async function
|
|
54
|
-
if (
|
|
68
|
+
async function readJson(path) {
|
|
69
|
+
if (path === '') return null
|
|
55
70
|
try {
|
|
56
|
-
const text = await readFile(
|
|
57
|
-
// BOM срезается намеренно: файл
|
|
58
|
-
//
|
|
59
|
-
// обновлений молча решила бы, что установки не было.
|
|
71
|
+
const text = await readFile(path, 'utf8')
|
|
72
|
+
// BOM срезается намеренно: файл мог быть записан инструментом Windows,
|
|
73
|
+
// а JSON.parse на строке с \uFEFF падает.
|
|
60
74
|
return JSON.parse(text.replace(/^\uFEFF/, ''))
|
|
61
75
|
} catch {
|
|
62
76
|
return null
|
|
@@ -64,104 +78,116 @@ async function readStamp(profileRoot) {
|
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
81
|
+
* Сравнение версий в объёме, который нам нужен: числовые части слева
|
|
82
|
+
* направо, предрелиз считается СТАРШЕ своего релиза (1.0.0-rc.1 < 1.0.0).
|
|
83
|
+
* @returns отрицательное, если a меньше b.
|
|
69
84
|
*/
|
|
85
|
+
export function compareVersions(a, b) {
|
|
86
|
+
const split = (value) => {
|
|
87
|
+
const text = String(value)
|
|
88
|
+
const dash = text.indexOf('-')
|
|
89
|
+
const core = dash < 0 ? text : text.slice(0, dash)
|
|
90
|
+
const pre = dash < 0 ? '' : text.slice(dash + 1)
|
|
91
|
+
return { parts: core.split('.').map((n) => Number.parseInt(n, 10) || 0), pre }
|
|
92
|
+
}
|
|
93
|
+
const left = split(a)
|
|
94
|
+
const right = split(b)
|
|
95
|
+
for (let i = 0; i < 3; i += 1) {
|
|
96
|
+
const diff = (left.parts[i] ?? 0) - (right.parts[i] ?? 0)
|
|
97
|
+
if (diff !== 0) return diff
|
|
98
|
+
}
|
|
99
|
+
if (left.pre === right.pre) return 0
|
|
100
|
+
if (left.pre === '') return 1
|
|
101
|
+
if (right.pre === '') return -1
|
|
102
|
+
return left.pre < right.pre ? -1 : 1
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Отпечаток host-половины: по нему видно, хватит ли перезагрузки страницы. */
|
|
106
|
+
function hostHalfDigest(profileRoot) {
|
|
107
|
+
try {
|
|
108
|
+
const path = join(profileRoot, 'node_modules', PACKAGE_NAME, 'lib', 'index.js')
|
|
109
|
+
return createHash('sha1').update(readFileSync(path)).digest('hex')
|
|
110
|
+
} catch {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Состояние обновлений. Ничего не меняет. */
|
|
70
116
|
export async function collectStatus() {
|
|
71
117
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
72
118
|
const profileRoot = findProfileRoot(here)
|
|
73
|
-
const stamp = await
|
|
119
|
+
const stamp = await readJson(profileRoot === null ? '' : join(profileRoot, 'tensorgrid-installed.json'))
|
|
74
120
|
|
|
75
121
|
const status = {
|
|
122
|
+
installedVersion: null,
|
|
123
|
+
latestVersion: null,
|
|
76
124
|
installedCommit: stamp?.commit ?? null,
|
|
77
125
|
installedAt: stamp?.installedAt ?? null,
|
|
78
126
|
dshExpected: stamp?.dshPinned ?? null,
|
|
79
127
|
dshActual: stamp?.dshActual ?? null,
|
|
80
|
-
|
|
81
|
-
available: null,
|
|
82
|
-
behind: 0,
|
|
128
|
+
profileId: stamp?.profileId ?? null,
|
|
83
129
|
updateAvailable: false,
|
|
84
130
|
canUpdate: false,
|
|
85
131
|
problem: null,
|
|
132
|
+
problemCode: null,
|
|
86
133
|
checkedAt: new Date().toISOString(),
|
|
87
134
|
}
|
|
88
135
|
|
|
89
|
-
if (stamp === null) {
|
|
90
|
-
status.problem = 'нет отметки об установке —
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
if (status.source === null || !existsSync(status.source)) {
|
|
94
|
-
status.problem = 'каталог с исходниками недоступен; обновление через интерфейс невозможно'
|
|
136
|
+
if (profileRoot === null || stamp === null) {
|
|
137
|
+
status.problem = 'нет отметки об установке — переустановите'
|
|
138
|
+
status.problemCode = 'noStamp'
|
|
95
139
|
return status
|
|
96
140
|
}
|
|
97
141
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
status.problem = `не удалось связаться с репозиторием: ${fetched.stderr || fetched.message || 'неизвестно'}`
|
|
142
|
+
const manifest = await readJson(join(profileRoot, 'node_modules', PACKAGE_NAME, 'package.json'))
|
|
143
|
+
if (manifest === null || typeof manifest.version !== 'string') {
|
|
144
|
+
status.problem = 'установленный пакет не найден — переустановите'
|
|
145
|
+
status.problemCode = 'noPackage'
|
|
103
146
|
return status
|
|
104
147
|
}
|
|
148
|
+
status.installedVersion = manifest.version
|
|
105
149
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
status.
|
|
150
|
+
let published
|
|
151
|
+
try {
|
|
152
|
+
// Сокращённый формат ответа: полный документ пакета весит на порядок
|
|
153
|
+
// больше и целиком нам не нужен.
|
|
154
|
+
const response = await fetch(`${REGISTRY}/${PACKAGE_NAME}`, {
|
|
155
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
156
|
+
signal: AbortSignal.timeout(20000),
|
|
157
|
+
})
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
status.problem = `реестр ответил ${response.status}`
|
|
160
|
+
status.problemCode = 'registryStatus'
|
|
161
|
+
return status
|
|
162
|
+
}
|
|
163
|
+
published = await response.json()
|
|
164
|
+
} catch (error) {
|
|
165
|
+
status.problem = `не удалось связаться с реестром: ${String(error?.message ?? error)}`
|
|
166
|
+
status.problemCode = 'registryUnreachable'
|
|
123
167
|
return status
|
|
124
168
|
}
|
|
125
169
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
status.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// ОПЕРЕЖАТЬ выпущенное: так выглядит машина разработчика, который уже
|
|
132
|
-
// поставил себе то, что ещё не запушил. Решает направление, а не факт
|
|
133
|
-
// расхождения.
|
|
134
|
-
const behind = await run('git', ['rev-list', '--count', `${installed}..${remoteRef}`], status.source)
|
|
135
|
-
const ahead = await run('git', ['rev-list', '--count', `${remoteRef}..${installed}`], status.source)
|
|
136
|
-
|
|
137
|
-
if (!behind.ok) {
|
|
138
|
-
// Установленного коммита нет в истории — например, ветку переписали.
|
|
139
|
-
// Сравнить нечем, но молчать нельзя.
|
|
140
|
-
status.behind = 0
|
|
141
|
-
status.updateAvailable = true
|
|
142
|
-
status.unknownHistory = true
|
|
143
|
-
} else {
|
|
144
|
-
status.behind = Number.parseInt(behind.stdout, 10) || 0
|
|
145
|
-
status.ahead = ahead.ok ? Number.parseInt(ahead.stdout, 10) || 0 : 0
|
|
146
|
-
status.updateAvailable = status.behind > 0
|
|
147
|
-
}
|
|
170
|
+
const latest = published?.['dist-tags']?.latest
|
|
171
|
+
if (typeof latest !== 'string' || !SAFE_VERSION.test(latest)) {
|
|
172
|
+
status.problem = 'реестр не сообщил корректную версию'
|
|
173
|
+
status.problemCode = 'badVersion'
|
|
174
|
+
return status
|
|
148
175
|
}
|
|
176
|
+
status.latestVersion = latest
|
|
149
177
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
status.canUpdate = status.updateAvailable && !isDirty && head.ok
|
|
155
|
-
if (status.updateAvailable && isDirty) {
|
|
156
|
-
status.problem = 'в каталоге с исходниками есть несохранённые правки — обновление через интерфейс отключено'
|
|
178
|
+
status.updateAvailable = compareVersions(status.installedVersion, latest) < 0
|
|
179
|
+
if (status.updateAvailable && typeof status.profileId !== 'string') {
|
|
180
|
+
status.problem = 'в отметке об установке нет профиля — переустановите'
|
|
181
|
+
status.problemCode = 'noProfile'
|
|
157
182
|
}
|
|
183
|
+
status.canUpdate = status.updateAvailable && typeof status.profileId === 'string'
|
|
158
184
|
|
|
159
185
|
return status
|
|
160
186
|
}
|
|
161
187
|
|
|
162
188
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
189
|
+
* Ставит последнюю версию из реестра и сообщает, достаточно ли
|
|
190
|
+
* перезагрузки страницы.
|
|
165
191
|
*/
|
|
166
192
|
export async function applyUpdate() {
|
|
167
193
|
const status = await collectStatus()
|
|
@@ -169,47 +195,35 @@ export async function applyUpdate() {
|
|
|
169
195
|
return { ok: false, problem: status.problem ?? 'обновлять нечего', status }
|
|
170
196
|
}
|
|
171
197
|
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
return { ok: false, problem: `git pull не удался: ${pulled.stderr || pulled.message}`, status }
|
|
176
|
-
}
|
|
177
|
-
const after = await run('git', ['rev-parse', 'HEAD'], status.source)
|
|
178
|
-
|
|
179
|
-
// Что именно изменилось, решает, хватит ли перезагрузки страницы.
|
|
180
|
-
// Браузерный бандл читается при загрузке страницы, а host-половина,
|
|
181
|
-
// патч-слой и пресет — только при старте приложения.
|
|
182
|
-
const changed = await run('git', ['diff', '--name-only', before.stdout, after.stdout], status.source)
|
|
183
|
-
const files = changed.ok ? changed.stdout.split('\n').map((line) => line.trim()).filter(Boolean) : []
|
|
184
|
-
const needsRestart = files.some((file) =>
|
|
185
|
-
file === 'lib/index.js' || file.startsWith('profile/') || file.startsWith('preset/') || file === 'install.ps1',
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
const installer = join(status.source, 'install.ps1')
|
|
189
|
-
if (!existsSync(installer)) {
|
|
190
|
-
return { ok: false, problem: `не найден установщик: ${installer}`, status }
|
|
191
|
-
}
|
|
198
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
199
|
+
const profileRoot = findProfileRoot(here)
|
|
200
|
+
const before = hostHalfDigest(profileRoot)
|
|
192
201
|
|
|
193
202
|
const installed = await run(
|
|
194
|
-
'
|
|
195
|
-
['
|
|
196
|
-
|
|
203
|
+
'dsh',
|
|
204
|
+
['plugin', '--profile', status.profileId, 'add', `${PACKAGE_NAME}@${status.latestVersion}`],
|
|
205
|
+
profileRoot,
|
|
197
206
|
180000,
|
|
198
207
|
)
|
|
199
208
|
if (!installed.ok) {
|
|
200
209
|
return {
|
|
201
210
|
ok: false,
|
|
202
|
-
problem: '
|
|
203
|
-
|
|
211
|
+
problem: 'установка из реестра не удалась',
|
|
212
|
+
problemCode: 'installFailed',
|
|
213
|
+
detail: (installed.stderr || installed.stdout || installed.message || '').split('\n').slice(-8).join('\n'),
|
|
204
214
|
status,
|
|
205
215
|
}
|
|
206
216
|
}
|
|
207
217
|
|
|
218
|
+
// Браузерный бандл читается при загрузке страницы, а host-половина —
|
|
219
|
+
// только при старте приложения. Решаем по её отпечатку, а не наугад.
|
|
220
|
+
const after = hostHalfDigest(profileRoot)
|
|
221
|
+
const needsRestart = before === null || after === null || before !== after
|
|
222
|
+
|
|
208
223
|
return {
|
|
209
224
|
ok: true,
|
|
210
|
-
from:
|
|
211
|
-
to:
|
|
212
|
-
files: files.length,
|
|
225
|
+
from: status.installedVersion,
|
|
226
|
+
to: status.latestVersion,
|
|
213
227
|
needsRestart,
|
|
214
228
|
status: await collectStatus(),
|
|
215
229
|
}
|
package/package.json
CHANGED