# Posly print agent installer for Windows. # # Called from the admin Connect-Agent wizard's "Install command" step as: # # powershell -Command "& { # $env:POSLY_TENANT_ID = '...'; # $env:POSLY_API_URL = '...'; # $env:POSLY_AGENT_TOKEN = '...'; # iwr https://install.posly.xyz/agent.ps1 -useb | iex # }" # # Required env vars: POSLY_TENANT_ID, POSLY_AGENT_TOKEN, POSLY_API_URL. # Optional: POSLY_AGENT_DIR, POSLY_INSTALL_BASE, POSLY_AGENT_VERSION. # # This script is platform-twin to agent.sh. Keep features in sync. $ErrorActionPreference = "Stop" function Say { Write-Host "[posly] $args" -ForegroundColor Cyan } function Fail { Write-Host "[posly] $args" -ForegroundColor Red; exit 1 } # ---- 1. Required env vars ---- foreach ($name in @("POSLY_TENANT_ID","POSLY_AGENT_TOKEN","POSLY_API_URL")) { $val = [Environment]::GetEnvironmentVariable($name, "Process") if ([string]::IsNullOrWhiteSpace($val)) { Fail "missing $name. Re-run the install command from the admin Connect-Agent wizard." } } $installBase = if ($env:POSLY_INSTALL_BASE) { $env:POSLY_INSTALL_BASE } else { "https://install.posly.xyz" } $agentVersion = if ($env:POSLY_AGENT_VERSION) { $env:POSLY_AGENT_VERSION } else { "latest" } $agentDir = if ($env:POSLY_AGENT_DIR) { $env:POSLY_AGENT_DIR } else { Join-Path $env:USERPROFILE "posly-print-agent" } # ---- 2. Verify host has the runtime ---- Say "checking Node 20+ on PATH..." $node = Get-Command node -ErrorAction SilentlyContinue if (-not $node) { Fail "Node.js is not installed. Install Node 20 LTS first: https://nodejs.org/en/download/ Then re-run this command." } $nodeVer = & node -v $nodeMajor = [int]($nodeVer -replace '^v(\d+)\..*','$1') if ($nodeMajor -lt 20) { Fail "Node $nodeMajor detected. Posly print agent requires Node 20+. Reinstall from nodejs.org and re-run." } Say "node $nodeVer ok" $npm = Get-Command npm -ErrorAction SilentlyContinue if (-not $npm) { Fail "npm not found on PATH. Reinstall Node from nodejs.org (npm ships with it)." } # ---- 3. Download + extract the agent tarball ---- $tarballUrl = if ($agentVersion -eq "latest") { "$installBase/posly-print-agent.tar.gz" } else { "$installBase/posly-print-agent-$agentVersion.tar.gz" } Say "downloading print-agent ($agentVersion)" New-Item -ItemType Directory -Force -Path $agentDir | Out-Null $tmpTar = Join-Path $env:TEMP "posly-print-agent.tar.gz" # Default to TLS 1.2 because older PowerShell on Server 2016 defaults to # SSL3 / TLS 1.0 and gets rejected by Cloudflare with no clear error. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 try { Invoke-WebRequest -Uri $tarballUrl -OutFile $tmpTar -UseBasicParsing -ErrorAction Stop } catch { Fail "tarball download failed from $tarballUrl. Check your internet, then retry. ($_)" } Say "extracting to $agentDir" # tar.exe ships with Windows 10 1803+. Older Windows users see the # clearer error here than from a generic "command not found". $tar = Get-Command tar.exe -ErrorAction SilentlyContinue if (-not $tar) { Fail "tar.exe not found. You need Windows 10 1803+ or Windows 11. Update Windows or install Git for Windows (ships with tar)." } & tar.exe -xzf $tmpTar -C $agentDir --strip-components=1 Remove-Item -Force $tmpTar # ---- 4. Write .env from the injected POSLY_* values ---- Say "writing .env" @" POSLY_API_URL=$env:POSLY_API_URL POSLY_TENANT_ID=$env:POSLY_TENANT_ID POSLY_AGENT_TOKEN=$env:POSLY_AGENT_TOKEN POSLY_PRINTER_CONFIG_PATH=./printer-config.json "@ | Set-Content -Path (Join-Path $agentDir ".env") -Encoding ASCII # Restrict .env to current user + SYSTEM so the token isn't world-readable. $envPath = Join-Path $agentDir ".env" $acl = Get-Acl $envPath $acl.SetAccessRuleProtection($true, $false) $me = [Security.Principal.WindowsIdentity]::GetCurrent().Name $acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($me,"FullControl","Allow"))) $acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule("SYSTEM","FullControl","Allow"))) Set-Acl $envPath $acl # Seed a default printer-config.json so first launch doesn't crash. $cfgPath = Join-Path $agentDir "printer-config.json" if (-not (Test-Path $cfgPath)) { "{}" | Set-Content -Path $cfgPath -Encoding ASCII } # ---- 5. Install production dependencies ---- Say "installing dependencies (this may take 1-2 minutes)" Push-Location $agentDir & npm install --omit=dev --no-audit --no-fund Pop-Location # ---- 6. Register as a scheduled task that runs at logon + survives crashes ---- # nssm or sc.exe would be cleaner, but Scheduled Tasks works without any # extra installer and survives Windows updates. $taskName = "PoslyPrintAgent" Say "registering scheduled task: $taskName" $nodeExe = (Get-Command node).Source $action = New-ScheduledTaskAction -Execute $nodeExe -Argument "--env-file=.env dist/index.js" -WorkingDirectory $agentDir $trigger = New-ScheduledTaskTrigger -AtLogOn $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Days 365) $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Limited # Unregister an old version of the task if present so a re-run upgrades cleanly. Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings -Principal $principal | Out-Null Start-ScheduledTask -TaskName $taskName # ---- 7. Done ---- Write-Host "" Say "install complete. The agent is running and will dial home within 30s." Write-Host " Install dir: $agentDir" Write-Host " Task name: $taskName" Write-Host " Stop task: Stop-ScheduledTask -TaskName $taskName" Write-Host " Tail logs: Get-Content `"$agentDir\agent.log`" -Wait (after agent starts logging)" Write-Host "" Say "next: return to admin > Settings > Printing to confirm the green 'connected' light."