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/LICENSE +15 -0
- package/brand/tensorgrid.ico +0 -0
- package/env/snapshot.json +916 -0
- package/install.ps1 +457 -0
- package/lib/cli.mjs +67 -0
- package/lib/client.js +134 -104
- package/lib/http-runtime.js +58 -23
- package/lib/index.js +62 -2
- package/lib/review-log.js +296 -35
- package/lib/review-settings.js +205 -144
- package/lib/review.js +36 -26
- package/lib/reviewers.js +73 -16
- package/package.json +12 -2
- package/preset/agent.cordis.yml +304 -0
- package/preset/preset.yml +5 -0
- package/profile/cordis.patch.yml +65 -0
- package/profile/package.json +14 -0
- package/tools/round-ico-corners.mjs +113 -0
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
|
+
})
|