# SocialLive installer for Windows 10/11 # # irm https://raw.githubusercontent.com/AxionAura/social-live/main/install.ps1 | iex # # Provisions a user-local Node.js + FFmpeg runtime (no admin rights needed), # downloads and builds SocialLive into %USERPROFILE%\.social-live, registers a # Scheduled Task so the dashboard starts with Windows, and opens the browser. # # Flags: -Uninstall -Port 3000 -Dir "C:\path" -NoService -Update param( [switch]$Uninstall, [switch]$Update, [switch]$NoService, [int]$Port = 3000, [string]$Dir = "$env:USERPROFILE\.social-live" ) # ══ AxionInstaller v1 (auto-embedded — edit AxionInstaller/lib instead) ══ # ══════════════════════════════════════════════════════════════════ # AxionInstaller — PowerShell UI library v1.0 · AxionAura # Branded terminal UI for Windows installers (Windows 10/11). # Design tokens: indigo #6366f1 · violet #8b5cf6 · purple #a855f7 · # fuchsia #d946ef · bg #06090f · fg #e6edf3 · muted #8b949e # # Public API: # Initialize-Axion [Tagline] # Invoke-AxionStep -Label "..." -Script { ... } (atom spinner) # Write-AxionInfo / Ok / Warn / Fail "msg" # Write-AxionBanner # Complete-Axion -Lines "a","b" # Rules: # • Call Initialize-Axion first. Non-interactive hosts fall back # to plain logs automatically (CI safe). # ══════════════════════════════════════════════════════════════════ $script:AxProject = 'AxionInstaller' $script:AxTagline = 'Open source by default. Free for everyone.' $script:AxTty = $false $script:AxFrameIx = 0 $script:AxEsc = [char]27 $script:AxIndigo = "$esc[38;2;99;102;241m" $script:AxViolet = "$esc[38;2;139;92;246m" $script:AxPurple = "$esc[38;2;168;85;247m" $script:AxFuchsia= "$esc[38;2;217;70;239m" $script:AxFg = "$esc[38;2;230;237;243m" $script:AxMuted = "$esc[38;2;139;148;158m" $script:AxDIM = "$esc[38;2;48;54;61m" $script:AxOk = "$esc[38;2;63;185;80m" $script:AxErr = "$esc[38;2;248;81;73m" $script:AxBold = "$esc[1m" $script:AxReset = "$esc[0m" $script:AxFrames = @( " ·────────·`n ╱ ╲`n ●────┼──────●`n ╲ ╱`n ·────────·", " ·───●────·`n ╱ │ ╲`n ·─────┼─────●`n ╲ │ ╱`n ·────────·", " ·──────·─·`n ╱ │ ╲`n ·──────┼────●`n ╲ │ ╱`n ·──●─────·", " ·──────·─●`n ╱ │ ╲`n ·──────┼────·`n ╲ │ ╱`n ·──●─────·", " ·──●─────·`n ╱ │ ╲`n ·─────┼─────·`n ╲ │ ╱`n ·────────●·", " ·────────·`n ╱ ● ╲`n ●─────┼─────·`n ╲ │ ╱`n ·────────·" ) $script:AxSpin = '⠋⠙⠹⠸⠼⠴⠦⠧' function Initialize-Axion { param([string]$Project = 'AxionInstaller', [string]$Tagline = 'Open source by default. Free for everyone.') $script:AxProject = $Project $script:AxTagline = $Tagline $script:AxTty = $false try { if (-not [Console]::IsOutputRedirected) { # VT-সম্ভাবনা প্রি-চেক: এই console যদি VT রেন্ডারই না করতে পারে, # probe-ও করা হবে না (probe-এর [6n লেখাটা তখন স্ক্রিনে আসত) $vtLikely = $false if ($env:WT_SESSION) { $vtLikely = $true } elseif ($PSVersionTable.PSVersion.Major -ge 7) { $vtLikely = $true } else { try { $vt = (Get-ItemProperty 'HKCU:\Console' -Name VirtualTerminalLevel -ErrorAction Stop).VirtualTerminalLevel if ($vt -eq 1) { $vtLikely = $true } } catch {} } if (-not $vtLikely) { return } # conhost-এ ANSI/VT enable — Windows Terminal-এ এটা ইতিমধ্যেই থাকে if (-not ('Win32.AxK32' -as [type])) { Add-Type -MemberDefinition @' [DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr GetStdHandle(int nStdHandle); [DllImport("kernel32.dll", SetLastError=true)] public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); [DllImport("kernel32.dll", SetLastError=true)] public static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); '@ -Name 'AxK32' -Namespace 'Win32' | Out-Null } $h = [Win32.AxK32]::GetStdHandle(-11) $mode = [uint32]0 if (-not [Win32.AxK32]::GetConsoleMode($h, [ref]$mode)) { return } if (-not [Win32.AxK32]::SetConsoleMode($h, $mode -bor 0x0004)) { return } # 0x0004 = ENABLE_VT # ধাপ ২: আসল প্রমাণ চাই — DSR probe (cursor position report এলেই VT কাজ করছে) [Console]::Write("$esc[6n") $sw = [Diagnostics.Stopwatch]::StartNew() $resp = '' while ($sw.ElapsedMilliseconds -lt 600) { if ([Console]::KeyAvailable) { $resp += [Console]::ReadKey($true).KeyChar if ($resp -match '\d+;\d+R$') { break } } else { Start-Sleep -Milliseconds 20 } } if ($resp -match '\d+;\d+R') { $script:AxTty = $true } else { # VT ভাঙা — console mode restore করে প্লেইন মোডে নামো [Win32.AxK32]::SetConsoleMode($h, $mode) | Out-Null $script:AxTty = $false } } } catch { $script:AxTty = $false } # non-TTY/CI হলে সব রঙ খালি — প্লেইন লগ নিশ্চিত if (-not $script:AxTty) { foreach ($v in 'Indigo','Violet','Purple','Fuchsia','Fg','Muted','Ok','Err','Bold','Reset','Dim') { Set-Variable -Name "Ax$v" -Value '' -Scope Script } } } function Write-AxionInfo([string]$m) { if ($AxTty) { Write-Host "$AxViolet●$AxFg $m$AxReset" } else { Write-Host "● $m" } } function Write-AxionOk([string]$m) { if ($AxTty) { Write-Host "$AxOk✔$AxFg $m$AxReset" } else { Write-Host "✔ $m" } } function Write-AxionWarn([string]$m) { if ($AxTty) { Write-Host "$AxFuchsia▲$AxFg $m$AxReset" } else { Write-Host "▲ $m" } } function Write-AxionFail([string]$m) { if ($AxTty) { Write-Host "$AxErr✘$AxFg $m$AxReset" } else { Write-Host "✘ $m" } } function Write-AxionBanner { if (-not $AxTty) { Write-Host ("═" * 32); Write-Host " $AxProject — $AxTagline"; Write-Host ("═" * 32); return } Write-Host @" $AxDIM ·────────· ╱ $AxPurple●$AxDIM ╲ $AxIndigo●$AxDIM──────┼──────$AxFuchsia●$AxDIM ╲ ╱ ·────────·$AxReset "@ Write-Host "$AxBold$AxIndigo Axion${AxPurple}Aura$AxFg $AxReset${AxMuted}presents$AxReset" Write-Host "$AxBold$AxFg $AxProject$AxReset $AxMuted$AxTagline$AxReset`n" } function Invoke-AxionStep { # Steps are fatal by default: a failed step stops the installer instead of # silently continuing with stale/broken state. Opt out with -NoFatal. param([string]$Label, [scriptblock]$Script, [object[]]$StepArgs = @(), [switch]$NoFatal) if (-not $AxTty) { Write-AxionInfo $Label try { & $Script @StepArgs $ok = ($null -eq $LASTEXITCODE -or $LASTEXITCODE -eq 0) if ($ok) { Write-AxionOk $Label } else { Write-AxionFail "$Label (exit $LASTEXITCODE)" } $script:AxLastStepOk = $ok if (-not $ok -and -not $NoFatal) { throw "AxionInstaller step failed: $Label" } return } catch { Write-AxionFail "$Label — $_" $script:AxLastStepOk = $false if (-not $NoFatal) { throw } return } } $job = Start-Job -ScriptBlock $Script -ArgumentList $StepArgs $si = 0 Write-Host "$esc[?25l" -NoNewline while ($job.State -eq 'Running') { $frame = $AxFrames[$script:AxFrameIx % $AxFrames.Count] $script:AxFrameIx++ $b = $AxSpin[$si % $AxSpin.Length]; $si++ $lit = $frame -replace '●', "$AxPurple●$AxDIM" Write-Host "$esc[2K$esc[1A$esc[2K$esc[1A$esc[2K$esc[1A$esc[2K$esc[1A$esc[2K`r" -NoNewline Write-Host "$AxDIM ────────────────────────────$AxReset" Write-Host "$AxDIM$lit$AxReset" Write-Host "$AxViolet$b$AxReset $AxFg$Label$AxReset $AxMuted…$AxReset" -NoNewline Start-Sleep -Milliseconds 120 } $out = Receive-Job $job 2>&1 $ok = ($job.State -eq 'Completed') Remove-Job $job -Force -ErrorAction SilentlyContinue Write-Host "$esc[2K$esc[1A$esc[2K$esc[1A$esc[2K$esc[1A$esc[2K$esc[1A$esc[2K`r$esc[?25h" -NoNewline if ($out) { $out | ForEach-Object { Write-Host "$AxMuted | $_$AxReset" } } if ($ok) { Write-AxionOk $Label } else { Write-AxionFail "$Label (job $($job.State))" } $script:AxLastStepOk = $ok if (-not $ok -and -not $NoFatal) { throw "AxionInstaller step failed: $Label" } } function Complete-Axion { param([string[]]$Lines) Write-Host "$AxDIM ────────────────────────────$AxReset" Write-Host "$AxOk ✔$AxBold$AxFg $AxProject is ready$AxReset" foreach ($l in $Lines) { Write-Host "$AxMuted · $AxFg$l$AxReset" } Write-Host "$AxViolet ── AxionAura · Open source by default. Free for everyone. ──$AxReset`n" } # ══ end AxionInstaller ══ $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # much faster Invoke-WebRequest [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $NodeVersion = '22.14.0' $RepoUrl = 'https://github.com/AxionAura/social-live' $RuntimeDir = Join-Path $Dir 'runtime' function Write-Step([string]$msg) { Write-AxionInfo $msg } function Write-Info([string]$msg) { Write-AxionInfo $msg } function Write-Warn2([string]$msg) { Write-AxionWarn $msg } function Die([string]$msg) { Write-AxionFail $msg; exit 1 } function Get-LatestHtml([string]$url, [string]$out) { Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing } # ────────────────────────── uninstall ────────────────────────── if ($Uninstall) { Write-Step "Uninstalling SocialLive from $Dir" $ErrorActionPreference = 'Continue' schtasks /delete /tn "SocialLive" /f 2>$null | Out-Null Get-CimInstance Win32_Process -Filter "Name='node.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like "*$Dir*" } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } if (Test-Path "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\SocialLive.lnk") { Remove-Item "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\SocialLive.lnk" -Force } if (Test-Path "$env:LOCALAPPDATA\Microsoft\WindowsApps\social-live.cmd") { Remove-Item "$env:LOCALAPPDATA\Microsoft\WindowsApps\social-live.cmd" -Force } if (Test-Path $Dir) { Remove-Item $Dir -Recurse -Force } Write-Info "Removed. Your dashboard data went with $Dir — the encryption key too." exit 0 } # ────────────────────────── update path ────────────────────────── if ($Update) { if (-not (Test-Path $Dir)) { Die "SocialLive is not installed at $Dir — run the installer first." } Write-Step "Updating SocialLive in $Dir" & (Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\social-live.cmd') stop 2>$null | Out-Null } # ────────────────────────── Node.js runtime ────────────────────────── # আগের install-এর runtime থাকলে PATH-এ বসাও — re-run দ্রুত হবে if (Test-Path "$RuntimeDir\node.exe") { $env:Path = "$RuntimeDir;$env:Path" } if (Test-Path "$RuntimeDir\ffmpeg.exe") { $env:Path = "$RuntimeDir;$env:Path" } function Test-NodeOk { try { $v = & node -v 2>$null if (-not $v) { return $false } return [version]($v.TrimStart('v')) -ge [version]'22.13' } catch { return $false } } if (-not ($Update -and (Test-Path $RuntimeDir))) { if (Test-NodeOk) { Write-Step "Node.js $(& node -v) found — OK (needs >= 22.13)" } else { Invoke-AxionStep -Label "Downloading Node.js v$NodeVersion (portable, no admin needed)" -Script { param($ver, $rdir) $zip = "$env:TEMP\node-portable.zip" Invoke-WebRequest -Uri "https://nodejs.org/dist/v$ver/node-v$ver-win-x64.zip" -OutFile $zip -UseBasicParsing New-Item -ItemType Directory -Force -Path $rdir | Out-Null $tmp = "$env:TEMP\node-extract" if (Test-Path $tmp) { Remove-Item $tmp -Recurse -Force } Expand-Archive -Path $zip -DestinationPath $tmp -Force Copy-Item "$tmp\node-v$ver-win-x64\*" $rdir -Recurse -Force Remove-Item $zip, $tmp -Recurse -Force -ErrorAction SilentlyContinue } -StepArgs @($NodeVersion, $RuntimeDir) $env:Path = "$RuntimeDir;$env:Path" Write-Info "Node.js $(& node -v) provisioned at $RuntimeDir" } } $env:Path = "$RuntimeDir;$env:Path" # ────────────────────────── FFmpeg runtime ────────────────────────── if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue)) { Invoke-AxionStep -Label 'Downloading FFmpeg (essential build, ~80 MB) — one time only' -Script { param($rdir) $zip = "$env:TEMP\ffmpeg.zip" Invoke-WebRequest -Uri 'https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip' -OutFile $zip -UseBasicParsing $tmp = "$env:TEMP\ffmpeg-extract" if (Test-Path $tmp) { Remove-Item $tmp -Recurse -Force } Expand-Archive -Path $zip -DestinationPath $tmp -Force $bin = Get-ChildItem $tmp -Recurse -Filter 'ffmpeg.exe' | Select-Object -First 1 if (-not $bin) { throw 'ffmpeg.exe not found in archive' } New-Item -ItemType Directory -Force -Path $rdir | Out-Null Copy-Item $bin.FullName $rdir -Force Copy-Item (Join-Path $bin.Directory 'ffprobe.exe') $rdir -Force Remove-Item $zip, $tmp -Recurse -Force -ErrorAction SilentlyContinue } -StepArgs @($RuntimeDir) $env:Path = "$RuntimeDir;$env:Path" Write-Info "FFmpeg $(& ffmpeg -version | Select-Object -First 1) provisioned at $RuntimeDir" } else { Write-Step "FFmpeg found — OK" } # ────────────────────────── application ────────────────────────── $tag = 'main' try { $tag = (Invoke-RestMethod 'https://api.github.com/repos/AxionAura/social-live/releases/latest').tag_name } catch { Write-Warn2 'Could not read latest release tag — using main branch.' } if ($Update) { Write-Step "Updating SocialLive sources ($tag)" } else { Write-Step "Downloading SocialLive $tag → $Dir" } # পুরো ডাউনলোড+এক্সট্র্যাক্ট+কপি job-এর ভেতরে self-contained — # job-এর ভেতর থেকে parent scope-এ variable ফেরত যায় না Invoke-AxionStep -Label "Downloading SocialLive $tag" -Script { $zip = "$env:TEMP\social-live-src.zip" $last = $null foreach ($try in 1..3) { # GitHub archive মাঝেমধ্যে flaky — ৩ বার চেষ্টা try { Invoke-WebRequest -Uri "https://github.com/AxionAura/social-live/archive/refs/tags/$tag.zip" -OutFile $zip -UseBasicParsing $last = $null; break } catch { $last = $_; Start-Sleep -Seconds (2 * $try) } } if ($last) { throw $last } $tmp = "$env:TEMP\social-live-src" if (Test-Path $tmp) { Remove-Item $tmp -Recurse -Force } Expand-Archive -Path $zip -DestinationPath $tmp -Force $inner = Get-ChildItem $tmp -Directory | Select-Object -First 1 New-Item -ItemType Directory -Force -Path $Dir | Out-Null # data/ (ডাটাবেজ, ভিডিও, encryption key) অক্ষত রেখে সোর্স রিপ্লেস Get-ChildItem "$($inner.FullName)" -Exclude 'data' | Copy-Item -Destination $Dir -Recurse -Force Remove-Item $zip, $tmp -Recurse -Force -ErrorAction SilentlyContinue } Push-Location $Dir try { $ErrorActionPreference = 'Continue' # npm writes to stderr; don't turn that into a terminating error Initialize-Axion -Project 'SocialLive' -Tagline 'Self-hosted live streaming dashboard' Write-AxionBanner Invoke-AxionStep -Label 'Installing dependencies (npm ci)' -Script { param($d) Set-Location $d; & npm.cmd ci --no-audit --no-fund if ($LASTEXITCODE -ne 0) { throw 'npm ci failed' } } -StepArgs @($Dir) Invoke-AxionStep -Label 'Building dashboard and server' -Script { param($d) Set-Location $d; & npm.cmd run build if ($LASTEXITCODE -ne 0) { throw 'build failed' } } -StepArgs @($Dir) } finally { Pop-Location; $ErrorActionPreference = 'Stop' } # .env: port + data dir — secrets are auto-generated by the server on first start $envPath = Join-Path $Dir '.env' if (-not (Test-Path $envPath)) { $dataDir = ($Dir -replace '\\', '/') + '/data' "APP_PORT=$Port`nDATA_DIR=$dataDir" | Set-Content -Path $envPath -Encoding Ascii } # ────────────────────────── control shim ────────────────────────── $shimDir = "$env:LOCALAPPDATA\Microsoft\WindowsApps" New-Item -ItemType Directory -Force -Path $shimDir | Out-Null @" @echo off setlocal set "SOCIAL_LIVE_DIR=$Dir" set "PATH=$RuntimeDir;%PATH%" cd /d "$Dir" for /f "usebackq eol=# tokens=1,* delims==" %%a in ("%envPath%") do set "%%a=%%b" if /i "%~1"=="update" ( powershell -NoProfile -ExecutionPolicy Bypass -File "%Dir%\install.ps1" -Update exit /b %errorlevel% ) node scripts\social-live.mjs %* "@ | Set-Content -Path "$shimDir\social-live.cmd" -Encoding Ascii Write-Info "Control command installed: social-live (start | stop | status | doctor | update)" # helper: hidden server launcher used by the Scheduled Task @" `$env:Path = "$RuntimeDir;`$env:Path" Set-Location "$Dir" Get-Content "$envPath" | ForEach-Object { if (`$_ -match '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*`$') { Set-Item -Path ("env:" + `$Matches[1]) -Value `$Matches[2] } } node apps\server\dist\index.js "@ | Set-Content -Path "$Dir\start-server.ps1" -Encoding Ascii # ────────────────────────── autostart + launch ────────────────────────── if (-not $NoService) { Initialize-Axion -Project 'SocialLive' -Tagline 'Self-hosted live streaming dashboard' Write-AxionBanner Write-Step 'Registering Scheduled Task (starts with Windows, hidden window)' $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$Dir\start-server.ps1`"" $trigger = New-ScheduledTaskTrigger -AtLogOn Register-ScheduledTask -TaskName 'SocialLive' -Action $action -Trigger $trigger -Force | Out-Null } Write-Step 'Starting SocialLive' Start-Process powershell -WindowStyle Hidden -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$Dir\start-server.ps1`"" $ok = $false foreach ($i in 1..20) { Start-Sleep -Milliseconds 700 try { $r = Invoke-WebRequest "http://localhost:$Port/api/auth/status" -UseBasicParsing -TimeoutSec 2 if ($r.StatusCode -eq 200) { $ok = $true; break } } catch {} } Start-Process "http://localhost:$Port" Write-Host '' if ($ok) { Write-AxionOk "Dashboard: http://localhost:$Port (opened in your browser)" } else { Write-AxionWarn "Server is warming up — open http://localhost:$Port in a moment." } Complete-Axion -Lines @( "Install dir: $Dir", 'First step: create the admin account, add a destination, upload a video', 'Update: social-live update', 'Uninstall: reinstall install.ps1 with -Uninstall', 'Firewall: if Windows asks about Node.js — click Allow (private networks)', "Keys: $Dir\data\config\ — back that folder up" )