<# .SYNOPSIS Compresses work/ into a ZIP and uploads it to Ka-Note AI endpoint. .PARAMETER Prod Target production server. .PARAMETER Token Bearer token for prod auth. Falls back to $env:KA_NOTE_TOKEN. .PARAMETER Force Overwrite conflicting entities (adds ?force=true). .EXAMPLE .\upload.ps1 .\upload.ps1 -Force .\upload.ps1 -Prod -Token eyJ... #> param( [switch]$Prod, [string]$Token, [switch]$Force ) $ErrorActionPreference = 'Stop' # --- Helpers ----------------------------------------------------------------- function Write-Header([string]$Text) { Write-Host "" Write-Host " $Text" -ForegroundColor White Write-Host (" " + ("-" * $Text.Length)) -ForegroundColor DarkGray } function Write-KV([string]$Key, [string]$Value, [string]$Color = "Gray") { Write-Host (" {0,-14} " -f $Key) -NoNewline -ForegroundColor DarkGray Write-Host $Value -ForegroundColor $Color } function Write-Ok([string]$Msg) { Write-Host " [OK] $Msg" -ForegroundColor Green } function Write-Warn([string]$Msg) { Write-Host " [!!] $Msg" -ForegroundColor Yellow } function Write-Err([string]$Msg) { Write-Host " [!!] $Msg" -ForegroundColor Red } function Write-Hint([string]$Msg) { Write-Host " -> $Msg" -ForegroundColor DarkCyan } # --- Paths ------------------------------------------------------------------- $RepoRoot = Resolve-Path "$PSScriptRoot\..\.." $WorkDir = Join-Path $RepoRoot "work" $ManifestPath = Join-Path $WorkDir "manifest.json" $ZipPath = Join-Path $WorkDir "_upload.zip" if (-not (Test-Path $ManifestPath)) { Write-Err "manifest.json not found in work/. Run .\download.ps1 first." exit 1 } $Manifest = Get-Content $ManifestPath -Raw -Encoding UTF8 | ConvertFrom-Json $LockToken = $Manifest.lockToken if (-not $LockToken) { Write-Err "No lockToken found in manifest.json." exit 1 } # --- Server ------------------------------------------------------------------ if ($Prod) { $BaseUrl = "https://ka-note.azurewebsites.net" $BearerToken = if ($Token) { $Token } elseif ($env:KA_NOTE_TOKEN) { $env:KA_NOTE_TOKEN } else { Write-Host " Acquiring token via MSAL..." -ForegroundColor DarkGray & "$PSScriptRoot\get-token.ps1" } if (-not $BearerToken) { Write-Err "Failed to acquire a Bearer token." exit 1 } $Headers = @{ Authorization = "Bearer $BearerToken"; "Content-Type" = "application/octet-stream" } $Env = "prod" } else { $BaseUrl = "http://localhost:3001" $Headers = @{ "Content-Type" = "application/octet-stream" } $Env = "dev" } # --- Compress work/ → ZIP ---------------------------------------------------- Write-Header "Ka-Note AI Upload" Write-KV "server" $BaseUrl $(if ($Prod) { "Yellow" } else { "Cyan" }) Write-KV "mode" $Env Write-KV "lock" $LockToken "Yellow" Write-KV "force" $Force.IsPresent Write-Host "" Write-Host " Compressing work/..." -ForegroundColor DarkGray if (Test-Path $ZipPath) { Remove-Item $ZipPath -Force } # Use .NET ZipArchive directly to ensure forward-slash entry names (Compress-Archive uses backslashes on Windows) Add-Type -Assembly 'System.IO.Compression' Add-Type -Assembly 'System.IO.Compression.FileSystem' $zipStream = [System.IO.File]::Open($ZipPath, [System.IO.FileMode]::Create) $archive = [System.IO.Compression.ZipArchive]::new($zipStream, [System.IO.Compression.ZipArchiveMode]::Create) Get-ChildItem -Path $WorkDir -Recurse -File | Where-Object { $_.FullName -ne $ZipPath } | ForEach-Object { $entryName = $_.FullName.Substring($WorkDir.Length + 1).Replace('\', '/') $entry = $archive.CreateEntry($entryName, [System.IO.Compression.CompressionLevel]::Optimal) $entryStream = $entry.Open() $fileStream = [System.IO.File]::OpenRead($_.FullName) $fileStream.CopyTo($entryStream) $fileStream.Dispose() $entryStream.Dispose() } $archive.Dispose() $zipStream.Dispose() $ZipSizeKb = [math]::Round((Get-Item $ZipPath).Length / 1KB, 1) Write-KV "zip size" "${ZipSizeKb} KB" Write-Host "" Write-Host " Uploading..." -ForegroundColor DarkGray # --- Upload ------------------------------------------------------------------ $Url = "$BaseUrl/api/ai/upload" if ($Force) { $Url += "?force=true" } try { $Response = Invoke-WebRequest -Uri $Url ` -Method POST -Headers $Headers -InFile $ZipPath -UseBasicParsing Remove-Item $ZipPath -Force $Result = $Response.Content | ConvertFrom-Json Write-Header "Result" Write-KV "accepted" $Result.accepted "Green" Write-KV "skipped" $Result.skipped Write-KV "conflicts" $Result.conflicts.Count Write-Host "" Write-Ok "Upload complete. Lock released." Write-Host "" } catch { if (Test-Path $ZipPath) { Remove-Item $ZipPath -Force } $StatusCode = $_.Exception.Response.StatusCode.value__ $ErrBody = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue Write-Host "" if ($StatusCode -eq 409) { Write-Header "Version Conflicts (409)" $ErrBody.conflicts | ForEach-Object { Write-Host (" {0,-12} {1}" -f $_.entityType, $_.entityId) -ForegroundColor White -NoNewline Write-Host " client v$($_.clientVersion) server v$($_.serverVersion)" -ForegroundColor DarkGray } Write-Host "" Write-Warn "Nothing was applied." Write-Hint "Re-run with -Force to overwrite server versions." } elseif ($StatusCode -eq 401) { Write-Err "401 Unauthorized - lock token invalid or expired." Write-Hint "Run .\download.ps1 again to acquire a new lock." } elseif ($StatusCode -eq 423) { Write-Err "423 Locked - no active lock for this token." } else { Write-Err "HTTP $StatusCode - $($_.Exception.Message)" } Write-Host "" exit 1 }