gemini-computer-use 0.4.3 → 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 -2013
- 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.ps1
DELETED
|
@@ -1,1287 +0,0 @@
|
|
|
1
|
-
# ==============================================================================
|
|
2
|
-
# Antigravity Mesh - Windows PowerShell Universal Installer
|
|
3
|
-
# ==============================================================================
|
|
4
|
-
param(
|
|
5
|
-
[switch]$Quick,
|
|
6
|
-
[string]$Mode = "tunnel",
|
|
7
|
-
[string]$User = "",
|
|
8
|
-
[string]$Token = "",
|
|
9
|
-
# Empty on purpose: the shared domain is resolved from the one source of truth
|
|
10
|
-
# (see the PUBLIC DOMAIN block below), never from a literal in this file.
|
|
11
|
-
[string]$Gateway = "",
|
|
12
|
-
[int]$Port = 8096,
|
|
13
|
-
[string]$Lang = "en",
|
|
14
|
-
[switch]$DryRun
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
# Ensure UTF-8 output encoding in PowerShell console
|
|
18
|
-
try {
|
|
19
|
-
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
20
|
-
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
21
|
-
} catch {}
|
|
22
|
-
|
|
23
|
-
$ErrorActionPreference = "Continue"
|
|
24
|
-
if (Test-Path variable:global:PSNativeCommandUseErrorActionPreference) {
|
|
25
|
-
$global:PSNativeCommandUseErrorActionPreference = $false
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
$Hostname = $env:COMPUTERNAME.ToLower() -replace '[^a-z0-9_-]', ''
|
|
29
|
-
$IsLaptop = [bool](Get-CimInstance -ClassName Win32_Battery -ErrorAction SilentlyContinue)
|
|
30
|
-
|
|
31
|
-
if ($Lang -eq "ru") {
|
|
32
|
-
$DetectedType = if ($IsLaptop) { "Windows Ноутбук (Laptop)" } else { "Windows Десктоп / Сервер" }
|
|
33
|
-
Write-Host "+----------------------------------------------------------------------+" -ForegroundColor Cyan
|
|
34
|
-
Write-Host "| [*] Обнаружено устройство: |" -ForegroundColor Cyan
|
|
35
|
-
Write-Host "| * Имя хоста : $Hostname" -ForegroundColor Green
|
|
36
|
-
Write-Host "| * Тип : $DetectedType" -ForegroundColor Yellow
|
|
37
|
-
Write-Host "| * ОС : Windows $([System.Environment]::OSVersion.Version)" -ForegroundColor White
|
|
38
|
-
Write-Host "+----------------------------------------------------------------------+" -ForegroundColor Cyan
|
|
39
|
-
} else {
|
|
40
|
-
$DetectedType = if ($IsLaptop) { "Windows Laptop" } else { "Windows Desktop / Server" }
|
|
41
|
-
Write-Host "+----------------------------------------------------------------------+" -ForegroundColor Cyan
|
|
42
|
-
Write-Host "| [*] Detected Device: |" -ForegroundColor Cyan
|
|
43
|
-
Write-Host "| * Hostname : $Hostname" -ForegroundColor Green
|
|
44
|
-
Write-Host "| * Type : $DetectedType" -ForegroundColor Yellow
|
|
45
|
-
Write-Host "| * OS : Windows $([System.Environment]::OSVersion.Version)" -ForegroundColor White
|
|
46
|
-
Write-Host "+----------------------------------------------------------------------+" -ForegroundColor Cyan
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
# The dry run is handled after the (read-only) interpreter detection below: a
|
|
50
|
-
# preflight that exits before checking anything cannot honestly report that the
|
|
51
|
-
# dependencies were verified.
|
|
52
|
-
|
|
53
|
-
# Python check & auto-install.
|
|
54
|
-
# Windows ships a "python.exe" App Execution Alias under
|
|
55
|
-
# %LOCALAPPDATA%\Microsoft\WindowsApps that runs Python only while the Microsoft
|
|
56
|
-
# Store package stays healthy: it can be on PATH, print nothing usable and exit
|
|
57
|
-
# non-zero, and it stops working silently after a Store repair or an update. A
|
|
58
|
-
# node whose autostart pins that alias never comes back after a reboot - the only
|
|
59
|
-
# trace is a truncated "Python " line in agent.log - so an alias is never
|
|
60
|
-
# accepted here. Every candidate must be a real interpreter outside WindowsApps
|
|
61
|
-
# that prints 3 for sys.version_info[0] and whose own sys.executable does not
|
|
62
|
-
# resolve back into WindowsApps.
|
|
63
|
-
$WindowsAppsMarker = "\WindowsApps\"
|
|
64
|
-
|
|
65
|
-
# ------------------------------------------------------------------------------
|
|
66
|
-
# ARCHITECTURE - which build of Python this machine actually needs
|
|
67
|
-
#
|
|
68
|
-
# This installer used to download python-3.12.5-amd64.exe unconditionally, which
|
|
69
|
-
# is wrong in two measurable ways:
|
|
70
|
-
#
|
|
71
|
-
# * on 32-bit Windows the amd64 installer refuses to run at all, so the
|
|
72
|
-
# automatic step failed and the operator was told to install Python by hand;
|
|
73
|
-
# * on ARM64 Windows it installs an x64 build that only works under emulation,
|
|
74
|
-
# which is slower and breaks as soon as a dependency has no emulated wheel.
|
|
75
|
-
#
|
|
76
|
-
# RuntimeInformation.OSArchitecture is asked FIRST because it reports the native
|
|
77
|
-
# OS architecture even when this PowerShell is itself emulated. The environment
|
|
78
|
-
# variables only describe the process: a 32-bit PowerShell on 64-bit Windows
|
|
79
|
-
# reports PROCESSOR_ARCHITECTURE=x86 and hides the truth in PROCESSOR_ARCHITEW6432.
|
|
80
|
-
# Both are read so the answer still works on a .NET runtime without
|
|
81
|
-
# RuntimeInformation (it needs 4.7.1+).
|
|
82
|
-
# ------------------------------------------------------------------------------
|
|
83
|
-
function Get-OsArchitecture {
|
|
84
|
-
$raw = ""
|
|
85
|
-
try { $raw = "$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)" } catch {}
|
|
86
|
-
if (-not $raw) {
|
|
87
|
-
if ($env:PROCESSOR_ARCHITEW6432) { $raw = $env:PROCESSOR_ARCHITEW6432 }
|
|
88
|
-
elseif ($env:PROCESSOR_ARCHITECTURE) { $raw = $env:PROCESSOR_ARCHITECTURE }
|
|
89
|
-
}
|
|
90
|
-
switch -Regex ("$raw".Trim().ToUpperInvariant()) {
|
|
91
|
-
# PyOrgSuffix is the python.org file-name suffix; UvTarget is the
|
|
92
|
-
# astral-sh/uv release asset. uv ships no 32-bit Windows build, so that
|
|
93
|
-
# column stays empty for x86 and the chain simply skips it there.
|
|
94
|
-
'^(AMD64|X64)$' { return @{ Key='amd64'; Label='x64'; PyOrgSuffix='-amd64'; UvTarget='x86_64-pc-windows-msvc'; Supported=$true } }
|
|
95
|
-
'^(ARM64)$' { return @{ Key='arm64'; Label='ARM64'; PyOrgSuffix='-arm64'; UvTarget='aarch64-pc-windows-msvc'; Supported=$true } }
|
|
96
|
-
'^(X86|I386|I686)$' { return @{ Key='x86'; Label='x86 (32-bit)'; PyOrgSuffix=''; UvTarget=$null; Supported=$true } }
|
|
97
|
-
default { return @{ Key='unknown'; Label="$raw"; PyOrgSuffix=$null; UvTarget=$null; Supported=$false } }
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
$OsArch = Get-OsArchitecture
|
|
102
|
-
|
|
103
|
-
# The exact CPython the direct-download fallback installs. Kept in one place so the
|
|
104
|
-
# URL, the cached file name and the version can never drift apart.
|
|
105
|
-
$PyOrgVersion = '3.12.5'
|
|
106
|
-
|
|
107
|
-
# uv is the last resort: one static binary that can fetch its own CPython and
|
|
108
|
-
# install packages without pip. That is what makes it useful on a machine where
|
|
109
|
-
# winget is blocked and pip is missing or refuses to write (PEP 668).
|
|
110
|
-
$UvDir = "$env:LOCALAPPDATA\uv"
|
|
111
|
-
$UvExe = $null
|
|
112
|
-
|
|
113
|
-
function Get-PyOrgInstallerName {
|
|
114
|
-
param($Arch)
|
|
115
|
-
if (-not $Arch -or -not $Arch.Supported -or $null -eq $Arch.PyOrgSuffix) { return "" }
|
|
116
|
-
return "python-$PyOrgVersion$($Arch.PyOrgSuffix).exe"
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
# A failed download usually leaves an HTML error page on disk, and running that as
|
|
120
|
-
# an installer produces a bare "not a valid Win32 application" with no hint of the
|
|
121
|
-
# real cause. Checking the DOS 'MZ' magic keeps the diagnosis inside this script.
|
|
122
|
-
# The size floor only has to separate a real binary from an error page, so it stays
|
|
123
|
-
# small: a large floor would reject a perfectly valid small executable and turn this
|
|
124
|
-
# into an "is it big" test rather than an "is it a PE" test.
|
|
125
|
-
function Test-PeExecutable {
|
|
126
|
-
param([string]$Path)
|
|
127
|
-
if (-not $Path -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false }
|
|
128
|
-
try {
|
|
129
|
-
if ((Get-Item -LiteralPath $Path).Length -lt 64KB) { return $false }
|
|
130
|
-
# Two bytes through the stream API: "Get-Content -Encoding Byte" is
|
|
131
|
-
# PowerShell 5.1 only, and this file also runs under pwsh 7.
|
|
132
|
-
$fs = [System.IO.File]::OpenRead($Path)
|
|
133
|
-
try { $b0 = $fs.ReadByte(); $b1 = $fs.ReadByte() } finally { $fs.Dispose() }
|
|
134
|
-
return ($b0 -eq 0x4D -and $b1 -eq 0x5A)
|
|
135
|
-
} catch { return $false }
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function Test-RealPython {
|
|
139
|
-
param([string]$Path)
|
|
140
|
-
if (-not $Path) { return $false }
|
|
141
|
-
if ($Path -like "*$WindowsAppsMarker*") { return $false }
|
|
142
|
-
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false }
|
|
143
|
-
try {
|
|
144
|
-
# Two answers in one probe: the major version, and the interpreter the
|
|
145
|
-
# alias would actually run (an alias reports a WindowsApps path there).
|
|
146
|
-
$probe = @(& $Path -c "import sys; print(sys.version_info[0]); print(sys.executable)" 2>&1)
|
|
147
|
-
if ($LASTEXITCODE -ne 0) { return $false }
|
|
148
|
-
$rows = @($probe | ForEach-Object { "$_".Trim() } | Where-Object { $_ })
|
|
149
|
-
if ($rows.Count -lt 1 -or "$($rows[0])" -ne "3") { return $false }
|
|
150
|
-
if ($rows.Count -ge 2 -and "$($rows[1])" -like "*$WindowsAppsMarker*") { return $false }
|
|
151
|
-
return $true
|
|
152
|
-
} catch { return $false }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function Test-PythonWebsockets {
|
|
156
|
-
param([string]$Path)
|
|
157
|
-
try {
|
|
158
|
-
& $Path -c "import websockets" 2>$null
|
|
159
|
-
return ($LASTEXITCODE -eq 0)
|
|
160
|
-
} catch { return $false }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
# ------------------------------------------------------------------------------
|
|
164
|
-
# AUTOMATIC PYTHON INSTALLATION - three sources, tried in that order
|
|
165
|
-
#
|
|
166
|
-
# winget first, because it is the platform's own mechanism and picks the correct
|
|
167
|
-
# architecture by itself; the python.org download second, because it also works
|
|
168
|
-
# where winget is missing, blocked by policy or has no configured source; uv last,
|
|
169
|
-
# because it is the only one of the three that needs neither an installer nor a
|
|
170
|
-
# working pip.
|
|
171
|
-
#
|
|
172
|
-
# Every step re-resolves the interpreter with Select-RealPython instead of trusting
|
|
173
|
-
# the installer's exit code. A silent per-user install, a queued Store update and
|
|
174
|
-
# an "already installed" no-op all report success, and only a probe tells them
|
|
175
|
-
# apart - which is exactly the failure this chain exists to remove.
|
|
176
|
-
# ------------------------------------------------------------------------------
|
|
177
|
-
|
|
178
|
-
function Install-PythonFromPythonOrg {
|
|
179
|
-
param($Arch)
|
|
180
|
-
$name = Get-PyOrgInstallerName $Arch
|
|
181
|
-
if (-not $name) { return $false }
|
|
182
|
-
$url = "https://www.python.org/ftp/python/$PyOrgVersion/$name"
|
|
183
|
-
$installer = "$env:TEMP\$name"
|
|
184
|
-
# Reuse an earlier good download: this is the same ~25 MB file on every re-run,
|
|
185
|
-
# and a half-written .exe from an interrupted attempt is rejected by the MZ
|
|
186
|
-
# check rather than executed.
|
|
187
|
-
if (-not (Test-PeExecutable $installer)) {
|
|
188
|
-
try { Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing -ErrorAction Stop } catch { return $false }
|
|
189
|
-
}
|
|
190
|
-
if (-not (Test-PeExecutable $installer)) { return $false }
|
|
191
|
-
try {
|
|
192
|
-
# InstallAllUsers=0 keeps this per-user and therefore elevation-free.
|
|
193
|
-
# Include_pip=1 is explicit so the dependency step always has a pip.
|
|
194
|
-
$proc = Start-Process -FilePath $installer `
|
|
195
|
-
-ArgumentList '/quiet InstallAllUsers=0 PrependPath=1 Include_test=0 Include_pip=1' `
|
|
196
|
-
-Wait -PassThru -ErrorAction Stop
|
|
197
|
-
# 3010 is "success, reboot required" and is a success here.
|
|
198
|
-
return ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq 3010)
|
|
199
|
-
} catch { return $false }
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function Install-UvWindows {
|
|
203
|
-
# Returns the path to uv.exe, or $null. The release archive is fetched and
|
|
204
|
-
# unpacked here rather than piping astral.sh's install script into iex: a script
|
|
205
|
-
# that arrives over the network and is executed unread is a worse default than
|
|
206
|
-
# an archive this function unpacks itself.
|
|
207
|
-
param($Arch)
|
|
208
|
-
if (-not $Arch -or -not $Arch.UvTarget) { return $null }
|
|
209
|
-
$exe = Join-Path $UvDir 'uv.exe'
|
|
210
|
-
if (Test-Path -LiteralPath $exe) { return $exe }
|
|
211
|
-
$zip = Join-Path $env:TEMP "uv-$($Arch.Key).zip"
|
|
212
|
-
try {
|
|
213
|
-
Invoke-WebRequest -Uri "https://github.com/astral-sh/uv/releases/latest/download/uv-$($Arch.UvTarget).zip" `
|
|
214
|
-
-OutFile $zip -UseBasicParsing -ErrorAction Stop
|
|
215
|
-
if (!(Test-Path $UvDir)) { New-Item -ItemType Directory -Path $UvDir -Force | Out-Null }
|
|
216
|
-
# The asset carries uv.exe, uvw.exe and uvx.exe at its top level.
|
|
217
|
-
Expand-Archive -LiteralPath $zip -DestinationPath $UvDir -Force -ErrorAction Stop
|
|
218
|
-
} catch { return $null }
|
|
219
|
-
if (Test-Path -LiteralPath $exe) { return $exe }
|
|
220
|
-
return $null
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function Install-PythonViaUv {
|
|
224
|
-
# uv downloads a standalone CPython into its own directory, so no installer,
|
|
225
|
-
# no elevation and no writable system Python is involved. Get-PythonCandidates
|
|
226
|
-
# knows that directory, which is how the freshly fetched interpreter is found
|
|
227
|
-
# by the ordinary resolution path.
|
|
228
|
-
param([string]$Uv)
|
|
229
|
-
if (-not $Uv) { return $false }
|
|
230
|
-
try {
|
|
231
|
-
& $Uv python install $PyOrgVersion 2>&1 | Out-Null
|
|
232
|
-
return $true
|
|
233
|
-
} catch { return $false }
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
# ------------------------------------------------------------------------------
|
|
237
|
-
# DEPENDENCY INSTALLATION - websockets is the node's only external requirement
|
|
238
|
-
#
|
|
239
|
-
# core/mcp_tools.py and core/server.py are standard library only, so this one
|
|
240
|
-
# package is the entire dependency surface. It is installed through five different
|
|
241
|
-
# mechanisms because each fails for a different reason on a locked-down machine:
|
|
242
|
-
# a plain pip needs a writable site-packages, --user needs a writable profile,
|
|
243
|
-
# ensurepip needs an interpreter that shipped without pip, uv needs neither pip nor
|
|
244
|
-
# a writable Python directory, and a venv needs only the interpreter itself.
|
|
245
|
-
#
|
|
246
|
-
# Returns the interpreter that actually ended up with the package. That return
|
|
247
|
-
# value matters: when only the venv fallback works, the pinned autostart
|
|
248
|
-
# interpreter must BECOME the venv's python.exe, or the node autostarts with an
|
|
249
|
-
# interpreter that cannot import websockets.
|
|
250
|
-
# ------------------------------------------------------------------------------
|
|
251
|
-
function Install-WebsocketsFor {
|
|
252
|
-
param([string]$Python, [string]$Uv)
|
|
253
|
-
|
|
254
|
-
if (Test-PythonWebsockets $Python) { return @{ Ok = $true; Python = $Python } }
|
|
255
|
-
|
|
256
|
-
# 1. plain pip, after upgrading it: an old pip cannot find or build wheels.
|
|
257
|
-
& $Python -m pip install --disable-pip-version-check --quiet --upgrade pip 2>&1 | Out-Null
|
|
258
|
-
& $Python -m pip install --disable-pip-version-check --quiet websockets 2>&1 | Out-Null
|
|
259
|
-
if (Test-PythonWebsockets $Python) { return @{ Ok = $true; Python = $Python } }
|
|
260
|
-
|
|
261
|
-
# 2. --user: site-packages is read-only, the profile is not.
|
|
262
|
-
& $Python -m pip install --disable-pip-version-check --quiet --user websockets 2>&1 | Out-Null
|
|
263
|
-
if (Test-PythonWebsockets $Python) { return @{ Ok = $true; Python = $Python } }
|
|
264
|
-
|
|
265
|
-
# 3. ensurepip: a per-user install lands without pip when the feature set was
|
|
266
|
-
# trimmed, or when the launcher is disabled by policy.
|
|
267
|
-
& $Python -m ensurepip --default-pip 2>&1 | Out-Null
|
|
268
|
-
& $Python -m pip install --disable-pip-version-check --quiet websockets 2>&1 | Out-Null
|
|
269
|
-
if (Test-PythonWebsockets $Python) { return @{ Ok = $true; Python = $Python } }
|
|
270
|
-
|
|
271
|
-
# 4. uv: installs into a managed environment without needing pip at all.
|
|
272
|
-
if ($Uv) {
|
|
273
|
-
& $Uv pip install --python $Python websockets 2>&1 | Out-Null
|
|
274
|
-
if (Test-PythonWebsockets $Python) { return @{ Ok = $true; Python = $Python } }
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
# 5. venv: the only fallback that needs no write access to the system Python.
|
|
278
|
-
$venvDir = "$env:USERPROFILE\.config\antigravity-mesh\venv"
|
|
279
|
-
$venvPython = "$venvDir\Scripts\python.exe"
|
|
280
|
-
if (-not (Test-Path -LiteralPath $venvPython)) { & $Python -m venv $venvDir 2>&1 | Out-Null }
|
|
281
|
-
if (Test-Path -LiteralPath $venvPython) {
|
|
282
|
-
& $venvPython -m pip install --disable-pip-version-check --quiet --upgrade pip 2>&1 | Out-Null
|
|
283
|
-
& $venvPython -m pip install --disable-pip-version-check --quiet websockets 2>&1 | Out-Null
|
|
284
|
-
if (Test-PythonWebsockets $venvPython) { return @{ Ok = $true; Python = $venvPython } }
|
|
285
|
-
if ($Uv) {
|
|
286
|
-
& $Uv pip install --python $venvPython websockets 2>&1 | Out-Null
|
|
287
|
-
if (Test-PythonWebsockets $venvPython) { return @{ Ok = $true; Python = $venvPython } }
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return @{ Ok = $false; Python = $Python }
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function Get-PythonCandidates {
|
|
294
|
-
$list = @()
|
|
295
|
-
# Every "python" on PATH, not just the first: the first hit is usually the
|
|
296
|
-
# Store alias while a real install sits further down the same PATH.
|
|
297
|
-
foreach ($cmd in @(Get-Command python -All -ErrorAction SilentlyContinue)) {
|
|
298
|
-
if ($cmd -and $cmd.Source) { $list += $cmd.Source }
|
|
299
|
-
}
|
|
300
|
-
$list += @(
|
|
301
|
-
"$env:LOCALAPPDATA\Programs\Python\Python313\python.exe",
|
|
302
|
-
"$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
|
|
303
|
-
"$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
|
|
304
|
-
"$env:ProgramFiles\Python313\python.exe",
|
|
305
|
-
"$env:ProgramFiles\Python312\python.exe",
|
|
306
|
-
"$env:ProgramFiles\Python311\python.exe"
|
|
307
|
-
)
|
|
308
|
-
# Any per-user 3.x the python.org installer left behind, newest first. This
|
|
309
|
-
# used to be a second, separate fallback that forgot to record the path it
|
|
310
|
-
# found, so a machine with only 3.13 was pushed into a fresh download.
|
|
311
|
-
$localRoot = "$env:LOCALAPPDATA\Programs\Python"
|
|
312
|
-
if (Test-Path -LiteralPath $localRoot) {
|
|
313
|
-
$list += @(Get-ChildItem -Path $localRoot -Directory -Filter 'Python3*' -ErrorAction SilentlyContinue |
|
|
314
|
-
Sort-Object Name -Descending |
|
|
315
|
-
ForEach-Object { Join-Path $_.FullName 'python.exe' })
|
|
316
|
-
}
|
|
317
|
-
# The uv-managed CPython, for the case where the uv fallback is what produced
|
|
318
|
-
# an interpreter: it lives outside PATH and outside the python.org directories,
|
|
319
|
-
# so without this the freshly fetched Python would be installed and then never
|
|
320
|
-
# found again. UV_PYTHON_INSTALL_DIR is honoured because uv lets a user move it.
|
|
321
|
-
$uvRoots = @("$env:APPDATA\uv\python")
|
|
322
|
-
if ($env:UV_PYTHON_INSTALL_DIR) { $uvRoots += $env:UV_PYTHON_INSTALL_DIR }
|
|
323
|
-
foreach ($uvRoot in $uvRoots) {
|
|
324
|
-
if (Test-Path -LiteralPath $uvRoot) {
|
|
325
|
-
$list += @(Get-ChildItem -Path $uvRoot -Directory -Filter 'cpython-3*' -ErrorAction SilentlyContinue |
|
|
326
|
-
Sort-Object Name -Descending |
|
|
327
|
-
ForEach-Object { Join-Path $_.FullName 'python.exe' })
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
# The "py" launcher is not an interpreter: ask it which real python.exe it
|
|
331
|
-
# runs, so the autostart entry always names a python.exe, never "py -3".
|
|
332
|
-
$cmdPy = Get-Command py -ErrorAction SilentlyContinue
|
|
333
|
-
if ($cmdPy -and $cmdPy.Source) {
|
|
334
|
-
try {
|
|
335
|
-
$real = @(& $cmdPy.Source -3 -c "import sys; print(sys.executable)" 2>$null)
|
|
336
|
-
if ($LASTEXITCODE -eq 0 -and $real.Count -ge 1 -and "$($real[0])".Trim()) {
|
|
337
|
-
$list += "$($real[0])".Trim()
|
|
338
|
-
}
|
|
339
|
-
} catch {}
|
|
340
|
-
}
|
|
341
|
-
return @($list | Where-Object { $_ } | Select-Object -Unique)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function Select-RealPython {
|
|
345
|
-
# An interpreter that already has websockets wins: the dependency step then
|
|
346
|
-
# cannot silently install into a different, more fragile Python.
|
|
347
|
-
$fallback = $null
|
|
348
|
-
foreach ($cand in (Get-PythonCandidates)) {
|
|
349
|
-
if (-not (Test-RealPython $cand)) { continue }
|
|
350
|
-
if (Test-PythonWebsockets $cand) { return $cand }
|
|
351
|
-
if (-not $fallback) { $fallback = $cand }
|
|
352
|
-
}
|
|
353
|
-
return $fallback
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function Add-PythonToPath {
|
|
357
|
-
param([string]$Path)
|
|
358
|
-
if (-not $Path) { return }
|
|
359
|
-
$dir = Split-Path -Parent $Path
|
|
360
|
-
if ($dir -and ($env:Path -notlike "*$dir*")) {
|
|
361
|
-
$env:Path = "$dir;$dir\Scripts;$env:Path"
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
$hasPython = $false
|
|
366
|
-
$PyExe = $null
|
|
367
|
-
$PyExe = Select-RealPython
|
|
368
|
-
if ($PyExe) {
|
|
369
|
-
Add-PythonToPath $PyExe
|
|
370
|
-
$hasPython = $true
|
|
371
|
-
$pyVer = & $PyExe --version 2>&1
|
|
372
|
-
if ($Lang -eq "ru") {
|
|
373
|
-
Write-Host "[1/3] Python обнаружен: $pyVer ($PyExe)" -ForegroundColor Green
|
|
374
|
-
} else {
|
|
375
|
-
Write-Host "[1/3] Python detected: $pyVer ($PyExe)" -ForegroundColor Green
|
|
376
|
-
}
|
|
377
|
-
if (-not (Test-PythonWebsockets $PyExe)) {
|
|
378
|
-
if ($Lang -eq "ru") {
|
|
379
|
-
Write-Host "[1/3] В выбранном Python нет websockets - он будет установлен на шаге 2." -ForegroundColor DarkYellow
|
|
380
|
-
} else {
|
|
381
|
-
Write-Host "[1/3] The selected Python has no websockets yet - it will be installed in step 2." -ForegroundColor DarkYellow
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
} elseif ($Lang -eq "ru") {
|
|
385
|
-
Write-Host "[1/3] Рабочий Python 3 не найден (ярлык Microsoft Store за интерпретатор не считается)." -ForegroundColor Yellow
|
|
386
|
-
} else {
|
|
387
|
-
Write-Host "[1/3] No working Python 3 found (the Microsoft Store alias does not count)." -ForegroundColor Yellow
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
# ------------------------------------------------------------------------------
|
|
392
|
-
# PUBLIC DOMAIN - resolved from the one source of truth
|
|
393
|
-
#
|
|
394
|
-
# The domain is declared exactly once, in core/domain.py. This block asks that
|
|
395
|
-
# same chain, in the same order, and it is the ONLY place in this script that
|
|
396
|
-
# picks a hostname:
|
|
397
|
-
#
|
|
398
|
-
# 1. -Gateway on the command line;
|
|
399
|
-
# 2. $env:MESH_PUBLIC_URL;
|
|
400
|
-
# 3. $env:AGY_PUBLIC_BASE_URL (legacy alias, same meaning);
|
|
401
|
-
# 4. the domain file - $env:MESH_DOMAIN_FILE when set, otherwise
|
|
402
|
-
# %USERPROFILE%\.config\antigravity-mesh\domain.env;
|
|
403
|
-
# 5. $PublishedDomain - the domain this copy was published with. The repository
|
|
404
|
-
# carries the placeholder __MESH_DOMAIN__ there and deploy_gateway.sh
|
|
405
|
-
# rewrites that line while publishing, so a served copy knows its domain;
|
|
406
|
-
# 6. core/domain.py's DEFAULT_PUBLIC_BASE_URL, read through the interpreter
|
|
407
|
-
# found above (a repository checkout, or a node that bootstrapped already).
|
|
408
|
-
#
|
|
409
|
-
# If none of them yields a real hostname the installer stops. A node registered
|
|
410
|
-
# against a domain that does not resolve can never connect, and its autostart
|
|
411
|
-
# entry would retry that dead endpoint forever.
|
|
412
|
-
# ------------------------------------------------------------------------------
|
|
413
|
-
$DomainPlaceholder = "__MESH_DOMAIN__"
|
|
414
|
-
# deploy_gateway.sh rewrites THIS line only (anchored on $PublishedDomain), so the
|
|
415
|
-
# placeholder above survives as the "this copy was not published" marker.
|
|
416
|
-
$PublishedDomain = "__MESH_DOMAIN__"
|
|
417
|
-
$ScriptRoot = $null
|
|
418
|
-
if ($MyInvocation.MyCommand -and $MyInvocation.MyCommand.Path) {
|
|
419
|
-
$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function Get-NormalisedHost([string]$Value) {
|
|
423
|
-
if ([string]::IsNullOrWhiteSpace($Value)) { return "" }
|
|
424
|
-
$text = $Value.Trim().Trim('"').Trim("'")
|
|
425
|
-
$text = $text -replace '^[A-Za-z][A-Za-z0-9+.-]*://', ''
|
|
426
|
-
$text = $text.Split('/')[0]
|
|
427
|
-
return $text.Trim()
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function Get-DomainFileValue([string]$Path) {
|
|
431
|
-
if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { return "" }
|
|
432
|
-
$values = @{}
|
|
433
|
-
try {
|
|
434
|
-
# -Encoding UTF8 also absorbs the BOM that PowerShell itself writes.
|
|
435
|
-
foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8)) {
|
|
436
|
-
$trimmed = $line.Trim()
|
|
437
|
-
if (-not $trimmed -or $trimmed.StartsWith('#') -or -not $trimmed.Contains('=')) { continue }
|
|
438
|
-
$index = $trimmed.IndexOf('=')
|
|
439
|
-
$key = $trimmed.Substring(0, $index).Trim()
|
|
440
|
-
$value = $trimmed.Substring($index + 1).Trim().Trim('"').Trim("'")
|
|
441
|
-
if ($key) { $values[$key] = $value }
|
|
442
|
-
}
|
|
443
|
-
} catch { return "" }
|
|
444
|
-
if ($values['MESH_PUBLIC_URL']) { return $values['MESH_PUBLIC_URL'] }
|
|
445
|
-
if ($values['AGY_PUBLIC_BASE_URL']) { return $values['AGY_PUBLIC_BASE_URL'] }
|
|
446
|
-
return ""
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function Get-ModuleDefaultDomain {
|
|
450
|
-
if (-not $ScriptRoot) { return "" }
|
|
451
|
-
$module = Join-Path $ScriptRoot 'core\domain.py'
|
|
452
|
-
if (-not (Test-Path -LiteralPath $module)) { return "" }
|
|
453
|
-
$value = ""
|
|
454
|
-
if ($PyExe) {
|
|
455
|
-
$interpreter = ($PyExe -split ' ')[0]
|
|
456
|
-
$probe = "import sys; sys.path.insert(0, r'$ScriptRoot'); from core import domain; print(domain.DEFAULT_PUBLIC_BASE_URL)"
|
|
457
|
-
try { $value = & $interpreter -c $probe 2>$null } catch { $value = "" }
|
|
458
|
-
}
|
|
459
|
-
if (-not $value) {
|
|
460
|
-
$match = Select-String -LiteralPath $module -Pattern '^DEFAULT_PUBLIC_BASE_URL\s*=\s*"([^"]+)"' |
|
|
461
|
-
Select-Object -First 1
|
|
462
|
-
if ($match) { $value = $match.Matches[0].Groups[1].Value }
|
|
463
|
-
}
|
|
464
|
-
return "$value".Trim()
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
# ------------------------------------------------------------------------------
|
|
468
|
-
# PERSIST THE DOMAIN (what the node reads to build its public links)
|
|
469
|
-
#
|
|
470
|
-
# core/domain.py resolves the public domain from MESH_PUBLIC_URL in the
|
|
471
|
-
# environment, then from the domain file, then from its built-in default - and it
|
|
472
|
-
# never reads the legacy MESH_GATEWAY that agent.env carries. A node installed
|
|
473
|
-
# with only agent.env therefore dials the right gateway (gateway_host() does
|
|
474
|
-
# honour MESH_GATEWAY) while minting share links on the built-in default.
|
|
475
|
-
#
|
|
476
|
-
# The domain is already resolved here, so it is written down here: the place that
|
|
477
|
-
# decides the domain is then also the place that records it, and the node's
|
|
478
|
-
# links, its tunnel and the gateway agree by construction.
|
|
479
|
-
#
|
|
480
|
-
# Never fatal: a profile directory that cannot be written still leaves a working
|
|
481
|
-
# node, so a failure only prints the command the operator can run by hand.
|
|
482
|
-
# ------------------------------------------------------------------------------
|
|
483
|
-
function Write-DomainFile {
|
|
484
|
-
param([string]$Domain, [string]$Path)
|
|
485
|
-
|
|
486
|
-
# The caller hands over a bare host (the resolution block above normalises it),
|
|
487
|
-
# but this function is the last thing between a value and the file the runtime
|
|
488
|
-
# trusts: a scheme or a trailing slash would be written verbatim and become
|
|
489
|
-
# "MESH_PUBLIC_URL=https://https://host/", which no reader recovers from.
|
|
490
|
-
# Normalise exactly the way the resolution chain does.
|
|
491
|
-
$Domain = Get-NormalisedHost $Domain
|
|
492
|
-
if ([string]::IsNullOrWhiteSpace($Domain) -or $Domain -eq $DomainPlaceholder) { return }
|
|
493
|
-
if ([string]::IsNullOrWhiteSpace($Path)) { return }
|
|
494
|
-
|
|
495
|
-
# Never record the project's built-in default. It is a fallback, not a decision:
|
|
496
|
-
# writing it would turn "nothing is configured" into a pinned domain file, and
|
|
497
|
-
# once that file exists core/domain.py stops consulting the legacy MESH_GATEWAY
|
|
498
|
-
# at all - the operator's own agent.env knob would silently go inert. Nothing is
|
|
499
|
-
# lost by skipping it: with no file the resolver's last fallback IS that value,
|
|
500
|
-
# so the node's links and its tunnel still name the same host.
|
|
501
|
-
$moduleDefault = Get-NormalisedHost (Get-ModuleDefaultDomain)
|
|
502
|
-
if ($moduleDefault -and $Domain -eq $moduleDefault) { return }
|
|
503
|
-
|
|
504
|
-
$existing = Get-NormalisedHost (Get-DomainFileValue $Path)
|
|
505
|
-
if ($existing -eq $Domain) { return }
|
|
506
|
-
|
|
507
|
-
$line = "MESH_PUBLIC_URL=https://$Domain"
|
|
508
|
-
$directory = Split-Path -Parent $Path
|
|
509
|
-
try {
|
|
510
|
-
if ($directory -and -not (Test-Path -LiteralPath $directory)) {
|
|
511
|
-
New-Item -ItemType Directory -Force -Path $directory | Out-Null
|
|
512
|
-
}
|
|
513
|
-
# Written without a BOM. Every reader tolerates one (core/domain.py reads
|
|
514
|
-
# utf-8-sig, install.sh strips it), but a BOM-less file is readable by all
|
|
515
|
-
# of them, sed and grep included.
|
|
516
|
-
[System.IO.File]::WriteAllText(
|
|
517
|
-
$Path, $line + "`r`n", (New-Object System.Text.UTF8Encoding($false)))
|
|
518
|
-
} catch {
|
|
519
|
-
if ($Lang -eq "ru") {
|
|
520
|
-
Write-Host "[!] Не удалось записать домен в $Path : $($_.Exception.Message)" -ForegroundColor Yellow
|
|
521
|
-
Write-Host " Ссылки узла будут строиться на встроенном домене по умолчанию." -ForegroundColor Yellow
|
|
522
|
-
Write-Host " Запишите вручную: Set-Content '$Path' '$line'" -ForegroundColor Yellow
|
|
523
|
-
} else {
|
|
524
|
-
Write-Host "[!] Could not write the domain to $Path : $($_.Exception.Message)" -ForegroundColor Yellow
|
|
525
|
-
Write-Host " The node's links would use the built-in default domain." -ForegroundColor Yellow
|
|
526
|
-
Write-Host " Write it by hand: Set-Content '$Path' '$line'" -ForegroundColor Yellow
|
|
527
|
-
}
|
|
528
|
-
return
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if ($Lang -eq "ru") {
|
|
532
|
-
if ($existing) {
|
|
533
|
-
Write-Host "[OK] Домен узла: $line ($Path, было https://$existing)" -ForegroundColor Green
|
|
534
|
-
} else {
|
|
535
|
-
Write-Host "[OK] Домен узла: $line ($Path)" -ForegroundColor Green
|
|
536
|
-
}
|
|
537
|
-
} else {
|
|
538
|
-
if ($existing) {
|
|
539
|
-
Write-Host "[OK] Node domain: $line ($Path, was https://$existing)" -ForegroundColor Green
|
|
540
|
-
} else {
|
|
541
|
-
Write-Host "[OK] Node domain: $line ($Path)" -ForegroundColor Green
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
$DomainFilePath = if ($env:MESH_DOMAIN_FILE) { $env:MESH_DOMAIN_FILE } else { "$env:USERPROFILE\.config\antigravity-mesh\domain.env" }
|
|
547
|
-
$ResolvedDomain = Get-NormalisedHost $Gateway
|
|
548
|
-
if (-not $ResolvedDomain) { $ResolvedDomain = Get-NormalisedHost $env:MESH_PUBLIC_URL }
|
|
549
|
-
if (-not $ResolvedDomain) { $ResolvedDomain = Get-NormalisedHost $env:AGY_PUBLIC_BASE_URL }
|
|
550
|
-
if (-not $ResolvedDomain) { $ResolvedDomain = Get-NormalisedHost (Get-DomainFileValue $DomainFilePath) }
|
|
551
|
-
if (-not $ResolvedDomain -and $PublishedDomain -ne $DomainPlaceholder) {
|
|
552
|
-
$ResolvedDomain = Get-NormalisedHost $PublishedDomain
|
|
553
|
-
}
|
|
554
|
-
if (-not $ResolvedDomain) { $ResolvedDomain = Get-NormalisedHost (Get-ModuleDefaultDomain) }
|
|
555
|
-
|
|
556
|
-
$DomainMissing = [string]::IsNullOrWhiteSpace($ResolvedDomain) -or $ResolvedDomain -eq $DomainPlaceholder
|
|
557
|
-
if ($DomainMissing) {
|
|
558
|
-
if ($Lang -eq "ru") {
|
|
559
|
-
Write-Host ""
|
|
560
|
-
Write-Host "[!] Домен шлюза не задан." -ForegroundColor Red
|
|
561
|
-
# ${...} matters: "$DomainPlaceholder:" would be read as a drive-qualified
|
|
562
|
-
# variable reference and the whole script fails to parse in PowerShell 5.1.
|
|
563
|
-
Write-Host " В этой копии установщика остался плейсхолдер ${DomainPlaceholder}: она не была" -ForegroundColor Red
|
|
564
|
-
Write-Host " опубликована скриптом deploy_gateway.sh, а домена нет ни в окружении, ни в" -ForegroundColor Red
|
|
565
|
-
Write-Host " файле $DomainFilePath." -ForegroundColor Red
|
|
566
|
-
Write-Host " Укажите домен одним из способов:" -ForegroundColor Yellow
|
|
567
|
-
Write-Host " .\install.ps1 -Gateway <общий-домен>" -ForegroundColor Yellow
|
|
568
|
-
Write-Host " `$env:MESH_PUBLIC_URL='https://<общий-домен>'; .\install.ps1" -ForegroundColor Yellow
|
|
569
|
-
Write-Host " Set-Content '$DomainFilePath' 'MESH_PUBLIC_URL=https://<общий-домен>'" -ForegroundColor Yellow
|
|
570
|
-
Write-Host " Установка остановлена: регистрация на несуществующем домене не выполняется," -ForegroundColor Yellow
|
|
571
|
-
Write-Host " файл конфигурации и автозапуск не создаются." -ForegroundColor Yellow
|
|
572
|
-
} else {
|
|
573
|
-
Write-Host ""
|
|
574
|
-
Write-Host "[!] No gateway domain configured." -ForegroundColor Red
|
|
575
|
-
Write-Host " This copy of the installer still carries the $DomainPlaceholder placeholder: it" -ForegroundColor Red
|
|
576
|
-
Write-Host " was not published by deploy_gateway.sh, and no domain was found in the" -ForegroundColor Red
|
|
577
|
-
Write-Host " environment or in $DomainFilePath." -ForegroundColor Red
|
|
578
|
-
Write-Host " Pass the domain in one of these ways:" -ForegroundColor Yellow
|
|
579
|
-
Write-Host " .\install.ps1 -Gateway <shared-domain>" -ForegroundColor Yellow
|
|
580
|
-
Write-Host " `$env:MESH_PUBLIC_URL='https://<shared-domain>'; .\install.ps1" -ForegroundColor Yellow
|
|
581
|
-
Write-Host " Set-Content '$DomainFilePath' 'MESH_PUBLIC_URL=https://<shared-domain>'" -ForegroundColor Yellow
|
|
582
|
-
Write-Host " Stopping: a node is never registered against a domain that does not exist," -ForegroundColor Yellow
|
|
583
|
-
Write-Host " no config file and no autostart are created." -ForegroundColor Yellow
|
|
584
|
-
}
|
|
585
|
-
Write-Host ""
|
|
586
|
-
if (-not $DryRun) { exit 1 }
|
|
587
|
-
} else {
|
|
588
|
-
$Gateway = $ResolvedDomain
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
# Preflight: report what was detected and change nothing. This runs after the
|
|
592
|
-
# detection steps above and before any install (winget, the Python installer,
|
|
593
|
-
# pip), so the reported state is real and the machine is untouched.
|
|
594
|
-
if ($DryRun) {
|
|
595
|
-
$probeExe = if ($PyExe) { ($PyExe -split " ")[0] } else { $null }
|
|
596
|
-
$wsState = "unknown"
|
|
597
|
-
if ($probeExe) {
|
|
598
|
-
& $probeExe -c "import websockets" 2>$null
|
|
599
|
-
$wsState = if ($LASTEXITCODE -eq 0) { "installed" } else { "missing" }
|
|
600
|
-
}
|
|
601
|
-
$pyState = if ($hasPython) { $PyExe } else { "not found" }
|
|
602
|
-
# The architecture is reported here because it decides which build every
|
|
603
|
-
# automatic step above will fetch, and "why did it install the x64 build on my
|
|
604
|
-
# ARM machine" is not answerable from the rest of this report.
|
|
605
|
-
$archState = if ($OsArch.Supported) { $OsArch.Label } else { "$($OsArch.Label) (unsupported)" }
|
|
606
|
-
$domainState = if ($DomainMissing) { "NOT CONFIGURED" } else { $Gateway }
|
|
607
|
-
# The live node state, so a preflight can say "your node is offline right now"
|
|
608
|
-
# before anything on disk is touched. The name may still gain a suffix at
|
|
609
|
-
# registration (auto_suffix), so this stays best effort.
|
|
610
|
-
$nodeState = "not checked"
|
|
611
|
-
if (-not $DomainMissing) {
|
|
612
|
-
$probeNode = if ($User) { $User } else { $Hostname }
|
|
613
|
-
try {
|
|
614
|
-
$probeHealth = Invoke-RestMethod -Uri "https://$Gateway/health?user=$probeNode" -TimeoutSec 5 -ErrorAction Stop
|
|
615
|
-
$nodeState = if ($probeHealth.node_online) { "online ($probeNode)" } else { "offline ($probeNode)" }
|
|
616
|
-
} catch {
|
|
617
|
-
$nodeState = "gateway unreachable"
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
# What the domain file would receive: the very line the real run writes. A dry
|
|
621
|
-
# run changes nothing, so this only reports it - and it reports a skip when the
|
|
622
|
-
# writer would skip, so the two never disagree.
|
|
623
|
-
$domainFileState = if ($DomainMissing) {
|
|
624
|
-
"$DomainFilePath (not written: no domain configured)"
|
|
625
|
-
} elseif ($Gateway -eq (Get-NormalisedHost (Get-ModuleDefaultDomain))) {
|
|
626
|
-
"$DomainFilePath (not written: the built-in default is a fallback, not a configuration)"
|
|
627
|
-
} else {
|
|
628
|
-
"$DomainFilePath -> MESH_PUBLIC_URL=https://$Gateway"
|
|
629
|
-
}
|
|
630
|
-
if ($Lang -eq "ru") {
|
|
631
|
-
Write-Host "[DRY-RUN] Python : $pyState"
|
|
632
|
-
Write-Host "[DRY-RUN] websockets : $wsState"
|
|
633
|
-
Write-Host "[DRY-RUN] Домен : $domainState"
|
|
634
|
-
Write-Host "[DRY-RUN] Конфиг : $env:USERPROFILE\.config\antigravity-mesh"
|
|
635
|
-
Write-Host "[DRY-RUN] Автозапуск : $([Environment]::GetFolderPath('Startup'))\antigravity-agent.vbs"
|
|
636
|
-
Write-Host "[DRY-RUN] Узел : $nodeState"
|
|
637
|
-
Write-Host "[DRY-RUN] Архитектура : $archState"
|
|
638
|
-
Write-Host "[DRY-RUN] Файл домена : $domainFileState"
|
|
639
|
-
Write-Host "[DRY-RUN] Действий не выполнено." -ForegroundColor Yellow
|
|
640
|
-
} else {
|
|
641
|
-
Write-Host "[DRY-RUN] Python : $pyState"
|
|
642
|
-
Write-Host "[DRY-RUN] websockets : $wsState"
|
|
643
|
-
Write-Host "[DRY-RUN] Domain : $domainState"
|
|
644
|
-
Write-Host "[DRY-RUN] Config : $env:USERPROFILE\.config\antigravity-mesh"
|
|
645
|
-
Write-Host "[DRY-RUN] Autostart : $([Environment]::GetFolderPath('Startup'))\antigravity-agent.vbs"
|
|
646
|
-
Write-Host "[DRY-RUN] Node : $nodeState"
|
|
647
|
-
Write-Host "[DRY-RUN] Arch : $archState"
|
|
648
|
-
Write-Host "[DRY-RUN] Domain file : $domainFileState"
|
|
649
|
-
Write-Host "[DRY-RUN] Nothing was changed." -ForegroundColor Yellow
|
|
650
|
-
}
|
|
651
|
-
if ($DomainMissing -or -not $hasPython) { exit 2 }
|
|
652
|
-
exit 0
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function Resolve-PythonAfterInstall {
|
|
656
|
-
# Re-resolve with exactly the same rules the detection above used, and put the
|
|
657
|
-
# winner on PATH for the child processes below. This is a function because the
|
|
658
|
-
# chain re-resolves after each of the three sources and those call sites must
|
|
659
|
-
# stay identical: a fresh interpreter only counts if it is a real python.exe
|
|
660
|
-
# outside WindowsApps and it actually runs.
|
|
661
|
-
$found = Select-RealPython
|
|
662
|
-
if ($found) { Add-PythonToPath $found }
|
|
663
|
-
return $found
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
if (-not $hasPython) {
|
|
667
|
-
if ($Lang -eq "ru") {
|
|
668
|
-
Write-Host "[1/3] Python не найден. Архитектура: $($OsArch.Label). Автоматическая установка..." -ForegroundColor Yellow
|
|
669
|
-
} else {
|
|
670
|
-
Write-Host "[1/3] Python not found. Architecture: $($OsArch.Label). Installing automatically..." -ForegroundColor Yellow
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
# 1. winget - the platform's own mechanism, and the only source that needs no
|
|
674
|
-
# architecture mapping from this script.
|
|
675
|
-
if (-not $hasPython -and (Get-Command winget -ErrorAction SilentlyContinue)) {
|
|
676
|
-
if ($Lang -eq "ru") { Write-Host "[1/3] Источник 1/3: winget..." -ForegroundColor Yellow }
|
|
677
|
-
else { Write-Host "[1/3] Source 1/3: winget..." -ForegroundColor Yellow }
|
|
678
|
-
& winget install --id Python.Python.3.12 -e --silent --accept-source-agreements --accept-package-agreements
|
|
679
|
-
$fresh = Resolve-PythonAfterInstall
|
|
680
|
-
if ($fresh) { $PyExe = $fresh; $hasPython = $true }
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
# 2. python.org, for the architecture this machine actually runs. This used to
|
|
684
|
-
# be the "winget is missing" branch, so a machine WITH winget never reached
|
|
685
|
-
# it - and a winget that is present but blocked by policy, has no source, or
|
|
686
|
-
# silently fails left the installer with nothing to fall back on.
|
|
687
|
-
if (-not $hasPython -and $OsArch.Supported) {
|
|
688
|
-
if ($Lang -eq "ru") { Write-Host "[1/3] Источник 2/3: установщик python.org для $($OsArch.Label)..." -ForegroundColor Yellow }
|
|
689
|
-
else { Write-Host "[1/3] Source 2/3: the python.org installer for $($OsArch.Label)..." -ForegroundColor Yellow }
|
|
690
|
-
if (Install-PythonFromPythonOrg $OsArch) {
|
|
691
|
-
$fresh = Resolve-PythonAfterInstall
|
|
692
|
-
if ($fresh) { $PyExe = $fresh; $hasPython = $true }
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
# 3. uv - brings its own CPython and needs neither an installer nor a pip.
|
|
697
|
-
if (-not $hasPython) {
|
|
698
|
-
if ($Lang -eq "ru") { Write-Host "[1/3] Источник 3/3: uv..." -ForegroundColor Yellow }
|
|
699
|
-
else { Write-Host "[1/3] Source 3/3: uv..." -ForegroundColor Yellow }
|
|
700
|
-
$UvExe = Install-UvWindows $OsArch
|
|
701
|
-
if ($UvExe -and (Install-PythonViaUv $UvExe)) {
|
|
702
|
-
$fresh = Resolve-PythonAfterInstall
|
|
703
|
-
if ($fresh) { $PyExe = $fresh; $hasPython = $true }
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if ($hasPython) {
|
|
708
|
-
if ($Lang -eq "ru") { Write-Host "[1/3] Python обнаружен: $PyExe" -ForegroundColor Green }
|
|
709
|
-
else { Write-Host "[1/3] Python detected: $PyExe" -ForegroundColor Green }
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
# Fail closed. Without a real interpreter the autostart entry points at nothing and
|
|
714
|
-
# the node stays offline until a human reads the log - the exact state the
|
|
715
|
-
# Microsoft Store alias produced, where the only trace was one torn log line.
|
|
716
|
-
if (-not $hasPython -or -not $PyExe) {
|
|
717
|
-
if ($Lang -eq "ru") {
|
|
718
|
-
Write-Host "[!] Рабочий Python 3 не найден." -ForegroundColor Red
|
|
719
|
-
Write-Host " Установите Python с python.org (НЕ из Microsoft Store) и повторите установку." -ForegroundColor Yellow
|
|
720
|
-
Write-Host " Остановка: без интерпретатора автозапуск узла работать не будет." -ForegroundColor Yellow
|
|
721
|
-
} else {
|
|
722
|
-
Write-Host "[!] No working Python 3 found." -ForegroundColor Red
|
|
723
|
-
Write-Host " Install Python from python.org (NOT from the Microsoft Store) and re-run." -ForegroundColor Yellow
|
|
724
|
-
Write-Host " Stopping: an autostart entry without an interpreter can never work." -ForegroundColor Yellow
|
|
725
|
-
}
|
|
726
|
-
exit 1
|
|
727
|
-
}
|
|
728
|
-
if ("$PyExe" -like "*$WindowsAppsMarker*") {
|
|
729
|
-
if ($Lang -eq "ru") {
|
|
730
|
-
Write-Host "[!] Отказ: выбранный интерпретатор - ярлык Microsoft Store ($PyExe)." -ForegroundColor Red
|
|
731
|
-
} else {
|
|
732
|
-
Write-Host "[!] Refusing to use the Microsoft Store alias as the interpreter ($PyExe)." -ForegroundColor Red
|
|
733
|
-
}
|
|
734
|
-
exit 1
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
# Install dependencies.
|
|
738
|
-
# The tunnel agent needs "websockets"; core/mcp_tools.py and core/server.py
|
|
739
|
-
# are standard library only, so this is the single external requirement.
|
|
740
|
-
if ($Lang -eq "ru") {
|
|
741
|
-
Write-Host "[2/3] Проверка библиотек (websockets)..." -ForegroundColor Yellow
|
|
742
|
-
} else {
|
|
743
|
-
Write-Host "[2/3] Checking dependencies (websockets)..." -ForegroundColor Yellow
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
# Use the interpreter resolved above; fall back to PATH only if that failed.
|
|
747
|
-
$pyRun = if ($PyExe) { $PyExe } else { "python" }
|
|
748
|
-
$pyExeOnly = ($pyRun -split " ")[0]
|
|
749
|
-
|
|
750
|
-
# The dependency step may have to SWITCH interpreters (the venv fallback), and
|
|
751
|
-
# everything below - the Startup launcher, the watchdog task, the final "run
|
|
752
|
-
# now" - must use the interpreter that can actually import websockets. Pinning
|
|
753
|
-
# the pre-install choice here is what left a node autostarting against a Python
|
|
754
|
-
# that could not import the one package the agent needs.
|
|
755
|
-
$result = Install-WebsocketsFor -Python $pyExeOnly -Uv $UvExe
|
|
756
|
-
|
|
757
|
-
# uv is only fetched when everything else failed. It was not needed for Python
|
|
758
|
-
# itself on this machine, so downloading it up front would be an 18 MB cost for
|
|
759
|
-
# a machine that never needs it - but a pip that cannot write anywhere is
|
|
760
|
-
# precisely the case uv fixes, so the chain is retried once with it.
|
|
761
|
-
if (-not $result.Ok -and -not $UvExe) {
|
|
762
|
-
$UvExe = Install-UvWindows $OsArch
|
|
763
|
-
if ($UvExe) { $result = Install-WebsocketsFor -Python $pyExeOnly -Uv $UvExe }
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
if ($result.Ok -and $result.Python) {
|
|
767
|
-
$pyExeOnly = $result.Python
|
|
768
|
-
$PyExe = $result.Python
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
if (-not $result.Ok) {
|
|
772
|
-
if ($Lang -eq "ru") {
|
|
773
|
-
Write-Host "[!] Не удалось установить websockets автоматически." -ForegroundColor Red
|
|
774
|
-
Write-Host " Проверены: pip, pip --user, ensurepip, uv и venv." -ForegroundColor Yellow
|
|
775
|
-
Write-Host " Выполните вручную: $pyExeOnly -m pip install websockets" -ForegroundColor Yellow
|
|
776
|
-
} else {
|
|
777
|
-
Write-Host "[!] Could not install websockets automatically." -ForegroundColor Red
|
|
778
|
-
Write-Host " Tried: pip, pip --user, ensurepip, uv and a venv." -ForegroundColor Yellow
|
|
779
|
-
Write-Host " Run manually: $pyExeOnly -m pip install websockets" -ForegroundColor Yellow
|
|
780
|
-
}
|
|
781
|
-
exit 1
|
|
782
|
-
}
|
|
783
|
-
if ($Lang -eq "ru") {
|
|
784
|
-
Write-Host "[OK] websockets установлен ($pyExeOnly)." -ForegroundColor Green
|
|
785
|
-
} else {
|
|
786
|
-
Write-Host "[OK] websockets is installed ($pyExeOnly)." -ForegroundColor Green
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
$ConfigDir = "$env:USERPROFILE\.config\antigravity-mesh"
|
|
790
|
-
if (!(Test-Path $ConfigDir)) { New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null }
|
|
791
|
-
|
|
792
|
-
# Bootstrap project files if piped from irm/iex
|
|
793
|
-
$ScriptDir = if ($MyInvocation.MyCommand -and $MyInvocation.MyCommand.Path) { Split-Path -Parent $MyInvocation.MyCommand.Path } else { $null }
|
|
794
|
-
if (-not $ScriptDir -or -not (Test-Path "$ScriptDir\core\agent.py")) {
|
|
795
|
-
$BootstrapDir = "$env:USERPROFILE\.gemini-computer-use"
|
|
796
|
-
$CoreDir = "$BootstrapDir\core"
|
|
797
|
-
$SkillsDir = "$BootstrapDir\skills"
|
|
798
|
-
# The watchdog, the doctor and the updater are fetched too: the scheduled
|
|
799
|
-
# tasks registered below run the first and the last, and a node whose agent
|
|
800
|
-
# dies - or whose code goes stale - must heal and update itself without
|
|
801
|
-
# anyone re-running the installer by hand.
|
|
802
|
-
$OpsDir = "$BootstrapDir\ops\windows"
|
|
803
|
-
if (!(Test-Path $CoreDir)) { New-Item -ItemType Directory -Path $CoreDir -Force | Out-Null }
|
|
804
|
-
if (!(Test-Path $SkillsDir)) { New-Item -ItemType Directory -Path $SkillsDir -Force | Out-Null }
|
|
805
|
-
if (!(Test-Path $OpsDir)) { New-Item -ItemType Directory -Path $OpsDir -Force | Out-Null }
|
|
806
|
-
|
|
807
|
-
$files = @(
|
|
808
|
-
"core/agent.py",
|
|
809
|
-
"core/server.py",
|
|
810
|
-
"core/mcp_tools.py",
|
|
811
|
-
"core/web_share.py",
|
|
812
|
-
"core/domain.py",
|
|
813
|
-
"core/vitals.py",
|
|
814
|
-
"core/updater.py",
|
|
815
|
-
"core/version.py",
|
|
816
|
-
"core/__init__.py",
|
|
817
|
-
"skills/orchestrator.md",
|
|
818
|
-
"ops/windows/agent-watchdog.ps1",
|
|
819
|
-
"ops/windows/run-hidden.vbs",
|
|
820
|
-
"ops/update.ps1",
|
|
821
|
-
"ops/doctor.ps1"
|
|
822
|
-
)
|
|
823
|
-
foreach ($f in $files) {
|
|
824
|
-
$dest = "$BootstrapDir\$($f -replace '/', '\')"
|
|
825
|
-
try {
|
|
826
|
-
Invoke-WebRequest -Uri "https://$Gateway/$f" -OutFile $dest -UseBasicParsing -ErrorAction Stop
|
|
827
|
-
} catch {
|
|
828
|
-
try {
|
|
829
|
-
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/LevRa7/Computer-use-for-Gemini-App-Web/main/$f" -OutFile $dest -UseBasicParsing
|
|
830
|
-
} catch {}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
$ScriptDir = $BootstrapDir
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function Print-McpBox($Title, $Url, $CopiedText, $BorderColor = "Green") {
|
|
837
|
-
$MinLen = [Math]::Max($Title.Length, $Url.Length)
|
|
838
|
-
if ($CopiedText) {
|
|
839
|
-
$MinLen = [Math]::Max($MinLen, $CopiedText.Length)
|
|
840
|
-
}
|
|
841
|
-
$ContentWidth = [Math]::Max($MinLen + 4, 84)
|
|
842
|
-
$Border = " +" + ("-" * $ContentWidth) + "+"
|
|
843
|
-
$Empty = " |" + (" " * $ContentWidth) + "|"
|
|
844
|
-
|
|
845
|
-
Write-Host $Border -ForegroundColor $BorderColor
|
|
846
|
-
|
|
847
|
-
$padTitle = $ContentWidth - 2 - $Title.Length
|
|
848
|
-
Write-Host " | " -NoNewline -ForegroundColor $BorderColor
|
|
849
|
-
Write-Host $Title -NoNewline -ForegroundColor White
|
|
850
|
-
Write-Host ((" " * $padTitle) + " |") -ForegroundColor $BorderColor
|
|
851
|
-
|
|
852
|
-
Write-Host $Empty -ForegroundColor $BorderColor
|
|
853
|
-
|
|
854
|
-
$padUrl = $ContentWidth - 2 - $Url.Length
|
|
855
|
-
Write-Host " | " -NoNewline -ForegroundColor $BorderColor
|
|
856
|
-
Write-Host $Url -NoNewline -ForegroundColor Yellow
|
|
857
|
-
Write-Host ((" " * $padUrl) + " |") -ForegroundColor $BorderColor
|
|
858
|
-
|
|
859
|
-
Write-Host $Empty -ForegroundColor $BorderColor
|
|
860
|
-
|
|
861
|
-
if ($CopiedText) {
|
|
862
|
-
$padCopy = $ContentWidth - 2 - $CopiedText.Length
|
|
863
|
-
Write-Host " | " -NoNewline -ForegroundColor $BorderColor
|
|
864
|
-
Write-Host $CopiedText -NoNewline -ForegroundColor Green
|
|
865
|
-
Write-Host ((" " * $padCopy) + " |") -ForegroundColor $BorderColor
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
Write-Host $Border -ForegroundColor $BorderColor
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
if ($Mode -eq "standalone") {
|
|
872
|
-
if ($Lang -eq "ru") {
|
|
873
|
-
Write-Host "=== Запуск в режиме Local Standalone на порту $Port ===" -ForegroundColor Blue
|
|
874
|
-
} else {
|
|
875
|
-
Write-Host "=== Starting in Local Standalone Mode on port $Port ===" -ForegroundColor Blue
|
|
876
|
-
}
|
|
877
|
-
Start-Process $pyExeOnly -ArgumentList "-m core.server --host 127.0.0.1 --port=$Port" -WorkingDirectory $ScriptDir -WindowStyle Hidden
|
|
878
|
-
$LocalUrl = "http://localhost:$Port/sse"
|
|
879
|
-
$Copied = $false
|
|
880
|
-
try {
|
|
881
|
-
Set-Clipboard -Value $LocalUrl -ErrorAction Stop
|
|
882
|
-
$Copied = $true
|
|
883
|
-
} catch {
|
|
884
|
-
try { $LocalUrl | clip.exe 2>$null; $Copied = $true } catch {}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
Clear-Host
|
|
888
|
-
|
|
889
|
-
if ($Lang -eq "ru") {
|
|
890
|
-
Write-Host ""
|
|
891
|
-
$copyMsg = if ($Copied) { "[OK] ССЫЛКА СКОПИРОВАНА В БУФЕР ОБМЕНА! (Вставьте через Ctrl+V)" } else { "Скопируйте ссылку выше" }
|
|
892
|
-
Print-McpBox "ССЫЛКА ЛОКАЛЬНОГО MCP-СЕРВЕРА (СКОПИРУЙТЕ):" $LocalUrl $copyMsg
|
|
893
|
-
Write-Host ""
|
|
894
|
-
Write-Host " Куда вставлять ссылку в Google Gemini:" -ForegroundColor Yellow
|
|
895
|
-
Write-Host " 1. Откройте в браузере: https://gemini.google.com/spark/apps" -ForegroundColor Cyan
|
|
896
|
-
Write-Host " 2. Нажмите 'Добавить приложение' (Add app / Настройки MCP)"
|
|
897
|
-
Write-Host " 3. Вставьте скопированную ссылку в поле 'URL сервера' (Ctrl+V) и нажмите Подключить." -ForegroundColor White
|
|
898
|
-
Write-Host ""
|
|
899
|
-
} else {
|
|
900
|
-
Write-Host ""
|
|
901
|
-
$copyMsg = if ($Copied) { "[OK] URL COPIED TO CLIPBOARD! (Press Ctrl+V to paste)" } else { "Copy the URL above" }
|
|
902
|
-
Print-McpBox "LOCAL MCP SERVER URL (COPY THIS):" $LocalUrl $copyMsg
|
|
903
|
-
Write-Host ""
|
|
904
|
-
Write-Host " Where to paste this URL in Google Gemini:" -ForegroundColor Yellow
|
|
905
|
-
Write-Host " 1. Open in your browser: https://gemini.google.com/spark/apps" -ForegroundColor Cyan
|
|
906
|
-
Write-Host " 2. Click 'Add app' (or navigate to MCP settings)"
|
|
907
|
-
Write-Host " 3. Paste the URL into the 'Server URL' field (Ctrl+V) and click Connect." -ForegroundColor White
|
|
908
|
-
Write-Host ""
|
|
909
|
-
}
|
|
910
|
-
exit 0
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
# Gateway Tunnel Mode
|
|
914
|
-
if ([string]::IsNullOrWhiteSpace($User)) {
|
|
915
|
-
$User = $Hostname
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
if ($Lang -eq "ru") {
|
|
919
|
-
Write-Host "[2/3] Регистрация узла '$User' на общем домене ($Gateway)..." -ForegroundColor Yellow
|
|
920
|
-
} else {
|
|
921
|
-
Write-Host "[2/3] Registering node '$User' on the shared domain ($Gateway)..." -ForegroundColor Yellow
|
|
922
|
-
}
|
|
923
|
-
$detectedMac = ""
|
|
924
|
-
try {
|
|
925
|
-
$nic = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled = 'TRUE'" -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
926
|
-
if ($nic -and $nic.MACAddress) {
|
|
927
|
-
$detectedMac = $nic.MACAddress
|
|
928
|
-
}
|
|
929
|
-
} catch {}
|
|
930
|
-
if ([string]::IsNullOrWhiteSpace($detectedMac)) {
|
|
931
|
-
try {
|
|
932
|
-
# Use the interpreter resolved above: a bare "python" may be the Microsoft
|
|
933
|
-
# Store App Execution Alias, which is on PATH but runs nothing.
|
|
934
|
-
$res = & $pyExeOnly -c "import uuid; print(':'.join(['{:02x}'.format((uuid.getnode() >> ele) & 0xff) for ele in range(0,8*6,8)][::-1]))" 2>&1
|
|
935
|
-
if ($LASTEXITCODE -eq 0 -and $res) {
|
|
936
|
-
$detectedMac = $res.ToString().Trim()
|
|
937
|
-
}
|
|
938
|
-
} catch {}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
$detectedOs = "Windows $([System.Environment]::OSVersion.Version.ToString())"
|
|
942
|
-
|
|
943
|
-
$regPayloadObj = @{
|
|
944
|
-
username = $User
|
|
945
|
-
auto_suffix = $true
|
|
946
|
-
mac_address = $detectedMac
|
|
947
|
-
os = $detectedOs
|
|
948
|
-
# The architecture is part of the device fingerprint: two machines behind one
|
|
949
|
-
# NAT can share a MAC-derived identity only if their OS and architecture agree,
|
|
950
|
-
# and a gateway operator needs to know which build a node is running when a
|
|
951
|
-
# wheel-related failure is reported.
|
|
952
|
-
arch = $OsArch.Key
|
|
953
|
-
}
|
|
954
|
-
if (-not [string]::IsNullOrWhiteSpace($Token)) {
|
|
955
|
-
$regPayloadObj.token = $Token
|
|
956
|
-
}
|
|
957
|
-
$regPayload = $regPayloadObj | ConvertTo-Json
|
|
958
|
-
# Registration is the one step that cannot be skipped or guessed: without a real
|
|
959
|
-
# node name and token the agent can never connect. This used to run under
|
|
960
|
-
# $ErrorActionPreference = "Continue", so a DNS/TLS/HTTP failure printed an error,
|
|
961
|
-
# the script carried on, wrote an EMPTY agent.env, installed a permanently broken
|
|
962
|
-
# Startup launcher, printed a link with empty user/token and claimed that link had
|
|
963
|
-
# been copied - leaving the operator with a node that retries a dead endpoint after
|
|
964
|
-
# every login. Fail closed instead, before anything on disk is touched.
|
|
965
|
-
try {
|
|
966
|
-
$response = Invoke-RestMethod -Uri "https://$Gateway/api/register" -Method Post `
|
|
967
|
-
-Body $regPayload -ContentType "application/json" -ErrorAction Stop
|
|
968
|
-
} catch {
|
|
969
|
-
if ($Lang -eq "ru") {
|
|
970
|
-
Write-Host "[!] Не удалось зарегистрировать узел на '$Gateway': $($_.Exception.Message)" -ForegroundColor Red
|
|
971
|
-
Write-Host " Проверьте, что домен шлюза разрешается в DNS и сервер доступен, затем повторите:" -ForegroundColor Yellow
|
|
972
|
-
Write-Host " .\install.ps1 -Gateway <ваш-домен>" -ForegroundColor Yellow
|
|
973
|
-
Write-Host " Ничего не установлено: файл конфигурации и автозапуск не создавались." -ForegroundColor Yellow
|
|
974
|
-
} else {
|
|
975
|
-
Write-Host "[!] Could not register this node on '$Gateway': $($_.Exception.Message)" -ForegroundColor Red
|
|
976
|
-
Write-Host " Check that the gateway domain resolves in DNS and the server is reachable, then re-run:" -ForegroundColor Yellow
|
|
977
|
-
Write-Host " .\install.ps1 -Gateway <your-domain>" -ForegroundColor Yellow
|
|
978
|
-
Write-Host " Nothing was installed: no config file and no autostart were created." -ForegroundColor Yellow
|
|
979
|
-
}
|
|
980
|
-
exit 1
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
$AssignedUser = $response.username
|
|
984
|
-
$AssignedToken = $response.token
|
|
985
|
-
|
|
986
|
-
if ([string]::IsNullOrWhiteSpace($AssignedUser) -or [string]::IsNullOrWhiteSpace($AssignedToken)) {
|
|
987
|
-
if ($Lang -eq "ru") {
|
|
988
|
-
Write-Host "[!] Шлюз '$Gateway' ответил без имени узла или токена - установка остановлена." -ForegroundColor Red
|
|
989
|
-
} else {
|
|
990
|
-
Write-Host "[!] Gateway '$Gateway' answered without a node name or token - stopping." -ForegroundColor Red
|
|
991
|
-
}
|
|
992
|
-
exit 1
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
# ------------------------------------------------------------------------------
|
|
996
|
-
# SHARED-DOMAIN CONTRACT: one public domain serves every node and the node is
|
|
997
|
-
# selected by ?user=<node-name>. Never build a per-device subdomain URL: each
|
|
998
|
-
# extra hostname needs its own DNS record and TLS SAN, and a missing SAN makes
|
|
999
|
-
# the Gemini client fail with an opaque "cannot connect to host" error.
|
|
1000
|
-
# The gateway returns this canonical URL; the fallback keeps older gateways
|
|
1001
|
-
# working without ever advertising a subdomain.
|
|
1002
|
-
# ------------------------------------------------------------------------------
|
|
1003
|
-
$PublicBaseUrl = "https://$Gateway"
|
|
1004
|
-
$SseUrl = $response.sse_url
|
|
1005
|
-
if ([string]::IsNullOrWhiteSpace($SseUrl)) {
|
|
1006
|
-
$SseUrl = "$PublicBaseUrl/sse?user=$AssignedUser&token=$AssignedToken"
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
# Quick Copy to Clipboard
|
|
1010
|
-
$Copied = $false
|
|
1011
|
-
try {
|
|
1012
|
-
Set-Clipboard -Value $SseUrl -ErrorAction Stop
|
|
1013
|
-
$Copied = $true
|
|
1014
|
-
} catch {
|
|
1015
|
-
try {
|
|
1016
|
-
$SseUrl | clip.exe 2>$null
|
|
1017
|
-
$Copied = $true
|
|
1018
|
-
} catch {}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
if ($Lang -eq "ru") {
|
|
1022
|
-
Write-Host "[3/3] Настройка автозапуска в Windows..." -ForegroundColor Green
|
|
1023
|
-
} else {
|
|
1024
|
-
Write-Host "[3/3] Configuring Windows background autostart..." -ForegroundColor Green
|
|
1025
|
-
}
|
|
1026
|
-
$envFile = "$ConfigDir\agent.env"
|
|
1027
|
-
@"
|
|
1028
|
-
MESH_GATEWAY=$Gateway
|
|
1029
|
-
MESH_USER=$AssignedUser
|
|
1030
|
-
MESH_TOKEN=$AssignedToken
|
|
1031
|
-
"@ | Out-File -FilePath $envFile -Encoding utf8
|
|
1032
|
-
|
|
1033
|
-
# agent.env carries only the legacy MESH_GATEWAY, which core/domain.py honours for
|
|
1034
|
-
# the tunnel but never for the links a node publishes. The domain file is what
|
|
1035
|
-
# keeps share links on the gateway this node is registered on, so it is written
|
|
1036
|
-
# down before the agent is started.
|
|
1037
|
-
Write-DomainFile -Domain $Gateway -Path $DomainFilePath
|
|
1038
|
-
|
|
1039
|
-
# Start agent in background
|
|
1040
|
-
$env:MESH_GATEWAY = $Gateway
|
|
1041
|
-
$env:MESH_USER = $AssignedUser
|
|
1042
|
-
$env:MESH_TOKEN = $AssignedToken
|
|
1043
|
-
|
|
1044
|
-
# Create Windows Startup launcher for persistent autostart.
|
|
1045
|
-
# The interpreter is pinned by full path because PATH may hold the Microsoft
|
|
1046
|
-
# Store alias, which stops working after a Store repair or an update. "-u"
|
|
1047
|
-
# keeps stdout unbuffered, so a killed process cannot leave a torn log line as
|
|
1048
|
-
# the only evidence of what happened.
|
|
1049
|
-
if (-not $pyExeOnly -or "$pyExeOnly" -like "*$WindowsAppsMarker*") {
|
|
1050
|
-
Write-Host "[!] Refusing to write an autostart entry for '$pyExeOnly'." -ForegroundColor Red
|
|
1051
|
-
exit 1
|
|
1052
|
-
}
|
|
1053
|
-
$startupDir = [Environment]::GetFolderPath("Startup")
|
|
1054
|
-
$startupVbs = "$startupDir\antigravity-agent.vbs"
|
|
1055
|
-
$pyExeForVbs = $pyExeOnly
|
|
1056
|
-
$agentLog = "$ConfigDir\agent.log"
|
|
1057
|
-
@"
|
|
1058
|
-
Set WshShell = CreateObject("WScript.Shell")
|
|
1059
|
-
WshShell.Environment("PROCESS")("MESH_GATEWAY") = "$Gateway"
|
|
1060
|
-
WshShell.Environment("PROCESS")("MESH_USER") = "$AssignedUser"
|
|
1061
|
-
WshShell.Environment("PROCESS")("MESH_TOKEN") = "$AssignedToken"
|
|
1062
|
-
WshShell.CurrentDirectory = "$ScriptDir"
|
|
1063
|
-
' Log the output so a silent autostart failure can be diagnosed later.
|
|
1064
|
-
WshShell.Run "cmd /c """"$pyExeForVbs"" -u -m core.agent >> """"$agentLog"""" 2>&1""", 0, False
|
|
1065
|
-
"@ | Out-File -FilePath $startupVbs -Encoding Unicode
|
|
1066
|
-
|
|
1067
|
-
# A Startup entry runs once per logon: it cannot recover a node whose agent died
|
|
1068
|
-
# (crash, Windows Update reboot, broken dependency). A Scheduled Task repeats the
|
|
1069
|
-
# watchdog every five minutes, so a dead agent comes back on its own.
|
|
1070
|
-
#
|
|
1071
|
-
# Both tasks are started through ops\windows\run-hidden.vbs rather than
|
|
1072
|
-
# powershell.exe directly. A task has no console of its own, so Windows allocates
|
|
1073
|
-
# one for powershell.exe and only then honours "-WindowStyle Hidden": the window is
|
|
1074
|
-
# created, drawn and hidden again, and the user sees a black window flash every
|
|
1075
|
-
# five minutes. wscript.exe is a GUI host - it never allocates a console - and
|
|
1076
|
-
# WshShell.Run with window style 0 passes SW_HIDE to the child, so nothing is ever
|
|
1077
|
-
# drawn. The task argument names the node script relative to the payload, which
|
|
1078
|
-
# run-hidden.vbs resolves from its own location, so the action keeps working if the
|
|
1079
|
-
# payload moves.
|
|
1080
|
-
$watchdog = Join-Path $ScriptDir 'ops\windows\agent-watchdog.ps1'
|
|
1081
|
-
$runHidden = Join-Path $ScriptDir 'ops\windows\run-hidden.vbs'
|
|
1082
|
-
if (Test-Path -LiteralPath $watchdog) {
|
|
1083
|
-
# Two spellings of the same action, because the two registration paths parse
|
|
1084
|
-
# quotes differently: schtasks.exe needs the quotes around the script path
|
|
1085
|
-
# backslash-escaped (otherwise a profile path with a space is split into two
|
|
1086
|
-
# arguments and the task is rejected outright), while the ScheduledTasks
|
|
1087
|
-
# cmdlets take the argument literally.
|
|
1088
|
-
if (Test-Path -LiteralPath $runHidden) {
|
|
1089
|
-
$watchdogHost = 'wscript.exe'
|
|
1090
|
-
$watchdogArguments = '"' + $runHidden + '" "ops\windows\agent-watchdog.ps1" -Quiet'
|
|
1091
|
-
$watchdogTaskCommand = 'wscript.exe \"' + $runHidden + '\" \"ops\windows\agent-watchdog.ps1\" -Quiet'
|
|
1092
|
-
} else {
|
|
1093
|
-
# A payload without the launcher still self-heals; it just flashes. Saying
|
|
1094
|
-
# so is better than leaving the operator with a window they cannot explain.
|
|
1095
|
-
if ($Lang -eq "ru") {
|
|
1096
|
-
Write-Host "[!] $runHidden не найден: задача-сторож будет запускаться напрямую и мелькать окном." -ForegroundColor Yellow
|
|
1097
|
-
} else {
|
|
1098
|
-
Write-Host "[!] $runHidden is missing: the watchdog task will run PowerShell directly and flash a window." -ForegroundColor Yellow
|
|
1099
|
-
}
|
|
1100
|
-
$watchdogHost = 'powershell.exe'
|
|
1101
|
-
$watchdogArguments = '-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $watchdog + '" -Quiet'
|
|
1102
|
-
$watchdogTaskCommand = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"' + $watchdog + '\" -Quiet'
|
|
1103
|
-
}
|
|
1104
|
-
$watchdogRegistered = $false
|
|
1105
|
-
$watchdogError = ''
|
|
1106
|
-
# schtasks.exe is tried first because it registers a task for the CURRENT user
|
|
1107
|
-
# without elevation, while Register-ScheduledTask needs write access to the task
|
|
1108
|
-
# store (denied for a normal user on a hardened or domain-joined machine - that
|
|
1109
|
-
# was measured on the machine this watchdog was built for). The minute schedule
|
|
1110
|
-
# repeats indefinitely, so logon is already covered by the Startup entry above
|
|
1111
|
-
# and needs no second trigger here.
|
|
1112
|
-
$watchdogError = (& schtasks.exe /Create /TN 'AntigravityMeshWatchdog' /TR $watchdogTaskCommand `
|
|
1113
|
-
/SC MINUTE /MO 5 /F 2>&1 | Out-String).Trim()
|
|
1114
|
-
if ($LASTEXITCODE -eq 0) { $watchdogRegistered = $true }
|
|
1115
|
-
if (-not $watchdogRegistered) {
|
|
1116
|
-
try {
|
|
1117
|
-
$watchdogAction = New-ScheduledTaskAction -Execute $watchdogHost -Argument $watchdogArguments -WorkingDirectory $ScriptDir
|
|
1118
|
-
# No -RepetitionDuration on purpose: PowerShell serialises
|
|
1119
|
-
# [TimeSpan]::MaxValue as P99999999DT23H59M59S, which the Task Scheduler
|
|
1120
|
-
# schema rejects outright; an omitted duration means "repeat
|
|
1121
|
-
# indefinitely", which is exactly what this needs.
|
|
1122
|
-
$watchdogTriggers = @(
|
|
1123
|
-
(New-ScheduledTaskTrigger -AtLogOn),
|
|
1124
|
-
(New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 5))
|
|
1125
|
-
)
|
|
1126
|
-
$watchdogSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
|
1127
|
-
-StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
|
|
1128
|
-
Register-ScheduledTask -TaskName 'AntigravityMeshWatchdog' -Action $watchdogAction `
|
|
1129
|
-
-Trigger $watchdogTriggers -Settings $watchdogSettings -Force `
|
|
1130
|
-
-Description 'Restarts the Antigravity Mesh node agent when it dies.' | Out-Null
|
|
1131
|
-
$watchdogRegistered = $true
|
|
1132
|
-
$watchdogError = ''
|
|
1133
|
-
} catch {
|
|
1134
|
-
$watchdogRegistered = $false
|
|
1135
|
-
$watchdogError = $_.Exception.Message
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
if ($watchdogRegistered) {
|
|
1139
|
-
if ($Lang -eq "ru") {
|
|
1140
|
-
Write-Host "[OK] Задача-сторож 'AntigravityMeshWatchdog' зарегистрирована (каждые 5 минут)." -ForegroundColor Green
|
|
1141
|
-
} else {
|
|
1142
|
-
Write-Host "[OK] Watchdog task 'AntigravityMeshWatchdog' registered (every 5 minutes)." -ForegroundColor Green
|
|
1143
|
-
}
|
|
1144
|
-
} else {
|
|
1145
|
-
if ($Lang -eq "ru") {
|
|
1146
|
-
Write-Host "[!] Не удалось зарегистрировать задачу-сторож. Автозапуск при входе в систему всё равно создан." -ForegroundColor Yellow
|
|
1147
|
-
Write-Host " Запустить проверку вручную: $watchdog" -ForegroundColor Yellow
|
|
1148
|
-
} else {
|
|
1149
|
-
Write-Host "[!] Could not register the watchdog task. The Startup entry still starts the agent at logon." -ForegroundColor Yellow
|
|
1150
|
-
Write-Host " Run the check by hand: $watchdog" -ForegroundColor Yellow
|
|
1151
|
-
}
|
|
1152
|
-
if ($watchdogError) { Write-Host " $watchdogError" -ForegroundColor DarkYellow }
|
|
1153
|
-
}
|
|
1154
|
-
} elseif ($Lang -eq "ru") {
|
|
1155
|
-
Write-Host "[!] $watchdog не найден: самовосстановления у узла не будет." -ForegroundColor Yellow
|
|
1156
|
-
} else {
|
|
1157
|
-
Write-Host "[!] $watchdog not found: the node has no self-healing task." -ForegroundColor Yellow
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
# The watchdog above recovers a node that is DOWN; this task moves a node that is
|
|
1161
|
-
# merely OUT OF DATE. A machine nobody logs into would otherwise keep running
|
|
1162
|
-
# whatever was installed the day it was set up, so the updater checks the newest
|
|
1163
|
-
# GitHub release once a day, installs it with a rollback backup and lets its
|
|
1164
|
-
# helper bring the agent back on the new code. The agent also checks for itself
|
|
1165
|
-
# (MESH_UPDATE_* in core/updater.py); this task is what covers a node whose agent
|
|
1166
|
-
# is not running at 03:30.
|
|
1167
|
-
$updaterScript = Join-Path $ScriptDir 'ops\update.ps1'
|
|
1168
|
-
if (Test-Path -LiteralPath $updaterScript) {
|
|
1169
|
-
# Same windowless host as the watchdog: a daily flash is still a flash.
|
|
1170
|
-
if (Test-Path -LiteralPath $runHidden) {
|
|
1171
|
-
$updaterHost = 'wscript.exe'
|
|
1172
|
-
$updaterArguments = '"' + $runHidden + '" "ops\update.ps1" -Quiet'
|
|
1173
|
-
$updaterTaskCommand = 'wscript.exe \"' + $runHidden + '\" \"ops\update.ps1\" -Quiet'
|
|
1174
|
-
} else {
|
|
1175
|
-
$updaterHost = 'powershell.exe'
|
|
1176
|
-
$updaterArguments = '-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $updaterScript + '" -Quiet'
|
|
1177
|
-
$updaterTaskCommand = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"' + $updaterScript + '\" -Quiet'
|
|
1178
|
-
}
|
|
1179
|
-
$updaterRegistered = $false
|
|
1180
|
-
$updaterError = ''
|
|
1181
|
-
# schtasks.exe first, for the same reason as the watchdog: it needs no write
|
|
1182
|
-
# access to the task store, which a normal user does not have on a hardened
|
|
1183
|
-
# or domain-joined machine.
|
|
1184
|
-
$updaterError = (& schtasks.exe /Create /TN 'AntigravityMeshUpdater' /TR $updaterTaskCommand `
|
|
1185
|
-
/SC DAILY /MO 1 /ST 03:30 /F 2>&1 | Out-String).Trim()
|
|
1186
|
-
if ($LASTEXITCODE -eq 0) { $updaterRegistered = $true }
|
|
1187
|
-
if (-not $updaterRegistered) {
|
|
1188
|
-
try {
|
|
1189
|
-
$updaterAction = New-ScheduledTaskAction -Execute $updaterHost -Argument $updaterArguments -WorkingDirectory $ScriptDir
|
|
1190
|
-
$updaterTriggers = @((New-ScheduledTaskTrigger -Daily -At '03:30'))
|
|
1191
|
-
$updaterSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
|
1192
|
-
-StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 30)
|
|
1193
|
-
Register-ScheduledTask -TaskName 'AntigravityMeshUpdater' -Action $updaterAction `
|
|
1194
|
-
-Trigger $updaterTriggers -Settings $updaterSettings -Force `
|
|
1195
|
-
-Description 'Updates the Antigravity Mesh node from the newest GitHub release.' | Out-Null
|
|
1196
|
-
$updaterRegistered = $true
|
|
1197
|
-
$updaterError = ''
|
|
1198
|
-
} catch {
|
|
1199
|
-
$updaterRegistered = $false
|
|
1200
|
-
$updaterError = $_.Exception.Message
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
if ($updaterRegistered) {
|
|
1204
|
-
if ($Lang -eq "ru") {
|
|
1205
|
-
Write-Host "[OK] Задача обновления 'AntigravityMeshUpdater' зарегистрирована (ежедневно в 03:30)." -ForegroundColor Green
|
|
1206
|
-
Write-Host " Проверить сейчас: powershell -File `"$updaterScript`" -Check" -ForegroundColor DarkGray
|
|
1207
|
-
} else {
|
|
1208
|
-
Write-Host "[OK] Update task 'AntigravityMeshUpdater' registered (daily at 03:30)." -ForegroundColor Green
|
|
1209
|
-
Write-Host " Check right now: powershell -File `"$updaterScript`" -Check" -ForegroundColor DarkGray
|
|
1210
|
-
}
|
|
1211
|
-
} else {
|
|
1212
|
-
if ($Lang -eq "ru") {
|
|
1213
|
-
Write-Host "[!] Не удалось зарегистрировать задачу обновления. Узел всё равно проверяет версию сам, пока агент запущен." -ForegroundColor Yellow
|
|
1214
|
-
Write-Host " Запустить проверку вручную: powershell -File `"$updaterScript`" -Check" -ForegroundColor Yellow
|
|
1215
|
-
} else {
|
|
1216
|
-
Write-Host "[!] Could not register the update task. A running agent still checks for releases itself." -ForegroundColor Yellow
|
|
1217
|
-
Write-Host " Check by hand: powershell -File `"$updaterScript`" -Check" -ForegroundColor Yellow
|
|
1218
|
-
}
|
|
1219
|
-
if ($updaterError) { Write-Host " $updaterError" -ForegroundColor DarkYellow }
|
|
1220
|
-
}
|
|
1221
|
-
} elseif ($Lang -eq "ru") {
|
|
1222
|
-
Write-Host "[!] $updaterScript не найден: автоматического обновления у узла не будет." -ForegroundColor Yellow
|
|
1223
|
-
} else {
|
|
1224
|
-
Write-Host "[!] $updaterScript not found: the node has no automatic update task." -ForegroundColor Yellow
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
# Launch now
|
|
1228
|
-
Start-Process $pyExeOnly -ArgumentList "-u -m core.agent" -WorkingDirectory $ScriptDir -WindowStyle Hidden
|
|
1229
|
-
|
|
1230
|
-
Clear-Host
|
|
1231
|
-
|
|
1232
|
-
if ($Lang -eq "ru") {
|
|
1233
|
-
Write-Host ""
|
|
1234
|
-
$copyMsg = if ($Copied) { "[OK] ССЫЛКА СКОПИРОВАНА В БУФЕР ОБМЕНА! (Вставьте через Ctrl+V)" } else { "Скопируйте ссылку выше" }
|
|
1235
|
-
Print-McpBox "ССЫЛКА MCP-СЕРВЕРА ДЛЯ ПОДКЛЮЧЕНИЯ (СКОПИРУЙТЕ):" $SseUrl $copyMsg
|
|
1236
|
-
Write-Host ""
|
|
1237
|
-
Write-Host " Куда вставлять ссылку в Google Gemini:" -ForegroundColor Yellow
|
|
1238
|
-
Write-Host " 1. Откройте в браузере: https://gemini.google.com/spark/apps" -ForegroundColor Cyan
|
|
1239
|
-
Write-Host " 2. Нажмите 'Добавить приложение' (Add app / Настройки MCP)"
|
|
1240
|
-
Write-Host " 3. Вставьте скопированную ссылку в поле 'URL сервера' (Ctrl+V) и нажмите Подключить." -ForegroundColor White
|
|
1241
|
-
Write-Host ""
|
|
1242
|
-
} else {
|
|
1243
|
-
Write-Host ""
|
|
1244
|
-
$copyMsg = if ($Copied) { "[OK] URL COPIED TO CLIPBOARD! (Press Ctrl+V to paste)" } else { "Copy the URL above" }
|
|
1245
|
-
Print-McpBox "MCP SERVER URL (COPY THIS):" $SseUrl $copyMsg
|
|
1246
|
-
Write-Host ""
|
|
1247
|
-
Write-Host " Where to paste this URL in Google Gemini:" -ForegroundColor Yellow
|
|
1248
|
-
Write-Host " 1. Open in your browser: https://gemini.google.com/spark/apps" -ForegroundColor Cyan
|
|
1249
|
-
Write-Host " 2. Click 'Add app' (or navigate to MCP settings)"
|
|
1250
|
-
Write-Host " 3. Paste the URL into the 'Server URL' field (Ctrl+V) and click Connect." -ForegroundColor White
|
|
1251
|
-
Write-Host ""
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
# ------------------------------------------------------------------------------
|
|
1255
|
-
# Post-install check. An install that looks successful while the agent never
|
|
1256
|
-
# connects is what leaves a client with an opaque frontend error, so the node's
|
|
1257
|
-
# state is verified here and reported with the interpreter and the log to read.
|
|
1258
|
-
#
|
|
1259
|
-
# Deliberately no non-zero exit code: install-gui.ps1 only extracts the MCP URL
|
|
1260
|
-
# from a successful run, and that link matters most exactly when the node is not
|
|
1261
|
-
# up yet. The watchdog task retries every five minutes in the meantime.
|
|
1262
|
-
# ------------------------------------------------------------------------------
|
|
1263
|
-
$NodeOnline = $false
|
|
1264
|
-
for ($attempt = 1; $attempt -le 15; $attempt++) {
|
|
1265
|
-
Start-Sleep -Seconds 2
|
|
1266
|
-
try {
|
|
1267
|
-
$health = Invoke-RestMethod -Uri "$PublicBaseUrl/health?user=$AssignedUser" -TimeoutSec 10 -ErrorAction Stop
|
|
1268
|
-
if ($health.node_online) { $NodeOnline = $true; break }
|
|
1269
|
-
} catch {}
|
|
1270
|
-
}
|
|
1271
|
-
if ($NodeOnline) {
|
|
1272
|
-
if ($Lang -eq "ru") {
|
|
1273
|
-
Write-Host "[OK] Узел '$AssignedUser' на связи со шлюзом." -ForegroundColor Green
|
|
1274
|
-
} else {
|
|
1275
|
-
Write-Host "[OK] Node '$AssignedUser' is online on the gateway." -ForegroundColor Green
|
|
1276
|
-
}
|
|
1277
|
-
} elseif ($Lang -eq "ru") {
|
|
1278
|
-
Write-Host "[!] Узел '$AssignedUser' не вышел на связь за 30 с." -ForegroundColor Red
|
|
1279
|
-
Write-Host " Интерпретатор автозапуска: $pyExeOnly" -ForegroundColor Yellow
|
|
1280
|
-
Write-Host " Смотрите хвост $agentLog и запустите .\ops\doctor.ps1" -ForegroundColor Yellow
|
|
1281
|
-
Write-Host " Задача 'AntigravityMeshWatchdog' повторит попытку через 5 минут." -ForegroundColor Yellow
|
|
1282
|
-
} else {
|
|
1283
|
-
Write-Host "[!] Node '$AssignedUser' did not come online within 30 s." -ForegroundColor Red
|
|
1284
|
-
Write-Host " Autostart interpreter: $pyExeOnly" -ForegroundColor Yellow
|
|
1285
|
-
Write-Host " Read the tail of $agentLog and run .\ops\doctor.ps1" -ForegroundColor Yellow
|
|
1286
|
-
Write-Host " The 'AntigravityMeshWatchdog' task retries every 5 minutes." -ForegroundColor Yellow
|
|
1287
|
-
}
|