tensorgrid-ui 1.6.1 → 2.0.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.
package/install.ps1 ADDED
@@ -0,0 +1,457 @@
1
+ #Requires -Version 5.1
2
+ <#
3
+ .SYNOPSIS
4
+ Устанавливает TENSOR GRID на машину сотрудника.
5
+
6
+ .DESCRIPTION
7
+ Собирает приложение из трёх частей, которые живут в разных местах:
8
+
9
+ профиль ~/.dsh/profiles/<id>/ какие бандлы составляют приложение
10
+ пакет <профиль>/packages/ палитра, айдентика, русский язык
11
+ пресет ~/.dsh/.agent-presets/<id>/ персона агента и его инструменты
12
+
13
+ Скрипт повторно запускаемый: тем же файлом ставят и обновляют. Чужого
14
+ не трогает — настройки, ключи и историю сессий не переписывает, а
15
+ поставочный профиль `web` остаётся рабочим запасным вариантом.
16
+
17
+ .PARAMETER ProfileId
18
+ Идентификатор профиля и пресета. По умолчанию tensorgrid.
19
+
20
+ .PARAMETER NoAutostart
21
+ Не создавать ярлык автозапуска при входе в Windows.
22
+
23
+ .PARAMETER DryRun
24
+ Показать, что будет сделано, ничего не меняя.
25
+
26
+ .EXAMPLE
27
+ .\install.ps1
28
+ .\install.ps1 -DryRun
29
+ .\install.ps1 -NoAutostart
30
+ #>
31
+ [CmdletBinding()]
32
+ param(
33
+ [string]$ProfileId = 'tensorgrid',
34
+ [switch]$NoAutostart,
35
+ [switch]$DryRun
36
+ )
37
+
38
+ $ErrorActionPreference = 'Stop'
39
+ $source = $PSScriptRoot
40
+
41
+ # ── вывод ────────────────────────────────────────────────────────────────
42
+ $script:stepNumber = 0
43
+ function Write-Step([string]$text) {
44
+ $script:stepNumber++
45
+ Write-Host ''
46
+ Write-Host (" {0}. {1}" -f $script:stepNumber, $text) -ForegroundColor Cyan
47
+ }
48
+ function Write-Ok([string]$text) { Write-Host " $text" -ForegroundColor DarkGray }
49
+ function Write-Warn([string]$text) { Write-Host " $text" -ForegroundColor Yellow }
50
+ function Write-Die([string]$text) {
51
+ Write-Host ''
52
+ Write-Host " ОСТАНОВКА: $text" -ForegroundColor Red
53
+ Write-Host ''
54
+ exit 1
55
+ }
56
+
57
+ function Invoke-Action([string]$what, [scriptblock]$action) {
58
+ if ($DryRun) { Write-Ok "[сухой прогон] $what"; return }
59
+ Write-Ok $what
60
+ & $action
61
+ }
62
+
63
+ Write-Host ''
64
+ Write-Host ' ╔══════════════════════════════════════╗' -ForegroundColor Cyan
65
+ Write-Host ' ║ T E N S O R G R I D ║' -ForegroundColor Cyan
66
+ Write-Host ' ╚══════════════════════════════════════╝' -ForegroundColor Cyan
67
+ if ($DryRun) { Write-Host ' СУХОЙ ПРОГОН — ничего не изменится' -ForegroundColor Yellow }
68
+
69
+ # ── 1. Что должно быть на машине ─────────────────────────────────────────
70
+ Write-Step 'Проверяю, что нужно для работы'
71
+
72
+ $node = Get-Command node -ErrorAction SilentlyContinue
73
+ if (-not $node) {
74
+ Write-Die "Node.js не найден. Поставьте LTS с https://nodejs.org и запустите скрипт заново."
75
+ }
76
+ $nodeVersion = (& node --version).TrimStart('v')
77
+ $nodeMajor = [int]($nodeVersion -split '\.')[0]
78
+ if ($nodeMajor -lt 20) {
79
+ Write-Die "Node.js $nodeVersion слишком старый, нужен 20 или новее."
80
+ }
81
+ Write-Ok "Node.js $nodeVersion"
82
+
83
+ if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
84
+ Write-Die 'npm не найден, хотя Node.js есть. Переустановите Node.js.'
85
+ }
86
+
87
+ # ── 2. Сам dsh ───────────────────────────────────────────────────────────
88
+ Write-Step 'Проверяю среду выполнения'
89
+
90
+ # Версия берётся из снимка окружения — того самого, против которого пакет
91
+ # проверялся. Так у всех стоит ровно то, на чём он испытан.
92
+ #
93
+ # Закрепление обязательно, а не перестраховка: в npm метка `latest` у dsh
94
+ # указывает на 0.1.5-rc.1, тогда как 0.1.5-rc.2 опубликован позже. Простое
95
+ # `npm install -g @deepseek-ai/dsh` поставило бы сотруднику версию СТАРШЕ
96
+ # той, на которой всё проверено.
97
+ $pinnedVersion = $null
98
+ $snapshotPath = Join-Path $source 'env\snapshot.json'
99
+ if (Test-Path $snapshotPath) {
100
+ $pinnedVersion = (Get-Content -Raw $snapshotPath | ConvertFrom-Json).dshVersion
101
+ }
102
+ if (-not $pinnedVersion) { Write-Die "Не удалось прочитать проверенную версию dsh из $snapshotPath" }
103
+ Write-Ok "Проверенная версия среды: $pinnedVersion"
104
+
105
+ $dshCommand = Get-Command dsh -ErrorAction SilentlyContinue
106
+ if ($dshCommand) {
107
+ $dshVersion = (& dsh --version 2>&1 | Select-Object -First 1).Trim()
108
+ if ($dshVersion -eq $pinnedVersion) {
109
+ Write-Ok "среда $dshVersion — совпадает с проверенной"
110
+ } else {
111
+ Write-Warn "установлена среда $dshVersion, а проверялись на $pinnedVersion"
112
+ Write-Warn "обновить вручную: npm install -g `@deepseek-ai/dsh@$pinnedVersion"
113
+ }
114
+ } else {
115
+ Write-Warn "среда не найдена — устанавливаю (займёт пару минут)"
116
+ Invoke-Action "устанавливаю среду выполнения $pinnedVersion" {
117
+ # Вывод npm заглушается намеренно: он называет чужие пакеты, а
118
+ # сотрудник должен видеть только наш продукт.
119
+ & npm install -g "@deepseek-ai/dsh@$pinnedVersion" --loglevel=error 2>&1 | Out-Null
120
+ if ($LASTEXITCODE -ne 0) { Write-Die 'Не удалось установить среду выполнения. Проверьте доступ в интернет.' }
121
+ }
122
+ if (-not $DryRun -and -not (Get-Command dsh -ErrorAction SilentlyContinue)) {
123
+ Write-Die 'Среда установлена, но не видна системе. Закройте окно, откройте новое и повторите.'
124
+ }
125
+ }
126
+
127
+ $dshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' }
128
+ Write-Ok "Каталог данных: $dshHome"
129
+
130
+ # ── 3. Профиль ───────────────────────────────────────────────────────────
131
+ Write-Step "Профиль $ProfileId"
132
+
133
+ $profilePath = Join-Path $dshHome "profiles\$ProfileId"
134
+ if (Test-Path (Join-Path $profilePath 'package.json')) {
135
+ Write-Ok 'уже существует — создавать не нужно'
136
+ } else {
137
+ # --dump-config создаёт профиль и выходит, не поднимая сервер:
138
+ # обычный запуск занял бы порт и остался бы висеть.
139
+ Invoke-Action "создаю из поставочного шаблона web" {
140
+ & dsh --profile $ProfileId --from-default-profile web --dump-config 2>&1 | Out-Null
141
+ if (-not (Test-Path (Join-Path $profilePath 'package.json'))) {
142
+ Write-Die "Профиль не создался. Проверьте вручную: dsh --profile $ProfileId --from-default-profile web --dump-config"
143
+ }
144
+ }
145
+ }
146
+
147
+ # Пакет лежит там, куда его кладёт pnpm, а не в profile\packages.
148
+ $packageTarget = Join-Path $profilePath 'node_modules\tensorgrid-ui'
149
+ $presetTargetEarly = Join-Path $dshHome ".agent-presets\$ProfileId"
150
+ $backupRoot = Join-Path $profilePath '.backup'
151
+
152
+ # ── 4. Резервная копия ───────────────────────────────────────────────────
153
+ Write-Step 'Резервная копия того, что стоит сейчас'
154
+
155
+ # Копия делается ДО перезаписи и не зависит от git: если обновление
156
+ # сломает интерфейс, чинить надо быстро, а не разбираться с репозиторием.
157
+ # Сотруднику откат нужен одной командой, даже когда git у него не настроен.
158
+ if (-not (Test-Path (Join-Path $packageTarget 'lib\client.js'))) {
159
+ Write-Ok 'первая установка — копировать нечего'
160
+ } else {
161
+ $stamp = Get-Date -Format 'yyyy-MM-dd_HH-mm-ss'
162
+ $backupPath = Join-Path $backupRoot $stamp
163
+ Invoke-Action "сохраняю в .backup\$stamp" {
164
+ New-Item -ItemType Directory -Force -Path $backupPath | Out-Null
165
+ $null = robocopy $packageTarget (Join-Path $backupPath 'package') /MIR /NFL /NDL /NJH /NJS
166
+ if ($LASTEXITCODE -ge 8) { Write-Die "Не удалось сохранить копию пакета (robocopy $LASTEXITCODE)." }
167
+
168
+ $patchNow = Join-Path $profilePath 'cordis.patch.yml'
169
+ if (Test-Path $patchNow) { Copy-Item $patchNow (Join-Path $backupPath 'cordis.patch.yml') -Force }
170
+
171
+ if (Test-Path $presetTargetEarly) {
172
+ $null = robocopy $presetTargetEarly (Join-Path $backupPath 'preset') /MIR /NFL /NDL /NJH /NJS
173
+ }
174
+
175
+ $stampNow = Join-Path $profilePath 'tensorgrid-installed.json'
176
+ if (Test-Path $stampNow) { Copy-Item $stampNow (Join-Path $backupPath 'installed.json') -Force }
177
+ }
178
+
179
+ # Держим последние три: больше не нужно, а место занимают словари.
180
+ if (-not $DryRun -and (Test-Path $backupRoot)) {
181
+ $old = @(Get-ChildItem -Directory $backupRoot | Sort-Object Name -Descending | Select-Object -Skip 3)
182
+ foreach ($item in $old) {
183
+ Write-Ok "убираю старую копию: $($item.Name)"
184
+ Remove-Item -Recurse -Force $item.FullName
185
+ }
186
+ }
187
+ }
188
+
189
+ # ── 5. Уборка прежней схемы ──────────────────────────────────────────────
190
+ Write-Step 'Прежняя установка'
191
+
192
+ # До перехода на реестр пакет копировался в profile\packages\. После
193
+ # перехода этот каталог больше не читается: строка композиции указывает на
194
+ # имя пакета. Оставленный, он сбивает с толку при разборе — и выглядит как
195
+ # действующая установка, которой уже нет.
196
+ $legacyPackage = Join-Path $profilePath 'packages\dsh-ui-obsidian-ion'
197
+ if (Test-Path $legacyPackage) {
198
+ Invoke-Action 'убираю пакет прежней схемы (копия в profile\packages)' {
199
+ Remove-Item -Recurse -Force $legacyPackage
200
+ $legacyRoot = Join-Path $profilePath 'packages'
201
+ if ((Test-Path $legacyRoot) -and (@(Get-ChildItem -Force $legacyRoot).Count -eq 0)) {
202
+ Remove-Item -Force $legacyRoot
203
+ }
204
+ }
205
+ } else {
206
+ Write-Ok 'следов прежней схемы нет'
207
+ }
208
+
209
+ # ── 6. Пакет интерфейса ──────────────────────────────────────────────────
210
+ Write-Step 'Пакет интерфейса'
211
+
212
+ # Пакет берётся из реестра npm штатной командой dsh, а не копированием.
213
+ #
214
+ # Копирование требовало, чтобы у сотрудника были git и доступ к приватному
215
+ # репозиторию; на этом мы потеряли полдня, разбираясь с токеном чужого
216
+ # аккаунта в хранилище Windows. Реестр отдаёт пакет анонимно.
217
+ #
218
+ # Версия закреплена манифестом репозитория: у всех встаёт ровно то, что
219
+ # проверено, а не то, что оказалось свежим в момент установки.
220
+ $packageName = 'tensorgrid-ui'
221
+ $packageVersion = (Get-Content -Raw (Join-Path $source 'package.json') | ConvertFrom-Json).version
222
+ $packageSpec = "$packageName@$packageVersion"
223
+
224
+ # Версия из манифеста может опережать реестр: на машине разработчика номер
225
+ # поднимают раньше публикации. Без этой проверки установщик падал с
226
+ # сообщением про интернет, хотя интернет был ни при чём.
227
+ if (-not $DryRun) {
228
+ $available = @()
229
+ try {
230
+ $doc = Invoke-WebRequest -Uri "https://registry.npmjs.org/$packageName" -UseBasicParsing -TimeoutSec 30
231
+ $available = @((([string]$doc.Content) | ConvertFrom-Json).versions.PSObject.Properties.Name)
232
+ } catch {
233
+ Write-Die "Не удалось прочитать реестр. Проверьте доступ в интернет."
234
+ }
235
+ if ($available -notcontains $packageVersion) {
236
+ Write-Die "Версия $packageVersion ещё не опубликована. В реестре есть: $($available -join ', '). Опубликуйте её командой npm publish либо укажите в package.json номер существующей версии."
237
+ }
238
+ }
239
+
240
+ Invoke-Action "ставлю $packageSpec из реестра" {
241
+ # dsh пишет предупреждения в поток ошибок — например, что пакет не
242
+ # объявляет dsh.bundle (для нас это норма: мы монтируем его своей
243
+ # строкой). При ErrorActionPreference = 'Stop' PowerShell превращает
244
+ # любую строку stderr в исключение, и безобидное предупреждение роняет
245
+ # установку. Поэтому на время вызова переходим на мягкий режим и судим
246
+ # по коду возврата, а не по наличию вывода.
247
+ $previous = $ErrorActionPreference
248
+ $ErrorActionPreference = 'Continue'
249
+ try {
250
+ & dsh plugin --profile $ProfileId add $packageSpec 2>&1 | Out-Null
251
+ } finally {
252
+ $ErrorActionPreference = $previous
253
+ }
254
+ if ($LASTEXITCODE -ne 0) { Write-Die "Не удалось поставить $packageSpec. Проверьте доступ в интернет." }
255
+ }
256
+
257
+ # ── 7. Строка композиции ─────────────────────────────────────────────────
258
+ Write-Step 'Подключение пакета к профилю'
259
+
260
+ $patchSource = Join-Path $source 'profile\cordis.patch.yml'
261
+ if (-not (Test-Path $patchSource)) { Write-Die "Не найден $patchSource — репозиторий неполный." }
262
+
263
+ Invoke-Action 'кладу патч-слой профиля' {
264
+ Copy-Item $patchSource (Join-Path $profilePath 'cordis.patch.yml') -Force
265
+ }
266
+
267
+ # ── 8. Агент-пресет ──────────────────────────────────────────────────────
268
+ Write-Step 'Агент-пресет'
269
+
270
+ $presetSource = Join-Path $source 'preset'
271
+ $presetTarget = Join-Path $dshHome ".agent-presets\$ProfileId"
272
+ if (-not (Test-Path (Join-Path $presetSource 'agent.cordis.yml'))) {
273
+ Write-Die "Не найден $presetSource\agent.cordis.yml — репозиторий неполный."
274
+ }
275
+
276
+ Invoke-Action "копирую в $presetTarget" {
277
+ New-Item -ItemType Directory -Force -Path $presetTarget | Out-Null
278
+ Copy-Item (Join-Path $presetSource '*') $presetTarget -Force
279
+ }
280
+
281
+ # ── 9. Автозапуск ────────────────────────────────────────────────────────
282
+ Write-Step 'Автозапуск при входе в Windows'
283
+
284
+ if ($NoAutostart) {
285
+ Write-Ok 'пропущен по ключу -NoAutostart'
286
+ } else {
287
+ $startupDir = [Environment]::GetFolderPath('Startup')
288
+ $startupFile = Join-Path $startupDir 'TENSOR GRID.cmd'
289
+ # Тело строго в ASCII и без BOM: .cmd читается в OEM-кодировке, и
290
+ # кириллица внутри превратилась бы в нечитаемый мусор.
291
+ $cmdBody = @"
292
+ @echo off
293
+ rem TENSOR GRID - autostart. Boots the branded profile.
294
+ rem Stock fallback if it ever fails: dsh web
295
+ rem To disable: delete this file. Folder: shell:startup
296
+ cd /d "%USERPROFILE%"
297
+ start "TENSOR GRID" /min "%APPDATA%\npm\dsh.cmd" --profile $ProfileId
298
+ "@
299
+ # Ранние установки могли оставить файл под другим именем. Два автозапуска
300
+ # подрались бы за один порт, и второй молча не поднялся бы.
301
+ foreach ($legacy in @('DeepSeek Harness.cmd', 'dsh.cmd')) {
302
+ $legacyPath = Join-Path $startupDir $legacy
303
+ if ((Test-Path $legacyPath) -and ($legacyPath -ne $startupFile)) {
304
+ Invoke-Action "убираю прежний автозапуск: $legacy" {
305
+ Remove-Item $legacyPath -Force
306
+ }
307
+ }
308
+ }
309
+
310
+ Invoke-Action "создаю $startupFile" {
311
+ [IO.File]::WriteAllText($startupFile, ($cmdBody -replace "`r?`n", "`r`n"), (New-Object Text.ASCIIEncoding))
312
+ }
313
+ }
314
+
315
+ # ── 10. Отметка об установке ──────────────────────────────────────────────
316
+ Write-Step 'Записываю, что именно установлено'
317
+
318
+ # Без этой отметки на вопрос «что у тебя стоит» нет однозначного ответа, а
319
+ # при десятке машин он возникает на первом же разборе «у меня не работает».
320
+ # Файл лежит в корне профиля, а не в каталоге пакета: тот зеркалится
321
+ # robocopy /MIR и лишнее оттуда удаляется.
322
+ $stampPath = Join-Path $profilePath 'tensorgrid-installed.json'
323
+
324
+ $commit = 'неизвестно'
325
+ $commitDate = 'неизвестно'
326
+ if (Get-Command git -ErrorAction SilentlyContinue) {
327
+ Push-Location $source
328
+ try {
329
+ $probe = & git rev-parse --short HEAD 2>$null
330
+ if ($LASTEXITCODE -eq 0 -and $probe) {
331
+ $commit = $probe.Trim()
332
+ $commitDate = (& git log -1 --format=%cI 2>$null).Trim()
333
+ }
334
+ } catch { } finally { Pop-Location }
335
+ }
336
+
337
+ $stamp = [ordered]@{
338
+ product = 'TENSOR GRID'
339
+ profileId = $ProfileId
340
+ installedAt = (Get-Date).ToString('o')
341
+ commit = $commit
342
+ commitDate = $commitDate
343
+ dshPinned = $pinnedVersion
344
+ dshActual = if ($DryRun) { 'сухой прогон' } else { (& dsh --version 2>&1 | Select-Object -First 1).Trim() }
345
+ source = $source
346
+ machine = $env:COMPUTERNAME
347
+ }
348
+
349
+ Invoke-Action "отметка: коммит $commit, dsh $pinnedVersion" {
350
+ # Без BOM: Set-Content -Encoding UTF8 в Windows PowerShell 5.1 его
351
+ # добавляет, а JSON.parse на строке с BOM падает — и Host-половина
352
+ # молча решает, что установки не было.
353
+ [IO.File]::WriteAllText($stampPath, ($stamp | ConvertTo-Json), (New-Object Text.UTF8Encoding($false)))
354
+ }
355
+
356
+ # ── 11. Ярлык запуска ────────────────────────────────────────────────────
357
+ Write-Step 'Ярлык запуска'
358
+
359
+ # Сотрудник не должен вводить команд и видеть чужих имён. Запускатор
360
+ # кладётся рядом с профилем, ярлык ведёт на него, а заголовок окна
361
+ # переименован — иначе в панели задач светилось бы чужое имя.
362
+ $launcherPath = Join-Path $profilePath 'TENSOR GRID.cmd'
363
+ $launcherBody = @"
364
+ @echo off
365
+ title TENSOR GRID
366
+ cd /d "%USERPROFILE%"
367
+ "%APPDATA%\npm\dsh.cmd" --profile $ProfileId
368
+ "@
369
+
370
+ Invoke-Action 'создаю запускатор' {
371
+ # Строго ASCII и без BOM: .cmd читается в OEM-кодировке.
372
+ [IO.File]::WriteAllText($launcherPath, ($launcherBody -replace "`r?`n", "`r`n"), (New-Object Text.ASCIIEncoding))
373
+ }
374
+
375
+ # Ярлык создаётся через COM-оболочку Windows: другого способа записать .lnk
376
+ # штатными средствами нет.
377
+ $shortcutTargets = @(
378
+ [Environment]::GetFolderPath('Desktop'),
379
+ (Join-Path ([Environment]::GetFolderPath('StartMenu')) 'Programs')
380
+ )
381
+ # Иконка копируется В ПРОФИЛЬ, а не берётся из каталога разработки.
382
+ #
383
+ # Ярлык хранит путь к файлу и читает его при каждой отрисовке. Если
384
+ # оставить путь в репозитории, ярлык у сотрудника осыплется, как только
385
+ # тот удалит клон — а удалить его после установки совершенно естественно.
386
+ $iconSource = Join-Path $source 'brand\tensorgrid.ico'
387
+ $iconPath = Join-Path $profilePath 'TENSOR GRID.ico'
388
+ if (Test-Path $iconSource) {
389
+ Invoke-Action 'кладу иконку рядом с профилем' {
390
+ Copy-Item $iconSource $iconPath -Force
391
+ # Скругление накладывается здесь, а не рисуется в SVG: конвертеры
392
+ # SVG в ICO заливают прозрачность белым, и углы приезжают белыми
393
+ # треугольниками. Исходник остаётся сплошным квадратом, который
394
+ # испортить нельзя, а углы вырезаются альфа-каналом.
395
+ $rounder = Join-Path $source 'tools\round-ico-corners.mjs'
396
+ if (Test-Path $rounder) {
397
+ $previousPref = $ErrorActionPreference
398
+ $ErrorActionPreference = 'Continue'
399
+ try { & node $rounder $iconPath 2>&1 | Out-Null } finally { $ErrorActionPreference = $previousPref }
400
+ }
401
+ }
402
+ } else {
403
+ Write-Ok 'иконки нет — ярлык получит стандартную (см. brand/README.md)'
404
+ }
405
+
406
+ foreach ($dir in $shortcutTargets) {
407
+ if (-not (Test-Path $dir)) { continue }
408
+ $linkPath = Join-Path $dir 'TENSOR GRID.lnk'
409
+ Invoke-Action "ярлык: $linkPath" {
410
+ $shell = New-Object -ComObject WScript.Shell
411
+ $link = $shell.CreateShortcut($linkPath)
412
+ $link.TargetPath = $launcherPath
413
+ $link.WorkingDirectory = $env:USERPROFILE
414
+ $link.Description = 'TENSOR GRID'
415
+ # Свёрнутое окно: консоль не мешает, но остаётся доступной, если
416
+ # что-то пойдёт не так.
417
+ $link.WindowStyle = 7
418
+ if (Test-Path $iconPath) { $link.IconLocation = $iconPath }
419
+ $link.Save()
420
+ }
421
+ }
422
+
423
+ # ── 12. Проверка ──────────────────────────────────────────────────────────
424
+ Write-Step 'Проверяю установку'
425
+
426
+ if ($DryRun) {
427
+ Write-Ok '[сухой прогон] проверка пропущена'
428
+ } else {
429
+ $checks = @(
430
+ @{ Name = 'манифест профиля'; Path = Join-Path $profilePath 'package.json' }
431
+ @{ Name = 'патч-слой'; Path = Join-Path $profilePath 'cordis.patch.yml' }
432
+ @{ Name = 'host-половина'; Path = Join-Path $profilePath 'node_modules\tensorgrid-ui\lib\index.js' }
433
+ @{ Name = 'браузерный бандл'; Path = Join-Path $profilePath 'node_modules\tensorgrid-ui\lib\client.js' }
434
+ @{ Name = 'композиция пресета'; Path = Join-Path $presetTarget 'agent.cordis.yml' }
435
+ )
436
+ $bad = 0
437
+ foreach ($check in $checks) {
438
+ if (Test-Path $check.Path) { Write-Ok ("есть: " + $check.Name) }
439
+ else { Write-Warn ("НЕТ: " + $check.Name + " -> " + $check.Path); $bad++ }
440
+ }
441
+ if ($bad -gt 0) { Write-Die "$bad из $($checks.Count) частей не на месте." }
442
+ }
443
+
444
+ # ── Итог ─────────────────────────────────────────────────────────────────
445
+ Write-Host ''
446
+ Write-Host ' ──────────────────────────────────────' -ForegroundColor DarkGray
447
+ if ($DryRun) {
448
+ Write-Host ' Сухой прогон завершён. Запустите без -DryRun.' -ForegroundColor Yellow
449
+ } else {
450
+ Write-Host ' Готово.' -ForegroundColor Green
451
+ Write-Host ''
452
+ Write-Host ' Запуск — ярлык TENSOR GRID на рабочем столе или в меню Пуск.' -ForegroundColor White
453
+ Write-Host ''
454
+ Write-Host ' При первом запуске откроется адрес с одноразовым ключом —'
455
+ Write-Host ' откройте именно его, дальше браузер запомнит вход.'
456
+ }
457
+ Write-Host ''
package/lib/cli.mjs ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Запускатор установки для сотрудника.
4
+ *
5
+ * ЗАЧЕМ ОН НУЖЕН
6
+ *
7
+ * Установщик — сценарий PowerShell, и до этого файла он лежал только в
8
+ * git-репозитории. Сотруднику git не ставят, значит он не получал ни
9
+ * ярлыков, ни автозапуска, ни агент-пресета — только голые три команды. Это
10
+ * обнаружилось при разборе доставки: инструкция обещала ярлык на рабочем
11
+ * столе, которого при таком способе не появлялось.
12
+ *
13
+ * Теперь установщик едет в пакете, а этот файл делает его достижимым одной
14
+ * командой:
15
+ *
16
+ * npx --yes tensorgrid-ui
17
+ *
18
+ * Своей логики здесь нет намеренно. Вся установка живёт в `install.ps1`, и
19
+ * раздваивать её между двумя языками значило бы получить две расходящиеся
20
+ * версии правды — ровно то, что в этом проекте уже стоило починки трижды.
21
+ */
22
+ import { spawn } from 'node:child_process'
23
+ import { existsSync } from 'node:fs'
24
+ import { dirname, join } from 'node:path'
25
+ import { fileURLToPath } from 'node:url'
26
+
27
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
28
+ const script = join(packageRoot, 'install.ps1')
29
+
30
+ if (process.platform !== 'win32') {
31
+ console.error('TENSOR GRID пока устанавливается только на Windows.')
32
+ process.exit(1)
33
+ }
34
+
35
+ if (!existsSync(script)) {
36
+ console.error(`Не найден установщик: ${script}`)
37
+ console.error('Похоже, пакет установлен не полностью. Поставьте его заново:')
38
+ console.error(' npm install -g tensorgrid-ui')
39
+ process.exit(1)
40
+ }
41
+
42
+ /**
43
+ * Аргументы передаются установщику как есть: -DryRun, -NoAutostart и прочие
44
+ * работают одинаково, откуда бы его ни запустили.
45
+ */
46
+ const args = [
47
+ '-NoProfile',
48
+ // Политика запуска сценариев у сотрудника обычно закрыта, и без обхода
49
+ // установка падала бы с отказом ещё до первой строки.
50
+ '-ExecutionPolicy', 'Bypass',
51
+ '-File', script,
52
+ ...process.argv.slice(2),
53
+ ]
54
+
55
+ // Потоки НАСЛЕДУЮТСЯ, а не перехватываются: установщик разговаривает с
56
+ // человеком — печатает шаги, задаёт вопросы, — и перехват превратил бы
57
+ // живой диалог в молчание до самого конца.
58
+ const child = spawn('powershell.exe', args, { stdio: 'inherit' })
59
+
60
+ child.on('error', (error) => {
61
+ console.error(`Не удалось запустить PowerShell: ${error.message}`)
62
+ process.exit(1)
63
+ })
64
+
65
+ child.on('exit', (code, signal) => {
66
+ process.exit(signal !== null ? 1 : code ?? 0)
67
+ })