This repository was archived by the owner on Sep 19, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathinstall.ps1
More file actions
417 lines (361 loc) · 28.7 KB
/
Copy pathinstall.ps1
File metadata and controls
417 lines (361 loc) · 28.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# AETHER_01 — One-Click Installer
# Supports 14+ MCP-compatible editors and IDEs with an interactive selection menu.
#
# Usage:
# Local: pwsh -ExecutionPolicy Bypass -File install.ps1
# Remote: pwsh -c "irm https://raw.githubusercontent.com/foursecondfivefour/aether-mcp-server/main/install.ps1 | iex"
# Silent: pwsh -File install.ps1 -Targets cursor,claude -Silent
[CmdletBinding()]
param(
[string[]]$Targets = @(""),
[string]$BinaryPath = "",
[string]$ReleaseTag = "v1.0.0",
[switch]$Force,
[switch]$NoAdminWarning,
[Switch]$Silent,
[string]$DownloadDir = "$env:LOCALAPPDATA\AetherMCP",
[int]$RetryCount = 3,
[int]$RetryDelaySeconds = 5,
[int]$DownloadTimeoutSeconds = 300
)
# ════════════════════════════════════════════════════════════════════════
# Boot-level safety
# ════════════════════════════════════════════════════════════════════════
$script:ErrorActionPreference = "Stop"
$script:ProgressPreference = "SilentlyContinue"
$script:errors = [System.Collections.Generic.List[string]]::new()
$script:warnings = [System.Collections.Generic.List[string]]::new()
$script:exitCode = 0
function Register-Error { param($msg) $script:errors.Add($msg) ; $script:exitCode = [Math]::Max($script:exitCode, 1) }
function Register-Warning { param($msg) $script:warnings.Add($msg) }
# ════════════════════════════════════════════════════════════════════════
# Constants
# ════════════════════════════════════════════════════════════════════════
$REPO_OWNER = "foursecondfivefour"
$REPO_NAME = "aether-mcp-server"
$REPO_URL = "https://github.com/$REPO_OWNER/$REPO_NAME"
$RAW_URL = "https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main"
$API_URL = "https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$ReleaseTag"
$EXE_NAME = "aether-mcp-server.exe"
$MIN_EXE_SIZE_BYTES = 10240
# ════════════════════════════════════════════════════════════════════════
# Output helpers
# ════════════════════════════════════════════════════════════════════════
function Write-Step { param($msg) Write-Host " [OK] $msg" -ForegroundColor Green }
function Write-Warn { param($msg) Write-Host " [!!] $msg" -ForegroundColor Yellow }
function Write-Info { param($msg) Write-Host " [..] $msg" -ForegroundColor Cyan }
function Write-Err { param($msg) Write-Host " [XX] $msg" -ForegroundColor Red ; Register-Error $msg }
function Write-Dbg { param($msg) Write-Debug $msg }
# Detect execution mode
$isPiped = (-not $PSScriptRoot -or $PSScriptRoot -eq "")
if (-not $PSVersionTable.PSVersion.Major -ge 6) {
Write-Err "Requires PowerShell 7+ (detected: $($PSVersionTable.PSVersion))"
Write-Host " winget install Microsoft.PowerShell"
exit 2
}
if (-not $IsWindows -and $PSVersionTable.Platform -ne "Win32NT") {
Write-Err "Windows 10/11 x64 required"
exit 5
}
if (-not $env:USERPROFILE) { Write-Err "USERPROFILE not set"; exit 6 }
if (-not $env:LOCALAPPDATA) { $env:LOCALAPPDATA = "$env:USERPROFILE\AppData\Local"; Write-Warn "LOCALAPPDATA fallback: $env:LOCALAPPDATA" }
try { $principal = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()); $isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } catch { $isAdmin = $false }
try { $Host.UI.RawUI.WindowTitle = "AETHER_01 — Installer" } catch { }
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " AETHER_01 — Windows MCP Server Installer" -ForegroundColor Cyan
Write-Host " PS $($PSVersionTable.PSVersion) — $(Get-Date -Format 'yyyy-MM-dd HH:mm')" -ForegroundColor DarkGray
if ($isPiped) { Write-Host " Source: remote (irm | iex)" -ForegroundColor DarkGray }
else { Write-Host " Source: local file" -ForegroundColor DarkGray }
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host ""
if (-not $isAdmin -and -not $NoAdminWarning) {
Write-Warn "Not Administrator — MCP config will be added, but tools will fail without admin rights"
Write-Host " Suppress: install.ps1 -NoAdminWarning"
Write-Host ""
}
# ════════════════════════════════════════════════════════════════════════
# Utility functions (retry, safe io, disk check)
# ════════════════════════════════════════════════════════════════════════
function Write-File-Safe { param([string]$LiteralPath, [string]$Content, [string]$Description)
$dir = [IO.Path]::GetDirectoryName($LiteralPath)
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir -ErrorAction Stop | Out-Null }
try { [IO.File]::WriteAllText($LiteralPath, $Content, [Text.UTF8Encoding]::new($false)); return } catch { Write-Dbg "Write to '$LiteralPath' failed: $_" }
$tmp = "$LiteralPath.aether-tmp-$(Get-Random)"
try { [IO.File]::WriteAllText($tmp, $Content, [Text.UTF8Encoding]::new($false)); Move-Item $tmp $LiteralPath -Force -ErrorAction Stop }
catch { Remove-Item $tmp -Force -ErrorAction SilentlyContinue; throw "Cannot write $Description to '$LiteralPath': $_" }
}
function Read-File-Safe { param([string]$LiteralPath)
if (-not (Test-Path $LiteralPath)) { return $null }
try { $raw = Get-Content $LiteralPath -Raw -Encoding UTF8 -ErrorAction Stop; if ([string]::IsNullOrWhiteSpace($raw)) { $null } else { $raw } } catch { $null }
}
function Backup-File { param([string]$LiteralPath, [string]$Reason)
if (-not (Test-Path $LiteralPath)) { return }
$bkp = "$LiteralPath.aether-backup-$((Get-Date).ToString('yyyyMMdd-HHmmss'))"
try { Copy-Item $LiteralPath $bkp -Force -ErrorAction Stop; Write-Warn "$Reason — $bkp" } catch { Write-Dbg "Backup failed: $_" }
}
function Test-DiskSpace { param([string]$Path, [long]$Bytes)
$drv = [IO.Path]::GetPathRoot($Path); if (-not $drv) { return $true }
try { $di = [IO.DriveInfo]::new($drv); if ($di.AvailableFreeSpace -lt $Bytes) { Write-Err "Disk full on $drv"; return $false }; return $true } catch { return $true }
}
# ════════════════════════════════════════════════════════════════════════
# Step 1: Find or download binary (unchanged from previous version)
# ════════════════════════════════════════════════════════════════════════
$exePath = $null
if ($BinaryPath) {
if (-not (Test-Path -LiteralPath $BinaryPath -PathType Leaf)) { Write-Err "Not a file: $BinaryPath"; exit 7 }
$exePath = (Resolve-Path -LiteralPath $BinaryPath -ErrorAction Stop).Path; Write-Step "Binary: $exePath"
}
if (-not $exePath -and -not $isPiped -and $PSScriptRoot) {
foreach ($p in @("$PSScriptRoot\target\debug\$EXE_NAME","$PSScriptRoot\target\release\$EXE_NAME")) { if (Test-Path $p) { $exePath = $p; Write-Step "Local build: $exePath"; break } }
if (-not $exePath) { try { $f = Get-ChildItem $PSScriptRoot -Recurse -Filter $EXE_NAME -Depth 5 -ErrorAction SilentlyContinue | ? { $_ -match "\\target\\(debug|release)\\" } | select -First 1; if ($f) { $exePath = $f.FullName; Write-Step "Deep: $exePath" } } catch {} }
}
if (-not $exePath) {
Write-Warn "Downloading from GitHub..."
if (-not (Test-DiskSpace $DownloadDir (120*1MB))) { exit 9 }
try { New-Item -ItemType Directory -Force -Path $DownloadDir -ErrorAction Stop | Out-Null } catch { Write-Err "Cannot create $DownloadDir"; exit 10 }
$dlPath = Join-Path $DownloadDir $EXE_NAME
$dlUrl = "$REPO_URL/releases/download/$ReleaseTag/$EXE_NAME"
Write-Host " $dlUrl -> $dlPath" -ForegroundColor DarkGray
Write-Host " (~110MB — please wait)" -ForegroundColor DarkGray
for ($a=1; $a -le $RetryCount; $a++) {
try {
if ($a -gt 1) { Write-Host " Retry $a/$RetryCount..." -ForegroundColor DarkGray; Start-Sleep $RetryDelaySeconds }
Remove-Item $dlPath -Force -ErrorAction SilentlyContinue
Invoke-WebRequest -Uri $dlUrl -OutFile $dlPath -TimeoutSec $DownloadTimeoutSeconds -ErrorAction Stop
if (-not (Test-Path $dlPath)) { throw "Not written to disk" }
$sz = (Get-Item $dlPath).Length
if ($sz -lt $MIN_EXE_SIZE_BYTES) { $bc = try { Get-Content $dlPath -Raw -TotalCount 200 -EA SilentlyContinue } catch { "unreadable" }; Remove-Item $dlPath -Force -EA SilentlyContinue; throw "Too small: $sz bytes ($bc)" }
try { $hdr = [IO.File]::ReadAllBytes($dlPath); if ($hdr[0] -ne 0x4D -or $hdr[1] -ne 0x5A) { Remove-Item $dlPath -Force -EA SilentlyContinue; throw "Not a valid .exe (no MZ header)" } } catch [IO.IOException] { throw "Locked file: $_" }
$exePath = $dlPath; Write-Step "Downloaded: $exePath ($('{0:N0}' -f ($sz/1MB)) MB)"; break
} catch { if ($a -ge $RetryCount) { Remove-Item $dlPath -Force -EA SilentlyContinue; Write-Err "Download failed after $RetryCount attempts: $_"; Write-Host " Build locally: git clone $REPO_URL && cargo build --release"; exit 3 } }
}
}
if (-not $exePath -or -not (Test-Path $exePath)) { Write-Err "No binary"; exit 4 }
# ════════════════════════════════════════════════════════════════════════
# Step 2: Create .env
# ════════════════════════════════════════════════════════════════════════
$envDir = [IO.Path]::GetDirectoryName($exePath); if (-not $envDir) { $envDir = "." }
$envFile = Join-Path $envDir ".env"
$envContent = @"
# AETHER_01 — Feature Gates
# 0=disabled 1=enabled
AETHER_BCD_EDIT=0
AETHER_HAL_CONFIG=0
AETHER_OFFLINE_REGISTRY=0
AETHER_DLL_INJECT=0
AETHER_TOKEN_MANIPULATION=0
AETHER_LSA_SECRETS=0
"@
if (Test-Path $envFile) { if ($Force) { try { Write-File-Safe $envFile $envContent ".env"; Write-Step ".env overwritten" } catch { Write-Err ".env write failed: $_" } } else { Write-Info ".env exists — skip. Use --Force to overwrite." } }
else { try { Write-File-Safe $envFile $envContent ".env"; Write-Step ".env created: $envFile" } catch { Write-Err ".env create failed: $_" } }
# ════════════════════════════════════════════════════════════════════════
# Step 3: MCP config engine
# ════════════════════════════════════════════════════════════════════════
$mcpEntry = @{ command = $exePath; env = @{ RUST_LOG = "info" } }
function Parse-McpConfig { param([string]$LiteralPath)
$raw = Read-File-Safe $LiteralPath; if (-not $raw) { return $null }
try { return $raw | ConvertFrom-Json -AsHashtable -NoEnumerate -ErrorAction Stop } catch { return $null }
}
function Write-McpConfig {
param([string]$ConfigPath,[string]$ClientName,[string]$RootKey,[bool]$IsYaml=$false,[string]$YamlIndent="")
Write-Host " --- $ClientName ---" -ForegroundColor Magenta
$configDir = [IO.Path]::GetDirectoryName($ConfigPath)
if (-not (Test-Path $configDir)) { try { New-Item -ItemType Directory -Force -Path $configDir -ErrorAction Stop | Out-Null } catch { Write-Err "Cannot create dir '$configDir' for $ClientName`: $_"; return } }
# ── YAML path (Continue.dev, Goose) ────────────────────────────────
if ($IsYaml) {
if (Test-Path $ConfigPath) {
$existingYaml = Read-File-Safe $ConfigPath
if ($existingYaml -match "aether-01") {
if ($Force) { Write-Warn "${ClientName}: already configured — overwriting (--Force)" }
else { Write-Info "${ClientName}: already configured — skip. Use --Force to overwrite."; return }
}
}
# Append aether entry
$yamlEntry = @"
$YamlIndent aether-01:
$YamlIndent command: $($exePath -replace '\\','/')
$YamlIndent env:
$YamlIndent RUST_LOG: info
"@
if (Test-Path $ConfigPath) {
$content = Read-File-Safe $ConfigPath
if ($content -match "mcpServers:") {
$content = $content -replace "(mcpServers:\r?\n)", "`$1$yamlEntry"
} else {
$content = "mcpServers:`n$yamlEntry`n$content"
}
try { Write-File-Safe $ConfigPath $content "YAML config for $ClientName" } catch { Write-Err "Cannot write YAML for $ClientName`: $_"; return }
} else {
try { Write-File-Safe $ConfigPath "mcpServers:`n$yamlEntry" "YAML config for $ClientName" } catch { Write-Err "Cannot create YAML for $ClientName`: $_"; return }
}
Write-Step "${ClientName}: added to YAML config"
return
}
# ── JSON path (all others) ─────────────────────────────────────────
$existing = $null
if (Test-Path $ConfigPath) { $existing = Parse-McpConfig $ConfigPath; if ($null -eq $existing) { Backup-File $ConfigPath "Invalid JSON" } }
if ($null -eq $existing) {
$newCfg = @{ $RootKey = @{ "aether-01" = $mcpEntry } }
try { Write-File-Safe $ConfigPath ($newCfg | ConvertTo-Json -Depth 10 -Compress) "JSON for $ClientName"; Write-Step "${ClientName}: created" }
catch { Write-Err "Write $ClientName` failed: $_" }
return
}
if (-not $existing.ContainsKey($RootKey) -or $existing[$RootKey] -isnot [hashtable]) { $existing[$RootKey] = @{} }
if ($existing[$RootKey].ContainsKey("aether-01")) {
if ($Force) { $existing[$RootKey]["aether-01"] = $mcpEntry; try { Write-File-Safe $ConfigPath ($existing | ConvertTo-Json -Depth 10 -Compress) "JSON for $ClientName"; Write-Step "${ClientName}: updated (--Force)" } catch { Write-Err "Update $ClientName` failed: $_" } }
else { Write-Info "${ClientName}: already installed — skip. Use --Force to overwrite." }
} else {
$existing[$RootKey]["aether-01"] = $mcpEntry
try { Write-File-Safe $ConfigPath ($existing | ConvertTo-Json -Depth 10 -Compress) "JSON for $ClientName"; Write-Step "${ClientName}: added" }
catch { Write-Err "Write $ClientName` failed: $_" }
}
}
# ════════════════════════════════════════════════════════════════════════
# Step 4: Environment catalog (14+ editors/IDEs/plugins)
# ════════════════════════════════════════════════════════════════════════
$ALL_ENVIRONMENTS = @(
# ─── AI-first editors ──────────────────────────────────────────────
@{ Key="cursor"; Dir="$env:USERPROFILE\.cursor"; Config="$env:USERPROFILE\.cursor\mcp.json"; Root="mcpServers"; Name="Cursor (AI-first IDE)"; Icon="🖥️" },
@{ Key="windsurf"; Dir="$env:USERPROFILE\.codeium\windsurf"; Config="$env:USERPROFILE\.codeium\windsurf\mcp_config.json"; Root="mcpServers"; Name="Windsurf (Codeium Cascade)"; Icon="🌊" },
@{ Key="claude-dt"; Dir="$env:APPDATA\Claude"; Config="$env:APPDATA\Claude\claude_desktop_config.json"; Root="mcpServers"; Name="Claude Desktop (Anthropic)"; Icon="🧠" },
# ─── Microsoft ecosystem ───────────────────────────────────────────
@{ Key="vscode-mcp"; Dir="$env:APPDATA\Code"; Config="$env:APPDATA\Code\User\globalStorage\anthropic.claude-mcp\mcp.json"; Root="mcpServers"; Name="VS Code (MCP extension)"; Icon="🔵" },
@{ Key="vscode-copilot";Dir="$env:APPDATA\Code"; Config="$env:USERPROFILE\.vscode\mcp.json"; Root="servers"; Name="VS Code (GitHub Copilot)"; Icon="🤖" },
# ─── Open-source agents ────────────────────────────────────────────
@{ Key="cline"; Dir="$env:APPDATA\Code"; Config="$env:APPDATA\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json"; Root="mcpServers"; Name="Cline (VS Code Agent)"; Icon="🤖" },
@{ Key="continue"; Dir="$env:USERPROFILE\.continue"; Config="$env:USERPROFILE\.continue\config.yaml"; Root="mcpServers"; Name="Continue.dev (OSS AI Assistant)"; Icon="🔄"; Yaml=$true; Indent=" " },
# ─── Terminal / CLI ────────────────────────────────────────────────
@{ Key="claude-code"; Dir="$env:USERPROFILE"; Config="$env:USERPROFILE\.claude.json"; Root="mcpServers"; Name="Claude Code (Anthropic CLI)"; Icon="⌨️" },
@{ Key="gemini-cli"; Dir="$env:USERPROFILE"; Config="$env:USERPROFILE\.gemini\mcp.json"; Root="mcpServers"; Name="Gemini CLI (Google)"; Icon="🌐" },
# ─── High-performance editors ──────────────────────────────────────
@{ Key="zed"; Dir="$env:USERPROFILE\.config\zed"; Config="$env:USERPROFILE\.config\zed\settings.json"; Root="context_servers"; Name="Zed (Rust-native IDE)"; Icon="⚡" },
# ─── JetBrains ecosystem ───────────────────────────────────────────
@{ Key="jetbrains-idea";Dir="$env:APPDATA\JetBrains\IntelliJIdea2025.1"; Config="$env:APPDATA\JetBrains\IntelliJIdea2025.1\options\mcp-settings.xml"; Root="mcpServers"; Name="IntelliJ IDEA (JetBrains)"; Icon="🧩" },
@{ Key="jetbrains-pc"; Dir="$env:APPDATA\JetBrains\PyCharm2025.1"; Config="$env:APPDATA\JetBrains\PyCharm2025.1\options\mcp-settings.xml"; Root="mcpServers"; Name="PyCharm (JetBrains)"; Icon="🐍" },
@{ Key="jetbrains-ws"; Dir="$env:APPDATA\JetBrains\WebStorm2025.1"; Config="$env:APPDATA\JetBrains\WebStorm2025.1\options\mcp-settings.xml"; Root="mcpServers"; Name="WebStorm (JetBrains)"; Icon="🌐" },
# ─── Other environments ────────────────────────────────────────────
@{ Key="goose"; Dir="$env:USERPROFILE\.config\goose"; Config="$env:USERPROFILE\.config\goose\config.yaml"; Root="extensions"; Name="Goose (Block/Square Agent)"; Icon="🦆"; Yaml=$true; Indent="" }
)
# ════════════════════════════════════════════════════════════════════════
# Step 5: Interactive menu system
# ════════════════════════════════════════════════════════════════════════
function Show-Menu {
param([array]$Options, [string]$Title, [string]$Prompt)
Write-Host ""
Write-Host $Title -ForegroundColor Cyan
Write-Host ("─" * 65) -ForegroundColor DarkGray
$selected = @{}
$optionsMap = @{}
for ($i = 0; $i -lt $Options.Count; $i++) {
$num = $i + 1
$opt = $Options[$i]
$key = $opt.Key
$name = $opt.Name
$found = Test-Path -LiteralPath $opt.Dir -ErrorAction SilentlyContinue
$icon = if ($found) { $opt.Icon } else { "⬜" }
$status = if ($found) { "FOUND" } else { "not installed" }
$sc = if ($found) { "Green" } else { "DarkGray" }
$optionsMap[$num] = $opt
Write-Host " [$num] $icon $name" -ForegroundColor White -NoNewline
Write-Host " ($status)" -ForegroundColor $sc
}
Write-Host ("─" * 65) -ForegroundColor DarkGray
Write-Host " [A] ALL — select all found environments"
Write-Host " [S] SKIP — don't install into any editor"
Write-Host " [Q] QUIT — abort installation entirely"
Write-Host ("─" * 65) -ForegroundColor DarkGray
Write-Host ""
Write-Host $Prompt -ForegroundColor Yellow -NoNewline
Write-Host " " -NoNewline
$input = (Read-Host).Trim().ToUpperInvariant()
$result = @()
switch ($input) {
"Q" { Write-Host ""; Write-Err "Installation cancelled by user"; exit 0 }
"S" { Write-Host ""; Write-Info "Skipping editor installation — binary and .env are ready"; return @() }
"A" { foreach ($idx in $optionsMap.Keys | Sort-Object) { $opt = $optionsMap[$idx]; if (Test-Path $opt.Dir) { $result += $opt } }; break }
default {
foreach ($part in ($input -split '[\s,;]+')) {
$num = try { [int]$part } catch { 0 }
if ($num -gt 0 -and $optionsMap.ContainsKey($num)) { $result += $optionsMap[$num] }
}
}
}
if ($result.Count -eq 0) {
Write-Warn "No valid selections — installing only binary and .env"
}
return $result
}
# ════════════════════════════════════════════════════════════════════════
# Step 6: Dispatch: silent mode or interactive menu
# ════════════════════════════════════════════════════════════════════════
$toInstall = @()
if ($Silent -or ($Targets.Count -gt 0 -and $Targets[0] -ne "")) {
# ── Silent / CLI mode ──────────────────────────────────────────────
$targetSet = [Collections.Generic.HashSet[string]]::new()
$autoMode = ($Targets -contains "auto" -or $Targets.Count -eq 0 -or ($Targets.Count -eq 1 -and $Targets[0] -eq ""))
if ($Targets -contains "all" -or $autoMode) {
foreach ($env in $ALL_ENVIRONMENTS) { $targetSet.Add($env.Key) | Out-Null }
}
foreach ($t in ($Targets | Where-Object { $_ -ne "auto" -and $_ -ne "all" -and $_ -ne "" })) { $targetSet.Add($t) | Out-Null }
foreach ($env in $ALL_ENVIRONMENTS) {
if (($autoMode -or $targetSet.Contains($env.Key)) -and (Test-Path $env.Dir)) { $toInstall += $env }
}
}
else {
# ── Interactive menu ───────────────────────────────────────────────
$menuOptions = $ALL_ENVIRONMENTS | Where-Object {
# Filter: only show items where the parent directory structure makes sense
# (JetBrains versions are wild — show all variants; users see which exist)
$true
}
$toInstall = @(Show-Menu $menuOptions "Select editors to install AETHER_01 into:" "Enter numbers, 'A' for all, 'S' to skip, 'Q' to quit:")
}
# ════════════════════════════════════════════════════════════════════════
# Step 7: Install into chosen environments
# ════════════════════════════════════════════════════════════════════════
$installedCount = 0
$skippedCount = 0
$failedCount = 0
foreach ($env in $toInstall) {
if (-not (Test-Path -LiteralPath $env.Dir)) {
Write-Host " [--] $($env.Icon) $($env.Name): not found — skipping" -ForegroundColor DarkGray
$skippedCount++
continue
}
try {
Write-McpConfig -ConfigPath $env.Config -ClientName "$($env.Icon) $($env.Name)" -RootKey $env.Root `
-IsYaml:($env.Yaml -eq $true) -YamlIndent:($env.Indent)
$installedCount++
}
catch {
Write-Err "$($env.Name): $_"
$failedCount++
}
}
# ════════════════════════════════════════════════════════════════════════
# Step 8: Summary
# ════════════════════════════════════════════════════════════════════════
Write-Host ""
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " Installation Complete" -ForegroundColor Cyan
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " Binary: $exePath" -ForegroundColor White
Write-Host " .env: $envFile" -ForegroundColor White
Write-Host " Installed: $installedCount environment(s)" -ForegroundColor Green
if ($skippedCount -gt 0) { Write-Host " Skipped: $skippedCount" -ForegroundColor DarkGray }
if ($failedCount -gt 0) { Write-Host " Failed: $failedCount" -ForegroundColor Red }
if ($script:warnings.Count -gt 0) { Write-Host ""; Write-Host " Warnings:" -ForegroundColor Yellow; foreach ($w in $script:warnings) { Write-Host " - $w" -ForegroundColor DarkGray } }
if ($script:errors.Count -gt 0) { Write-Host ""; Write-Host " Errors:" -ForegroundColor Red; foreach ($e in $script:errors) { Write-Host " - $e" -ForegroundColor Red } }
Write-Host ""
Write-Host " Next steps:" -ForegroundColor Cyan
Write-Host " 1. Restart your editor/IDE" -ForegroundColor White
Write-Host " 2. Check AETHER_01 in the MCP panel" -ForegroundColor White
Write-Host " 3. For JetBrains: Settings → Tools → MCP → Refresh" -ForegroundColor White
Write-Host " 4. For Continue.dev: restart the extension" -ForegroundColor White
Write-Host ""
if (-not $isAdmin -and -not $NoAdminWarning) {
Write-Host " REMINDER: Run editor as Administrator." -ForegroundColor Magenta
Write-Host ""
}
Write-Host "============================================================" -ForegroundColor Cyan
exit $script:exitCode