welsonjs/postInstall.ps1
Namhyeon, Go dec8817a32 Add WinDivert and Android Tools components
Introduces WinDivert and Android Platform Tools as selectable components in the installer. Updates DownloadUrls.psd1 with their download URLs, adds their handling in postInstall.ps1, and registers them in setup.iss for installation.
2025-12-01 15:44:36 +09:00

829 lines
26 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WelsonJS post-install script
# Namhyeon Go <gnh1201@catswords.re.kr>, and Catswords OSS contributors.
# https://github.com/gnh1201/welsonjs
# ================================
# PARAMETERS
# ================================
param(
[string]$TelemetryProvider = "",
[string]$TelemetryApiKey = "",
[string]$Version = "",
[string]$DistinctId = "",
[string]$Components = ""
)
# ================================
# SCRIPT ROOT RESOLUTION
# ================================
# Ensure $ScriptRoot is available even on older PowerShell
if (-not (Get-Variable -Name PSScriptRoot -ErrorAction SilentlyContinue)) {
$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
}
else {
$ScriptRoot = $PSScriptRoot
}
# ================================
# LOAD DOWNLOAD URL TABLE (DownloadUrls.psd1 in /data folder)
# ================================
$DownloadUrls = @{}
$urlsFilePath = Join-Path $ScriptRoot "data/DownloadUrls.psd1"
if (Test-Path $urlsFilePath) {
try {
$DownloadUrls = Import-PowerShellDataFile -Path $urlsFilePath
}
catch {
Write-Host "[WARN] Failed to load DownloadUrls.psd1. Falling back to empty URL table."
$DownloadUrls = @{}
}
}
else {
Write-Host "[WARN] DownloadUrls.psd1 not found at: $urlsFilePath"
$DownloadUrls = @{}
}
function Get-DownloadUrl {
param(
[Parameter(Mandatory = $true)]
[string]$Component,
[Parameter(Mandatory = $true)]
[string]$Arch # x64, arm64, x86
)
$componentKey = $Component.ToLowerInvariant()
if (-not $DownloadUrls.ContainsKey($componentKey)) {
return $null
}
$entry = $DownloadUrls[$componentKey]
# Prefer arch-specific URL
if ($entry.ContainsKey($Arch)) {
return $entry[$Arch]
}
# Fallback to "any" (arch-independent)
if ($entry.ContainsKey("any")) {
return $entry["any"]
}
return $null
}
# ================================
# TELEMETRY (ANONYMOUS BY DEFAULT)
# ================================
if ($TelemetryProvider -and $TelemetryProvider.ToLower() -eq "posthog") {
# Skip telemetry if API key is missing
if (-not $TelemetryApiKey -or $TelemetryApiKey.Trim() -eq "") {
# No-op: continue script
}
else {
# Resolve distinct ID (fallback to machine name)
$finalDistinctId = if ($DistinctId -and $DistinctId.Trim() -ne "") {
$DistinctId
} else {
$env:COMPUTERNAME
}
if ($finalDistinctId -and $finalDistinctId.Trim() -ne "") {
# Build single event payload for PostHog /i/v0/e endpoint
# Anonymous event is default: $process_person_profile = false
$body = @{
api_key = $TelemetryApiKey
event = "app_installed"
distinct_id = $finalDistinctId
properties = @{
product = "welsonjs"
version = $Version
os = "windows"
source = "post-install.ps1"
components = $Components # Keep raw string here
}
timestamp = (Get-Date).ToString("o") # ISO 8601 format
} | ConvertTo-Json -Depth 5
try {
Invoke-RestMethod `
-Uri "https://us.i.posthog.com/i/v0/e/" `
-Method Post `
-ContentType "application/json" `
-Body $body | Out-Null
}
catch {
# Ignore telemetry failure (installer must not break)
}
}
}
}
# ================================
# CONFIGURATION
# ================================
$AppName = "welsonjs"
$TargetDir = Join-Path $env:APPDATA $AppName
$TmpDir = Join-Path $env:TEMP "$AppName-downloads"
Write-Host ""
Write-Host "[*] Target directory : $TargetDir"
Write-Host "[*] Temporary directory: $TmpDir"
Write-Host ""
# Ensure base directories exist
New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null
New-Item -ItemType Directory -Path $TmpDir -Force | Out-Null
# ================================
# COMPONENT SELECTION (SINGLE PARSE)
# ================================
# Convert Components (string) → array exactly once.
# Example: "python,curl,websocat"
# If empty → treat as "all selected" for backward compatibility.
$SelectedComponents = @()
$AllComponentsSelected = $true
if ($Components -and $Components.Trim() -ne "") {
$SelectedComponents =
$Components.Split(",") |
ForEach-Object { $_.Trim().ToLowerInvariant() }
$AllComponentsSelected = $false
}
function Test-ComponentSelected {
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
if ($AllComponentsSelected) {
return $true
}
return $SelectedComponents -contains $Name.ToLowerInvariant()
}
Write-Host "[*] Selected components (raw): $Components"
if ($AllComponentsSelected) {
Write-Host "[*] Component filter : <none> (treat as ALL selected)"
} else {
Write-Host "[*] Component filter : $($SelectedComponents -join ', ')"
}
Write-Host ""
# ================================
# ARCHITECTURE DETECTION
# ================================
function Get-NativeArchitecture {
# 0 = x86, 5 = ARM, 9 = x64
# https://learn.microsoft.com/windows/win32/cimwin32prov/win32-processor
$arch = $null
try {
$proc = Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop |
Select-Object -First 1
switch ($proc.Architecture) {
0 { $arch = "x86" } # 32-bit Intel/AMD
5 { $arch = "arm64" } # treat ARM as arm64 target
9 { $arch = "x64" } # 64-bit Intel/AMD
default { $arch = "x86" } # fallback
}
}
catch {
# Fallback: only 32/64 bit detection if WMI/CIM is not available
if ([System.Environment]::Is64BitOperatingSystem) {
$arch = "x64"
}
else {
$arch = "x86"
}
}
return $arch
}
$arch = Get-NativeArchitecture
Write-Host "[*] Detected architecture: $arch"
Write-Host ""
# ================================
# HELPER FUNCTIONS
# ================================
function Ensure-EmptyDirectory {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
# If a file exists at this path, delete it
if (Test-Path $Path -PathType Leaf) {
Write-Host "[WARN] File exists at '$Path'. Removing..."
Remove-Item -Path $Path -Force
}
# Ensure a directory exists at this path
if (-not (Test-Path $Path -PathType Container)) {
Write-Host "[*] Creating directory: $Path"
New-Item -ItemType Directory -Path $Path -Force | Out-Null
}
}
function Download-File {
param(
[Parameter(Mandatory = $true)]
[string]$Url,
[Parameter(Mandatory = $true)]
[string]$DestinationPath
)
Write-Host "[*] Downloading:"
Write-Host " $Url"
Write-Host " -> $DestinationPath"
# Ensure destination directory exists
$destDir = Split-Path -Parent $DestinationPath
if ($destDir -and -not (Test-Path $destDir)) {
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
}
$maxRetries = 3
$attempt = 0
$success = $false
while (-not $success -and $attempt -lt $maxRetries) {
$attempt++
try {
Invoke-WebRequest -Uri $Url -OutFile $DestinationPath -UseBasicParsing
$success = $true
}
catch {
Write-Host "[WARN] Download failed (attempt $attempt of $maxRetries): $($_.Exception.Message)"
if ($attempt -lt $maxRetries) {
Start-Sleep -Seconds 5
}
}
}
if (-not $success) {
throw "Failed to download $Url after $maxRetries attempts."
}
}
function Extract-CompressedFile {
param(
[Parameter(Mandatory = $true)]
[string]$CompressedPath,
[Parameter(Mandatory = $true)]
[string]$DestinationDirectory
)
Write-Host "[*] Extracting compressed file:"
Write-Host " $CompressedPath"
Write-Host " -> $DestinationDirectory"
# Ensure destination directory exists
Ensure-EmptyDirectory -Path $DestinationDirectory
# Temporary extraction workspace inside destination directory
$tmpExtractDir = Join-Path $DestinationDirectory "_tmp_extract"
Ensure-EmptyDirectory -Path $tmpExtractDir
# Extract archive
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($CompressedPath, $tmpExtractDir)
# Move all items from temp folder to final destination
Get-ChildItem -Path $tmpExtractDir -Force | ForEach-Object {
$targetPath = Join-Path $DestinationDirectory $_.Name
if (Test-Path $targetPath) {
# If the target exists, remove it (file or directory)
Remove-Item -Path $targetPath -Recurse -Force
}
Move-Item -Path $_.FullName -Destination $targetPath
}
# Remove the temporary extraction directory
Remove-Item -Path $tmpExtractDir -Recurse -Force
}
function Extract-TarGzArchive {
param(
[Parameter(Mandatory = $true)]
[string]$ArchivePath,
[Parameter(Mandatory = $true)]
[string]$DestinationDirectory
)
Write-Host "[*] Extracting TAR.GZ archive:"
Write-Host " $ArchivePath"
Write-Host " -> $DestinationDirectory"
Ensure-EmptyDirectory -Path $DestinationDirectory
# Validate tar availability
$tarCommand = Get-Command tar -ErrorAction SilentlyContinue
if (-not $tarCommand) {
Write-Host "[ERROR] 'tar' command not found."
throw "tar not available on this system."
}
Write-Host "[DEBUG] tar command:"
Write-Host " tar -xzf `"$ArchivePath`" -C `"$DestinationDirectory`""
try {
& tar -xzf "$ArchivePath" -C "$DestinationDirectory"
if ($LASTEXITCODE -ne 0) {
throw "tar exited with code $LASTEXITCODE."
}
}
catch {
throw "Failed to extract TAR.GZ archive: $($_.Exception.Message)"
}
}
# ================================
# COMPRESSED / INSTALLER PATHS
# ================================
$PythonCompressed = Join-Path $TmpDir "python.zip"
$CurlCompressed = Join-Path $TmpDir "curl.zip"
$YaraCompressed = Join-Path $TmpDir "yara.zip"
$WamrArchive = Join-Path $TmpDir "wamr.tar.gz"
$WebsocatCompressed = Join-Path $TmpDir "websocat.zip"
$ArtifactsCompressed = Join-Path $TmpDir "artifacts.zip"
$GtkRuntimeInstaller = Join-Path $TmpDir "gtk-runtime.exe"
$TessdataCompressed = Join-Path $TmpDir "tessdata.zip"
$TessdataBestCompressed = Join-Path $TmpDir "tessdata_best.zip"
$TessdataFastCompressed = Join-Path $TmpDir "tessdata_fast.zip"
$NpcapInstaller = Join-Path $TmpDir "npcap-setup.exe"
$NmapInstaller = Join-Path $TmpDir "nmap-setup.exe"
$GtkServerCompressed = Join-Path $TmpDir "gtkserver.zip"
$WinDivertCompressed = Join-Path $TmpDir "windivert.zip"
$AndroidToolsCompressed = Join-Path $TmpDir "android-platform-tools.zip"
# ================================
# DOWNLOAD PHASE
# ================================
try {
# Python (component: python)
if (Test-ComponentSelected -Name "python") {
$url = Get-DownloadUrl -Component "python" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $PythonCompressed
}
else {
Write-Host "[*] Python URL not available for arch: $arch. Skipping download."
}
}
else {
Write-Host "[*] Python component not selected. Skipping download."
}
# curl (component: curl)
if (Test-ComponentSelected -Name "curl") {
$url = Get-DownloadUrl -Component "curl" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $CurlCompressed
}
else {
Write-Host "[*] curl URL not available for arch: $arch. Skipping download."
}
}
else {
Write-Host "[*] curl component not selected. Skipping download."
}
# YARA (component: yara)
if (Test-ComponentSelected -Name "yara") {
$url = Get-DownloadUrl -Component "yara" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $YaraCompressed
}
else {
Write-Host "[*] YARA URL not available for arch: $arch. Skipping download."
}
}
else {
Write-Host "[*] YARA component not selected. Skipping download."
}
# WAMR (component: wamr)
if (Test-ComponentSelected -Name "wamr") {
$url = Get-DownloadUrl -Component "wamr" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $WamrArchive
}
else {
Write-Host "[*] WAMR URL not available for arch: $arch. Skipping download."
}
}
else {
Write-Host "[*] WAMR component not selected. Skipping download."
}
# websocat (component: websocat)
if (Test-ComponentSelected -Name "websocat") {
$url = Get-DownloadUrl -Component "websocat" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $WebsocatCompressed
}
else {
Write-Host "[*] websocat URL not available for arch: $arch. Skipping download."
}
}
else {
Write-Host "[*] websocat component not selected. Skipping download."
}
# artifacts (component: artifacts)
if (Test-ComponentSelected -Name "artifacts") {
$url = Get-DownloadUrl -Component "artifacts" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $ArtifactsCompressed
}
else {
Write-Host "[*] artifacts URL not available for arch: $arch. Skipping download."
}
}
else {
Write-Host "[*] artifacts component not selected. Skipping download."
}
# GTK3 runtime (component: gtk3runtime)
if (Test-ComponentSelected -Name "gtk3runtime") {
$url = Get-DownloadUrl -Component "gtk3runtime" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $GtkRuntimeInstaller
}
else {
Write-Host "[*] gtk3runtime URL not available for arch: $arch. Skipping download."
}
}
else {
Write-Host "[*] gtk3runtime component not selected. Skipping download."
}
# GTK server (component: gtkserver)
if (Test-ComponentSelected -Name "gtkserver") {
$url = Get-DownloadUrl -Component "gtkserver" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $GtkServerCompressed
}
else {
Write-Host "[*] gtkserver URL not available for arch: $arch. Skipping download."
}
}
else {
Write-Host "[*] gtkserver component not selected. Skipping download."
}
# tessdata (component: tessdata)
if (Test-ComponentSelected -Name "tessdata") {
$url = Get-DownloadUrl -Component "tessdata" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $TessdataCompressed
}
else {
Write-Host "[*] tessdata URL not available. Skipping download."
}
}
else {
Write-Host "[*] tessdata component not selected. Skipping download."
}
# tessdata_best (component: tessdata_best)
if (Test-ComponentSelected -Name "tessdata_best") {
$url = Get-DownloadUrl -Component "tessdata_best" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $TessdataBestCompressed
}
else {
Write-Host "[*] tessdata_best URL not available. Skipping download."
}
}
else {
Write-Host "[*] tessdata_best component not selected. Skipping download."
}
# tessdata_fast (component: tessdata_fast)
if (Test-ComponentSelected -Name "tessdata_fast") {
$url = Get-DownloadUrl -Component "tessdata_fast" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $TessdataFastCompressed
}
else {
Write-Host "[*] tessdata_fast URL not available. Skipping download."
}
}
else {
Write-Host "[*] tessdata_fast component not selected. Skipping download."
}
# Nmap bundle (component: nmap) includes Npcap + Nmap installer
if (Test-ComponentSelected -Name "nmap") {
# Npcap
$url = Get-DownloadUrl -Component "npcap" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $NpcapInstaller
}
else {
Write-Host "[*] npcap URL not available. Skipping npcap download."
}
# Nmap
$url = Get-DownloadUrl -Component "nmap" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $NmapInstaller
}
else {
Write-Host "[*] nmap URL not available. Skipping nmap download."
}
}
else {
Write-Host "[*] nmap component not selected. Skipping Npcap/Nmap download."
}
# windivert (component: windivert)
if (Test-ComponentSelected -Name "windivert") {
$url = Get-DownloadUrl -Component "windivert" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $WinDivertCompressed
}
else {
Write-Host "[*] windivert URL not available. Skipping download."
}
}
else {
Write-Host "[*] windivert component not selected. Skipping download."
}
# Android Platform Tools (component: android_tools)
if (Test-ComponentSelected -Name "android_tools") {
$url = Get-DownloadUrl -Component "android_tools" -Arch $arch
if ($url) {
Download-File -Url $url -DestinationPath $AndroidToolsCompressed
}
else {
Write-Host "[*] android_tools URL not available. Skipping download."
}
}
else {
Write-Host "[*] android_tools component not selected. Skipping download."
}
}
catch {
Write-Host "[FATAL] Download phase failed."
if ($_ -is [System.Exception]) {
Write-Host $_.Exception.Message
} else {
Write-Host $_
}
exit 1
}
# ================================
# EXTRACT / INSTALL PHASE
# ================================
try {
# Python (component: python)
if (Test-ComponentSelected -Name "python") {
if (Test-Path $PythonCompressed) {
Extract-CompressedFile `
-CompressedPath $PythonCompressed `
-DestinationDirectory (Join-Path $TargetDir "python")
}
else {
Write-Host "[WARN] Python archive not found. Skipping installation."
}
}
else {
Write-Host "[*] Python component not selected. Skipping installation."
}
# curl (component: curl)
if (Test-ComponentSelected -Name "curl") {
if (Test-Path $CurlCompressed) {
Extract-CompressedFile `
-CompressedPath $CurlCompressed `
-DestinationDirectory (Join-Path $TargetDir "curl")
}
else {
Write-Host "[WARN] curl archive not found. Skipping installation."
}
}
else {
Write-Host "[*] curl component not selected. Skipping installation."
}
# YARA (component: yara)
if (Test-ComponentSelected -Name "yara") {
if (Test-Path $YaraCompressed) {
Extract-CompressedFile `
-CompressedPath $YaraCompressed `
-DestinationDirectory (Join-Path $TargetDir "yara")
}
else {
Write-Host "[WARN] YARA archive not found. Skipping installation."
}
}
else {
Write-Host "[*] YARA component not selected. Skipping installation."
}
# WAMR (component: wamr, TAR.GZ)
if (Test-ComponentSelected -Name "wamr") {
if (Test-Path $WamrArchive) {
Extract-TarGzArchive `
-ArchivePath $WamrArchive `
-DestinationDirectory (Join-Path $TargetDir "wamr")
}
else {
Write-Host "[WARN] WAMR archive not found. Skipping installation."
}
}
else {
Write-Host "[*] WAMR component not selected. Skipping installation."
}
# websocat (component: websocat)
if (Test-ComponentSelected -Name "websocat") {
if (Test-Path $WebsocatCompressed) {
Extract-CompressedFile `
-CompressedPath $WebsocatCompressed `
-DestinationDirectory (Join-Path $TargetDir "websocat")
}
else {
Write-Host "[WARN] websocat archive not found. Skipping installation."
}
}
else {
Write-Host "[*] websocat component not selected. Skipping installation."
}
# artifacts (component: artifacts)
if (Test-ComponentSelected -Name "artifacts") {
if (Test-Path $ArtifactsCompressed) {
Extract-CompressedFile `
-CompressedPath $ArtifactsCompressed `
-DestinationDirectory (Join-Path $TargetDir "bin")
}
else {
Write-Host "[WARN] artifacts archive not found. Skipping installation."
}
}
else {
Write-Host "[*] artifacts component not selected. Skipping installation."
}
# GTK3 runtime (component: gtk3runtime) just run installer; no need to wait
if (Test-ComponentSelected -Name "gtk3runtime") {
if (Test-Path $GtkRuntimeInstaller) {
Write-Host "[*] Starting GTK runtime installer (no wait): $GtkRuntimeInstaller"
Start-Process -FilePath $GtkRuntimeInstaller
}
else {
Write-Host "[WARN] GTK runtime installer not found. Skipping."
}
}
else {
Write-Host "[*] gtk3runtime component not selected. Skipping installation."
}
# GTK server (component: gtkserver) extract ZIP into AppData
if (Test-ComponentSelected -Name "gtkserver") {
if (Test-Path $GtkServerCompressed) {
Extract-CompressedFile `
-CompressedPath $GtkServerCompressed `
-DestinationDirectory (Join-Path $TargetDir "gtkserver")
}
else {
Write-Host "[WARN] gtkserver archive not found. Skipping installation."
}
}
else {
Write-Host "[*] gtkserver component not selected. Skipping installation."
}
# tessdata (component: tessdata)
if (Test-ComponentSelected -Name "tessdata") {
if (Test-Path $TessdataCompressed) {
Extract-CompressedFile `
-CompressedPath $TessdataCompressed `
-DestinationDirectory (Join-Path $TargetDir "tessdata")
}
else {
Write-Host "[WARN] tessdata archive not found. Skipping installation."
}
}
else {
Write-Host "[*] tessdata component not selected. Skipping installation."
}
# tessdata_best (component: tessdata_best)
if (Test-ComponentSelected -Name "tessdata_best") {
if (Test-Path $TessdataBestCompressed) {
Extract-CompressedFile `
-CompressedPath $TessdataBestCompressed `
-DestinationDirectory (Join-Path $TargetDir "tessdata_best")
}
else {
Write-Host "[WARN] tessdata_best archive not found. Skipping installation."
}
}
else {
Write-Host "[*] tessdata_best component not selected. Skipping installation."
}
# tessdata_fast (component: tessdata_fast)
if (Test-ComponentSelected -Name "tessdata_fast") {
if (Test-Path $TessdataFastCompressed) {
Extract-CompressedFile `
-CompressedPath $TessdataFastCompressed `
-DestinationDirectory (Join-Path $TargetDir "tessdata_fast")
}
else {
Write-Host "[WARN] tessdata_fast archive not found. Skipping installation."
}
}
else {
Write-Host "[*] tessdata_fast component not selected. Skipping installation."
}
# Nmap bundle (component: nmap) Npcap → Nmap → VC_redist.x86.exe
if (Test-ComponentSelected -Name "nmap") {
# Npcap
if (Test-Path $NpcapInstaller) {
Write-Host "[*] Running Npcap installer (wait): $NpcapInstaller"
Start-Process -FilePath $NpcapInstaller -ArgumentList "/S" -Wait -ErrorAction Stop
}
else {
Write-Host "[WARN] Npcap installer not found. Skipping Npcap."
}
# Nmap
if (Test-Path $NmapInstaller) {
Write-Host "[*] Running Nmap installer (wait): $NmapInstaller"
Start-Process -FilePath $NmapInstaller -ArgumentList "/S" -Wait -ErrorAction Stop
}
else {
Write-Host "[WARN] Nmap installer not found. Skipping Nmap."
}
# Find and run VC_redist.x86.exe inside Nmap installation directory
$searchDirs = @()
if (${env:ProgramFiles(x86)}) {
$searchDirs += (Join-Path ${env:ProgramFiles(x86)} "Nmap")
}
if ($env:ProgramFiles) {
$searchDirs += (Join-Path $env:ProgramFiles "Nmap")
}
$vcRedist = $null
foreach ($dir in $searchDirs) {
if (Test-Path $dir) {
$candidate = Get-ChildItem -Path $dir -Filter "vc_redist.x86.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($candidate) {
$vcRedist = $candidate
break
}
}
}
if ($vcRedist) {
Write-Host "[*] Running VC_redist.x86 installer: $($vcRedist.FullName)"
Start-Process -FilePath $vcRedist.FullName -ArgumentList "/install /quiet /norestart" -Wait -ErrorAction SilentlyContinue
}
else {
Write-Host "[WARN] VC_redist.x86.exe not found under expected Nmap directories."
}
}
else {
Write-Host "[*] nmap component not selected. Skipping Npcap/Nmap installation."
}
}
catch {
Write-Host "[FATAL] Extraction/installation phase failed."
if ($_ -is [System.Exception]) {
Write-Host $_.Exception.Message
} else {
Write-Host $_
}
exit 1
}
# ================================
# FINISH
# ================================
Write-Host "[*] Installation completed successfully."
Write-Host "[*] Installed into: $TargetDir"
Write-Host ""
exit 0