gemini-computer-use 0.4.2 → 0.4.4
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/README.md +18 -1086
- package/cli.js +56 -0
- package/package.json +14 -24
- package/LICENSE +0 -21
- package/bin/cli.js +0 -122
- package/core/__init__.py +0 -0
- package/core/agent.py +0 -767
- package/core/agent_harness.py +0 -67
- package/core/code_cache.py +0 -244
- package/core/device.py +0 -2400
- package/core/domain.py +0 -303
- package/core/hooks.py +0 -61
- package/core/mcp_tools.py +0 -3497
- package/core/policies.py +0 -132
- package/core/schemas.py +0 -49
- package/core/server.py +0 -972
- package/core/subagents.py +0 -55
- package/core/termux.py +0 -916
- package/core/triggers.py +0 -70
- package/core/updater.py +0 -1713
- package/core/version.py +0 -31
- package/core/vitals.py +0 -181
- package/core/web_share.py +0 -814
- package/gateway.py +0 -2862
- package/install-gui.cmd +0 -40
- package/install-gui.ps1 +0 -1882
- package/install-gui.strings.json +0 -211
- package/install.ps1 +0 -1287
- package/install.sh +0 -1998
- package/ops/doctor.ps1 +0 -735
- package/ops/mesh-domain.sh +0 -86
- package/ops/nginx/README.md +0 -77
- package/ops/nginx/antigravity-mesh-mcp.conf +0 -172
- package/ops/nginx/mesh-domain.vhost.template +0 -58
- package/ops/nginx/render-domain.sh +0 -123
- package/ops/update.ps1 +0 -373
- package/ops/windows/README.md +0 -298
- package/ops/windows/agent-watchdog.ps1 +0 -575
- package/ops/windows/run-hidden.vbs +0 -64
- package/skills/orchestrator.md +0 -26
package/install-gui.ps1
DELETED
|
@@ -1,1882 +0,0 @@
|
|
|
1
|
-
# ==============================================================================
|
|
2
|
-
# Antigravity Mesh - Windows Visual Installer (WinForms wizard)
|
|
3
|
-
# ==============================================================================
|
|
4
|
-
#
|
|
5
|
-
# WHY THIS FILE IS PURE ASCII
|
|
6
|
-
#
|
|
7
|
-
# Windows PowerShell 5.1 decodes a script that has no UTF-8 BOM using the ANSI
|
|
8
|
-
# code page. Russian text then turns into characters the parser reads as string
|
|
9
|
-
# delimiters and the whole file fails to parse - exactly why install.ps1 carries
|
|
10
|
-
# a BOM and why docs/WINDOWS_TESTING.md tells you not to normalise it. Keeping
|
|
11
|
-
# this source ASCII-only makes the wizard parse correctly no matter how the file
|
|
12
|
-
# was saved or transferred, and it moves every user-visible string into
|
|
13
|
-
# install-gui.strings.json, which is read explicitly as UTF-8. That file is also
|
|
14
|
-
# what makes the language switch a data change instead of a code change.
|
|
15
|
-
#
|
|
16
|
-
# WHY IT CALLS install.ps1 INSTEAD OF REIMPLEMENTING IT
|
|
17
|
-
#
|
|
18
|
-
# The wizard collects the same options as the console installer and then runs
|
|
19
|
-
# install.ps1 with them. Node registration, the fail-closed check on an empty
|
|
20
|
-
# token, the autostart entry and the shared-domain URL contract therefore stay in
|
|
21
|
-
# one place, and the visual and console installers cannot drift apart.
|
|
22
|
-
#
|
|
23
|
-
# SSH mode is the one exception: install.ps1 has no remote branch, so the wizard
|
|
24
|
-
# drives ssh.exe/scp.exe the way install.sh --ssh does and runs install.sh on the
|
|
25
|
-
# remote host, which is where the real work happens.
|
|
26
|
-
#
|
|
27
|
-
# Usage:
|
|
28
|
-
# powershell -NoProfile -ExecutionPolicy Bypass -Sta -File install-gui.ps1
|
|
29
|
-
# install-gui.cmd (double-click launcher)
|
|
30
|
-
# install-gui.ps1 -Lang ru (start in Russian)
|
|
31
|
-
# install-gui.ps1 -SelfTest (headless self-check, no window)
|
|
32
|
-
# ==============================================================================
|
|
33
|
-
|
|
34
|
-
[CmdletBinding()]
|
|
35
|
-
param(
|
|
36
|
-
# Start in this language instead of following the OS UI language.
|
|
37
|
-
[ValidateSet('en', 'ru')]
|
|
38
|
-
[string]$Lang = '',
|
|
39
|
-
|
|
40
|
-
# Build every page in every language, exercise the process pipeline and the
|
|
41
|
-
# URL parser, then exit. Used by tests/test_installer_gui.py.
|
|
42
|
-
[switch]$SelfTest,
|
|
43
|
-
|
|
44
|
-
# Install without opening a window: the same steps the wizard runs, their
|
|
45
|
-
# output on stdout, and the resulting MCP URL as MESH_URL=<url>. This is what
|
|
46
|
-
# makes the compiled installer verifiable end to end.
|
|
47
|
-
#
|
|
48
|
-
# Every name here is prefixed on purpose. At script scope "$script:Mode" and a
|
|
49
|
-
# parameter "$Mode" are the SAME variable, so the initialiser for the wizard's
|
|
50
|
-
# state would overwrite whatever the caller passed - the bug that once made
|
|
51
|
-
# -Lang do nothing at all.
|
|
52
|
-
[switch]$RunInstall,
|
|
53
|
-
[ValidateSet('quick', 'custom', 'ssh')]
|
|
54
|
-
[string]$InstallMode = 'quick',
|
|
55
|
-
[string]$InstallUser = '',
|
|
56
|
-
[string]$InstallGateway = '',
|
|
57
|
-
[string]$InstallToken = '',
|
|
58
|
-
[string]$InstallSshTarget = '',
|
|
59
|
-
[int]$InstallSshPort = 22
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
$ErrorActionPreference = 'Continue'
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
66
|
-
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
67
|
-
} catch { }
|
|
68
|
-
|
|
69
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
70
|
-
Add-Type -AssemblyName System.Drawing
|
|
71
|
-
|
|
72
|
-
# ------------------------------------------------------------------------------
|
|
73
|
-
# Paths and constants
|
|
74
|
-
# ------------------------------------------------------------------------------
|
|
75
|
-
|
|
76
|
-
$ScriptPath = $null
|
|
77
|
-
if ($MyInvocation.MyCommand -and $MyInvocation.MyCommand.Path) {
|
|
78
|
-
$ScriptPath = $MyInvocation.MyCommand.Path
|
|
79
|
-
}
|
|
80
|
-
$ScriptDir = $null
|
|
81
|
-
if ($ScriptPath) { $ScriptDir = Split-Path -Parent $ScriptPath }
|
|
82
|
-
if (-not $ScriptDir) { $ScriptDir = (Get-Location).Path }
|
|
83
|
-
if (-not $ScriptPath) { $ScriptPath = Join-Path $ScriptDir 'install-gui.ps1' }
|
|
84
|
-
|
|
85
|
-
$StringsFile = Join-Path $ScriptDir 'install-gui.strings.json'
|
|
86
|
-
$ConsoleInstaller = Join-Path $ScriptDir 'install.ps1'
|
|
87
|
-
$ShellInstaller = Join-Path $ScriptDir 'install.sh'
|
|
88
|
-
$CoreDir = Join-Path $ScriptDir 'core'
|
|
89
|
-
$SkillsDir = Join-Path $ScriptDir 'skills'
|
|
90
|
-
|
|
91
|
-
$ConfigDir = Join-Path $env:USERPROFILE '.config\antigravity-mesh'
|
|
92
|
-
$AgentEnvFile = Join-Path $ConfigDir 'agent.env'
|
|
93
|
-
$AgentLogFile = Join-Path $ConfigDir 'agent.log'
|
|
94
|
-
$StartupDir = [Environment]::GetFolderPath('Startup')
|
|
95
|
-
$AutostartFile = Join-Path $StartupDir 'antigravity-agent.vbs'
|
|
96
|
-
|
|
97
|
-
$GeminiAppsUrl = 'https://gemini.google.com/spark/apps'
|
|
98
|
-
|
|
99
|
-
# The canonical shared-domain URL: one domain for every node, the node selected
|
|
100
|
-
# by ?user=. This pattern only reads back the URL install.ps1 printed; it never
|
|
101
|
-
# invents one. The gateway's own sse_url has exactly this shape (see
|
|
102
|
-
# tests/test_gateway_url_contract.py), which is what makes the agent.env
|
|
103
|
-
# fallback below faithful rather than a guess.
|
|
104
|
-
$UrlPattern = 'https?://[^\s|"'']+/sse[^\s|"'']*'
|
|
105
|
-
|
|
106
|
-
# ------------------------------------------------------------------------------
|
|
107
|
-
# Tolerant readers
|
|
108
|
-
#
|
|
109
|
-
# One session sees three encodings on Windows: PowerShell and cmd write the
|
|
110
|
-
# console OEM code page, most programs write ANSI, and python/git/curl write
|
|
111
|
-
# UTF-8. Decoding with the wrong one produces mojibake, so try UTF-8 strictly
|
|
112
|
-
# first and fall back rather than trusting the locale. Same rule as
|
|
113
|
-
# core/mcp_tools.py:_decode_output.
|
|
114
|
-
# ------------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
function Read-TextFileTolerant {
|
|
117
|
-
param([string]$Path)
|
|
118
|
-
if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { return '' }
|
|
119
|
-
try { $bytes = [System.IO.File]::ReadAllBytes($Path) } catch { return '' }
|
|
120
|
-
if ($bytes.Length -eq 0) { return '' }
|
|
121
|
-
|
|
122
|
-
$text = $null
|
|
123
|
-
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
|
124
|
-
try {
|
|
125
|
-
$text = $strict.GetString($bytes)
|
|
126
|
-
} catch {
|
|
127
|
-
$codePage = 0
|
|
128
|
-
try { $codePage = [Console]::OutputEncoding.CodePage } catch { }
|
|
129
|
-
if ($codePage -le 0) { $codePage = 866 }
|
|
130
|
-
try { $text = [System.Text.Encoding]::GetEncoding($codePage).GetString($bytes) }
|
|
131
|
-
catch { $text = [System.Text.Encoding]::Default.GetString($bytes) }
|
|
132
|
-
}
|
|
133
|
-
return ($text -replace "`r`n", "`n")
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function Read-Utf8Json {
|
|
137
|
-
param([string]$Path)
|
|
138
|
-
$bytes = [System.IO.File]::ReadAllBytes($Path)
|
|
139
|
-
if ($bytes.Length -eq 0) { throw 'file is empty' }
|
|
140
|
-
$offset = 0
|
|
141
|
-
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
|
|
142
|
-
$offset = 3
|
|
143
|
-
}
|
|
144
|
-
$strict = New-Object System.Text.UTF8Encoding($false, $true)
|
|
145
|
-
try {
|
|
146
|
-
$text = $strict.GetString($bytes, $offset, $bytes.Length - $offset)
|
|
147
|
-
} catch {
|
|
148
|
-
# Not valid UTF-8: an editor saved it as ANSI. Russian still has a chance
|
|
149
|
-
# of being right under the system default code page.
|
|
150
|
-
$text = [System.Text.Encoding]::Default.GetString($bytes, $offset, $bytes.Length - $offset)
|
|
151
|
-
}
|
|
152
|
-
return ($text | ConvertFrom-Json)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# ------------------------------------------------------------------------------
|
|
157
|
-
# Localisation
|
|
158
|
-
# ------------------------------------------------------------------------------
|
|
159
|
-
|
|
160
|
-
$script:Strings = $null
|
|
161
|
-
$script:UiLang = 'en'
|
|
162
|
-
|
|
163
|
-
function Initialize-Strings {
|
|
164
|
-
if (-not (Test-Path -LiteralPath $StringsFile)) {
|
|
165
|
-
throw ('__STRINGS_MISSING__' + $StringsFile)
|
|
166
|
-
}
|
|
167
|
-
try {
|
|
168
|
-
$script:Strings = Read-Utf8Json -Path $StringsFile
|
|
169
|
-
} catch {
|
|
170
|
-
throw ('__STRINGS_INVALID__' + $_.Exception.Message)
|
|
171
|
-
}
|
|
172
|
-
foreach ($code in @('en', 'ru')) {
|
|
173
|
-
if (-not ($script:Strings.PSObject.Properties.Name -contains $code)) {
|
|
174
|
-
throw ('__LANG_MISSING__' + $code)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function Get-UiText {
|
|
180
|
-
param([string]$Key)
|
|
181
|
-
$table = $script:Strings.($script:UiLang)
|
|
182
|
-
if ($null -ne $table -and ($table.PSObject.Properties.Name -contains $Key)) {
|
|
183
|
-
return [string]$table.$Key
|
|
184
|
-
}
|
|
185
|
-
$fallback = $script:Strings.en
|
|
186
|
-
if ($null -ne $fallback -and ($fallback.PSObject.Properties.Name -contains $Key)) {
|
|
187
|
-
return [string]$fallback.$Key
|
|
188
|
-
}
|
|
189
|
-
return $Key
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
# get text and apply {0}-style formatting in one step
|
|
193
|
-
function Format-UiText {
|
|
194
|
-
param([string]$Key, [object[]]$Values)
|
|
195
|
-
$template = Get-UiText -Key $Key
|
|
196
|
-
if ($null -eq $Values -or $Values.Count -eq 0) { return $template }
|
|
197
|
-
try { return ($template -f $Values) } catch { return $template }
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
# ------------------------------------------------------------------------------
|
|
201
|
-
# Which language to start in
|
|
202
|
-
#
|
|
203
|
-
# CultureInfo.CurrentUICulture is NOT a reliable answer on Windows. On the
|
|
204
|
-
# machine this was developed on, Windows is installed and used in Russian
|
|
205
|
-
# (PreferredUILanguages = ru-RU, InstalledUICulture = ru-RU, CurrentCulture =
|
|
206
|
-
# ru-RU, Get-WinSystemLocale = ru-RU) while the process reports
|
|
207
|
-
# CurrentUICulture = en-US, so a check against it alone started the wizard in
|
|
208
|
-
# English. The user's own language list is the signal that matches what they
|
|
209
|
-
# actually see, so it is read first and the process culture is only a fallback.
|
|
210
|
-
# ------------------------------------------------------------------------------
|
|
211
|
-
|
|
212
|
-
# One registry value holding a ';'-separated language list, split into entries.
|
|
213
|
-
function Get-RegistryLanguageList {
|
|
214
|
-
param([string]$Path, [string]$Name)
|
|
215
|
-
$values = New-Object System.Collections.ArrayList
|
|
216
|
-
try {
|
|
217
|
-
$raw = (Get-ItemProperty -LiteralPath $Path -Name $Name -ErrorAction Stop).$Name
|
|
218
|
-
} catch {
|
|
219
|
-
return $values.ToArray()
|
|
220
|
-
}
|
|
221
|
-
foreach ($entry in @($raw)) {
|
|
222
|
-
foreach ($part in ("$entry" -split ';')) {
|
|
223
|
-
$trimmed = $part.Trim()
|
|
224
|
-
if ($trimmed) { [void]$values.Add($trimmed) }
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return $values.ToArray()
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
# Every language signal, most authoritative first.
|
|
231
|
-
function Get-PreferredLanguageCodes {
|
|
232
|
-
$codes = New-Object System.Collections.ArrayList
|
|
233
|
-
|
|
234
|
-
# 1. The Windows display language.
|
|
235
|
-
foreach ($value in @(Get-RegistryLanguageList 'HKCU:\Control Panel\Desktop' 'PreferredUILanguages')) {
|
|
236
|
-
[void]$codes.Add([string]$value)
|
|
237
|
-
}
|
|
238
|
-
# 2. The ordered list of preferred languages from Settings.
|
|
239
|
-
foreach ($value in @(Get-RegistryLanguageList 'HKCU:\Control Panel\International\User Profile' 'Languages')) {
|
|
240
|
-
[void]$codes.Add([string]$value)
|
|
241
|
-
}
|
|
242
|
-
# 3. The International module, only when the registry said nothing.
|
|
243
|
-
if ($codes.Count -eq 0 -and (Get-Command 'Get-WinUserLanguageList' -ErrorAction SilentlyContinue)) {
|
|
244
|
-
try {
|
|
245
|
-
foreach ($item in (Get-WinUserLanguageList -ErrorAction Stop)) {
|
|
246
|
-
if ($item.LanguageTag) { [void]$codes.Add([string]$item.LanguageTag) }
|
|
247
|
-
}
|
|
248
|
-
} catch { }
|
|
249
|
-
}
|
|
250
|
-
# 4. The process UI language, the OS install language, then the format.
|
|
251
|
-
foreach ($culture in @(
|
|
252
|
-
[System.Globalization.CultureInfo]::CurrentUICulture,
|
|
253
|
-
[System.Globalization.CultureInfo]::InstalledUICulture,
|
|
254
|
-
[System.Globalization.CultureInfo]::CurrentCulture)) {
|
|
255
|
-
if ($culture -and $culture.Name) { [void]$codes.Add([string]$culture.Name) }
|
|
256
|
-
}
|
|
257
|
-
return $codes.ToArray()
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
# First code that this wizard can actually speak, else English.
|
|
261
|
-
function Select-Language {
|
|
262
|
-
param([string[]]$Codes, [string[]]$Supported = @('en', 'ru'))
|
|
263
|
-
foreach ($code in @($Codes)) {
|
|
264
|
-
$short = ("$code" -split '-')[0].Trim().ToLower()
|
|
265
|
-
if ($short -and ($Supported -contains $short)) { return $short }
|
|
266
|
-
}
|
|
267
|
-
return 'en'
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function Get-DefaultLanguage {
|
|
271
|
-
return (Select-Language -Codes @(Get-PreferredLanguageCodes))
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
# ------------------------------------------------------------------------------
|
|
275
|
-
# Small helpers
|
|
276
|
-
# ------------------------------------------------------------------------------
|
|
277
|
-
|
|
278
|
-
function Quote-Arg {
|
|
279
|
-
param([string]$Value)
|
|
280
|
-
return '"' + ($Value -replace '"', '\"') + '"'
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function Get-DeviceInfo {
|
|
284
|
-
$rawHost = "$env:COMPUTERNAME"
|
|
285
|
-
$hostName = ($rawHost.ToLower() -replace '[^a-z0-9_-]', '')
|
|
286
|
-
$isLaptop = $false
|
|
287
|
-
try {
|
|
288
|
-
$isLaptop = [bool](Get-CimInstance -ClassName Win32_Battery -ErrorAction SilentlyContinue)
|
|
289
|
-
} catch { }
|
|
290
|
-
return @{
|
|
291
|
-
Hostname = $hostName
|
|
292
|
-
Raw = $rawHost
|
|
293
|
-
IsLaptop = $isLaptop
|
|
294
|
-
Os = 'Windows ' + [System.Environment]::OSVersion.Version.ToString()
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function Set-UrlClipboard {
|
|
299
|
-
param([string]$Text)
|
|
300
|
-
try { Set-Clipboard -Value $Text -ErrorAction Stop; return $true } catch { }
|
|
301
|
-
try { [System.Windows.Forms.Clipboard]::SetText($Text); return $true } catch { }
|
|
302
|
-
try { $Text | clip.exe 2>$null; return $true } catch { }
|
|
303
|
-
return $false
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function Start-Url {
|
|
307
|
-
param([string]$Url)
|
|
308
|
-
try { Start-Process $Url; return $true } catch { }
|
|
309
|
-
try { [System.Diagnostics.Process]::Start($Url) | Out-Null; return $true } catch { }
|
|
310
|
-
return $false
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function Open-Folder {
|
|
314
|
-
param([string]$Path)
|
|
315
|
-
if (-not (Test-Path -LiteralPath $Path)) { return $false }
|
|
316
|
-
try { Start-Process explorer.exe -ArgumentList (Quote-Arg $Path); return $true } catch { }
|
|
317
|
-
return $false
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
# A domain value is "configured" when it is non-empty, carries no whitespace and
|
|
321
|
-
# is not the deployment placeholder. Both install.ps1's English "NOT CONFIGURED"
|
|
322
|
-
# and its Russian equivalent contain a space, so this test does not depend on the
|
|
323
|
-
# language the dry run was printed in.
|
|
324
|
-
function Test-DomainConfigured {
|
|
325
|
-
param([string]$Value)
|
|
326
|
-
$text = "$Value".Trim()
|
|
327
|
-
if (-not $text) { return $false }
|
|
328
|
-
if ($text -match '\s') { return $false }
|
|
329
|
-
if ($text -eq '__MESH_DOMAIN__') { return $false }
|
|
330
|
-
return $true
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
# ------------------------------------------------------------------------------
|
|
334
|
-
# Process pipeline
|
|
335
|
-
#
|
|
336
|
-
# A queue of external commands run one after another, with stdout/stderr
|
|
337
|
-
# redirected straight to files and a WinForms timer polling them. The child
|
|
338
|
-
# writes to files itself, so there is nothing to deadlock on and no second
|
|
339
|
-
# runspace to marshal across - both of which a BeginOutputReadLine handler would
|
|
340
|
-
# have needed under PowerShell 5.1.
|
|
341
|
-
# ------------------------------------------------------------------------------
|
|
342
|
-
|
|
343
|
-
$script:Headless = $false
|
|
344
|
-
$script:Timer = $null
|
|
345
|
-
$script:ScratchDir = $null
|
|
346
|
-
|
|
347
|
-
$script:Pipe = @{
|
|
348
|
-
Steps = @()
|
|
349
|
-
Index = -1
|
|
350
|
-
Proc = $null
|
|
351
|
-
OutFile = ''
|
|
352
|
-
ErrFile = ''
|
|
353
|
-
Output = ''
|
|
354
|
-
Failed = $false
|
|
355
|
-
FailExit = 0
|
|
356
|
-
Running = $false
|
|
357
|
-
OnFinish = $null
|
|
358
|
-
LogBox = $null
|
|
359
|
-
StatusLabel = $null
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function New-PipelineStep {
|
|
363
|
-
param(
|
|
364
|
-
[string]$File,
|
|
365
|
-
[string]$Arguments,
|
|
366
|
-
[string]$Label,
|
|
367
|
-
[switch]$IgnoreExit
|
|
368
|
-
)
|
|
369
|
-
return @{
|
|
370
|
-
File = $File
|
|
371
|
-
Arguments = $Arguments
|
|
372
|
-
Label = $Label
|
|
373
|
-
IgnoreExit = [bool]$IgnoreExit
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
# Where the pipeline keeps the child's stdout/stderr and the self-test keeps its
|
|
378
|
-
# probes. %TEMP% is the obvious choice but not always a writable one: on a locked
|
|
379
|
-
# down machine, and inside a sandboxed folder, the write is refused and the child
|
|
380
|
-
# then produces nothing at all - which used to be reported as "no gateway domain
|
|
381
|
-
# configured". Each candidate is probed with a real write before it is used.
|
|
382
|
-
function Get-ScratchDirectory {
|
|
383
|
-
if ($script:ScratchDir) { return $script:ScratchDir }
|
|
384
|
-
|
|
385
|
-
$candidates = New-Object System.Collections.ArrayList
|
|
386
|
-
if ($env:TEMP) { [void]$candidates.Add($env:TEMP) }
|
|
387
|
-
[void]$candidates.Add((Join-Path $ScriptDir 'runtime'))
|
|
388
|
-
if ($env:LOCALAPPDATA) {
|
|
389
|
-
[void]$candidates.Add((Join-Path $env:LOCALAPPDATA 'AntigravityMesh\runtime'))
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
foreach ($candidate in $candidates) {
|
|
393
|
-
try {
|
|
394
|
-
if (-not (Test-Path -LiteralPath $candidate)) {
|
|
395
|
-
[void](New-Item -ItemType Directory -Path $candidate -Force -ErrorAction Stop)
|
|
396
|
-
}
|
|
397
|
-
$probe = Join-Path $candidate ('write-probe-' + [Guid]::NewGuid().ToString('N') + '.tmp')
|
|
398
|
-
Set-Content -LiteralPath $probe -Value '' -Encoding ASCII -ErrorAction Stop
|
|
399
|
-
Remove-Item -LiteralPath $probe -Force -ErrorAction Stop
|
|
400
|
-
$script:ScratchDir = $candidate
|
|
401
|
-
return $candidate
|
|
402
|
-
} catch {
|
|
403
|
-
continue
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
# Nothing worked; return the first choice so the failure names a real path.
|
|
408
|
-
$script:ScratchDir = $candidates[0]
|
|
409
|
-
return $script:ScratchDir
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function Reset-Pipeline {
|
|
413
|
-
param(
|
|
414
|
-
[object[]]$Steps,
|
|
415
|
-
[scriptblock]$OnFinish,
|
|
416
|
-
$LogBox = $null,
|
|
417
|
-
$StatusLabel = $null
|
|
418
|
-
)
|
|
419
|
-
$script:Pipe.Steps = @($Steps)
|
|
420
|
-
$script:Pipe.Index = -1
|
|
421
|
-
$script:Pipe.Output = ''
|
|
422
|
-
$script:Pipe.Failed = $false
|
|
423
|
-
$script:Pipe.FailExit = 0
|
|
424
|
-
$script:Pipe.Running = $true
|
|
425
|
-
$script:Pipe.OnFinish = $OnFinish
|
|
426
|
-
$script:Pipe.LogBox = $LogBox
|
|
427
|
-
$script:Pipe.StatusLabel = $StatusLabel
|
|
428
|
-
$script:Pipe.Proc = $null
|
|
429
|
-
|
|
430
|
-
$stamp = [Guid]::NewGuid().ToString('N')
|
|
431
|
-
$scratch = Get-ScratchDirectory
|
|
432
|
-
$script:Pipe.OutFile = Join-Path $scratch ('mesh-gui-out-' + $stamp + '.log')
|
|
433
|
-
$script:Pipe.ErrFile = Join-Path $scratch ('mesh-gui-err-' + $stamp + '.log')
|
|
434
|
-
try {
|
|
435
|
-
Set-Content -LiteralPath $script:Pipe.OutFile -Value '' -Encoding ASCII
|
|
436
|
-
Set-Content -LiteralPath $script:Pipe.ErrFile -Value '' -Encoding ASCII
|
|
437
|
-
} catch { }
|
|
438
|
-
|
|
439
|
-
Start-NextPipelineStep
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function Start-NextPipelineStep {
|
|
443
|
-
$script:Pipe.Index++
|
|
444
|
-
if ($script:Pipe.Index -ge $script:Pipe.Steps.Count) {
|
|
445
|
-
Complete-Pipeline
|
|
446
|
-
return
|
|
447
|
-
}
|
|
448
|
-
$step = $script:Pipe.Steps[$script:Pipe.Index]
|
|
449
|
-
if ($script:Pipe.StatusLabel) {
|
|
450
|
-
try { $script:Pipe.StatusLabel.Text = $step.Label } catch { }
|
|
451
|
-
}
|
|
452
|
-
$script:Pipe.Output += ('--- ' + $step.Label + "`n")
|
|
453
|
-
|
|
454
|
-
# cmd.exe applies the redirection, so the child writes straight to the files.
|
|
455
|
-
# Start-Process -RedirectStandardOutput captures output just as well, but the
|
|
456
|
-
# Process object it hands back has already lost the handle: its ExitCode comes
|
|
457
|
-
# back empty (measured on Windows PowerShell 5.1), and an installer that
|
|
458
|
-
# cannot tell success from failure is worse than useless. Creating the Process
|
|
459
|
-
# through ProcessStartInfo keeps ExitCode readable, and CreateNoWindow keeps
|
|
460
|
-
# the console from flashing.
|
|
461
|
-
$inner = (Quote-Arg $step.File) + ' ' + $step.Arguments
|
|
462
|
-
$cmdLine = '/c "' + $inner + ' > "' + $script:Pipe.OutFile + '" 2> "' + $script:Pipe.ErrFile + '""'
|
|
463
|
-
|
|
464
|
-
$proc = $null
|
|
465
|
-
try {
|
|
466
|
-
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
|
|
467
|
-
$startInfo.FileName = $env:ComSpec
|
|
468
|
-
$startInfo.Arguments = $cmdLine
|
|
469
|
-
$startInfo.UseShellExecute = $false
|
|
470
|
-
$startInfo.CreateNoWindow = $true
|
|
471
|
-
$proc = [System.Diagnostics.Process]::Start($startInfo)
|
|
472
|
-
} catch {
|
|
473
|
-
$script:Pipe.Failed = $true
|
|
474
|
-
$script:Pipe.FailExit = -1
|
|
475
|
-
$script:Pipe.Output += ('[launch failed] ' + $_.Exception.Message + "`n")
|
|
476
|
-
Complete-Pipeline
|
|
477
|
-
return
|
|
478
|
-
}
|
|
479
|
-
$script:Pipe.Proc = $proc
|
|
480
|
-
if ($script:Timer -and -not $script:Headless) { $script:Timer.Start() }
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function Set-LogBoxText {
|
|
484
|
-
param($Box, [string]$Text)
|
|
485
|
-
if (-not $Box) { return }
|
|
486
|
-
try {
|
|
487
|
-
$Box.Text = $Text
|
|
488
|
-
$Box.SelectionStart = $Box.TextLength
|
|
489
|
-
$Box.ScrollToCaret()
|
|
490
|
-
} catch { }
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function Update-Pipeline {
|
|
494
|
-
if (-not $script:Pipe.Running) { return }
|
|
495
|
-
|
|
496
|
-
$out = Read-TextFileTolerant -Path $script:Pipe.OutFile
|
|
497
|
-
$err = Read-TextFileTolerant -Path $script:Pipe.ErrFile
|
|
498
|
-
$live = $out
|
|
499
|
-
if ($err) { $live = $out + $err }
|
|
500
|
-
|
|
501
|
-
if ($live -ne $script:Pipe.Output) {
|
|
502
|
-
$script:Pipe.Output = $live
|
|
503
|
-
Set-LogBoxText -Box $script:Pipe.LogBox -Text $live
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
$proc = $script:Pipe.Proc
|
|
507
|
-
if ($null -eq $proc) { return }
|
|
508
|
-
$exited = $false
|
|
509
|
-
try { $exited = $proc.HasExited } catch { $exited = $true }
|
|
510
|
-
if (-not $exited) { return }
|
|
511
|
-
|
|
512
|
-
# The process has exited but its handles may not have flushed yet: read once,
|
|
513
|
-
# give the files a moment, then keep whichever read is longer.
|
|
514
|
-
$first = Read-TextFileTolerant -Path $script:Pipe.OutFile
|
|
515
|
-
$firstErr = Read-TextFileTolerant -Path $script:Pipe.ErrFile
|
|
516
|
-
Start-Sleep -Milliseconds 120
|
|
517
|
-
$second = Read-TextFileTolerant -Path $script:Pipe.OutFile
|
|
518
|
-
$secondErr = Read-TextFileTolerant -Path $script:Pipe.ErrFile
|
|
519
|
-
if ($second.Length -lt $first.Length) { $second = $first }
|
|
520
|
-
if ($secondErr.Length -lt $firstErr.Length) { $secondErr = $firstErr }
|
|
521
|
-
|
|
522
|
-
$final = $second
|
|
523
|
-
if ($secondErr) { $final = $second + $secondErr }
|
|
524
|
-
$script:Pipe.Output = $final
|
|
525
|
-
Set-LogBoxText -Box $script:Pipe.LogBox -Text $final
|
|
526
|
-
|
|
527
|
-
$code = 0
|
|
528
|
-
try { $code = $proc.ExitCode } catch { $code = -1 }
|
|
529
|
-
try { $proc.Dispose() } catch { }
|
|
530
|
-
$script:Pipe.Proc = $null
|
|
531
|
-
|
|
532
|
-
$step = $script:Pipe.Steps[$script:Pipe.Index]
|
|
533
|
-
if ($code -ne 0 -and -not $step.IgnoreExit) {
|
|
534
|
-
$script:Pipe.Failed = $true
|
|
535
|
-
$script:Pipe.FailExit = $code
|
|
536
|
-
Complete-Pipeline
|
|
537
|
-
return
|
|
538
|
-
}
|
|
539
|
-
Start-NextPipelineStep
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function Complete-Pipeline {
|
|
543
|
-
$script:Pipe.Running = $false
|
|
544
|
-
if ($script:Timer -and -not $script:Headless) { try { $script:Timer.Stop() } catch { } }
|
|
545
|
-
if ($script:Pipe.Proc) {
|
|
546
|
-
try { $script:Pipe.Proc.Dispose() } catch { }
|
|
547
|
-
$script:Pipe.Proc = $null
|
|
548
|
-
}
|
|
549
|
-
foreach ($path in @($script:Pipe.OutFile, $script:Pipe.ErrFile)) {
|
|
550
|
-
if ($path -and (Test-Path -LiteralPath $path)) {
|
|
551
|
-
try { Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue } catch { }
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
$onFinish = $script:Pipe.OnFinish
|
|
555
|
-
if ($onFinish) {
|
|
556
|
-
& $onFinish $script:Pipe.Failed $script:Pipe.FailExit $script:Pipe.Output
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function Stop-Pipeline {
|
|
561
|
-
$proc = $script:Pipe.Proc
|
|
562
|
-
if ($proc) {
|
|
563
|
-
try {
|
|
564
|
-
if (-not $proc.HasExited) {
|
|
565
|
-
# /T matters: install.ps1 spawns python children, and killing only
|
|
566
|
-
# the shell would leave the agent behind.
|
|
567
|
-
& taskkill.exe /T /F /PID $proc.Id 2>$null | Out-Null
|
|
568
|
-
}
|
|
569
|
-
} catch { }
|
|
570
|
-
}
|
|
571
|
-
$script:Pipe.Running = $false
|
|
572
|
-
if ($script:Timer -and -not $script:Headless) { try { $script:Timer.Stop() } catch { } }
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
# ------------------------------------------------------------------------------
|
|
576
|
-
# Reading install.ps1's output back
|
|
577
|
-
# ------------------------------------------------------------------------------
|
|
578
|
-
|
|
579
|
-
# install.ps1 -DryRun prints six "[DRY-RUN] <label> : <value>" lines in a fixed
|
|
580
|
-
# order (Python, websockets, domain, config, autostart, node). The labels are
|
|
581
|
-
# localised, so this reads them positionally instead of matching on the label.
|
|
582
|
-
function Get-PreflightFacts {
|
|
583
|
-
param([string]$Text)
|
|
584
|
-
$facts = @{ Python = ''; Websockets = ''; Domain = ''; Config = ''; Autostart = ''; Node = '' }
|
|
585
|
-
$values = New-Object System.Collections.ArrayList
|
|
586
|
-
foreach ($line in ($Text -split "`n")) {
|
|
587
|
-
$trimmed = $line.Trim()
|
|
588
|
-
if (-not $trimmed.StartsWith('[DRY-RUN]')) { continue }
|
|
589
|
-
$match = [regex]::Match($trimmed, '^\[DRY-RUN\]\s+[^:]+:\s*(.*)$')
|
|
590
|
-
if ($match.Success) { [void]$values.Add($match.Groups[1].Value.Trim()) }
|
|
591
|
-
}
|
|
592
|
-
if ($values.Count -ge 1) { $facts.Python = $values[0] }
|
|
593
|
-
if ($values.Count -ge 2) { $facts.Websockets = $values[1] }
|
|
594
|
-
if ($values.Count -ge 3) { $facts.Domain = $values[2] }
|
|
595
|
-
if ($values.Count -ge 4) { $facts.Config = $values[3] }
|
|
596
|
-
if ($values.Count -ge 5) { $facts.Autostart = $values[4] }
|
|
597
|
-
# An older install.ps1 copy prints five lines: Node stays empty and the report
|
|
598
|
-
# simply omits that row.
|
|
599
|
-
if ($values.Count -ge 6) { $facts.Node = $values[5] }
|
|
600
|
-
return $facts
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
# The URL install.ps1 printed, or - when that line is unreadable - the canonical
|
|
604
|
-
# URL rebuilt from agent.env.
|
|
605
|
-
function Get-ResultUrl {
|
|
606
|
-
param(
|
|
607
|
-
[string]$Text,
|
|
608
|
-
[string]$Mode,
|
|
609
|
-
[string]$AgentEnvPath = ''
|
|
610
|
-
)
|
|
611
|
-
if (-not $AgentEnvPath) { $AgentEnvPath = $AgentEnvFile }
|
|
612
|
-
|
|
613
|
-
$found = [regex]::Matches($Text, $UrlPattern)
|
|
614
|
-
if ($found.Count -gt 0) {
|
|
615
|
-
return $found[$found.Count - 1].Value
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
if ($Mode -eq 'ssh') { return '' }
|
|
619
|
-
|
|
620
|
-
$values = @{}
|
|
621
|
-
$raw = Read-TextFileTolerant -Path $AgentEnvPath
|
|
622
|
-
foreach ($line in ($raw -split "`n")) {
|
|
623
|
-
$trimmed = $line.Trim()
|
|
624
|
-
if (-not $trimmed -or $trimmed.StartsWith('#') -or -not $trimmed.Contains('=')) { continue }
|
|
625
|
-
$index = $trimmed.IndexOf('=')
|
|
626
|
-
$values[$trimmed.Substring(0, $index).Trim()] = $trimmed.Substring($index + 1).Trim()
|
|
627
|
-
}
|
|
628
|
-
$gateway = "$($values['MESH_GATEWAY'])".Trim()
|
|
629
|
-
$user = "$($values['MESH_USER'])".Trim()
|
|
630
|
-
$token = "$($values['MESH_TOKEN'])".Trim()
|
|
631
|
-
if ($gateway -and $user -and $token) {
|
|
632
|
-
return ('https://' + $gateway + '/sse?user=' + $user + '&token=' + $token)
|
|
633
|
-
}
|
|
634
|
-
return ''
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
# ------------------------------------------------------------------------------
|
|
638
|
-
# Command construction
|
|
639
|
-
# ------------------------------------------------------------------------------
|
|
640
|
-
|
|
641
|
-
function Get-PowerShellExe {
|
|
642
|
-
$candidate = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
|
643
|
-
if (Test-Path -LiteralPath $candidate) { return $candidate }
|
|
644
|
-
return 'powershell.exe'
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function New-ConsoleInstallerSteps {
|
|
648
|
-
param([string]$Mode, [string]$Node, [string]$Domain, [string]$Token)
|
|
649
|
-
|
|
650
|
-
$argList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', (Quote-Arg $ConsoleInstaller),
|
|
651
|
-
'-Lang', $script:UiLang)
|
|
652
|
-
if ($Mode -eq 'custom') {
|
|
653
|
-
$argList += @('-Mode', 'tunnel')
|
|
654
|
-
if ($Node) { $argList += @('-User', (Quote-Arg $Node)) }
|
|
655
|
-
if ($Domain) { $argList += @('-Gateway', (Quote-Arg $Domain)) }
|
|
656
|
-
if ($Token) { $argList += @('-Token', (Quote-Arg $Token)) }
|
|
657
|
-
}
|
|
658
|
-
return @(New-PipelineStep -File (Get-PowerShellExe) -Arguments ($argList -join ' ') `
|
|
659
|
-
-Label (Get-UiText -Key 'prog_running'))
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function New-SshSteps {
|
|
663
|
-
param([string]$Target, [int]$Port, [string]$Domain)
|
|
664
|
-
|
|
665
|
-
$steps = New-Object System.Collections.ArrayList
|
|
666
|
-
$sshOptions = @('-o', 'StrictHostKeyChecking=no', '-o', 'BatchMode=yes')
|
|
667
|
-
$scpOptions = @('-o', 'StrictHostKeyChecking=no', '-o', 'BatchMode=yes')
|
|
668
|
-
$remoteRoot = '~/antigravity-mesh'
|
|
669
|
-
|
|
670
|
-
# 1. create the destination tree. ssh joins the trailing arguments with
|
|
671
|
-
# spaces and hands the result to the remote shell, so ~ expands remotely.
|
|
672
|
-
# Quoted for cmd.exe as well as for ssh: unquoted, the local command
|
|
673
|
-
# interpreter would treat the remote command's metacharacters as its own.
|
|
674
|
-
$mkdirCmd = 'mkdir -p ' + $remoteRoot + '/core ' + $remoteRoot + '/skills'
|
|
675
|
-
$mkdirArgs = $sshOptions + @('-p', "$Port", $Target, (Quote-Arg $mkdirCmd))
|
|
676
|
-
[void]$steps.Add((New-PipelineStep -File 'ssh.exe' -Arguments ($mkdirArgs -join ' ') `
|
|
677
|
-
-Label 'ssh: prepare remote directories'))
|
|
678
|
-
|
|
679
|
-
# 2. core/ - top-level .py files only. install.sh ships core/* verbatim, but a
|
|
680
|
-
# __pycache__ built for another interpreter is useless on the far side.
|
|
681
|
-
if (Test-Path -LiteralPath $CoreDir) {
|
|
682
|
-
$coreFiles = @(Get-ChildItem -LiteralPath $CoreDir -File -Filter '*.py' -ErrorAction SilentlyContinue |
|
|
683
|
-
ForEach-Object { $_.FullName })
|
|
684
|
-
if ($coreFiles.Count -gt 0) {
|
|
685
|
-
$scpCore = $scpOptions + @('-P', "$Port") +
|
|
686
|
-
@($coreFiles | ForEach-Object { Quote-Arg $_ }) +
|
|
687
|
-
@("${Target}:$remoteRoot/core/")
|
|
688
|
-
[void]$steps.Add((New-PipelineStep -File 'scp.exe' -Arguments ($scpCore -join ' ') -Label 'scp: core/*'))
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
# 3. install.sh - the remote side needs it to do the actual installation
|
|
693
|
-
$scpShell = $scpOptions + @('-P', "$Port", (Quote-Arg $ShellInstaller), "${Target}:$remoteRoot/")
|
|
694
|
-
[void]$steps.Add((New-PipelineStep -File 'scp.exe' -Arguments ($scpShell -join ' ') -Label 'scp: install.sh'))
|
|
695
|
-
|
|
696
|
-
# 4. skills/ - optional, exactly as install.sh treats it
|
|
697
|
-
if (Test-Path -LiteralPath $SkillsDir) {
|
|
698
|
-
$skillFiles = @(Get-ChildItem -LiteralPath $SkillsDir -File -ErrorAction SilentlyContinue |
|
|
699
|
-
ForEach-Object { $_.FullName })
|
|
700
|
-
if ($skillFiles.Count -gt 0) {
|
|
701
|
-
$scpSkills = $scpOptions + @('-P', "$Port") +
|
|
702
|
-
@($skillFiles | ForEach-Object { Quote-Arg $_ }) +
|
|
703
|
-
@("${Target}:$remoteRoot/skills/")
|
|
704
|
-
[void]$steps.Add((New-PipelineStep -File 'scp.exe' -Arguments ($scpSkills -join ' ') -Label 'scp: skills/*'))
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
# 5. run the installer on the remote host. install.sh resolves the shared
|
|
709
|
-
# domain itself (core/domain.py travels with it); --domain only overrides.
|
|
710
|
-
$remoteCmd = 'cd ' + $remoteRoot + ' && bash install.sh --quick --lang=' + $script:UiLang
|
|
711
|
-
if ($Domain) { $remoteCmd += ' --domain=' + $Domain }
|
|
712
|
-
$runArgs = $sshOptions + @('-p', "$Port", $Target, (Quote-Arg $remoteCmd))
|
|
713
|
-
[void]$steps.Add((New-PipelineStep -File 'ssh.exe' -Arguments ($runArgs -join ' ') -Label 'ssh: run install.sh'))
|
|
714
|
-
|
|
715
|
-
return $steps.ToArray()
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function Get-CommandPreview {
|
|
719
|
-
param([string]$Mode, [string]$Node, [string]$Domain, [string]$Token, [string]$Target, [int]$Port)
|
|
720
|
-
|
|
721
|
-
if ($Mode -eq 'quick' -or $Mode -eq 'custom') {
|
|
722
|
-
# @() matters: PowerShell unrolls a function's return value, so a single
|
|
723
|
-
# step would arrive as a bare hashtable and $steps[0] would then be read
|
|
724
|
-
# as a key lookup that finds nothing.
|
|
725
|
-
$steps = @(New-ConsoleInstallerSteps -Mode $Mode -Node $Node -Domain $Domain -Token $Token)
|
|
726
|
-
return ($steps[0].File + ' ' + $steps[0].Arguments)
|
|
727
|
-
}
|
|
728
|
-
$lines = New-Object System.Collections.ArrayList
|
|
729
|
-
foreach ($step in (New-SshSteps -Target $Target -Port $Port -Domain $Domain)) {
|
|
730
|
-
[void]$lines.Add(($step.File + ' ' + $step.Arguments))
|
|
731
|
-
}
|
|
732
|
-
return ($lines -join "`r`n")
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
# ------------------------------------------------------------------------------
|
|
736
|
-
# UI state
|
|
737
|
-
# ------------------------------------------------------------------------------
|
|
738
|
-
|
|
739
|
-
$script:Form = $null
|
|
740
|
-
$script:Content = $null
|
|
741
|
-
$script:HeaderTitle = $null
|
|
742
|
-
$script:LangBox = $null
|
|
743
|
-
$script:StepLabel = $null
|
|
744
|
-
$script:BtnBack = $null
|
|
745
|
-
$script:BtnPrimary = $null
|
|
746
|
-
$script:BtnClose = $null
|
|
747
|
-
$script:Device = @{ Hostname = ''; Raw = ''; IsLaptop = $false; Os = '' }
|
|
748
|
-
|
|
749
|
-
$script:Page = 0
|
|
750
|
-
$script:Mode = 'quick'
|
|
751
|
-
$script:Node = ''
|
|
752
|
-
$script:Domain = ''
|
|
753
|
-
$script:Token = ''
|
|
754
|
-
$script:SshTarget = ''
|
|
755
|
-
$script:SshPort = 22
|
|
756
|
-
|
|
757
|
-
$script:Preflight = @{ Running = $false; Done = $false; Text = ''; Facts = $null; Problems = $false; Parsed = $false }
|
|
758
|
-
$script:Install = @{ Running = $false; Failed = $false; Exit = 0; Output = ''; Url = ''; Notice = ''; LinkNotice = '' }
|
|
759
|
-
$script:OptionError = ''
|
|
760
|
-
|
|
761
|
-
function New-Label {
|
|
762
|
-
param([string]$Text, [int]$X, [int]$Y, [int]$W, [int]$H, [switch]$Bold, [switch]$Dim)
|
|
763
|
-
$label = New-Object System.Windows.Forms.Label
|
|
764
|
-
$label.Text = $Text
|
|
765
|
-
$label.Location = New-Object System.Drawing.Point($X, $Y)
|
|
766
|
-
$label.Size = New-Object System.Drawing.Size($W, $H)
|
|
767
|
-
if ($Bold) { $label.Font = New-Object System.Drawing.Font('Segoe UI', 9, [System.Drawing.FontStyle]::Bold) }
|
|
768
|
-
if ($Dim) { $label.ForeColor = [System.Drawing.Color]::FromArgb(90, 90, 90) }
|
|
769
|
-
return $label
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function New-GroupBox {
|
|
773
|
-
param([string]$Text, [int]$X, [int]$Y, [int]$W, [int]$H)
|
|
774
|
-
$group = New-Object System.Windows.Forms.GroupBox
|
|
775
|
-
$group.Text = $Text
|
|
776
|
-
$group.Location = New-Object System.Drawing.Point($X, $Y)
|
|
777
|
-
$group.Size = New-Object System.Drawing.Size($W, $H)
|
|
778
|
-
return $group
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function New-ReadOnlyBox {
|
|
782
|
-
param([int]$X, [int]$Y, [int]$W, [int]$H, [switch]$MultiLine, [switch]$Monospace)
|
|
783
|
-
$box = New-Object System.Windows.Forms.TextBox
|
|
784
|
-
$box.Location = New-Object System.Drawing.Point($X, $Y)
|
|
785
|
-
$box.Size = New-Object System.Drawing.Size($W, $H)
|
|
786
|
-
$box.ReadOnly = $true
|
|
787
|
-
$box.BackColor = [System.Drawing.Color]::White
|
|
788
|
-
if ($MultiLine) {
|
|
789
|
-
$box.Multiline = $true
|
|
790
|
-
$box.ScrollBars = [System.Windows.Forms.ScrollBars]::Both
|
|
791
|
-
$box.WordWrap = $false
|
|
792
|
-
}
|
|
793
|
-
if ($Monospace) {
|
|
794
|
-
$box.Font = New-Object System.Drawing.Font('Consolas', 9)
|
|
795
|
-
}
|
|
796
|
-
return $box
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
function New-InputBox {
|
|
800
|
-
param([int]$X, [int]$Y, [int]$W, [string]$Text, [scriptblock]$OnChange)
|
|
801
|
-
$box = New-Object System.Windows.Forms.TextBox
|
|
802
|
-
$box.Location = New-Object System.Drawing.Point($X, $Y)
|
|
803
|
-
$box.Size = New-Object System.Drawing.Size($W, 23)
|
|
804
|
-
$box.Text = $Text
|
|
805
|
-
if ($OnChange) { $box.Add_TextChanged($OnChange) }
|
|
806
|
-
return $box
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function Set-Language {
|
|
810
|
-
param([string]$Code)
|
|
811
|
-
|
|
812
|
-
if ($Code -eq $script:UiLang) { return }
|
|
813
|
-
$script:UiLang = $Code
|
|
814
|
-
if ($script:Form) { $script:Form.Text = Get-UiText -Key 'app_title' }
|
|
815
|
-
if ($script:HeaderTitle) { $script:HeaderTitle.Text = Get-UiText -Key 'app_title' }
|
|
816
|
-
Show-Page -Index $script:Page
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
# ---- page 0: welcome ---------------------------------------------------------
|
|
820
|
-
|
|
821
|
-
function Build-WelcomePage {
|
|
822
|
-
$script:Content.Controls.Add((New-Label -Text (Get-UiText -Key 'welcome_intro') -X 16 -Y 10 -W 760 -H 34))
|
|
823
|
-
|
|
824
|
-
$device = New-GroupBox -Text (Get-UiText -Key 'device_group') -X 16 -Y 50 -W 760 -H 100
|
|
825
|
-
$typeKey = 'device_desktop'
|
|
826
|
-
if ($script:Device.IsLaptop) { $typeKey = 'device_laptop' }
|
|
827
|
-
$rows = @(
|
|
828
|
-
@((Get-UiText -Key 'device_hostname'), $script:Device.Raw),
|
|
829
|
-
@((Get-UiText -Key 'device_type'), (Get-UiText -Key $typeKey)),
|
|
830
|
-
@((Get-UiText -Key 'device_os'), $script:Device.Os)
|
|
831
|
-
)
|
|
832
|
-
$y = 24
|
|
833
|
-
foreach ($row in $rows) {
|
|
834
|
-
$device.Controls.Add((New-Label -Text ($row[0] + ':') -X 14 -Y $y -W 120 -H 20))
|
|
835
|
-
$device.Controls.Add((New-Label -Text $row[1] -X 140 -Y $y -W 600 -H 20))
|
|
836
|
-
$y += 22
|
|
837
|
-
}
|
|
838
|
-
$script:Content.Controls.Add($device)
|
|
839
|
-
|
|
840
|
-
$preflight = New-GroupBox -Text (Get-UiText -Key 'preflight_group') -X 16 -Y 160 -W 760 -H 300
|
|
841
|
-
$box = New-ReadOnlyBox -X 14 -Y 22 -W 732 -H 266 -MultiLine -Monospace
|
|
842
|
-
if ($script:Preflight.Done) {
|
|
843
|
-
$box.Text = $script:Preflight.Text
|
|
844
|
-
} else {
|
|
845
|
-
$box.Text = Get-UiText -Key 'preflight_running'
|
|
846
|
-
}
|
|
847
|
-
$preflight.Controls.Add($box)
|
|
848
|
-
$script:Content.Controls.Add($preflight)
|
|
849
|
-
|
|
850
|
-
# Gated on Parsed: when the preflight could not run, the report above already
|
|
851
|
-
# says so, and an extra "no domain configured" would be a wrong diagnosis.
|
|
852
|
-
if ($script:Preflight.Done -and $script:Preflight.Parsed) {
|
|
853
|
-
if (-not (Test-DomainConfigured -Value $script:Preflight.Facts.Domain)) {
|
|
854
|
-
$hint = New-Label -Text (Get-UiText -Key 'preflight_no_domain') -X 20 -Y 466 -W 752 -H 34
|
|
855
|
-
$hint.ForeColor = [System.Drawing.Color]::FromArgb(176, 96, 0)
|
|
856
|
-
$script:Content.Controls.Add($hint)
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
# ---- page 1: mode ------------------------------------------------------------
|
|
862
|
-
|
|
863
|
-
function Build-ModePage {
|
|
864
|
-
$script:Content.Controls.Add((New-Label -Text (Get-UiText -Key 'mode_prompt') -X 16 -Y 12 -W 760 -H 24))
|
|
865
|
-
|
|
866
|
-
$group = New-GroupBox -Text (Get-UiText -Key 'page_mode_title') -X 16 -Y 44 -W 760 -H 220
|
|
867
|
-
$modes = @(
|
|
868
|
-
@{ Id = 'quick'; Title = 'mode_quick_title'; Desc = 'mode_quick_desc' },
|
|
869
|
-
@{ Id = 'custom'; Title = 'mode_custom_title'; Desc = 'mode_custom_desc' },
|
|
870
|
-
@{ Id = 'ssh'; Title = 'mode_ssh_title'; Desc = 'mode_ssh_desc' }
|
|
871
|
-
)
|
|
872
|
-
$y = 26
|
|
873
|
-
foreach ($mode in $modes) {
|
|
874
|
-
$radio = New-Object System.Windows.Forms.RadioButton
|
|
875
|
-
$radio.Text = Get-UiText -Key $mode.Title
|
|
876
|
-
$radio.Location = New-Object System.Drawing.Point(16, $y)
|
|
877
|
-
$radio.Size = New-Object System.Drawing.Size(720, 22)
|
|
878
|
-
$radio.Font = New-Object System.Drawing.Font('Segoe UI', 9, [System.Drawing.FontStyle]::Bold)
|
|
879
|
-
$radio.Tag = $mode.Id
|
|
880
|
-
if ($script:Mode -eq $mode.Id) { $radio.Checked = $true }
|
|
881
|
-
# Attached after Checked is set, so building the page cannot look like a
|
|
882
|
-
# user click and overwrite the selected mode.
|
|
883
|
-
$radio.Add_CheckedChanged({
|
|
884
|
-
if ($this.Checked) {
|
|
885
|
-
$script:Mode = [string]$this.Tag
|
|
886
|
-
$script:OptionError = ''
|
|
887
|
-
}
|
|
888
|
-
})
|
|
889
|
-
$group.Controls.Add($radio)
|
|
890
|
-
$group.Controls.Add((New-Label -Text (Get-UiText -Key $mode.Desc) -X 36 -Y ($y + 22) -W 700 -H 20 -Dim))
|
|
891
|
-
$y += 62
|
|
892
|
-
}
|
|
893
|
-
$script:Content.Controls.Add($group)
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
# ---- page 2: options ---------------------------------------------------------
|
|
897
|
-
|
|
898
|
-
function Build-OptionsPage {
|
|
899
|
-
if ($script:Mode -eq 'quick') {
|
|
900
|
-
$group = New-GroupBox -Text (Get-UiText -Key 'opt_quick_group') -X 16 -Y 12 -W 760 -H 120
|
|
901
|
-
$domain = "$($script:Domain)".Trim()
|
|
902
|
-
if (-not $domain) { $domain = '- (resolved by install.ps1)' }
|
|
903
|
-
$summary = New-ReadOnlyBox -X 14 -Y 24 -W 732 -H 84 -MultiLine -Monospace
|
|
904
|
-
$summary.Text = (Format-UiText -Key 'opt_quick_summary' -Values @($script:Node, $domain))
|
|
905
|
-
$group.Controls.Add($summary)
|
|
906
|
-
$script:Content.Controls.Add($group)
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
if ($script:Mode -eq 'custom') {
|
|
910
|
-
$group = New-GroupBox -Text (Get-UiText -Key 'page_options_title') -X 16 -Y 12 -W 760 -H 230
|
|
911
|
-
|
|
912
|
-
$group.Controls.Add((New-Label -Text ((Get-UiText -Key 'opt_node_label') + ':') -X 16 -Y 32 -W 200 -H 20))
|
|
913
|
-
$group.Controls.Add((New-InputBox -X 220 -Y 29 -W 520 -Text $script:Node -OnChange {
|
|
914
|
-
$script:Node = $this.Text; $script:OptionError = ''
|
|
915
|
-
}))
|
|
916
|
-
$group.Controls.Add((New-Label -Text (Get-UiText -Key 'opt_node_hint') -X 220 -Y 54 -W 520 -H 18 -Dim))
|
|
917
|
-
|
|
918
|
-
$group.Controls.Add((New-Label -Text ((Get-UiText -Key 'opt_domain_label') + ':') -X 16 -Y 90 -W 200 -H 20))
|
|
919
|
-
$group.Controls.Add((New-InputBox -X 220 -Y 87 -W 520 -Text $script:Domain -OnChange {
|
|
920
|
-
$script:Domain = $this.Text; $script:OptionError = ''
|
|
921
|
-
}))
|
|
922
|
-
$group.Controls.Add((New-Label -Text (Get-UiText -Key 'opt_domain_hint') -X 220 -Y 112 -W 520 -H 18 -Dim))
|
|
923
|
-
|
|
924
|
-
$group.Controls.Add((New-Label -Text ((Get-UiText -Key 'opt_token_label') + ':') -X 16 -Y 148 -W 200 -H 20))
|
|
925
|
-
$group.Controls.Add((New-InputBox -X 220 -Y 145 -W 520 -Text $script:Token -OnChange {
|
|
926
|
-
$script:Token = $this.Text; $script:OptionError = ''
|
|
927
|
-
}))
|
|
928
|
-
$group.Controls.Add((New-Label -Text (Get-UiText -Key 'opt_token_hint') -X 220 -Y 170 -W 520 -H 18 -Dim))
|
|
929
|
-
|
|
930
|
-
$script:Content.Controls.Add($group)
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
if ($script:Mode -eq 'ssh') {
|
|
934
|
-
$group = New-GroupBox -Text (Get-UiText -Key 'page_options_title') -X 16 -Y 12 -W 760 -H 210
|
|
935
|
-
|
|
936
|
-
$group.Controls.Add((New-Label -Text ((Get-UiText -Key 'opt_ssh_target_label') + ':') -X 16 -Y 32 -W 200 -H 20))
|
|
937
|
-
$group.Controls.Add((New-InputBox -X 220 -Y 29 -W 520 -Text $script:SshTarget -OnChange {
|
|
938
|
-
$script:SshTarget = $this.Text; $script:OptionError = ''
|
|
939
|
-
}))
|
|
940
|
-
$group.Controls.Add((New-Label -Text (Get-UiText -Key 'opt_ssh_target_hint') -X 220 -Y 54 -W 520 -H 18 -Dim))
|
|
941
|
-
|
|
942
|
-
$group.Controls.Add((New-Label -Text ((Get-UiText -Key 'opt_ssh_port_label') + ':') -X 16 -Y 90 -W 200 -H 20))
|
|
943
|
-
$group.Controls.Add((New-InputBox -X 220 -Y 87 -W 120 -Text "$($script:SshPort)" -OnChange {
|
|
944
|
-
$parsed = 0
|
|
945
|
-
if ([int]::TryParse($this.Text.Trim(), [ref]$parsed)) { $script:SshPort = $parsed }
|
|
946
|
-
$script:OptionError = ''
|
|
947
|
-
}))
|
|
948
|
-
|
|
949
|
-
$group.Controls.Add((New-Label -Text ((Get-UiText -Key 'opt_ssh_domain_label') + ':') -X 16 -Y 126 -W 200 -H 20))
|
|
950
|
-
$group.Controls.Add((New-InputBox -X 220 -Y 123 -W 520 -Text $script:Domain -OnChange {
|
|
951
|
-
$script:Domain = $this.Text; $script:OptionError = ''
|
|
952
|
-
}))
|
|
953
|
-
|
|
954
|
-
$group.Controls.Add((New-Label -Text (Format-UiText -Key 'opt_ssh_note' -Values @($script:UiLang)) `
|
|
955
|
-
-X 16 -Y 162 -W 730 -H 20 -Dim))
|
|
956
|
-
$script:Content.Controls.Add($group)
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
$preview = New-GroupBox -Text (Get-UiText -Key 'opt_summary_group') -X 16 -Y 252 -W 760 -H 210
|
|
960
|
-
$box = New-ReadOnlyBox -X 14 -Y 22 -W 732 -H 176 -MultiLine -Monospace
|
|
961
|
-
$box.Text = Get-CommandPreview -Mode $script:Mode -Node $script:Node -Domain $script:Domain `
|
|
962
|
-
-Token $script:Token -Target $script:SshTarget -Port $script:SshPort
|
|
963
|
-
$preview.Controls.Add($box)
|
|
964
|
-
$script:Content.Controls.Add($preview)
|
|
965
|
-
|
|
966
|
-
if ($script:OptionError) {
|
|
967
|
-
$errorLabel = New-Label -Text $script:OptionError -X 20 -Y 470 -W 750 -H 22
|
|
968
|
-
$errorLabel.ForeColor = [System.Drawing.Color]::FromArgb(176, 0, 0)
|
|
969
|
-
$script:Content.Controls.Add($errorLabel)
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
# ---- page 3: progress --------------------------------------------------------
|
|
974
|
-
|
|
975
|
-
function Build-ProgressPage {
|
|
976
|
-
$status = New-Label -Text (Get-UiText -Key 'prog_running') -X 16 -Y 12 -W 760 -H 22
|
|
977
|
-
$status.Font = New-Object System.Drawing.Font('Segoe UI', 9, [System.Drawing.FontStyle]::Bold)
|
|
978
|
-
$script:Content.Controls.Add($status)
|
|
979
|
-
|
|
980
|
-
$bar = New-Object System.Windows.Forms.ProgressBar
|
|
981
|
-
$bar.Location = New-Object System.Drawing.Point(16, 38)
|
|
982
|
-
$bar.Size = New-Object System.Drawing.Size(760, 16)
|
|
983
|
-
$bar.Style = [System.Windows.Forms.ProgressBarStyle]::Marquee
|
|
984
|
-
$bar.MarqueeAnimationSpeed = 30
|
|
985
|
-
$script:Content.Controls.Add($bar)
|
|
986
|
-
|
|
987
|
-
$script:Content.Controls.Add((New-Label -Text (Get-UiText -Key 'prog_log') -X 16 -Y 64 -W 760 -H 20))
|
|
988
|
-
$log = New-ReadOnlyBox -X 16 -Y 86 -W 760 -H 372 -MultiLine -Monospace
|
|
989
|
-
$log.Text = $script:Install.Output
|
|
990
|
-
$script:Content.Controls.Add($log)
|
|
991
|
-
|
|
992
|
-
$script:Pipe.LogBox = $log
|
|
993
|
-
$script:Pipe.StatusLabel = $status
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
# ---- page 4: result ----------------------------------------------------------
|
|
997
|
-
|
|
998
|
-
function Build-ResultPage {
|
|
999
|
-
$failed = $script:Install.Failed
|
|
1000
|
-
$titleKey = 'res_success_title'
|
|
1001
|
-
if ($failed) { $titleKey = 'res_fail_title' }
|
|
1002
|
-
$title = New-Label -Text (Get-UiText -Key $titleKey) -X 16 -Y 10 -W 760 -H 26 -Bold
|
|
1003
|
-
if ($failed) {
|
|
1004
|
-
$title.ForeColor = [System.Drawing.Color]::FromArgb(176, 0, 0)
|
|
1005
|
-
} else {
|
|
1006
|
-
$title.ForeColor = [System.Drawing.Color]::FromArgb(0, 120, 60)
|
|
1007
|
-
}
|
|
1008
|
-
$script:Content.Controls.Add($title)
|
|
1009
|
-
|
|
1010
|
-
$url = $script:Install.Url
|
|
1011
|
-
|
|
1012
|
-
$urlGroup = New-GroupBox -Text (Get-UiText -Key 'res_url_group') -X 16 -Y 42 -W 760 -H 96
|
|
1013
|
-
$urlBox = New-Object System.Windows.Forms.TextBox
|
|
1014
|
-
$urlBox.Location = New-Object System.Drawing.Point(14, 26)
|
|
1015
|
-
$urlBox.Size = New-Object System.Drawing.Size(600, 23)
|
|
1016
|
-
$urlBox.ReadOnly = $true
|
|
1017
|
-
$urlBox.BackColor = [System.Drawing.Color]::White
|
|
1018
|
-
$urlBox.Font = New-Object System.Drawing.Font('Consolas', 9)
|
|
1019
|
-
if ($url) { $urlBox.Text = $url } else { $urlBox.Text = Get-UiText -Key 'res_url_missing' }
|
|
1020
|
-
$urlGroup.Controls.Add($urlBox)
|
|
1021
|
-
|
|
1022
|
-
$copy = New-Object System.Windows.Forms.Button
|
|
1023
|
-
$copy.Text = Get-UiText -Key 'btn_copy'
|
|
1024
|
-
# Sizes itself: the Russian label is longer than the English one, and a fixed
|
|
1025
|
-
# width clipped it.
|
|
1026
|
-
$copy.AutoSize = $true
|
|
1027
|
-
$copy.AutoSizeMode = [System.Windows.Forms.AutoSizeMode]::GrowAndShrink
|
|
1028
|
-
$copy.MinimumSize = New-Object System.Drawing.Size(122, 26)
|
|
1029
|
-
$copy.Enabled = [bool]$url
|
|
1030
|
-
$copy.Add_Click({
|
|
1031
|
-
if (-not $script:Install.Url) { return }
|
|
1032
|
-
if (Set-UrlClipboard -Text $script:Install.Url) {
|
|
1033
|
-
$script:Install.Notice = Get-UiText -Key 'res_copied'
|
|
1034
|
-
} else {
|
|
1035
|
-
$script:Install.Notice = Get-UiText -Key 'res_copy_failed'
|
|
1036
|
-
}
|
|
1037
|
-
Show-Page -Index 4
|
|
1038
|
-
})
|
|
1039
|
-
$urlGroup.Controls.Add($copy)
|
|
1040
|
-
$copy.Location = New-Object System.Drawing.Point((746 - $copy.Width), 24)
|
|
1041
|
-
|
|
1042
|
-
$notice = "$($script:Install.Notice)"
|
|
1043
|
-
$noticeLabel = New-Label -Text $notice -X 16 -Y 56 -W 730 -H 20
|
|
1044
|
-
if ($notice) { $noticeLabel.ForeColor = [System.Drawing.Color]::FromArgb(0, 120, 60) }
|
|
1045
|
-
$urlGroup.Controls.Add($noticeLabel)
|
|
1046
|
-
|
|
1047
|
-
if ($script:Mode -eq 'ssh' -and -not $failed) {
|
|
1048
|
-
$urlGroup.Controls.Add((New-Label -Text (Get-UiText -Key 'res_remote_note') -X 16 -Y 74 -W 730 -H 18 -Dim))
|
|
1049
|
-
}
|
|
1050
|
-
$script:Content.Controls.Add($urlGroup)
|
|
1051
|
-
|
|
1052
|
-
$stepsGroup = New-GroupBox -Text (Get-UiText -Key 'res_steps_title') -X 16 -Y 146 -W 760 -H 126
|
|
1053
|
-
$stepsGroup.Controls.Add((New-Label -Text (Get-UiText -Key 'res_step1_prefix') -X 16 -Y 24 -W 250 -H 20))
|
|
1054
|
-
|
|
1055
|
-
# A real link, not a label: clicking it opens the page, and the button beside
|
|
1056
|
-
# it puts the address on the clipboard. A plain label could do neither.
|
|
1057
|
-
$link = New-Object System.Windows.Forms.LinkLabel
|
|
1058
|
-
$link.Text = $GeminiAppsUrl
|
|
1059
|
-
$link.Location = New-Object System.Drawing.Point(268, 24)
|
|
1060
|
-
$link.Size = New-Object System.Drawing.Size(310, 20)
|
|
1061
|
-
$link.Add_LinkClicked({ [void](Start-Url -Url $GeminiAppsUrl) })
|
|
1062
|
-
$stepsGroup.Controls.Add($link)
|
|
1063
|
-
|
|
1064
|
-
$copyLink = New-Object System.Windows.Forms.Button
|
|
1065
|
-
$copyLink.Text = Get-UiText -Key 'btn_copy_link'
|
|
1066
|
-
$copyLink.AutoSize = $true
|
|
1067
|
-
$copyLink.AutoSizeMode = [System.Windows.Forms.AutoSizeMode]::GrowAndShrink
|
|
1068
|
-
$copyLink.MinimumSize = New-Object System.Drawing.Size(112, 26)
|
|
1069
|
-
$copyLink.Add_Click({
|
|
1070
|
-
if (Set-UrlClipboard -Text $GeminiAppsUrl) {
|
|
1071
|
-
$script:Install.LinkNotice = Get-UiText -Key 'res_copied'
|
|
1072
|
-
} else {
|
|
1073
|
-
$script:Install.LinkNotice = Get-UiText -Key 'res_copy_failed'
|
|
1074
|
-
}
|
|
1075
|
-
Show-Page -Index 4
|
|
1076
|
-
})
|
|
1077
|
-
$stepsGroup.Controls.Add($copyLink)
|
|
1078
|
-
$copyLink.Location = New-Object System.Drawing.Point((746 - $copyLink.Width), 21)
|
|
1079
|
-
|
|
1080
|
-
$linkNotice = "$($script:Install.LinkNotice)"
|
|
1081
|
-
$linkNoticeLabel = New-Label -Text $linkNotice -X 16 -Y 94 -W 720 -H 20
|
|
1082
|
-
if ($linkNotice) { $linkNoticeLabel.ForeColor = [System.Drawing.Color]::FromArgb(0, 120, 60) }
|
|
1083
|
-
$stepsGroup.Controls.Add($linkNoticeLabel)
|
|
1084
|
-
|
|
1085
|
-
$stepsGroup.Controls.Add((New-Label -Text (Get-UiText -Key 'res_step2') -X 16 -Y 46 -W 730 -H 20))
|
|
1086
|
-
$stepsGroup.Controls.Add((New-Label -Text (Get-UiText -Key 'res_step3') -X 16 -Y 68 -W 730 -H 20))
|
|
1087
|
-
$script:Content.Controls.Add($stepsGroup)
|
|
1088
|
-
|
|
1089
|
-
if ($failed) {
|
|
1090
|
-
$hint = New-Label -Text (Get-UiText -Key 'res_fail_hint') -X 20 -Y 278 -W 752 -H 40
|
|
1091
|
-
$hint.ForeColor = [System.Drawing.Color]::FromArgb(176, 96, 0)
|
|
1092
|
-
$script:Content.Controls.Add($hint)
|
|
1093
|
-
} else {
|
|
1094
|
-
$configGroup = New-GroupBox -Text (Get-UiText -Key 'res_config_group') -X 16 -Y 278 -W 760 -H 110
|
|
1095
|
-
$rows = @()
|
|
1096
|
-
if ($script:Mode -eq 'ssh') {
|
|
1097
|
-
$rows = @(@((Get-UiText -Key 'res_config_file'), '~/antigravity-mesh (remote)'))
|
|
1098
|
-
} else {
|
|
1099
|
-
$rows = @(
|
|
1100
|
-
@((Get-UiText -Key 'res_config_file'), $AgentEnvFile),
|
|
1101
|
-
@((Get-UiText -Key 'res_autostart_file'), $AutostartFile),
|
|
1102
|
-
@((Get-UiText -Key 'res_agent_log'), $AgentLogFile)
|
|
1103
|
-
)
|
|
1104
|
-
}
|
|
1105
|
-
$y = 24
|
|
1106
|
-
foreach ($row in $rows) {
|
|
1107
|
-
$configGroup.Controls.Add((New-Label -Text ($row[0] + ':') -X 14 -Y $y -W 130 -H 20))
|
|
1108
|
-
$configGroup.Controls.Add((New-Label -Text $row[1] -X 150 -Y $y -W 460 -H 20 -Dim))
|
|
1109
|
-
$y += 24
|
|
1110
|
-
}
|
|
1111
|
-
$openConfig = New-Object System.Windows.Forms.Button
|
|
1112
|
-
$openConfig.Text = Get-UiText -Key 'btn_open_config'
|
|
1113
|
-
$openConfig.AutoSize = $true
|
|
1114
|
-
$openConfig.AutoSizeMode = [System.Windows.Forms.AutoSizeMode]::GrowAndShrink
|
|
1115
|
-
$openConfig.MinimumSize = New-Object System.Drawing.Size(128, 26)
|
|
1116
|
-
$openConfig.Add_Click({ [void](Open-Folder -Path $ConfigDir) })
|
|
1117
|
-
$configGroup.Controls.Add($openConfig)
|
|
1118
|
-
$openConfig.Location = New-Object System.Drawing.Point((746 - $openConfig.Width), 74)
|
|
1119
|
-
$script:Content.Controls.Add($configGroup)
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
$logGroup = New-GroupBox -Text (Get-UiText -Key 'res_log_group') -X 16 -Y 396 -W 760 -H 84
|
|
1123
|
-
$log = New-ReadOnlyBox -X 14 -Y 20 -W 732 -H 56 -MultiLine -Monospace
|
|
1124
|
-
$log.Text = $script:Install.Output
|
|
1125
|
-
$logGroup.Controls.Add($log)
|
|
1126
|
-
$script:Content.Controls.Add($logGroup)
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
# ---- navigation --------------------------------------------------------------
|
|
1130
|
-
|
|
1131
|
-
# Buttons grow with their label, so the footer is positioned from the right edge
|
|
1132
|
-
# instead of at fixed coordinates: "Open Gemini Apps" is far wider than "Next >",
|
|
1133
|
-
# and the Russian labels are wider still.
|
|
1134
|
-
function Format-FooterButtons {
|
|
1135
|
-
$right = $script:Form.ClientSize.Width - 16
|
|
1136
|
-
foreach ($button in @($script:BtnClose, $script:BtnPrimary, $script:BtnBack)) {
|
|
1137
|
-
$right = $right - $button.Width
|
|
1138
|
-
$button.Location = New-Object System.Drawing.Point($right, 14)
|
|
1139
|
-
$right = $right - 8
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
function Show-Page {
|
|
1144
|
-
param([int]$Index)
|
|
1145
|
-
|
|
1146
|
-
$script:Page = $Index
|
|
1147
|
-
$script:Content.Controls.Clear()
|
|
1148
|
-
$script:Pipe.LogBox = $null
|
|
1149
|
-
$script:Pipe.StatusLabel = $null
|
|
1150
|
-
|
|
1151
|
-
$script:BtnBack.Visible = $false
|
|
1152
|
-
# Re-applied on every page: the label was set once in Initialize-Form, so a
|
|
1153
|
-
# runtime language switch left this button in the previous language.
|
|
1154
|
-
$script:BtnBack.Text = Get-UiText -Key 'btn_back'
|
|
1155
|
-
$script:BtnPrimary.Visible = $true
|
|
1156
|
-
$script:BtnPrimary.Enabled = $true
|
|
1157
|
-
$script:BtnClose.Text = Get-UiText -Key 'btn_close'
|
|
1158
|
-
|
|
1159
|
-
switch ($Index) {
|
|
1160
|
-
0 {
|
|
1161
|
-
$script:StepLabel.Text = Format-UiText -Key 'step_indicator' -Values @(1, 3)
|
|
1162
|
-
Build-WelcomePage
|
|
1163
|
-
$script:BtnPrimary.Text = Get-UiText -Key 'btn_next'
|
|
1164
|
-
}
|
|
1165
|
-
1 {
|
|
1166
|
-
$script:StepLabel.Text = Format-UiText -Key 'step_indicator' -Values @(2, 3)
|
|
1167
|
-
$script:BtnBack.Visible = $true
|
|
1168
|
-
Build-ModePage
|
|
1169
|
-
$script:BtnPrimary.Text = Get-UiText -Key 'btn_next'
|
|
1170
|
-
}
|
|
1171
|
-
2 {
|
|
1172
|
-
$script:StepLabel.Text = Format-UiText -Key 'step_indicator' -Values @(3, 3)
|
|
1173
|
-
$script:BtnBack.Visible = $true
|
|
1174
|
-
Build-OptionsPage
|
|
1175
|
-
$script:BtnPrimary.Text = Get-UiText -Key 'btn_install'
|
|
1176
|
-
}
|
|
1177
|
-
3 {
|
|
1178
|
-
$script:StepLabel.Text = Get-UiText -Key 'page_progress_title'
|
|
1179
|
-
Build-ProgressPage
|
|
1180
|
-
$script:BtnPrimary.Visible = $false
|
|
1181
|
-
}
|
|
1182
|
-
4 {
|
|
1183
|
-
$script:StepLabel.Text = Get-UiText -Key 'page_result_title'
|
|
1184
|
-
Build-ResultPage
|
|
1185
|
-
$script:BtnPrimary.Text = Get-UiText -Key 'btn_open_gemini'
|
|
1186
|
-
$script:BtnClose.Text = Get-UiText -Key 'btn_finish'
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
Format-FooterButtons
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
# ---- install -----------------------------------------------------------------
|
|
1194
|
-
|
|
1195
|
-
function Test-InstallOptions {
|
|
1196
|
-
if ($script:Mode -eq 'quick' -or $script:Mode -eq 'custom') {
|
|
1197
|
-
if (-not (Test-Path -LiteralPath $ConsoleInstaller)) {
|
|
1198
|
-
$script:OptionError = Format-UiText -Key 'opt_err_installer_missing' -Values @($ConsoleInstaller)
|
|
1199
|
-
return $false
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
if ($script:Mode -eq 'custom') {
|
|
1203
|
-
$node = "$($script:Node)".Trim()
|
|
1204
|
-
if (-not $node) {
|
|
1205
|
-
$script:OptionError = Get-UiText -Key 'opt_err_node_required'
|
|
1206
|
-
return $false
|
|
1207
|
-
}
|
|
1208
|
-
if ($node -notmatch '^[a-z0-9_-]+$') {
|
|
1209
|
-
$script:OptionError = Get-UiText -Key 'opt_err_node_chars'
|
|
1210
|
-
return $false
|
|
1211
|
-
}
|
|
1212
|
-
if (-not "$($script:Domain)".Trim()) {
|
|
1213
|
-
$script:OptionError = Get-UiText -Key 'opt_err_domain_required'
|
|
1214
|
-
return $false
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
if ($script:Mode -eq 'ssh') {
|
|
1218
|
-
$target = "$($script:SshTarget)".Trim()
|
|
1219
|
-
if (-not $target -or $target -notmatch '^[^@\s]+@[^@\s]+$') {
|
|
1220
|
-
$script:OptionError = Get-UiText -Key 'opt_err_ssh_required'
|
|
1221
|
-
return $false
|
|
1222
|
-
}
|
|
1223
|
-
if ($script:SshPort -lt 1 -or $script:SshPort -gt 65535) {
|
|
1224
|
-
$script:OptionError = Get-UiText -Key 'opt_err_ssh_port'
|
|
1225
|
-
return $false
|
|
1226
|
-
}
|
|
1227
|
-
if (-not (Test-Path -LiteralPath $ShellInstaller)) {
|
|
1228
|
-
$script:OptionError = Format-UiText -Key 'opt_err_install_sh_missing' -Values @($ShellInstaller)
|
|
1229
|
-
return $false
|
|
1230
|
-
}
|
|
1231
|
-
if (-not (Test-Path -LiteralPath $CoreDir)) {
|
|
1232
|
-
$script:OptionError = Format-UiText -Key 'opt_err_core_missing' -Values @($CoreDir)
|
|
1233
|
-
return $false
|
|
1234
|
-
}
|
|
1235
|
-
if (-not (Get-Command 'ssh.exe' -ErrorAction SilentlyContinue) -or
|
|
1236
|
-
-not (Get-Command 'scp.exe' -ErrorAction SilentlyContinue)) {
|
|
1237
|
-
$script:OptionError = Get-UiText -Key 'opt_err_ssh_missing'
|
|
1238
|
-
return $false
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
$script:OptionError = ''
|
|
1242
|
-
return $true
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
function Start-Installation {
|
|
1246
|
-
if (-not (Test-InstallOptions)) {
|
|
1247
|
-
Show-Page -Index 2
|
|
1248
|
-
return
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
$script:Install = @{ Running = $true; Failed = $false; Exit = 0; Output = ''; Url = ''; Notice = ''; LinkNotice = '' }
|
|
1252
|
-
Show-Page -Index 3
|
|
1253
|
-
|
|
1254
|
-
if ($script:Mode -eq 'ssh') {
|
|
1255
|
-
$steps = New-SshSteps -Target "$($script:SshTarget)".Trim() -Port $script:SshPort `
|
|
1256
|
-
-Domain "$($script:Domain)".Trim()
|
|
1257
|
-
} else {
|
|
1258
|
-
$steps = New-ConsoleInstallerSteps -Mode $script:Mode -Node "$($script:Node)".Trim() `
|
|
1259
|
-
-Domain "$($script:Domain)".Trim() -Token "$($script:Token)".Trim()
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
Reset-Pipeline -Steps $steps -OnFinish {
|
|
1263
|
-
param($failed, $exitCode, $output)
|
|
1264
|
-
$script:Install.Running = $false
|
|
1265
|
-
$script:Install.Failed = [bool]$failed
|
|
1266
|
-
$script:Install.Exit = $exitCode
|
|
1267
|
-
$script:Install.Output = $output
|
|
1268
|
-
if (-not $failed) {
|
|
1269
|
-
$script:Install.Url = Get-ResultUrl -Text $output -Mode $script:Mode
|
|
1270
|
-
}
|
|
1271
|
-
Show-Page -Index 4
|
|
1272
|
-
} -LogBox $script:Pipe.LogBox -StatusLabel $script:Pipe.StatusLabel
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
function Start-Preflight {
|
|
1276
|
-
if ($script:Preflight.Running -or $script:Preflight.Done) { return }
|
|
1277
|
-
if (-not (Test-Path -LiteralPath $ConsoleInstaller)) {
|
|
1278
|
-
$script:Preflight.Done = $true
|
|
1279
|
-
$script:Preflight.Text = Format-UiText -Key 'opt_err_installer_missing' -Values @($ConsoleInstaller)
|
|
1280
|
-
return
|
|
1281
|
-
}
|
|
1282
|
-
$script:Preflight.Running = $true
|
|
1283
|
-
$argList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', (Quote-Arg $ConsoleInstaller),
|
|
1284
|
-
'-DryRun', '-Lang', $script:UiLang) -join ' '
|
|
1285
|
-
$steps = @(New-PipelineStep -File (Get-PowerShellExe) -Arguments $argList `
|
|
1286
|
-
-Label (Get-UiText -Key 'preflight_group'))
|
|
1287
|
-
Reset-Pipeline -Steps $steps -OnFinish {
|
|
1288
|
-
param($failed, $exitCode, $output)
|
|
1289
|
-
$script:Preflight.Running = $false
|
|
1290
|
-
$script:Preflight.Done = $true
|
|
1291
|
-
$facts = Get-PreflightFacts -Text $output
|
|
1292
|
-
$script:Preflight.Facts = $facts
|
|
1293
|
-
# Whether the check actually produced anything. A run that could not even
|
|
1294
|
-
# write its output files parses to nothing, and that is not the same thing
|
|
1295
|
-
# as "no domain configured".
|
|
1296
|
-
$script:Preflight.Parsed = [bool]($facts.Python -or $facts.Domain)
|
|
1297
|
-
# exit 2 is install.ps1's "preflight found problems" code, not a failure
|
|
1298
|
-
# of the check itself, so the report is still worth showing.
|
|
1299
|
-
$script:Preflight.Problems = ($exitCode -eq 2)
|
|
1300
|
-
if (-not $facts.Python -and -not $facts.Domain) {
|
|
1301
|
-
$script:Preflight.Text = (Format-UiText -Key 'preflight_failed' -Values @("exit $exitCode")) +
|
|
1302
|
-
"`r`n`r`n" + $output
|
|
1303
|
-
} else {
|
|
1304
|
-
$lines = New-Object System.Collections.ArrayList
|
|
1305
|
-
[void]$lines.Add(((Get-UiText -Key 'preflight_python') + ' : ' + $facts.Python))
|
|
1306
|
-
[void]$lines.Add(((Get-UiText -Key 'preflight_websockets') + ' : ' + $facts.Websockets))
|
|
1307
|
-
[void]$lines.Add(((Get-UiText -Key 'preflight_domain') + ' : ' + $facts.Domain))
|
|
1308
|
-
[void]$lines.Add(((Get-UiText -Key 'preflight_config') + ' : ' + $facts.Config))
|
|
1309
|
-
[void]$lines.Add(((Get-UiText -Key 'preflight_autostart') + ' : ' + $facts.Autostart))
|
|
1310
|
-
if ($facts.Node) {
|
|
1311
|
-
[void]$lines.Add(((Get-UiText -Key 'preflight_node') + ' : ' + $facts.Node))
|
|
1312
|
-
}
|
|
1313
|
-
if ($script:Preflight.Problems) {
|
|
1314
|
-
[void]$lines.Add('')
|
|
1315
|
-
[void]$lines.Add((Get-UiText -Key 'preflight_problems'))
|
|
1316
|
-
}
|
|
1317
|
-
$script:Preflight.Text = ($lines -join "`r`n")
|
|
1318
|
-
}
|
|
1319
|
-
if ($script:Page -eq 0) { Show-Page -Index 0 }
|
|
1320
|
-
}
|
|
1321
|
-
if ($script:Page -eq 0) { Show-Page -Index 0 }
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
# ------------------------------------------------------------------------------
|
|
1325
|
-
# Form construction
|
|
1326
|
-
# ------------------------------------------------------------------------------
|
|
1327
|
-
|
|
1328
|
-
function Initialize-Form {
|
|
1329
|
-
$script:Form = New-Object System.Windows.Forms.Form
|
|
1330
|
-
# The layout below is absolute pixels. With the default AutoScaleMode of Font,
|
|
1331
|
-
# WinForms resized every control from the form's default font to Segoe UI 9,
|
|
1332
|
-
# which pushed the right-hand column and the language box past the window
|
|
1333
|
-
# edge. Measured on Windows 11, PowerShell 5.1.
|
|
1334
|
-
$script:Form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::None
|
|
1335
|
-
$script:Form.Text = Get-UiText -Key 'app_title'
|
|
1336
|
-
|
|
1337
|
-
$script:Form.ClientSize = New-Object System.Drawing.Size(804, 620)
|
|
1338
|
-
$script:Form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
|
|
1339
|
-
$script:Form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::Sizable
|
|
1340
|
-
$script:Form.MaximizeBox = $true
|
|
1341
|
-
$script:Form.MinimumSize = New-Object System.Drawing.Size(720, 560)
|
|
1342
|
-
$script:Form.Font = New-Object System.Drawing.Font('Segoe UI', 9)
|
|
1343
|
-
|
|
1344
|
-
$header = New-Object System.Windows.Forms.Panel
|
|
1345
|
-
$header.Dock = [System.Windows.Forms.DockStyle]::Top
|
|
1346
|
-
$header.Height = 56
|
|
1347
|
-
$header.BackColor = [System.Drawing.Color]::FromArgb(245, 247, 250)
|
|
1348
|
-
$script:Form.Controls.Add($header)
|
|
1349
|
-
|
|
1350
|
-
$script:HeaderTitle = New-Label -Text (Get-UiText -Key 'app_title') -X 16 -Y 14 -W 520 -H 28 -Bold
|
|
1351
|
-
$script:HeaderTitle.Font = New-Object System.Drawing.Font('Segoe UI', 11, [System.Drawing.FontStyle]::Bold)
|
|
1352
|
-
$header.Controls.Add($script:HeaderTitle)
|
|
1353
|
-
|
|
1354
|
-
$languageLabel = New-Label -Text ((Get-UiText -Key 'lang_label') + ':') -X 596 -Y 20 -W 74 -H 20
|
|
1355
|
-
$languageLabel.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Right
|
|
1356
|
-
$header.Controls.Add($languageLabel)
|
|
1357
|
-
|
|
1358
|
-
$script:LangBox = New-Object System.Windows.Forms.ComboBox
|
|
1359
|
-
$script:LangBox.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
|
|
1360
|
-
$script:LangBox.Location = New-Object System.Drawing.Point(672, 17)
|
|
1361
|
-
$script:LangBox.Size = New-Object System.Drawing.Size(118, 23)
|
|
1362
|
-
$script:LangBox.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Right
|
|
1363
|
-
[void]$script:LangBox.Items.Add('English')
|
|
1364
|
-
[void]$script:LangBox.Items.Add('Russian')
|
|
1365
|
-
# SelectedIndex is set before the handler is attached, so restoring the
|
|
1366
|
-
# startup language does not fire a rebuild.
|
|
1367
|
-
if ($script:UiLang -eq 'ru') { $script:LangBox.SelectedIndex = 1 } else { $script:LangBox.SelectedIndex = 0 }
|
|
1368
|
-
|
|
1369
|
-
$script:LangBox.Add_SelectedIndexChanged({
|
|
1370
|
-
$code = 'en'
|
|
1371
|
-
if ($this.SelectedIndex -eq 1) { $code = 'ru' }
|
|
1372
|
-
Set-Language -Code $code
|
|
1373
|
-
})
|
|
1374
|
-
$header.Controls.Add($script:LangBox)
|
|
1375
|
-
|
|
1376
|
-
$footer = New-Object System.Windows.Forms.Panel
|
|
1377
|
-
$footer.Dock = [System.Windows.Forms.DockStyle]::Bottom
|
|
1378
|
-
$footer.Height = 56
|
|
1379
|
-
$footer.BackColor = [System.Drawing.Color]::FromArgb(245, 247, 250)
|
|
1380
|
-
$script:Form.Controls.Add($footer)
|
|
1381
|
-
|
|
1382
|
-
$script:StepLabel = New-Label -Text '' -X 16 -Y 20 -W 360 -H 20 -Dim
|
|
1383
|
-
$footer.Controls.Add($script:StepLabel)
|
|
1384
|
-
|
|
1385
|
-
$script:BtnClose = New-Object System.Windows.Forms.Button
|
|
1386
|
-
$script:BtnClose.Text = Get-UiText -Key 'btn_close'
|
|
1387
|
-
$script:BtnClose.Location = New-Object System.Drawing.Point(672, 14)
|
|
1388
|
-
$script:BtnClose.AutoSize = $true
|
|
1389
|
-
$script:BtnClose.AutoSizeMode = [System.Windows.Forms.AutoSizeMode]::GrowAndShrink
|
|
1390
|
-
$script:BtnClose.MinimumSize = New-Object System.Drawing.Size(118, 28)
|
|
1391
|
-
$script:BtnClose.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Right
|
|
1392
|
-
$script:BtnClose.Add_Click({ $script:Form.Close() })
|
|
1393
|
-
$footer.Controls.Add($script:BtnClose)
|
|
1394
|
-
|
|
1395
|
-
$script:BtnPrimary = New-Object System.Windows.Forms.Button
|
|
1396
|
-
$script:BtnPrimary.Location = New-Object System.Drawing.Point(548, 14)
|
|
1397
|
-
$script:BtnPrimary.AutoSize = $true
|
|
1398
|
-
$script:BtnPrimary.AutoSizeMode = [System.Windows.Forms.AutoSizeMode]::GrowAndShrink
|
|
1399
|
-
$script:BtnPrimary.MinimumSize = New-Object System.Drawing.Size(118, 28)
|
|
1400
|
-
$script:BtnPrimary.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Right
|
|
1401
|
-
# One dispatcher for every page: attaching a handler per page would stack
|
|
1402
|
-
# them, and the second click would run the previous page's action as well.
|
|
1403
|
-
$script:BtnPrimary.Add_Click({
|
|
1404
|
-
switch ($script:Page) {
|
|
1405
|
-
0 { Show-Page -Index 1 }
|
|
1406
|
-
1 { $script:OptionError = ''; Show-Page -Index 2 }
|
|
1407
|
-
2 { Start-Installation }
|
|
1408
|
-
4 { [void](Start-Url -Url $GeminiAppsUrl) }
|
|
1409
|
-
}
|
|
1410
|
-
})
|
|
1411
|
-
$footer.Controls.Add($script:BtnPrimary)
|
|
1412
|
-
|
|
1413
|
-
# Enter activates the primary button, so the wizard can be driven from the
|
|
1414
|
-
# keyboard alone. On the progress page the button is hidden and the
|
|
1415
|
-
# dispatcher has no action for that page, so Enter is harmless there.
|
|
1416
|
-
$script:Form.AcceptButton = $script:BtnPrimary
|
|
1417
|
-
|
|
1418
|
-
$script:BtnBack = New-Object System.Windows.Forms.Button
|
|
1419
|
-
$script:BtnBack.Text = Get-UiText -Key 'btn_back'
|
|
1420
|
-
$script:BtnBack.Location = New-Object System.Drawing.Point(424, 14)
|
|
1421
|
-
$script:BtnBack.AutoSize = $true
|
|
1422
|
-
$script:BtnBack.AutoSizeMode = [System.Windows.Forms.AutoSizeMode]::GrowAndShrink
|
|
1423
|
-
$script:BtnBack.MinimumSize = New-Object System.Drawing.Size(118, 28)
|
|
1424
|
-
$script:BtnBack.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Right
|
|
1425
|
-
$script:BtnBack.Add_Click({
|
|
1426
|
-
if ($script:Page -gt 0 -and $script:Page -lt 3) {
|
|
1427
|
-
$script:OptionError = ''
|
|
1428
|
-
Show-Page -Index ($script:Page - 1)
|
|
1429
|
-
}
|
|
1430
|
-
})
|
|
1431
|
-
$footer.Controls.Add($script:BtnBack)
|
|
1432
|
-
|
|
1433
|
-
# Positioned explicitly rather than docked Fill: docking order depends on
|
|
1434
|
-
# z-order, and this form is fixed-size anyway.
|
|
1435
|
-
$script:Content = New-Object System.Windows.Forms.Panel
|
|
1436
|
-
$script:Content.Location = New-Object System.Drawing.Point(0, 56)
|
|
1437
|
-
$script:Content.Size = New-Object System.Drawing.Size(804, 508)
|
|
1438
|
-
$script:Content.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor `
|
|
1439
|
-
[System.Windows.Forms.AnchorStyles]::Bottom -bor `
|
|
1440
|
-
[System.Windows.Forms.AnchorStyles]::Left -bor `
|
|
1441
|
-
[System.Windows.Forms.AnchorStyles]::Right
|
|
1442
|
-
$script:Content.AutoScroll = $true
|
|
1443
|
-
$script:Content.BackColor = [System.Drawing.Color]::White
|
|
1444
|
-
$script:Form.Controls.Add($script:Content)
|
|
1445
|
-
|
|
1446
|
-
$script:Timer = New-Object System.Windows.Forms.Timer
|
|
1447
|
-
$script:Timer.Interval = 250
|
|
1448
|
-
$script:Timer.Add_Tick({ Update-Pipeline })
|
|
1449
|
-
|
|
1450
|
-
$script:Form.Add_FormClosing({
|
|
1451
|
-
param($sender, $eventArgs)
|
|
1452
|
-
if ($script:Pipe.Running) {
|
|
1453
|
-
$answer = [System.Windows.Forms.MessageBox]::Show(
|
|
1454
|
-
(Get-UiText -Key 'msg_cancel_confirm'),
|
|
1455
|
-
(Get-UiText -Key 'msg_cancel_title'),
|
|
1456
|
-
[System.Windows.Forms.MessageBoxButtons]::YesNo,
|
|
1457
|
-
[System.Windows.Forms.MessageBoxIcon]::Question)
|
|
1458
|
-
if ($answer -ne [System.Windows.Forms.DialogResult]::Yes) {
|
|
1459
|
-
$eventArgs.Cancel = $true
|
|
1460
|
-
return
|
|
1461
|
-
}
|
|
1462
|
-
Stop-Pipeline
|
|
1463
|
-
}
|
|
1464
|
-
})
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
# ------------------------------------------------------------------------------
|
|
1468
|
-
# Self-test
|
|
1469
|
-
# ------------------------------------------------------------------------------
|
|
1470
|
-
|
|
1471
|
-
$script:SelfTestResults = New-Object System.Collections.ArrayList
|
|
1472
|
-
|
|
1473
|
-
function Add-SelfTestResult {
|
|
1474
|
-
param([string]$Name, [bool]$Pass, [string]$Detail = '')
|
|
1475
|
-
[void]$script:SelfTestResults.Add([pscustomobject]@{ name = $Name; pass = $Pass; detail = $Detail })
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
function Invoke-SelfTest {
|
|
1479
|
-
$script:Headless = $true
|
|
1480
|
-
$script:SelfTestResults = New-Object System.Collections.ArrayList
|
|
1481
|
-
# The language the entry point actually settled on, before this self-test
|
|
1482
|
-
# starts switching languages for its own checks.
|
|
1483
|
-
$entryLang = $script:UiLang
|
|
1484
|
-
|
|
1485
|
-
# 1. the source must stay pure ASCII, or PowerShell 5.1 will not parse it
|
|
1486
|
-
# when the file is read with the ANSI code page
|
|
1487
|
-
$nonAscii = -1
|
|
1488
|
-
try {
|
|
1489
|
-
$bytes = [System.IO.File]::ReadAllBytes($ScriptPath)
|
|
1490
|
-
$nonAscii = 0
|
|
1491
|
-
foreach ($b in $bytes) { if ($b -gt 127) { $nonAscii++ } }
|
|
1492
|
-
} catch { $nonAscii = -1 }
|
|
1493
|
-
Add-SelfTestResult 'source_is_ascii' ($nonAscii -eq 0) "non_ascii_bytes=$nonAscii"
|
|
1494
|
-
|
|
1495
|
-
# 2. both languages carry exactly the same keys
|
|
1496
|
-
$enKeys = @($script:Strings.en.PSObject.Properties.Name | Sort-Object)
|
|
1497
|
-
$ruKeys = @($script:Strings.ru.PSObject.Properties.Name | Sort-Object)
|
|
1498
|
-
$missing = @($enKeys | Where-Object { $ruKeys -notcontains $_ })
|
|
1499
|
-
$extra = @($ruKeys | Where-Object { $enKeys -notcontains $_ })
|
|
1500
|
-
Add-SelfTestResult 'language_keys_match' (($missing.Count -eq 0) -and ($extra.Count -eq 0)) `
|
|
1501
|
-
("missing_in_ru=[" + ($missing -join ',') + "] extra_in_ru=[" + ($extra -join ',') + "]")
|
|
1502
|
-
|
|
1503
|
-
# 3. no key resolves to itself, which is what Get-UiText returns on a typo
|
|
1504
|
-
$unresolved = @()
|
|
1505
|
-
foreach ($key in $enKeys) {
|
|
1506
|
-
if ((Get-UiText -Key $key) -eq $key) { $unresolved += $key }
|
|
1507
|
-
}
|
|
1508
|
-
Add-SelfTestResult 'keys_resolve' ($unresolved.Count -eq 0) ("unresolved=[" + ($unresolved -join ',') + "]")
|
|
1509
|
-
|
|
1510
|
-
# 4. the tolerant reader decodes UTF-8 Russian rather than mangling it
|
|
1511
|
-
$probe = Join-Path (Get-ScratchDirectory) ('mesh-gui-selftest-' + [Guid]::NewGuid().ToString('N') + '.txt')
|
|
1512
|
-
$cyrillic = [string][char]0x041F + [char]0x0440 + [char]0x0438 + [char]0x0432 + [char]0x0435 + [char]0x0442
|
|
1513
|
-
try {
|
|
1514
|
-
[System.IO.File]::WriteAllText($probe, $cyrillic, (New-Object System.Text.UTF8Encoding($false)))
|
|
1515
|
-
$readBack = Read-TextFileTolerant -Path $probe
|
|
1516
|
-
Add-SelfTestResult 'tolerant_reader_utf8' ($readBack -eq $cyrillic) ("read='" + $readBack + "'")
|
|
1517
|
-
} catch {
|
|
1518
|
-
Add-SelfTestResult 'tolerant_reader_utf8' $false $_.Exception.Message
|
|
1519
|
-
} finally {
|
|
1520
|
-
if (Test-Path -LiteralPath $probe) { Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue }
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
# 5. the URL parser finds the URL in output shaped like install.ps1's box
|
|
1524
|
-
$boxOutput = ' +----------------------------------------------------------+' + "`n" +
|
|
1525
|
-
' | MCP SERVER URL (COPY THIS): |' + "`n" +
|
|
1526
|
-
' | |' + "`n" +
|
|
1527
|
-
' | https://mesh.example.test/sse?user=node-one&token=abc123 |' + "`n" +
|
|
1528
|
-
' | |' + "`n" +
|
|
1529
|
-
' | [OK] URL COPIED TO CLIPBOARD! |' + "`n" +
|
|
1530
|
-
' +----------------------------------------------------------+' + "`n"
|
|
1531
|
-
$parsed = Get-ResultUrl -Text $boxOutput -Mode 'custom'
|
|
1532
|
-
Add-SelfTestResult 'url_parser_box' `
|
|
1533
|
-
($parsed -eq 'https://mesh.example.test/sse?user=node-one&token=abc123') ("parsed='" + $parsed + "'")
|
|
1534
|
-
|
|
1535
|
-
$parsedStandalone = Get-ResultUrl -Text 'http://localhost:8096/sse' -Mode 'custom'
|
|
1536
|
-
Add-SelfTestResult 'url_parser_standalone' ($parsedStandalone -eq 'http://localhost:8096/sse') `
|
|
1537
|
-
("parsed='" + $parsedStandalone + "'")
|
|
1538
|
-
|
|
1539
|
-
# 6. the agent.env fallback rebuilds the canonical URL
|
|
1540
|
-
$envProbe = Join-Path (Get-ScratchDirectory) ('mesh-gui-selftest-env-' + [Guid]::NewGuid().ToString('N') + '.env')
|
|
1541
|
-
try {
|
|
1542
|
-
$envText = "MESH_GATEWAY=mesh.example.test`r`nMESH_USER=node-two`r`nMESH_TOKEN=tok456`r`n"
|
|
1543
|
-
[System.IO.File]::WriteAllText($envProbe, $envText, (New-Object System.Text.UTF8Encoding($false)))
|
|
1544
|
-
$fromEnv = Get-ResultUrl -Text 'no url in this text' -Mode 'custom' -AgentEnvPath $envProbe
|
|
1545
|
-
Add-SelfTestResult 'url_from_agent_env' `
|
|
1546
|
-
($fromEnv -eq 'https://mesh.example.test/sse?user=node-two&token=tok456') ("parsed='" + $fromEnv + "'")
|
|
1547
|
-
} catch {
|
|
1548
|
-
Add-SelfTestResult 'url_from_agent_env' $false $_.Exception.Message
|
|
1549
|
-
} finally {
|
|
1550
|
-
if (Test-Path -LiteralPath $envProbe) { Remove-Item -LiteralPath $envProbe -Force -ErrorAction SilentlyContinue }
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
# 7. command previews build for every mode (this also exercises New-SshSteps
|
|
1554
|
-
# without opening a single connection)
|
|
1555
|
-
$previewErrors = @()
|
|
1556
|
-
$savedMode = $script:Mode
|
|
1557
|
-
foreach ($mode in @('quick', 'custom', 'ssh')) {
|
|
1558
|
-
$script:Mode = $mode
|
|
1559
|
-
try {
|
|
1560
|
-
$preview = Get-CommandPreview -Mode $mode -Node 'node-x' -Domain 'mesh.example.test' `
|
|
1561
|
-
-Token 'tok' -Target 'user@host' -Port 22
|
|
1562
|
-
# -notmatch on an array filters rather than compares, and the filtered
|
|
1563
|
-
# result is truthy; collapse to one string first.
|
|
1564
|
-
$previewText = ($preview | Out-String)
|
|
1565
|
-
if (-not $previewText.Trim()) { $previewErrors += "${mode}: empty preview" }
|
|
1566
|
-
if ($mode -eq 'ssh' -and $previewText -notmatch 'install\.sh --quick --lang=') {
|
|
1567
|
-
$previewErrors += "${mode}: remote install command missing"
|
|
1568
|
-
}
|
|
1569
|
-
} catch {
|
|
1570
|
-
$previewErrors += "${mode}: $($_.Exception.Message)"
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
$script:Mode = $savedMode
|
|
1574
|
-
Add-SelfTestResult 'command_previews' ($previewErrors.Count -eq 0) ($previewErrors -join ' | ')
|
|
1575
|
-
|
|
1576
|
-
# 8. every page builds in every language and in every mode
|
|
1577
|
-
$script:Device = Get-DeviceInfo
|
|
1578
|
-
Initialize-Form
|
|
1579
|
-
$buildErrors = @()
|
|
1580
|
-
$savedLang = $script:UiLang
|
|
1581
|
-
foreach ($code in @('en', 'ru')) {
|
|
1582
|
-
$script:UiLang = $code
|
|
1583
|
-
foreach ($mode in @('quick', 'custom', 'ssh')) {
|
|
1584
|
-
$script:Mode = $mode
|
|
1585
|
-
foreach ($index in @(0, 1, 2, 3, 4)) {
|
|
1586
|
-
try {
|
|
1587
|
-
Show-Page -Index $index
|
|
1588
|
-
} catch {
|
|
1589
|
-
$buildErrors += "$code/${mode}/page${index}: $($_.Exception.Message)"
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
$script:UiLang = $savedLang
|
|
1595
|
-
$script:Mode = $savedMode
|
|
1596
|
-
Add-SelfTestResult 'pages_build' ($buildErrors.Count -eq 0) ($buildErrors -join ' | ')
|
|
1597
|
-
|
|
1598
|
-
# 9. the process pipeline really captures output and reports the exit code
|
|
1599
|
-
$probeStep = New-PipelineStep -File (Get-PowerShellExe) `
|
|
1600
|
-
-Arguments '-NoProfile -Command "Write-Host ''https://pipe.example.test/sse?user=probe&token=tk''"' `
|
|
1601
|
-
-Label 'selftest-success'
|
|
1602
|
-
$script:PipeResult = $null
|
|
1603
|
-
Reset-Pipeline -Steps @($probeStep) -OnFinish {
|
|
1604
|
-
param($failed, $exitCode, $output)
|
|
1605
|
-
$script:PipeResult = @{ Failed = $failed; Exit = $exitCode; Output = $output }
|
|
1606
|
-
}
|
|
1607
|
-
$deadline = (Get-Date).AddSeconds(60)
|
|
1608
|
-
while ($script:Pipe.Running -and (Get-Date) -lt $deadline) {
|
|
1609
|
-
Update-Pipeline
|
|
1610
|
-
Start-Sleep -Milliseconds 120
|
|
1611
|
-
}
|
|
1612
|
-
$pipeOk = $false
|
|
1613
|
-
$pipeDetail = 'pipeline did not finish'
|
|
1614
|
-
if ($script:PipeResult) {
|
|
1615
|
-
$captured = Get-ResultUrl -Text $script:PipeResult.Output -Mode 'custom'
|
|
1616
|
-
$pipeOk = (-not $script:PipeResult.Failed) -and ($captured -eq 'https://pipe.example.test/sse?user=probe&token=tk')
|
|
1617
|
-
$pipeDetail = "failed=$($script:PipeResult.Failed) exit=$($script:PipeResult.Exit) url='$captured'"
|
|
1618
|
-
}
|
|
1619
|
-
Add-SelfTestResult 'pipeline_captures_output' $pipeOk $pipeDetail
|
|
1620
|
-
|
|
1621
|
-
# 10. a non-zero exit is reported as a failure
|
|
1622
|
-
$failStep = New-PipelineStep -File (Get-PowerShellExe) -Arguments '-NoProfile -Command "exit 7"' -Label 'selftest-failure'
|
|
1623
|
-
$script:PipeResult = $null
|
|
1624
|
-
Reset-Pipeline -Steps @($failStep) -OnFinish {
|
|
1625
|
-
param($failed, $exitCode, $output)
|
|
1626
|
-
$script:PipeResult = @{ Failed = $failed; Exit = $exitCode; Output = $output }
|
|
1627
|
-
}
|
|
1628
|
-
$deadline = (Get-Date).AddSeconds(60)
|
|
1629
|
-
while ($script:Pipe.Running -and (Get-Date) -lt $deadline) {
|
|
1630
|
-
Update-Pipeline
|
|
1631
|
-
Start-Sleep -Milliseconds 120
|
|
1632
|
-
}
|
|
1633
|
-
$failOk = $false
|
|
1634
|
-
$failDetail = 'pipeline did not finish'
|
|
1635
|
-
if ($script:PipeResult) {
|
|
1636
|
-
$failOk = $script:PipeResult.Failed -and ($script:PipeResult.Exit -eq 7)
|
|
1637
|
-
$failDetail = "failed=$($script:PipeResult.Failed) exit=$($script:PipeResult.Exit)"
|
|
1638
|
-
}
|
|
1639
|
-
Add-SelfTestResult 'pipeline_reports_failure' $failOk $failDetail
|
|
1640
|
-
|
|
1641
|
-
# 11. the files the wizard depends on are where it expects them
|
|
1642
|
-
Add-SelfTestResult 'console_installer_present' (Test-Path -LiteralPath $ConsoleInstaller) $ConsoleInstaller
|
|
1643
|
-
Add-SelfTestResult 'shell_installer_present' (Test-Path -LiteralPath $ShellInstaller) $ShellInstaller
|
|
1644
|
-
|
|
1645
|
-
# 12. the dry-run parser reads install.ps1's six lines positionally, and an
|
|
1646
|
-
# older five-line copy still parses (the node row is simply empty)
|
|
1647
|
-
$dryRunText = @(
|
|
1648
|
-
'[DRY-RUN] Python : C:\Python312\python.exe',
|
|
1649
|
-
'[DRY-RUN] websockets : installed',
|
|
1650
|
-
'[DRY-RUN] Domain : mesh.example.test',
|
|
1651
|
-
'[DRY-RUN] Config : C:\Users\probe\.config\antigravity-mesh',
|
|
1652
|
-
'[DRY-RUN] Autostart : C:\Users\probe\Startup\antigravity-agent.vbs',
|
|
1653
|
-
'[DRY-RUN] Node : offline (probe)'
|
|
1654
|
-
) -join "`n"
|
|
1655
|
-
$facts = Get-PreflightFacts -Text $dryRunText
|
|
1656
|
-
$factsOk = ($facts.Python -eq 'C:\Python312\python.exe') -and
|
|
1657
|
-
($facts.Websockets -eq 'installed') -and
|
|
1658
|
-
($facts.Domain -eq 'mesh.example.test') -and
|
|
1659
|
-
($facts.Config -eq 'C:\Users\probe\.config\antigravity-mesh') -and
|
|
1660
|
-
($facts.Autostart -eq 'C:\Users\probe\Startup\antigravity-agent.vbs') -and
|
|
1661
|
-
($facts.Node -eq 'offline (probe)')
|
|
1662
|
-
$legacyText = (($dryRunText -split "`n")[0..4] -join "`n")
|
|
1663
|
-
$factsOk = $factsOk -and ((Get-PreflightFacts -Text $legacyText).Node -eq '')
|
|
1664
|
-
Add-SelfTestResult 'preflight_parser' $factsOk ("domain='" + $facts.Domain + "' python='" + $facts.Python + "'")
|
|
1665
|
-
|
|
1666
|
-
# 13. "not configured" is detected without depending on the language
|
|
1667
|
-
$domainOk = (-not (Test-DomainConfigured -Value 'NOT CONFIGURED')) -and
|
|
1668
|
-
(-not (Test-DomainConfigured -Value '')) -and
|
|
1669
|
-
(-not (Test-DomainConfigured -Value '__MESH_DOMAIN__')) -and
|
|
1670
|
-
(Test-DomainConfigured -Value 'mesh.example.test')
|
|
1671
|
-
Add-SelfTestResult 'domain_configured_detection' $domainOk ''
|
|
1672
|
-
|
|
1673
|
-
# 14. the window chrome follows a runtime language switch, not only the page
|
|
1674
|
-
$chromeErrors = @()
|
|
1675
|
-
$savedChromeLang = $script:UiLang
|
|
1676
|
-
foreach ($code in @('en', 'ru')) {
|
|
1677
|
-
$script:UiLang = $code
|
|
1678
|
-
Show-Page -Index 2
|
|
1679
|
-
if ($script:BtnBack.Text -ne (Get-UiText -Key 'btn_back')) {
|
|
1680
|
-
$chromeErrors += "${code}: back='" + $script:BtnBack.Text + "'"
|
|
1681
|
-
}
|
|
1682
|
-
if ($script:BtnClose.Text -ne (Get-UiText -Key 'btn_close')) {
|
|
1683
|
-
$chromeErrors += "${code}: close='" + $script:BtnClose.Text + "'"
|
|
1684
|
-
}
|
|
1685
|
-
if ($script:BtnPrimary.Text -ne (Get-UiText -Key 'btn_install')) {
|
|
1686
|
-
$chromeErrors += "${code}: install='" + $script:BtnPrimary.Text + "'"
|
|
1687
|
-
}
|
|
1688
|
-
if ($script:StepLabel.Text -notmatch [regex]::Escape((Get-UiText -Key 'page_options_title')) -and
|
|
1689
|
-
$script:StepLabel.Text -notmatch '\d') {
|
|
1690
|
-
$chromeErrors += "${code}: step='" + $script:StepLabel.Text + "'"
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
$script:UiLang = $savedChromeLang
|
|
1694
|
-
Add-SelfTestResult 'chrome_localised' ($chromeErrors.Count -eq 0) ($chromeErrors -join ' | ')
|
|
1695
|
-
|
|
1696
|
-
# 15. the result page must offer the Gemini address as a real link and a copy
|
|
1697
|
-
# button. It used to be a plain label, so it could be neither clicked nor
|
|
1698
|
-
# copied.
|
|
1699
|
-
$script:Install = @{
|
|
1700
|
-
Running = $false; Failed = $false; Exit = 0; Output = ''
|
|
1701
|
-
Url = 'https://mesh.example.test/sse?user=probe&token=tok'; Notice = ''; LinkNotice = ''
|
|
1702
|
-
}
|
|
1703
|
-
$script:Mode = 'quick'
|
|
1704
|
-
Show-Page -Index 4
|
|
1705
|
-
$linkFound = $false
|
|
1706
|
-
$copyButtonFound = $false
|
|
1707
|
-
$expectedCopy = Get-UiText -Key 'btn_copy_link'
|
|
1708
|
-
$pending = New-Object System.Collections.Stack
|
|
1709
|
-
foreach ($control in $script:Content.Controls) { $pending.Push($control) }
|
|
1710
|
-
while ($pending.Count -gt 0) {
|
|
1711
|
-
$control = $pending.Pop()
|
|
1712
|
-
if ($control -is [System.Windows.Forms.LinkLabel] -and $control.Text -eq $GeminiAppsUrl) {
|
|
1713
|
-
$linkFound = $true
|
|
1714
|
-
}
|
|
1715
|
-
if ($control -is [System.Windows.Forms.Button] -and $control.Text -eq $expectedCopy) {
|
|
1716
|
-
$copyButtonFound = $true
|
|
1717
|
-
}
|
|
1718
|
-
foreach ($child in $control.Controls) { $pending.Push($child) }
|
|
1719
|
-
}
|
|
1720
|
-
Add-SelfTestResult 'result_page_link' ($linkFound -and $copyButtonFound) `
|
|
1721
|
-
("link=$linkFound copyButton=$copyButtonFound")
|
|
1722
|
-
|
|
1723
|
-
# 16. no control may be wider than the container it sits in, in any language:
|
|
1724
|
-
# a fixed-width button clipped the Russian labels.
|
|
1725
|
-
$overflow = @()
|
|
1726
|
-
$savedOverflowLang = $script:UiLang
|
|
1727
|
-
foreach ($code in @('en', 'ru')) {
|
|
1728
|
-
$script:UiLang = $code
|
|
1729
|
-
foreach ($index in @(0, 1, 2, 4)) {
|
|
1730
|
-
Show-Page -Index $index
|
|
1731
|
-
$walk = New-Object System.Collections.Stack
|
|
1732
|
-
foreach ($control in $script:Content.Controls) { $walk.Push($control) }
|
|
1733
|
-
while ($walk.Count -gt 0) {
|
|
1734
|
-
$control = $walk.Pop()
|
|
1735
|
-
foreach ($child in $control.Controls) {
|
|
1736
|
-
if ($child.Right -gt $control.ClientSize.Width) {
|
|
1737
|
-
$overflow += ("{0}/page{1}: {2} right={3} > {4}" -f `
|
|
1738
|
-
$code, $index, $child.GetType().Name, $child.Right, $control.ClientSize.Width)
|
|
1739
|
-
}
|
|
1740
|
-
$walk.Push($child)
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
$script:UiLang = $savedOverflowLang
|
|
1746
|
-
Add-SelfTestResult 'no_control_overflow' ($overflow.Count -eq 0) (($overflow | Select-Object -First 5) -join ' | ')
|
|
1747
|
-
|
|
1748
|
-
# 17. the start language follows the user's language list. Checking
|
|
1749
|
-
# CurrentUICulture alone started this wizard in English on a Russian
|
|
1750
|
-
# Windows, because the process reported en-US while the display language
|
|
1751
|
-
# was ru-RU.
|
|
1752
|
-
$detectionErrors = @()
|
|
1753
|
-
if ((Select-Language -Codes @('ru-RU', 'en-US')) -ne 'ru') { $detectionErrors += 'ru-first' }
|
|
1754
|
-
if ((Select-Language -Codes @('en-US', 'ru-RU')) -ne 'en') { $detectionErrors += 'en-first' }
|
|
1755
|
-
if ((Select-Language -Codes @('ru')) -ne 'ru') { $detectionErrors += 'bare-ru' }
|
|
1756
|
-
if ((Select-Language -Codes @('de-DE')) -ne 'en') { $detectionErrors += 'unsupported' }
|
|
1757
|
-
if ((Select-Language -Codes @()) -ne 'en') { $detectionErrors += 'empty' }
|
|
1758
|
-
if ((Select-Language -Codes @('', 'ru-RU')) -ne 'ru') { $detectionErrors += 'blank-first' }
|
|
1759
|
-
$signals = @(Get-PreferredLanguageCodes)
|
|
1760
|
-
if ($signals.Count -eq 0) { $detectionErrors += 'no-signals' }
|
|
1761
|
-
Add-SelfTestResult 'language_detection' ($detectionErrors.Count -eq 0) `
|
|
1762
|
-
("detected=" + (Get-DefaultLanguage) + " effective=" + $entryLang +
|
|
1763
|
-
" signals=[" + ($signals -join ',') + "] " + ($detectionErrors -join '|'))
|
|
1764
|
-
|
|
1765
|
-
# 18. the pipeline must not depend on %TEMP% being writable
|
|
1766
|
-
$scratch = Get-ScratchDirectory
|
|
1767
|
-
$scratchOk = $false
|
|
1768
|
-
$scratchDetail = 'dir=' + $scratch
|
|
1769
|
-
try {
|
|
1770
|
-
$scratchProbe = Join-Path $scratch ('scratch-probe-' + [Guid]::NewGuid().ToString('N') + '.tmp')
|
|
1771
|
-
Set-Content -LiteralPath $scratchProbe -Value 'x' -Encoding ASCII -ErrorAction Stop
|
|
1772
|
-
Remove-Item -LiteralPath $scratchProbe -Force -ErrorAction Stop
|
|
1773
|
-
$scratchOk = $true
|
|
1774
|
-
} catch {
|
|
1775
|
-
$scratchDetail += ' error=' + $_.Exception.Message
|
|
1776
|
-
}
|
|
1777
|
-
Add-SelfTestResult 'scratch_directory' $scratchOk $scratchDetail
|
|
1778
|
-
|
|
1779
|
-
$failedCount = @($script:SelfTestResults | Where-Object { -not $_.pass }).Count
|
|
1780
|
-
return [pscustomobject]@{
|
|
1781
|
-
ok = ($failedCount -eq 0)
|
|
1782
|
-
passed = @($script:SelfTestResults | Where-Object { $_.pass }).Count
|
|
1783
|
-
failed = $failedCount
|
|
1784
|
-
results = $script:SelfTestResults
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
# ------------------------------------------------------------------------------
|
|
1789
|
-
# Main
|
|
1790
|
-
# ------------------------------------------------------------------------------
|
|
1791
|
-
|
|
1792
|
-
try {
|
|
1793
|
-
Initialize-Strings
|
|
1794
|
-
} catch {
|
|
1795
|
-
$message = $_.Exception.Message
|
|
1796
|
-
$text = $message
|
|
1797
|
-
if ($message.StartsWith('__STRINGS_MISSING__')) {
|
|
1798
|
-
$text = 'Language file not found: ' + $message.Substring(19)
|
|
1799
|
-
} elseif ($message.StartsWith('__STRINGS_INVALID__')) {
|
|
1800
|
-
$text = 'Language file is not valid JSON: ' + $message.Substring(19)
|
|
1801
|
-
} elseif ($message.StartsWith('__LANG_MISSING__')) {
|
|
1802
|
-
$text = 'Language is missing from the language file: ' + $message.Substring(16)
|
|
1803
|
-
}
|
|
1804
|
-
[Console]::Error.WriteLine($text)
|
|
1805
|
-
if (-not $SelfTest) {
|
|
1806
|
-
[void][System.Windows.Forms.MessageBox]::Show($text, 'Antigravity Mesh')
|
|
1807
|
-
}
|
|
1808
|
-
exit 1
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
if ($Lang) {
|
|
1812
|
-
# An explicit -Lang always wins over detection.
|
|
1813
|
-
$script:UiLang = $Lang
|
|
1814
|
-
} else {
|
|
1815
|
-
$script:UiLang = Get-DefaultLanguage
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
if ($SelfTest) {
|
|
1820
|
-
$report = Invoke-SelfTest
|
|
1821
|
-
$report | ConvertTo-Json -Depth 6
|
|
1822
|
-
if ($report.ok) { exit 0 }
|
|
1823
|
-
exit 1
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
if ($RunInstall) {
|
|
1827
|
-
$script:Mode = $InstallMode
|
|
1828
|
-
$script:Node = $InstallUser
|
|
1829
|
-
$script:Domain = $InstallGateway
|
|
1830
|
-
$script:Token = $InstallToken
|
|
1831
|
-
$script:SshTarget = $InstallSshTarget
|
|
1832
|
-
$script:SshPort = $InstallSshPort
|
|
1833
|
-
$script:Headless = $true
|
|
1834
|
-
|
|
1835
|
-
if (-not (Test-InstallOptions)) {
|
|
1836
|
-
Write-Output ('INSTALL_REFUSED ' + $script:OptionError)
|
|
1837
|
-
exit 2
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
if ($script:Mode -eq 'ssh') {
|
|
1841
|
-
$steps = New-SshSteps -Target "$($script:SshTarget)".Trim() -Port $script:SshPort `
|
|
1842
|
-
-Domain "$($script:Domain)".Trim()
|
|
1843
|
-
} else {
|
|
1844
|
-
$steps = New-ConsoleInstallerSteps -Mode $script:Mode -Node "$($script:Node)".Trim() `
|
|
1845
|
-
-Domain "$($script:Domain)".Trim() -Token "$($script:Token)".Trim()
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
$script:PipeResult = $null
|
|
1849
|
-
Reset-Pipeline -Steps $steps -OnFinish {
|
|
1850
|
-
param($failed, $exitCode, $output)
|
|
1851
|
-
$script:PipeResult = @{ Failed = $failed; Exit = $exitCode; Output = $output }
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
$deadline = (Get-Date).AddMinutes(15)
|
|
1855
|
-
while ($script:Pipe.Running -and (Get-Date) -lt $deadline) {
|
|
1856
|
-
Update-Pipeline
|
|
1857
|
-
Start-Sleep -Milliseconds 250
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
$failed = $true
|
|
1861
|
-
$code = 1
|
|
1862
|
-
$output = ''
|
|
1863
|
-
if ($script:PipeResult) {
|
|
1864
|
-
$failed = [bool]$script:PipeResult.Failed
|
|
1865
|
-
$code = $script:PipeResult.Exit
|
|
1866
|
-
$output = [string]$script:PipeResult.Output
|
|
1867
|
-
}
|
|
1868
|
-
Write-Output $output
|
|
1869
|
-
if ($failed) {
|
|
1870
|
-
Write-Output ('INSTALL_FAILED exit=' + $code)
|
|
1871
|
-
exit 1
|
|
1872
|
-
}
|
|
1873
|
-
$url = Get-ResultUrl -Text $output -Mode $script:Mode
|
|
1874
|
-
Write-Output ('MESH_URL=' + $url)
|
|
1875
|
-
exit 0
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
$script:Device = Get-DeviceInfo
|
|
1879
|
-
Initialize-Form
|
|
1880
|
-
Show-Page -Index 0
|
|
1881
|
-
Start-Preflight
|
|
1882
|
-
[void]$script:Form.ShowDialog()
|