[CmdletBinding()] param() $ErrorActionPreference = "Stop" $bundleUrl = "https://git.okk.cool/purp1e/zed-sync/raw/branch/master/zed-settings-sync.json" $utf8 = New-Object System.Text.UTF8Encoding($false) function Require-Command([string]$Name, [string]$InstallHint) { if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { throw "$Name is required. Install it first: $InstallHint" } } function Test-AllowedPath([string]$Path) { $fixed = @( "settings.json", "keymap.json", "tasks.json", "debug.json", "scripts/toggle-file-scan-exclusions.mjs", "scripts/file-exclusions.json", "scripts/zed-settings-sync.mjs" ) $segments = $Path.Split("/") if ( $Path.StartsWith("/") -or $Path.Contains("\") -or $segments -contains "" -or $segments -contains "." -or $segments -contains ".." ) { return $false } return $fixed -contains $Path -or $Path.StartsWith("snippets/") -or $Path.StartsWith("themes/") } function Write-Atomic([string]$Path, [string]$Contents) { $directory = Split-Path $Path -Parent New-Item -ItemType Directory -Force -Path $directory | Out-Null $temporary = Join-Path $directory ".$([IO.Path]::GetFileName($Path)).$PID.tmp" try { [IO.File]::WriteAllText($temporary, $Contents, $utf8) Move-Item -LiteralPath $temporary -Destination $Path -Force } finally { Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue } } Require-Command "node" "winget install OpenJS.NodeJS.LTS" if ($env:ZED_SETTINGS_BUNDLE_PATH) { $bundleSource = [IO.File]::ReadAllText($env:ZED_SETTINGS_BUNDLE_PATH, [Text.Encoding]::UTF8) } else { Write-Host "Downloading the public Zed configuration bundle..." $bundleSource = (Invoke-WebRequest -UseBasicParsing -Uri $bundleUrl).Content } try { $bundle = $bundleSource | ConvertFrom-Json } catch { throw "The downloaded bundle contains invalid JSON: $($_.Exception.Message)" } $properties = @($bundle.PSObject.Properties) foreach ($required in @("settings.json", "keymap.json", "tasks.json")) { if (-not ($properties.Name -contains $required)) { throw "The bundle is missing $required." } } foreach ($property in $properties) { if (-not (Test-AllowedPath $property.Name) -or $property.Value -isnot [string]) { throw "Invalid bundle file: $($property.Name)" } } $target = Join-Path $env:APPDATA "Zed" $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $backup = Join-Path $target "backups\bootstrap-$timestamp" New-Item -ItemType Directory -Force -Path $target, $backup, (Join-Path $target "scripts") | Out-Null foreach ($property in $properties) { $existing = Join-Path $target ($property.Name.Replace("/", "\")) if (Test-Path -LiteralPath $existing) { $backupPath = Join-Path $backup ($property.Name.Replace("/", "\")) New-Item -ItemType Directory -Force -Path (Split-Path $backupPath -Parent) | Out-Null Copy-Item -LiteralPath $existing -Destination $backupPath -Force } } $toggleScript = (Join-Path $target "scripts\toggle-file-scan-exclusions.mjs").Replace("\", "/") $syncScript = (Join-Path $target "scripts\zed-settings-sync.mjs").Replace("\", "/") foreach ($property in $properties) { $destination = Join-Path $target ($property.Name.Replace("/", "\")) $contents = [string]$property.Value if ($property.Name -eq "tasks.json") { $contents = $contents.Replace("__TOGGLE_SCRIPT__", $toggleScript).Replace("__SYNC_SCRIPT__", $syncScript) } Write-Atomic $destination $contents } $statePath = Join-Path $target "scripts\settings-sync.json" if (-not (Test-Path -LiteralPath $statePath)) { $state = [ordered]@{ gist_id = "" last_synced_hash = "" auto_sync = $false interval_minutes = 15 last_run_at = "" last_result = "" } | ConvertTo-Json Write-Atomic $statePath "$state`n" } else { $existingState = Get-Content -LiteralPath $statePath -Raw | ConvertFrom-Json $state = [ordered]@{ gist_id = if ($existingState.gist_id -is [string]) { $existingState.gist_id } else { "" } last_synced_hash = if ($existingState.last_synced_hash -is [string]) { $existingState.last_synced_hash } else { "" } auto_sync = $existingState.auto_sync -eq $true interval_minutes = if ($existingState.interval_minutes -is [int] -and $existingState.interval_minutes -ge 5) { $existingState.interval_minutes } else { 15 } last_run_at = if ($existingState.last_run_at -is [string]) { $existingState.last_run_at } else { "" } last_result = if ($existingState.last_result -is [string]) { $existingState.last_result } else { "" } } | ConvertTo-Json Write-Atomic $statePath "$state`n" } Write-Host "" Write-Host "Installed Zed settings to $target" Write-Host "Previous settings were backed up to $backup" Write-Host "GitHub and Gist setup are optional and did not block installation." if ($env:ZED_SYNC_SKIP_SETUP -ne "1") { try { $answer = Read-Host "Run the GitHub sync setup now? [Y/n]" if ([string]::IsNullOrWhiteSpace($answer) -or $answer -match "^[Yy]") { & node $syncScript setup if ($LASTEXITCODE -ne 0) { Write-Warning "Sync setup did not complete. Local installation is still valid; run the setup Task from Zed later." } } } catch { Write-Warning "Could not start interactive sync setup. Run 'Zed Settings: Set Up / Reconfigure Sync...' from Zed later." } } Write-Host "Restart or reload Zed to use the installed configuration."