tensorgrid-ui 1.0.0 → 1.0.2

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 CHANGED
@@ -183,14 +183,20 @@ window.__ModuleLoader__.load({
183
183
  'update.working': 'Working…',
184
184
  'update.checking': 'Checking…',
185
185
  'update.upToDate': 'Latest version installed',
186
- 'update.available': 'A new version is available ({count} commits)',
187
- 'update.installed': 'Installed {commit}, verified on dsh {dsh}',
186
+ 'update.available': 'Version {version} is available',
187
+ 'update.availableUnknown': 'A new version is available',
188
+ 'update.installed': 'Installed {version}, verified on dsh {dsh}',
188
189
  'update.doneReload': 'Updated. Reload the page.',
189
190
  'update.doneRestart': 'Updated. Restart the application.',
190
191
  'update.failed': 'Update failed',
192
+ 'update.problem.noStamp': 'No installation record — reinstall',
193
+ 'update.problem.noPackage': 'Installed package not found — reinstall',
194
+ 'update.problem.registryStatus': 'The registry refused the request',
195
+ 'update.problem.registryUnreachable': 'Could not reach the registry',
196
+ 'update.problem.badVersion': 'The registry returned no valid version',
197
+ 'update.problem.noProfile': 'The installation record has no profile — reinstall',
198
+ 'update.problem.installFailed': 'Installing from the registry failed',
191
199
  '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
200
  }
195
201
 
196
202
  const ZH_DICT = {
@@ -208,14 +214,20 @@ window.__ModuleLoader__.load({
208
214
  'update.working': '处理中…',
209
215
  'update.checking': '检查中…',
210
216
  'update.upToDate': '已是最新版本',
211
- 'update.available': '有新版本({count} 次提交)',
212
- 'update.installed': '已安装 {commit},在 dsh {dsh} 上验证',
217
+ 'update.available': '有新版本 {version}',
218
+ 'update.availableUnknown': '有新版本可用',
219
+ 'update.installed': '已安装 {version},在 dsh {dsh} 上验证',
213
220
  'update.doneReload': '已更新。请刷新页面。',
214
221
  'update.doneRestart': '已更新。请重启应用。',
215
222
  'update.failed': '更新失败',
223
+ 'update.problem.noStamp': '没有安装记录 — 请重新安装',
224
+ 'update.problem.noPackage': '未找到已安装的包 — 请重新安装',
225
+ 'update.problem.registryStatus': '注册表拒绝了请求',
226
+ 'update.problem.registryUnreachable': '无法连接到注册表',
227
+ 'update.problem.badVersion': '注册表未返回有效版本',
228
+ 'update.problem.noProfile': '安装记录中没有配置文件 — 请重新安装',
229
+ 'update.problem.installFailed': '从注册表安装失败',
216
230
  'update.dshMismatch': '注意:已安装 dsh {actual},但此版本在 {expected} 上验证',
217
- 'update.ahead': '已安装版本比已发布版本新 {count} 次提交',
218
- 'update.unknownHistory': '已安装版本不在历史记录中 — 请重新安装',
219
231
  }
220
232
 
221
233
  // ── RU-PACK:BEGIN — генерируется tools/build-ru.mjs, руками не править ──
@@ -1530,14 +1542,20 @@ window.__ModuleLoader__.load({
1530
1542
  'update.working': 'Работаю…',
1531
1543
  'update.checking': 'Проверяю…',
1532
1544
  'update.upToDate': 'Установлена последняя версия',
1533
- 'update.available': 'Доступна новая версия ({count} коммитов)',
1534
- 'update.installed': 'Установлено {commit}, проверено на dsh {dsh}',
1545
+ 'update.available': 'Доступна версия {version}',
1546
+ 'update.availableUnknown': 'Доступна новая версия',
1547
+ 'update.installed': 'Установлена {version}, проверено на dsh {dsh}',
1535
1548
  'update.doneReload': 'Обновлено. Обновите страницу.',
1536
1549
  'update.doneRestart': 'Обновлено. Перезапустите приложение.',
1537
1550
  'update.failed': 'Обновить не удалось',
1551
+ 'update.problem.noStamp': 'Нет отметки об установке — переустановите',
1552
+ 'update.problem.noPackage': 'Установленный пакет не найден — переустановите',
1553
+ 'update.problem.registryStatus': 'Реестр отклонил запрос',
1554
+ 'update.problem.registryUnreachable': 'Не удалось связаться с реестром',
1555
+ 'update.problem.badVersion': 'Реестр не сообщил корректную версию',
1556
+ 'update.problem.noProfile': 'В отметке об установке нет профиля — переустановите',
1557
+ 'update.problem.installFailed': 'Не удалось установить из реестра',
1538
1558
  'update.dshMismatch': 'Внимание: установлен dsh {actual}, а эта версия проверялась на {expected}',
1539
- 'update.ahead': 'Установленная версия на {count} коммитов новее выпущенной',
1540
- 'update.unknownHistory': 'Установленной версии нет в истории — переустановите',
1541
1559
  }
1542
1560
 
1543
1561
  // ── Стили ────────────────────────────────────────────────────────────
@@ -1922,20 +1940,40 @@ window.__ModuleLoader__.load({
1922
1940
  }
1923
1941
  }, [])
1924
1942
 
1943
+ // Host сообщает и код проблемы, и её текст. Код переводится, текст
1944
+ // остаётся запасным вариантом: сообщение с Host всегда по-русски, и
1945
+ // без кода англоязычный пользователь увидел бы кириллицу.
1946
+ const problemText = (value) => {
1947
+ if (!value.problemCode) return value.problem
1948
+ const key = 'update.problem.' + value.problemCode
1949
+ const translated = t(key)
1950
+ return translated === key ? value.problem : translated
1951
+ }
1952
+
1953
+ // Подставляем значение, только если оно действительно пришло. Иначе
1954
+ // берём формулировку без него.
1955
+ //
1956
+ // Это не перестраховка: при обновлении Host перечитывает свою
1957
+ // половину сразу, а браузерный бандл — только после перезагрузки
1958
+ // страницы. В этом промежутке старый клиент получает ответ новой
1959
+ // формы, и слепая подстановка выдавала пользователю «undefined».
1960
+ const fill = (key, fallbackKey, name, value) =>
1961
+ typeof value === 'string' && value !== ''
1962
+ ? t(key).replace('{' + name + '}', value)
1963
+ : t(fallbackKey)
1964
+
1925
1965
  let summary = t('update.checking')
1926
1966
  if (state !== null) {
1927
- if (state.problem) summary = state.problem
1928
- else if (state.unknownHistory) summary = t('update.unknownHistory')
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))
1967
+ if (state.problem) summary = problemText(state)
1968
+ else if (state.updateAvailable) summary = fill('update.available', 'update.availableUnknown', 'version', state.latestVersion)
1933
1969
  else summary = t('update.upToDate')
1934
1970
  }
1935
1971
 
1936
- const version = state === null || !state.installedCommit
1972
+ const version = state === null || typeof state.installedVersion !== 'string'
1937
1973
  ? ''
1938
- : t('update.installed').replace('{commit}', state.installedCommit).replace('{dsh}', state.dshExpected || '?')
1974
+ : t('update.installed')
1975
+ .replace('{version}', state.installedVersion)
1976
+ .replace('{dsh}', typeof state.dshExpected === 'string' ? state.dshExpected : '—')
1939
1977
 
1940
1978
  // Расхождение версий dsh — самый тихий способ всё сломать: пакет
1941
1979
  // опирается на контракты, а проверяли его на другой версии. Пользователь
@@ -1963,7 +2001,7 @@ window.__ModuleLoader__.load({
1963
2001
  className: 'dsx-setting__hint dsx-setting__hint--strong',
1964
2002
  children: result.ok
1965
2003
  ? (result.needsRestart ? t('update.doneRestart') : t('update.doneReload'))
1966
- : (result.problem || t('update.failed')),
2004
+ : (problemText(result) || t('update.failed')),
1967
2005
  }),
1968
2006
  ],
1969
2007
  }),
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Проверка и применение обновлений — Host-сторона.
3
3
  *
4
- * Обновления приходят с двух сторон: от разработчика (новый коммит) и от
5
- * DeepSeek (новая версия dsh в npm). Каналы независимы, и опасен второй без
6
- * первого: пакет опирается на контракты dsh, и если их никто не проверил,
7
- * поломка обнаружится у пользователя.
4
+ * Источник истины реестр npm, а не git. Прежняя версия сравнивала
5
+ * коммиты, и это требовало от каждой машины установленного git и доступа к
6
+ * приватному репозиторию. Реестр отдаёт версию анонимно обычным HTTP.
8
7
  *
9
- * Поэтому здесь НЕ предлагается обновить dsh самостоятельно. Показывается
10
- * только то, что выпустил разработчик, а он выпускает версию уже с
11
- * отметкой, на какой версии dsh она проверена.
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
- /** Запуск команды с жёстким таймаутом: висящий git не должен держать проверку. */
24
- function run(command, args, cwd, timeout = 20000) {
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
- execFile(command, args, { cwd, timeout, windowsHide: true, shell: false }, (error, stdout, stderr) => {
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
- * Каталог профиля: отсюда `lib/index.js` лежит в
39
- * `<профиль>/packages/<пакет>/lib/`. Поднимаемся, пока не найдём отметку об
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 < 6; depth += 1) {
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 readStamp(profileRoot) {
54
- if (profileRoot === null) return null
68
+ async function readJson(path) {
69
+ if (path === '') return null
55
70
  try {
56
- const text = await readFile(join(profileRoot, 'tensorgrid-installed.json'), 'utf8')
57
- // BOM срезается намеренно: файл может быть записан инструментом
58
- // Windows, а JSON.parse на строке с \uFEFF падает — и проверка
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
- * @returns сведения об установленной и доступной версиях плюс причина, если проверить не удалось.
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 readStamp(profileRoot)
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
- source: stamp?.source ?? null,
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 = 'нет отметки об установке — запустите install.ps1'
91
- return status
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
- // git fetch трогает сеть, но рабочее дерево не меняет: посмотреть, что
99
- // выпущено, можно не забирая изменений.
100
- const fetched = await run('git', ['fetch', '--quiet', 'origin'], status.source, 30000)
101
- if (!fetched.ok) {
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
- const head = await run('git', ['rev-parse', '--short', 'HEAD'], status.source)
107
- const remote = await run('git', ['rev-parse', '--short', 'origin/HEAD'], status.source)
108
- const remoteRef = remote.ok ? 'origin/HEAD' : 'origin/main'
109
- const remoteShort = remote.ok ? remote.stdout : (await run('git', ['rev-parse', '--short', remoteRef], status.source)).stdout
110
-
111
- status.available = remoteShort || null
112
-
113
- // Сравнивается УСТАНОВЛЕННОЕ с выпущенным, а не рабочая копия с сервером.
114
- //
115
- // Разница принципиальна на машине разработчика: он коммитит в ту же
116
- // рабочую копию, поэтому она всегда совпадает с сервером, а установленный
117
- // продукт при этом может отставать на десяток коммитов. Но и у сотрудника
118
- // это важно: `git pull` без запуска установщика обновляет исходники, не
119
- // трогая то, что реально работает.
120
- const installed = status.installedCommit
121
- if (installed === null || installed === 'неизвестно') {
122
- status.problem = 'не удалось определить установленную версию — переустановите'
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
- if (installed === status.available) {
127
- status.behind = 0
128
- status.updateAvailable = false
129
- } else {
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
- // Обновлять можно только из чистого дерева: иначе git pull упрётся в
151
- // локальные правки, а разбираться с этим из интерфейса нечем.
152
- const dirty = await run('git', ['status', '--porcelain'], status.source)
153
- const isDirty = dirty.ok && dirty.stdout.length > 0
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 before = await run('git', ['rev-parse', 'HEAD'], status.source)
173
- const pulled = await run('git', ['pull', '--ff-only'], status.source, 60000)
174
- if (!pulled.ok) {
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
- 'powershell',
195
- ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', installer, '-NoAutostart'],
196
- status.source,
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
- detail: (installed.stdout || installed.stderr || '').split('\n').slice(-12).join('\n'),
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: before.stdout.slice(0, 7),
211
- to: after.stdout.slice(0, 7),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tensorgrid-ui",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "TENSOR GRID — фирменный интерфейс поверх DeepSeek Harness: палитра, живой ambient-слой, айдентика и русский язык-пакет",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",