diff --git a/AppMap.ps1 b/AppMap.ps1 deleted file mode 100644 index 88802ee..0000000 --- a/AppMap.ps1 +++ /dev/null @@ -1,589 +0,0 @@ -$ProgressPreference = 'SilentlyContinue' - -$script:TecharyApps = @{ -#region Adobe Reader -"adobereader" = @{ - DisplayName = "Adobe Reader" - RepoPath = "a/Adobe/Acrobat/Reader/64-bit" - YamlFile = "Adobe.Acrobat.Reader.64-bit.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*AcroRdrDCx64\S*\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*AcroRdrDCx64\S*\.exe)' - InstallerType = "exe" - ExeInstallArgs = "-sfx_nu /sAll /rs /msi" - IsWinget = $true - WingetID = "Adobe.Acrobat.Reader.64-bit" - } -#endregion - -#region Adobe Creative Cloud -"adobecc" = @{ - DisplayName = "Adobe Creative Cloud" - RepoPath = "a/Adobe/CreativeCloud" - YamlFile = "Adobe.CreativeCloud.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(https://prod-rel-ffc-ccm\.oobesaas\.adobe\.com/adobe-ffc-external/core/v1/wam/download\?sapCode=KCCC&wamFeature=nuj-live)' - PatternARM64 = 'InstallerUrl:\s*(https://prod-rel-ffc-ccm\.oobesaas\.adobe\.com/adobe-ffc-external/core/v1/wam/download\?sapCode=KCCC&wamFeature=nuj-live)' - InstallerType = "exe" - ExeInstallArgs = "--mode=stub" - IsWinget = $true - WingetID = "Adobe.CreativeCloud" - } -#endregion - -#region Microsoft PowerToys -"powertoys" = @{ - DisplayName = "PowerToys" - RepoPath = "m/Microsoft/PowerToys" - YamlFile = "Microsoft.PowerToys.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/PowerToysSetup-\S*-x64\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/PowerToysSetup-\S*-arm64\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/quiet /norestart" - IsWinget = $true - WingetID = "Microsoft.PowerToys" -} -#endregion - -#region Mozilla Firefox -"firefox" = @{ - DisplayName = "Firefox" - RepoPath = "m/Mozilla/Firefox/en-GB" - YamlFile = "Mozilla.Firefox.en-GB.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/win64/en-GB/Firefox%20Setup%20\S+\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/win64-aarch64/en-GB/Firefox%20Setup%20\S+\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/S /PreventRebootRequired=true" - IsWinget = $true - WingetID = "Adobe.CreativeCloud" -} -#endregion - -#region Slack -"slack" = @{ - DisplayName = "Slack" - RepoPath = "s/SlackTechnologies/Slack" - YamlFile = "SlackTechnologies.Slack.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/x64/\S*/slack-standalone-\S+\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S*/x64/\S*/slack-standalone-\S+\.msi)' - InstallerType = "msi" - ExeInstallArgs = "/S /PreventRebootRequired=true" - IsWinget = $true - WingetID = "SlackTechnologies.Slack" - } -#endregion - -#region Winget Auto Update -"wingetautoupdate" = @{ - DisplayName = "Winget Auto Update" - RepoPath = "r/Romanitho/Winget-AutoUpdate" - YamlFile = "Romanitho.Winget-AutoUpdate.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/WAU\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S*/WAU\.msi)' - InstallerType = "msi" - ExeInstallArgs = "/S /PreventRebootRequired=true" - IsWinget = $true - WingetID = "Romanitho.Winget-AutoUpdate" - } -#endregion - -#region RingCentral -"ringcentral" = @{ - DisplayName = "RingCentral" - RepoPath = "r/RingCentral/RingCentral" - YamlFile = "RingCentral.RingCentral.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/RingCentral-\d+\.\d+\.\d+-x64\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S*/RingCentral-\d+\.\d+\.\d+-arm64\.msi)' - InstallerType = "msi" - IsWinget = $true - WingetID = "RingCentral.RingCentral" - } -#endregion - -#region Microsoft Visual Studio Code -"vscode" = @{ - DisplayName = "Microsoft Visual Studio Code" - RepoPath = "m/Microsoft/VisualStudioCode" - YamlFile = "Microsoft.VisualStudioCode.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*VSCodeSetup\S*x64\S*\.(exe|msi))' - PatternARM64 = 'InstallerUrl:\s*(\S*VSCodeSetup\S*arm64\S*\.(exe|msi))' - InstallerType = "exe" - ExeInstallArgs = "/VERYSILENT /MERGETASKS=!runcode" - IsWinget = $true - WingetID = "Microsoft.VisualStudioCode" -} -#endregion - -#region Jabra Direct -"jabradirect" = @{ - DisplayName = "Jabra Direct" - RepoPath = "j/Jabra/Direct" - YamlFile = "Jabra.Direct.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/JabraDirectSetup\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/JabraDirectSetup\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/install /quiet /norestart" - IsWinget = $true - WingetID = "Jabra.Direct" -} -#endregion - -#region Bitwarden -"bitwarden" = @{ - DisplayName = "Bitwarden" - RepoPath = "b/Bitwarden/Bitwarden" - YamlFile = "Bitwarden.Bitwarden.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/Bitwarden-Installer-\S+\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/Bitwarden-Installer-\S+\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/allusers /S" - IsWinget = $true - WingetID = "Bitwarden.Bitwarden" -} -#endregion - -#region Git -"git" = @{ - DisplayName = "Git" - RepoPath = "g/Git/Git" - YamlFile = "Git.Git.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/Git-\d+\.\d+\.\d+(-windows-\d+)?-64-bit\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/Git-\d+\.\d+\.\d+(-windows-\d+)?-arm64\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/SP- /VERYSILENT /SUPPRESSMSGBOXES /NORESTART" - IsWinget = $true - WingetID = "Git.Git" -} -#endregion - -#region 7zip -"7zip" = @{ - DisplayName = "7zip" - RepoPath = "7/7zip/7zip" - YamlFile = "7zip.7zip.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/7z\d+-x64\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/7z\d+-arm64\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/S" - IsWinget = $true - WingetID = "7zip.7zip" -} -#endregion - -#region Dell Command -"dellcommand" = @{ - DisplayName = "Dell Command" - RepoPath = "d/Dell/CommandUpdate" - YamlFile = "Dell.CommandUpdate.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/Dell-Command-Update-Application\S*WIN64\S*\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/Dell-Command-Update-Application\S*WIN64\S*\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/passthrough /S /V/quiet /V/norestart" - IsWinget = $true - WingetID = "Dell.CommandUpdate" -} -#endregion - -#region Microsoft Power Automate -"powerautomate" = @{ - DisplayName = "Microsoft Power Automate Desktop" - RepoPath = "m/Microsoft/PowerAutomateDesktop" - YamlFile = "Microsoft.PowerAutomateDesktop.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/Setup\.Microsoft\.PowerAutomate\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/Setup\.Microsoft\.PowerAutomate\.exe)' - InstallerType = "exe" - ExeInstallArgs = "-Silent -ACCEPTEULA" - IsWinget = $true - WingetID = "Microsoft.PowerAutomateDesktop" -} -#endregion - -#region Microsoft PowerBi -"powerbi" = @{ - DisplayName = "Microsoft Power BI" - RepoPath = "m/Microsoft/PowerBI" - YamlFile = "Microsoft.PowerBI.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/PBIDesktopSetup-\d{4}-\d{2}_x64\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/PBIDesktopSetup-\d{4}-\d{2}_x64\.exe)' - InstallerType = "exe" - ExeInstallArgs = "-silent ACCEPT_EULA=1" - IsWinget = $true - WingetID = "Microsoft.PowerBI" -} -#endregion - -#region Python 3.14 -"python314" = @{ - DisplayName = "Pyhton 3.14" - RepoPath = "p/Python/Python/3/14" - YamlFile = "Python.Python.3.14.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/python-\d+\.\d+\.\d+-amd64\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/python-\d+\.\d+\.\d+-arm64\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/passive /quiet InstallAllUsers=1" - IsWinget = $true - WingetID = "Python.Python.3.14" - } -#endregion - -#region Docker Desktop -"dockerdesktop" = @{ - DisplayName = "Docker Desktop" - RepoPath = "d/Docker/DockerDesktop" - YamlFile = "Docker.DockerDesktop.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/amd64/\d+/Docker%20Desktop%20Installer\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/arm64/\d+/Docker%20Desktop%20Installer\.exe)' - InstallerType = "exe" - ExeInstallArgs = "install --quiet" - IsWinget = $true - WingetID = "Docker.DockerDesktop" - } -#endregion - -#region Java Runtime Environment -"java" = @{ - DisplayName = "Java Runtime Environment" - RepoPath = "o/Oracle/JavaRuntimeEnvironment" - YamlFile = "Oracle.JavaRuntimeEnvironment.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(https://javadl\.oracle\.com/webapps/download/AutoDL\?BundleId=\d+_[a-fA-F0-9]+)' - PatternARM64 = 'InstallerUrl:\s*(https://javadl\.oracle\.com/webapps/download/AutoDL\?BundleId=\d+_[a-fA-F0-9]+)' - InstallerType = "exe" - ExeInstallArgs = "/s REBOOT=0" - IsWinget = $true - WingetID = "Oracle.JavaRuntimeEnvironment" -} -#endregion - -#region ReMarkable -"remarkable" = @{ - DisplayName = "ReMarkable Companion App" - RepoPath = "r/reMarkable/reMarkableCompanionApp" - YamlFile = "reMarkable.reMarkableCompanionApp.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/reMarkable-\S*-win64\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/reMarkable-\S*-win64\.exe)' - InstallerType = "exe" - ExeInstallArgs = "install --confirm-command --default-answer --accept-licenses" - IsWinget = $true - WingetID = "reMarkable.reMarkableCompanionApp" -} -#endregion - -#region Logi Options -"logioptions" = @{ - DisplayName = "Logi Options Plus" - RepoPath = "l/Logitech/OptionsPlus" - YamlFile = "Logitech.OptionsPlus.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/logioptionsplus_installer\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/logioptionsplus_installer\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/quiet /analytics no" - IsWinget = $true - WingetID = "Logitech.OptionsPlus" -} -#endregion - -#region Sublime Text -"sublimetext" = @{ - DisplayName = "Sublime Text" - RepoPath = "s/SublimeHQ/SublimeText/4" - YamlFile = "SublimeHQ.SublimeText.4.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/sublime_text_build_\d+_x64_setup\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/sublime_text_build_\d+_x64_setup\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/VERYSILENT /NORESTART" - IsWinget = $true - WingetID = "SublimeHQ.SublimeText.4" -} -#endregion - -#region Microsoft DotNet SDK 10 -"microsoftdotnetsdk" = @{ - DisplayName = "Microsoft DotNet SDK 10" - RepoPath = "m/Microsoft/DotNet/SDK/10" - YamlFile = "Microsoft.DotNet.SDK.10.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/dotnet-sdk-\d+\.\d+\.\d+-win-x64\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/dotnet-sdk-\d+\.\d+\.\d+-win-arm64\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/quiet" - IsWinget = $true - WingetID = "Microsoft.DotNet.SDK.10" -} -#endregion - -#region Microsoft DotNet Runtime 10 -"microsoftdotnetruntime" = @{ - DisplayName = "Microsoft DotNet Runtime 10" - RepoPath = "m/Microsoft/DotNet/Runtime/10" - YamlFile = "Microsoft.DotNet.Runtime.10.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/dotnet-runtime-\d+\.\d+\.\d+-win-x64\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/dotnet-runtime-\d+\.\d+\.\d+-win-arm64\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/quiet" - IsWinget = $true - WingetID = "Microsoft.DotNet.Runtime.10" -} -#endregion - -#region PuTTy -"putty" = @{ - DisplayName = "PuTTy" - RepoPath = "p/PuTTY/PuTTY" - YamlFile = "PuTTY.PuTTY.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/putty-64bit-\d+\.\d+-installer\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S*/putty-arm64-\d+\.\d+-installer\.msi)' - InstallerType = "msi" - IsWinget = $true - WingetID = "PuTTY.PuTTY" -} -#endregion - -#region Go Programming Language -"golang" = @{ - DisplayName = "Go Programming Language" - RepoPath = "g/GoLang/Go" - YamlFile = "GoLang.Go.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/go\d+\.\d+\.\d+\.windows-amd64\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S*/go\d+\.\d+\.\d+\.windows-arm64\.msi)' - InstallerType = "msi" - IsWinget = $true - WingetID = "GoLang.Go" -} -#endregion - -#region Cisco Webex -"webex" = @{ - DisplayName = "Cisco Webex" - RepoPath = "c/Cisco/Webex" - YamlFile = "Cisco.Webex.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/Webex\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S*/Webex\.msi)' - InstallerType = "msi" - IsWinget = $true - WingetID = "Cisco.Webex" -} -#endregion - -#region Microsoft Edge -"edge" = @{ - DisplayName = "Microsoft Edge" - RepoPath = "m/Microsoft/Edge" - YamlFile = "Microsoft.Edge.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/MicrosoftEdgeEnterpriseX64\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S*/MicrosoftEdgeEnterpriseARM64\.msi)' - InstallerType = "msi" - IsWinget = $true - WingetID = "Microsoft.Edge" -} -#endregion - -#region PDF24 -"pdf24" = @{ - DisplayName = "PDF24 Creator" - RepoPath = "g/geeksoftwareGmbH/PDF24Creator" - YamlFile = "geeksoftwareGmbH.PDF24Creator.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/pdf24-creator-\d+\.\d+\.\d+-x64\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S*/pdf24-creator-\d+\.\d+\.\d+-arm64\.msi)' - InstallerType = "msi" - IsWinget = $true - WingetID = "geeksoftwareGmbH.PDF24Creator" -} -#endregion - -#region 8x8 Work -"8x8work" = @{ - DisplayName = "8x8 Work" - RepoPath = "8/8x8/Work" - YamlFile = "8x8.Work.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S+)' - PatternARM64 = 'InstallerUrl:\s*(\S+)' - InstallerType = "msi" - IsWinget = $true - WingetID = "8x8.Work" -} -#endregion - -#region Powershell 7 -"powershell7" = @{ - DisplayName = "Powershell 7" - RepoPath = "m/Microsoft/PowerShell" - YamlFile = "Microsoft.PowerShell.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/PowerShell-\d+\.\d+\.\d+-win-x64\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S*/PowerShell-\d+\.\d+\.\d+-win-arm64\.msi)' - InstallerType = "msi" - IsWinget = $true - WingetID = "Microsoft.PowerShell" -} -#endregion - -#region Wireshark -"wireshark" = @{ - DisplayName = "Wireshark" - RepoPath = "w/WiresharkFoundation/Wireshark" - YamlFile = "WiresharkFoundation.Wireshark.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/Wireshark-\S*-x64\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S*/Wireshark-\S*-x64\.msi)' - InstallerType = "msi" - IsWinget = $true - WingetID = "WiresharkFoundation.Wireshark" -} -#endregion - -#region Zoom -"zoom" = @{ - DisplayName = "Zoom" - RepoPath = "z/Zoom/Zoom" - YamlFile = "Zoom.Zoom.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/ZoomInstallerFull\.msi\?archType=x64)' - PatternARM64 = 'InstallerUrl:\s*(\S*/ZoomInstallerFull\.msi\?archType=winarm64)' - InstallerType = "msi" - IsWinget = $true - WingetID = "Zoom.Zoom" -} -#endregion - -#region GitHub Desktop -"githubdesktop" = @{ - DisplayName = "GitHub Desktop" - RepoPath = "g/GitHub/GitHubDesktop" - YamlFile = "GitHub.GitHubDesktop.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S+.*64.*\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S+.*arm64.*\.msi)' - InstallerType = "msi" - IsWinget = $true - WingetID = "GitHub.GitHubDesktop" -} -#endregion - -#region VLC Media Player -"vlc" = @{ - DisplayName = "VLC Media Player" - RepoPath = "v/VideoLAN/VLC" - YamlFile = "VideoLAN.VLC.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/vlc-\d+\.\d+\.\d+-win64\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/vlc-\d+\.\d+\.\d+-win64\.exe)' - InstallerType = "exe" - IsWinget = $true - ExeInstallArgs = "/S" - WingetID = "VideoLAN.VLC" -} -#endregion - -#region NodeJS -"nodejs" = @{ - DisplayName = "NodeJS" - RepoPath = "o/OpenJS/NodeJS" - YamlFile = "OpenJS.NodeJS.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/node-v\S*-x64\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S*/node-v\S*-arm64\.msi)' - InstallerType = "msi" - IsWinget = $true - WingetID = "OpenJS.NodeJS" -} -#endregion - -#region Google Chrome -"chrome" = @{ - DisplayName = "Google Chrome" - RepoPath = "g/Google/Chrome" - YamlFile = "Google.Chrome.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S+.*64.*\.msi)' - PatternARM64 = 'InstallerUrl:\s*(\S+.*arm64.*\.msi)' - InstallerType = "msi" - IsWinget = $true - WingetID = "Google.Chrome" -} -#endregion - -#region Display Link -"displaylink" = @{ - DisplayName = "Display Link" - RepoPath = "d/DisplayLink/GraphicsDriver" - YamlFile = "DisplayLink.GraphicsDriver.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S+)' - PatternARM64 = 'InstallerUrl:\s*(\S+)' - InstallerType = "zip" - IsWinget = $true - WingetID = "DisplayLink.GraphicsDriver" -} -#endregion - - - - - - - -########################## -########################## -#region NOT WINGET APPS ## -########################## -########################## -########################## -########################## -########################## -#region Windows App -"windowsapp" = @{ - DisplayName = "Windows App" - IsWinget = $false - DownloadUrl = "https://go.microsoft.com/fwlink/?linkid=2262633" - InstallerType = "msix" -} -#endregion - -#region MyDPD -"mydpd" = @{ - DisplayName = "MyDPD Customer" - IsWinget = $false - DownloadUrl = "https://apis.my.dpd.co.uk/apps/download/public" - InstallerType = "exe" - ExeInstallArgs = "--Silent" -} -#endregion - -#region Royal Mail Print Assist -"royalmail" = @{ - DisplayName = "Royal Mail Print Assist" - IsWinget = $false - DownloadUrl = "http://app.printnode.com/download/client/royalmail/windows" - InstallerType = "exe" - ExeInstallArgs = "/VERYSILENT /SUPPRESSMSGBOXES" -} -#endregion - -#region Crosschex -"crosschex" = @{ - DisplayName = "Crosschex" - IsWinget = $false - DownloadUrl = "https://www.anviz.com/file/download/5539/CrossChex_Standard_4.3.16.exe" - InstallerType = "exe" - ExeInstallArgs = "/exenoui ALLUSERS=1 /qn" -} -#endregion - -#region Coreldraw -"coreldraw" = @{ - DisplayName = "Coreldraw" - IsWinget = $false - DownloadUrl = "https://www.corel.com/akdlm/6763/downloads/free/trials/GraphicsSuite/2019/R5tgO2Wx1/getdl/CorelDRAWGraphicsSuite2019Installer_AM.exe" - InstallerType = "exe" - ExeInstallArgs = "/qn" -} -#endregion - -#region N-Able RMM Agent -# How to Install -#Install-TecharyApp -AppName "nable" -Parameters @{ -# CustomerID = "123456" This can be found in N-Central under Administration > Customers, there you will see the Access Code column -# Token = "abcdefg" Token is got under Actions > Add/Import Devices > Get Registration Token -# CustomerName = '\"Company Name From N-Central\"' #This has to be formatted like this with the name of their customer in nable -# ServerAddress = "Refer to Confluence guide for the server address" -# } - -"nable" = @{ - DisplayName = "N-Able RMM Agent" - IsWinget = $false - InstallerType = "exe" - ExeInstallArgs = "/qn /v" -} -#endregion - - -} diff --git a/Intune-Packager.ps1 b/Intune-Packager.ps1 deleted file mode 100644 index 7c4c5bb..0000000 --- a/Intune-Packager.ps1 +++ /dev/null @@ -1,176 +0,0 @@ -$ProgressPreference = 'SilentlyContinue' - -Add-Type -AssemblyName System.Windows.Forms -Add-Type -AssemblyName System.Drawing - -# === Configurable Paths === -$global:SourceRoot = "C:\IntuneApps\Source" -$global:OutputRoot = "C:\IntuneApps\Output" -$global:WinAppToolDir = "C:\IntuneApps\IntuneWinAppTool" -$global:WinAppToolExe = Join-Path $WinAppToolDir "Microsoft-Win32-Content-Prep-Tool-1.8.7\IntuneWinAppUtil.exe" -$global:RemoteAppMapUrl = "https://raw.githubusercontent.com/Techary/TecharyGet/refs/heads/main/AppMap.ps1" - -# === Ensure Required Folders Exist === -$null = New-Item -ItemType Directory -Path $SourceRoot -Force -$null = New-Item -ItemType Directory -Path $OutputRoot -Force -$null = New-Item -ItemType Directory -Path $WinAppToolDir -Force - -# === Download IntuneWinAppUtil === -function Get-IntuneWinAppUtil { - if (-Not (Test-Path $global:WinAppToolExe)) { - Write-Host "Downloading IntuneWinAppUtil..." - $zipUrl = "https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool/archive/refs/tags/v1.8.7.zip" - $zipPath = Join-Path $WinAppToolDir "tool.zip" - Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing - Expand-Archive -Path $zipPath -DestinationPath $WinAppToolDir -Force - Remove-Item $zipPath -Force - } - - return $global:WinAppToolExe -} - -# === Load Remote AppMap === -function Load-TecharyApps { - $tempAppMap = Join-Path $env:TEMP "AppMap.ps1" - Invoke-WebRequest -Uri $global:RemoteAppMapUrl -OutFile $tempAppMap -UseBasicParsing - - $script:TecharyApps = @{} - . $tempAppMap - Remove-Item $tempAppMap -Force -} - -# === Build GUI === -$form = New-Object System.Windows.Forms.Form -$form.Text = "TecharyGet Intune App Packager" -$form.Size = New-Object System.Drawing.Size(460, 400) -$form.StartPosition = "CenterScreen" - -$label = New-Object System.Windows.Forms.Label -$label.Text = "Select an app to package:" -$label.Location = New-Object System.Drawing.Point(20, 20) -$label.AutoSize = $true -$form.Controls.Add($label) - -$comboBox = New-Object System.Windows.Forms.ComboBox -$comboBox.Location = New-Object System.Drawing.Point(20, 50) -$comboBox.Size = New-Object System.Drawing.Size(400, 30) -$comboBox.DropDownStyle = "DropDownList" -$form.Controls.Add($comboBox) - -# === Dynamic Fields for "nable" app === -$extraFields = @{} -$fieldDefinitions = @( - @{ Name = "CustomerID"; Label = "Customer ID:"; Y = 100 }, - @{ Name = "Token"; Label = "Token:"; Y = 130 }, - @{ Name = "CustomerName"; Label = "Customer Name:"; Y = 160 }, - @{ Name = "ServerAddress"; Label = "Server Address:"; Y = 190 } -) - -foreach ($field in $fieldDefinitions) { - $label = New-Object System.Windows.Forms.Label - $label.Text = $field.Label - $label.Location = New-Object System.Drawing.Point(20, $field.Y) - $label.Size = New-Object System.Drawing.Size(120, 20) - $label.Visible = $false - $form.Controls.Add($label) - - $textbox = New-Object System.Windows.Forms.TextBox - $textbox.Location = New-Object System.Drawing.Point(150, $field.Y) - $textbox.Size = New-Object System.Drawing.Size(270, 20) - $textbox.Visible = $false - $form.Controls.Add($textbox) - - $extraFields[$field.Name] = @{ Label = $label; TextBox = $textbox } -} - -# === Button === -$button = New-Object System.Windows.Forms.Button -$button.Text = "Create IntuneWin Package" -$button.Location = New-Object System.Drawing.Point(20, 230) -$button.Size = New-Object System.Drawing.Size(400, 40) -$form.Controls.Add($button) - -# === Status === -$statusLabel = New-Object System.Windows.Forms.Label -$statusLabel.Location = New-Object System.Drawing.Point(20, 290) -$statusLabel.Size = New-Object System.Drawing.Size(400, 60) -$form.Controls.Add($statusLabel) - -# === Load and Populate App List === -Load-TecharyApps -$comboBox.Items.AddRange(@($script:TecharyApps.Keys | Sort-Object)) - -# === ComboBox Change - show/hide "nable" fields === -$comboBox.Add_SelectedIndexChanged({ - $selected = $comboBox.SelectedItem - $isNable = $selected -eq "nable" - foreach ($field in $extraFields.Values) { - $field.Label.Visible = $isNable - $field.TextBox.Visible = $isNable - } -}) - -# === Button Logic === -$button.Add_Click({ - $appName = $comboBox.SelectedItem - if (-not $appName) { - $statusLabel.Text = "Please select an app." - return - } - - try { - $appFolder = Join-Path $SourceRoot $appName - $null = New-Item -ItemType Directory -Path $appFolder -Force - - # === Prepare install script - $installScript = Join-Path $appFolder "install-$appName.ps1" - if ($appName -eq "nable") { - $params = @{} - foreach ($key in $extraFields.Keys) { - $value = $extraFields[$key].TextBox.Text - if (-not $value) { - $statusLabel.Text = "$key is required for 'nable'." - return - } - $params[$key] = $value - } - - $installContent = @" -Install-TecharyApp -AppName `"nable`" -Parameters @{ - CustomerID = `'$($params.CustomerID)`' - Token = `'$($params.Token)`' - CustomerName = `'$($params.CustomerName)`' - ServerAddress = `'$($params.ServerAddress)`' -} -"@ - } else { - $installContent = "Install-TecharyApp -AppName `"$appName`"" - } - - # === Create uninstall script - $uninstallScript = Join-Path $appFolder "uninstall-$appName.ps1" - $uninstallContent = "Uninstall-TecharyApp -AppName `"$appName`"" - - # === Write Scripts - Set-Content -Path $installScript -Value $installContent -Encoding UTF8 - Set-Content -Path $uninstallScript -Value $uninstallContent -Encoding UTF8 - - # === Package using IntuneWinAppUtil - $intuneWinAppUtil = Get-IntuneWinAppUtil - $outputPath = Join-Path $OutputRoot $appName - $null = New-Item -ItemType Directory -Path $outputPath -Force - - $statusLabel.Text = "Packaging $appName..." - & $intuneWinAppUtil -c $appFolder -s ("install-$appName.ps1") -o $outputPath | Out-Null - - $statusLabel.Text = "$appName packaged successfully. Output: $outputPath" - } - catch { - $statusLabel.Text = "Error: $($_.Exception.Message)" - } -}) - -# === Show GUI === -$form.Topmost = $true -$form.Add_Shown({ $form.Activate() }) -[void]$form.ShowDialog() diff --git a/Private/CustomApps.json b/Private/CustomApps.json new file mode 100644 index 0000000..29e4be8 --- /dev/null +++ b/Private/CustomApps.json @@ -0,0 +1,23 @@ +[ + { + "Id": "MyDPD", + "Url": "https://apis.my.dpd.co.uk/apps/download/public", + "SilentArgs": "--Silent", + "InstallerType": "exe", + "DisplayName": "MyDPD" + }, + { + "Id": "RoyalMail", + "Url": "http://app.printnode.com/download/client/royalmail/windows", + "SilentArgs": "/VERYSILENT /SUPPRESSMSGBOXES", + "InstallerType": "exe", + "DisplayName": "Royal Mail" + }, + { + "Id": "Crosschex", + "Url": "https://www.anviz.com/file/download/5539/CrossChex_Standard_4.3.16.exe", + "SilentArgs": "/exenoui ALLUSERS=1 /qn", + "InstallerType": "exe", + "DisplayName": "Crosschex" + } +] \ No newline at end of file diff --git a/Private/Get-CustomApp.ps1 b/Private/Get-CustomApp.ps1 new file mode 100644 index 0000000..4a6cae4 --- /dev/null +++ b/Private/Get-CustomApp.ps1 @@ -0,0 +1,67 @@ +function Get-CustomApp { + param ( + [string]$Id + ) + + # --- 1. CLOUD SOURCE --- + # We use the "Raw" GitHub URL so we get just the JSON text. + # Structure: https://raw.githubusercontent.com//// + $CloudUrl = "https://raw.githubusercontent.com/Techary/TecharyGet/BETA/TecharyGet/Private/CustomApps.json" + + # --- 2. LOCAL CACHE --- + # We cache the file locally so the script works even if GitHub is briefly down + # or if the machine is offline (using the last known good copy). + $CacheDir = "$env:PROGRAMDATA\TecharyGet" + $CachePath = "$CacheDir\CustomApps_Cache.json" + + # --- 3. SYNC LOGIC --- + try { + if (-not (Test-Path $CacheDir)) { New-Item -ItemType Directory -Path $CacheDir -Force | Out-Null } + + # Logic: Only download if the cache doesn't exist OR it's older than 60 minutes. + # This prevents spamming GitHub every time you run a command. + $NeedUpdate = $true + if (Test-Path $CachePath) { + $LastWrite = (Get-Item $CachePath).LastWriteTime + if ((Get-Date) -lt $LastWrite.AddMinutes(60)) { $NeedUpdate = $false } + } + + if ($NeedUpdate) { + Write-PackagerLog -Message "Syncing Custom Catalog from GitHub..." -Severity Info + Invoke-WebRequest -Uri $CloudUrl -OutFile $CachePath -UseBasicParsing -ErrorAction Stop + } + } + catch { + Write-PackagerLog -Message "Could not sync from GitHub (Offline?). Using local cache." -Severity Warning + } + + # --- 4. READ DATA --- + $JsonContent = $null + + # Prefer the fresh Cache + if (Test-Path $CachePath) { + $JsonContent = Get-Content -Path $CachePath -Raw + } + # Fallback to the file shipped with the module (if cache is empty/broken) + else { + $LocalModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Private\CustomApps.json" + if (Test-Path $LocalModulePath) { + $JsonContent = Get-Content -Path $LocalModulePath -Raw + } + } + + # --- 5. PARSE & RETURN --- + if ($JsonContent) { + try { + $Data = $JsonContent | ConvertFrom-Json + $Match = $Data | Where-Object { $_.Id -eq $Id } + return $Match + } + catch { + Write-PackagerLog -Message "Error parsing CustomApps.json: $_" -Severity Error + return $null + } + } + + return $null +} \ No newline at end of file diff --git a/Public/Get-GitHubInstaller.ps1 b/Public/Get-GitHubInstaller.ps1 new file mode 100644 index 0000000..c028dd0 --- /dev/null +++ b/Public/Get-GitHubInstaller.ps1 @@ -0,0 +1,123 @@ +function Get-GitHubInstaller { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string]$Id, + [string]$DownloadPath = "$env:TEMP\AppPackager" + ) + + Write-PackagerLog -Message "Querying GitHub Manifests for: $Id" + + try { + # 1. Detect Architecture + if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { $SysArch = "arm64" } + elseif ([Environment]::Is64BitOperatingSystem) { $SysArch = "x64" } + else { $SysArch = "x86" } + + # 2. Construct API Path + $IdPath = $Id.Replace(".", "/") + $FirstChar = $Id.Substring(0,1).ToLower() + $BaseApi = "https://api.github.com/repos/microsoft/winget-pkgs/contents/manifests/$FirstChar/$IdPath" + + # 3. Get Version (Latest) + $VersionsResponse = Invoke-RestMethod -Uri $BaseApi -Method Get -ErrorAction Stop + $LatestVersionObj = $VersionsResponse | + Where-Object { $_.type -eq "dir" } | + Select-Object *, @{N='ParsedVersion'; E={ try { [Version]$_.name } catch { $null } }} | + Where-Object { $_.ParsedVersion -ne $null } | + Sort-Object ParsedVersion -Descending | + Select-Object -First 1 + + if (-not $LatestVersionObj) { throw "Could not determine a valid numeric version." } + $LatestVersion = $LatestVersionObj.Name + + # 4. Get Manifest + $VersionPath = "$BaseApi/$LatestVersion" + $VersionFiles = Invoke-RestMethod -Uri $VersionPath -Method Get + $InstallerFile = $VersionFiles | Where-Object { $_.name -like "*.installer.yaml" } | Select-Object -First 1 + if (-not $InstallerFile) { throw "No installer YAML found." } + + $YamlContent = Invoke-RestMethod -Uri $InstallerFile.download_url + + # --- PARSING LOGIC --- + # We split by "- Architecture" to separate blocks, but keep the delimiter to help identification + $Blocks = $YamlContent -split '(?=-\s*Architecture:)' + + $SelectedUrl = $null + $SelectedArgs = $null + $SelectedType = "exe" + $SelectedCode = $null + + foreach ($Block in $Blocks) { + if ([string]::IsNullOrWhiteSpace($Block)) { continue } + + # Extract Architecture from this block + if ($Block -match 'Architecture:\s*([a-zA-Z0-9]+)') { + $BlockArch = $Matches[1].Trim() + + # If this block matches our system, scrape it! + if ($BlockArch -eq $SysArch) { + if ($Block -match 'InstallerUrl:\s*["'']?([^"''\r\n]+)["'']?') { $SelectedUrl = $Matches[1].Trim() } + if ($Block -match 'InstallerType:\s*([a-zA-Z0-9]+)') { $SelectedType = $Matches[1].Trim() } + + # Scrape Arguments + if ($Block -match 'Silent:\s*(.+)') { $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') } + elseif ($Block -match 'SilentWithProgress:\s*(.+)') { $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') } + + # Scrape Product Code (Flexible Regex) + # This now matches "{GUID}" OR "SimpleString" + if ($Block -match 'ProductCode:\s*["'']?([^"''\r\n]+)["'']?') { + $SelectedCode = $Matches[1].Trim() + } + + # If we found a URL, we stop looking (we prefer the first match for our arch) + if ($SelectedUrl) { break } + } + } + } + + # Fallbacks (Global properties if not in block) + if (-not $SelectedUrl) { if ($YamlContent -match 'InstallerUrl:\s*["'']?([^"''\r\n]+)["'']?') { $SelectedUrl = $Matches[1].Trim() } } + if (-not $SelectedArgs) { + if ($YamlContent -match 'Silent:\s*(.+)') { $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') } + } + if (-not $SelectedCode) { + if ($YamlContent -match 'ProductCode:\s*["'']?([^"''\r\n]+)["'']?') { $SelectedCode = $Matches[1].Trim() } + } + + # Special Override for Dell (Command Update) + if ($Id -eq "Dell.CommandUpdate") { $SelectedArgs = '/s /l="C:\Windows\Temp\DellCommand.log" /v"/qn"' } + + # Special Override for 8x8 Work MSI + if ($Id -eq "8x8.Work") { $SelectedArgs = "/qn /norestart" } + + # Special Override for Sublime Text 4 + if ($Id -eq "SublimeHQ.SublimeText.4") { $SelectedArgs = "/VERYSILENT /NORESTART" } + + # --- DOWNLOAD --- + $UriObj = [System.Uri]$SelectedUrl + $RealExtension = [System.IO.Path]::GetExtension($UriObj.LocalPath).ToLower() + if (-not $RealExtension) { $RealExtension = ".$SelectedType" } + $FileName = "$Id-$LatestVersion-$SysArch$RealExtension" + + if (Test-Path $DownloadPath) { Remove-Item "$DownloadPath\*" -Recurse -Force -ErrorAction SilentlyContinue } + New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null + $FullPath = Join-Path $DownloadPath $FileName + + Write-PackagerLog -Message "Downloading to $FullPath..." + Invoke-WebRequest -Uri $SelectedUrl -OutFile $FullPath -UseBasicParsing -UserAgent "Mozilla/5.0" + + return [PSCustomObject]@{ + Name = $Id + InstallerPath = $FullPath + FileName = $FileName + SilentArgs = $SelectedArgs + InstallerType = $SelectedType + ProductCode = $SelectedCode + } + } + catch { + Write-PackagerLog -Message "GitHub Scraping Failed: $_" -Severity Error + throw $_ + } +} diff --git a/Public/Install-AppPackage.ps1 b/Public/Install-AppPackage.ps1 new file mode 100644 index 0000000..8fbf4c8 --- /dev/null +++ b/Public/Install-AppPackage.ps1 @@ -0,0 +1,103 @@ +function Install-AppPackage { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)][string]$FilePath, + [Parameter(Mandatory=$false)][object]$Arguments = @(), + [string]$Name = "Unknown Application" + ) + + $Extension = [System.IO.Path]::GetExtension($FilePath).ToLower() + if (Test-Path $FilePath) { Unblock-File -Path $FilePath } + + Write-PackagerLog -Message "Starting installation flow for: $Name ($Extension)" + + # --- ROUTING LOGIC --- + switch ($Extension) { + ".zip" { + Write-PackagerLog -Message "Detected ZIP. Extracting..." + $ZipName = [System.IO.Path]::GetFileNameWithoutExtension($FilePath) + $DestPath = Join-Path (Split-Path $FilePath) "Extracted_$ZipName" + if (Test-Path $DestPath) { Remove-Item $DestPath -Recurse -Force } + Expand-Archive -Path $FilePath -DestinationPath $DestPath -Force + + $Candidates = Get-ChildItem -Path $DestPath -Include *.exe,*.msi -Recurse + $Installer = $Candidates | Where-Object { $_.Name -match "setup" -or $_.Name -match "install" } | Select-Object -First 1 + if (-not $Installer) { $Installer = $Candidates | Sort-Object Length -Descending | Select-Object -First 1 } + if (-not $Installer) { throw "Extracted ZIP but could not find installer." } + + Write-PackagerLog -Message "Found installer inside ZIP: $($Installer.Name)" + Install-AppPackage -Name $Name -FilePath $Installer.FullName -Arguments $Arguments + return + } + { $_ -in ".msix", ".appx", ".msixbundle", ".appxbundle" } { + Write-PackagerLog -Message "Detected Modern App. Sideloading..." + try { + $PolicyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Appx" + if (-not (Test-Path $PolicyPath)) { New-Item -Path $PolicyPath -Force | Out-Null } + New-ItemProperty -Path $PolicyPath -Name "AllowAllTrustedApps" -Value 1 -PropertyType DWORD -Force | Out-Null + + $DevPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" + if (-not (Test-Path $DevPath)) { New-Item -Path $DevPath -Force | Out-Null } + New-ItemProperty -Path $DevPath -Name "AllowAllTrustedApps" -Value 1 -PropertyType DWORD -Force | Out-Null + + Add-AppxProvisionedPackage -Online -PackagePath $FilePath -SkipLicense -ErrorAction Stop | Out-Null + Write-PackagerLog -Message "MSIX Provisioned Successfully." + } + catch { + Write-PackagerLog -Message "Provisioning Failed ($($_)). Trying Per-User..." -Severity Warning + try { Add-AppxPackage -Path $FilePath -ErrorAction Stop } catch { throw $_ } + } + return + } + ".msi" { + Write-PackagerLog -Message "Detected MSI. Switching to msiexec." + $MsiPath = $FilePath + $FilePath = "msiexec.exe" + # Logic to ensure /i is prepended cleanly + if ($Arguments -is [string]) { $Arguments = "/i `"$MsiPath`" $Arguments" } + else { $Arguments = @("/i", $MsiPath) + $Arguments } + } + } + + # --- SAFE EXECUTION --- + # 1. Sanitize Arguments (The Fix for the Null Crash) + if ($null -eq $Arguments) { $Arguments = @() } + + if ($Arguments -is [string]) { + # Regex split that respects quotes + $Regex = ' (?=(?:[^"]*"[^"]*")*[^"]*$)' + $ArgList = [regex]::Split($Arguments, $Regex) + } else { + $ArgList = $Arguments + } + + # Filter out nulls/empties from the array + $ArgList = $ArgList | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + + Write-PackagerLog -Message "Executor: $FilePath" + Write-PackagerLog -Message "Final Args: $($ArgList -join ' | ')" + + try { + # If ArgList is empty, pass $null explicitly to avoid binding errors + if ($ArgList.Count -eq 0) { + $Process = Start-Process -FilePath $FilePath -PassThru -Wait -NoNewWindow + } else { + $Process = Start-Process -FilePath $FilePath -ArgumentList $ArgList -PassThru -Wait -NoNewWindow + } + + $ExitCode = $Process.ExitCode + Write-PackagerLog -Message "Finished. Exit Code: $ExitCode" + + switch ($ExitCode) { + 0 { Write-PackagerLog -Message "Success." } + 3010 { Write-PackagerLog -Message "Success (Reboot Required)." -Severity Warning } + 1641 { Write-PackagerLog -Message "Success (Hard Reboot Initiated)." -Severity Warning } + 4 { Write-PackagerLog -Message "Success (Reboot Required - Vendor Specific)." -Severity Warning } + default { throw "Failed with code $ExitCode" } + } + } + catch { + Write-PackagerLog -Message "Installation Failure: $_" -Severity Error + throw $_ + } +} \ No newline at end of file diff --git a/Public/Install-NableAgent.ps1 b/Public/Install-NableAgent.ps1 new file mode 100644 index 0000000..2662fef --- /dev/null +++ b/Public/Install-NableAgent.ps1 @@ -0,0 +1,107 @@ +function Install-NableAgent { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$CustomerID, + + [Parameter(Mandatory = $true)] + [string]$Token, + + [Parameter(Mandatory = $false)] + [string]$CustomerName, + + [Parameter(Mandatory = $true)] + [string]$ServerAddress, + + [int]$TimeoutSeconds = 600 + ) + + $Name = "N-able RMM Agent" + $DownloadPath = "C:\Temp\AppPackager" + $FileName = "Nable_RMMInstaller.exe" + $InstallerPath = Join-Path $DownloadPath $FileName + $DownloadUrl = "https://$ServerAddress/download/current/winnt/N-central/WindowsAgentSetup.exe" + + try { + Write-PackagerLog -Message "Starting N-able Deployment for Customer: $CustomerName" + + if (-not (Test-Path $DownloadPath)) { + New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null + } + + if (Test-Path $InstallerPath) { + Remove-Item $InstallerPath -Force -ErrorAction SilentlyContinue + } + + Write-PackagerLog -Message "Downloading installer from: $DownloadUrl" + + Invoke-WebRequest ` + -Uri $DownloadUrl ` + -OutFile $InstallerPath ` + -UseBasicParsing ` + -UserAgent "Mozilla/5.0" + + if (-not (Test-Path $InstallerPath)) { + throw "Download failed. File not found." + } + + Unblock-File -Path $InstallerPath -ErrorAction SilentlyContinue + + # If N-able requires the customer name value literally as: + # '\"Techary Internal\"' + # $FormattedCustomerName = "\`"$CustomerName\`"" + + # Build arguments EXACTLY like the working script pattern + $MsiArgs = "/qn CUSTOMERID=$CustomerID CUSTOMERSPECIFIC=1 REGISTRATION_TOKEN=$Token SERVERPROTOCOL=HTTPS SERVERADDRESS=$ServerAddress SERVERPORT=443" + $Arguments = "/S /v`"$MsiArgs`"" + + Write-PackagerLog -Message "Executing N-able installer..." + Write-PackagerLog -Message "Executor: $InstallerPath" + Write-PackagerLog -Message "Final Args: $Arguments" + + $Process = Start-Process ` + -FilePath $InstallerPath ` + -ArgumentList $Arguments ` + -Wait ` + -PassThru ` + -NoNewWindow + + Write-PackagerLog -Message "Installer finished with exit code: $($Process.ExitCode)" + + # Do NOT fail immediately on exit code. + # The working script's real success criteria is service validation. + Write-PackagerLog -Message "Starting Post-Install Validation (Timeout: ${TimeoutSeconds}s)..." + + $StartTime = Get-Date + + while ($true) { + # Check Services and Files + $Service1 = Get-Service -Name "Windows Agent Service" -ErrorAction SilentlyContinue + $Service2 = Get-Service -Name "N-able Take Control Service (N-Central)" -ErrorAction SilentlyContinue + $FileCheck = Test-Path "C:\Program Files (x86)\BeAnywhere Support Express\GetSupportService_N-Central\uninstall.exe" + + $Service1Running = $Service1 -and $Service1.Status -eq "Running" + $Service2Running = $Service2 -and $Service2.Status -eq "Running" + + if ($Service1Running -and $Service2Running -and $FileCheck) { + Write-PackagerLog -Message "Validation Successful: N-able services are running." + Remove-Item $InstallerPath -Force -ErrorAction SilentlyContinue + return + } + + # Check Timeout + $Elapsed = (Get-Date) - $StartTime + if ($Elapsed.TotalSeconds -ge $TimeoutSeconds) { + Write-PackagerLog -Message "Validation Timed Out. Services did not start in time." -Severity Error + throw "N-able installation finished, but validation failed (Timeout). ExitCode=$($Process.ExitCode)" + } + + Write-PackagerLog -Message "Waiting for services to start..." + Start-Sleep -Seconds 10 + } + } + catch { + Write-PackagerLog -Message "N-able Deployment Failed: $_" -Severity Error + throw $_ + } +} diff --git a/Public/Install-TecharyApp.ps1 b/Public/Install-TecharyApp.ps1 new file mode 100644 index 0000000..d54e73a --- /dev/null +++ b/Public/Install-TecharyApp.ps1 @@ -0,0 +1,68 @@ +$ProgressPreference = 'SilentlyContinue' + +function Install-TecharyApp { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [string]$Id + ) + + $Pkg = $null + + # --- ATTEMPT 1: GITHUB --- + try { + $Pkg = Get-GitHubInstaller -Id $Id -ErrorAction Stop + } + catch { + Write-PackagerLog -Message "Not found in GitHub ($Id). Checking Custom Catalog..." -Severity Info + } + + # --- ATTEMPT 2: CUSTOM CATALOG --- + if (-not $Pkg) { + # Load the internal helper to check JSON + # (Assuming Get-CustomApp is dot-sourced in .psm1) + $CustomData = Get-CustomApp -Id $Id + + if ($CustomData) { + Write-PackagerLog -Message "Found '$Id' in Custom Catalog." + + # Use the Web Installer logic to download it + # We can reuse the logic or call Get-WebInstaller if you created it. + # Here is the inline logic for simplicity: + + $DownloadPath = "$env:TEMP\AppPackager" + if (Test-Path $DownloadPath) { Remove-Item "$DownloadPath\*" -Recurse -Force -ErrorAction SilentlyContinue } + New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null + + $FileName = "$Id.$($CustomData.InstallerType)" + $FullPath = Join-Path $DownloadPath $FileName + + Write-PackagerLog -Message "Downloading Custom App from: $($CustomData.Url)" + Invoke-WebRequest -Uri $CustomData.Url -OutFile $FullPath -UseBasicParsing + + # Build the Package Object manually + $Pkg = [PSCustomObject]@{ + Name = $Id + InstallerPath = $FullPath + FileName = $FileName + SilentArgs = $CustomData.SilentArgs + InstallerType = $CustomData.InstallerType + } + } + } + + if (-not $Pkg) { + Write-PackagerLog -Message "Application '$Id' not found in GitHub OR Custom Catalog." -Severity Error + return + } + + # --- INSTALLATION --- + # MSI Fallback Logic + $Args = $Pkg.SilentArgs + if ([string]::IsNullOrWhiteSpace($Args) -and ($Pkg.InstallerPath -match ".msi$" -or $Pkg.InstallerType -eq "msi")) { + $Args = "/qb /norestart" + } + + Install-AppPackage -Name $Pkg.Name -FilePath $Pkg.InstallerPath -Arguments $Args + Invoke-PackagerCleanup -Paths "$env:TEMP\AppPackager" -Force +} \ No newline at end of file diff --git a/Public/Invoke-PackagerCleanup.ps1 b/Public/Invoke-PackagerCleanup.ps1 new file mode 100644 index 0000000..7b27a39 --- /dev/null +++ b/Public/Invoke-PackagerCleanup.ps1 @@ -0,0 +1,12 @@ +function Invoke-PackagerCleanup { + [CmdletBinding()] + param ([string[]]$Paths = @("$env:TEMP\AppPackager"), [switch]$Force) + process { + foreach ($Path in $Paths) { + if (Test-Path $Path) { + Remove-Item -Path $Path -Recurse -Force -ErrorAction SilentlyContinue + Write-PackagerLog -Message "Cleaned: $Path" + } + } + } +} diff --git a/Public/New-IntunePackage.ps1 b/Public/New-IntunePackage.ps1 new file mode 100644 index 0000000..caee5c8 --- /dev/null +++ b/Public/New-IntunePackage.ps1 @@ -0,0 +1,165 @@ +function New-IntunePackage { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string]$Id, # e.g. "Dell.CommandUpdate" or "Nable" + + [string]$OutputFolder = "C:\IntunePackages" + ) + + # --- 1. SETUP WORKSPACE --- + $PackagerTemp = "$env:TEMP\IntunePackager_Working\$Id" + $SourceDir = "$PackagerTemp\Source" + $IntuneUtil = "$env:TEMP\IntuneWinAppUtil.exe" + + if (Test-Path $PackagerTemp) { Remove-Item $PackagerTemp -Recurse -Force -ErrorAction SilentlyContinue } + New-Item -Path $SourceDir -ItemType Directory -Force | Out-Null + if (-not (Test-Path $OutputFolder)) { New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null } + + if (-not (Test-Path $IntuneUtil)) { + Write-PackagerLog -Message "Downloading IntuneWinAppUtil..." + Invoke-WebRequest "https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool/raw/master/IntuneWinAppUtil.exe" -OutFile $IntuneUtil + } + + # ========================================== + # SPECIAL LOGIC: N-ABLE AGENT + # ========================================== + if ($Id -eq "Nable") { + # 1. Prompt User for N-able Details (GUI) + Add-Type -AssemblyName System.Windows.Forms + Add-Type -AssemblyName System.Drawing + + $Form = New-Object System.Windows.Forms.Form + $Form.Text = "N-able Configuration" + $Form.Size = New-Object System.Drawing.Size(400, 350) + $Form.StartPosition = "CenterScreen" + + $Inputs = @{} + $Top = 20 + foreach ($Field in @("CustomerName", "CustomerID", "Token", "ServerAddress")) { + $Lbl = New-Object System.Windows.Forms.Label + $Lbl.Text = $Field + $Lbl.Location = New-Object System.Drawing.Point(20, $Top) + $Form.Controls.Add($Lbl) + + $Box = New-Object System.Windows.Forms.TextBox + $Box.Location = New-Object System.Drawing.Point(20, $Top + 25) + $Box.Size = New-Object System.Drawing.Size(340, 25) + $Form.Controls.Add($Box) + $Inputs[$Field] = $Box + $Top += 60 + } + + $Btn = New-Object System.Windows.Forms.Button + $Btn.Text = "Generate Package" + $Btn.Location = New-Object System.Drawing.Point(20, $Top) + $Btn.Size = New-Object System.Drawing.Size(340, 40) + $Btn.DialogResult = "OK" + $Form.Controls.Add($Btn) + + $Result = $Form.ShowDialog() + if ($Result -ne "OK") { throw "Cancelled by user." } + + # 2. Extract Values + $CName = $Inputs["CustomerName"].Text + $CId = $Inputs["CustomerID"].Text + $CToken= $Inputs["Token"].Text + $CServer=$Inputs["ServerAddress"].Text + + if (-not $CName -or -not $CId -or -not $CToken -or -not $CServer) { throw "All N-able fields are required." } + + # 3. Generate Install Script (With HARDCODED Values) + $InstallContent = @" +`$ErrorActionPreference = 'Stop' +Import-Module TecharyGet -ErrorAction SilentlyContinue + +Write-Host "Installing N-able Agent for $CName..." +Install-NableAgent -CustomerName "$CName" -CustomerID "$CId" -Token "$CToken" -ServerAddress "$CServer" +"@ + Set-Content -Path "$SourceDir\Install.ps1" -Value $InstallContent + + # 4. Generate Uninstall Script + Set-Content -Path "$SourceDir\Uninstall.ps1" -Value "`$ErrorActionPreference = 'Stop'; Import-Module TecharyGet; Uninstall-SmartApp -Name 'Windows Agent'" + + # 5. Generate Detection Script (Service Check) + $DetectContent = @" +`$Service = Get-Service -Name "Windows Agent Service" -ErrorAction SilentlyContinue +if (`$Service) { + Write-Output "Detected N-able Agent Service" + exit 0 +} else { + exit 1 +} +"@ + Set-Content -Path "$SourceDir\Detect.ps1" -Value $DetectContent + + # Override output name so you can have multiple packages (e.g. "Nable-ClientA.intunewin") + $PackageName = "Nable-$CName" + + } + # ========================================== + # STANDARD LOGIC: GENERIC APPS + # ========================================== + else { + # ... (Previous Logic for 7zip, Dell, etc.) ... + + # 1. Gather Info + $ProductCode = $null + $DisplayName = $Id + try { + if (Get-Command Get-CustomApp -ErrorAction SilentlyContinue) { + $Custom = Get-CustomApp -Id $Id + if ($Custom) { + if ($Custom.DisplayName) { $DisplayName = $Custom.DisplayName } + if ($Custom.ProductCode) { $ProductCode = $Custom.ProductCode } + } + } + if (-not $ProductCode) { + $Pkg = Get-GitHubInstaller -Id $Id -DownloadPath "$PackagerTemp\Probe" -ErrorAction SilentlyContinue + if ($Pkg.ProductCode) { $ProductCode = $Pkg.ProductCode } + } + } catch {} + + # 2. Generate Scripts + Set-Content -Path "$SourceDir\Install.ps1" -Value "`$ErrorActionPreference = 'Stop'; Import-Module TecharyGet -ErrorAction SilentlyContinue; Install-SmartApp -Id `"$Id`"" + Set-Content -Path "$SourceDir\Uninstall.ps1" -Value "`$ErrorActionPreference = 'Stop'; Import-Module TecharyGet -ErrorAction SilentlyContinue; Uninstall-SmartApp -Name `"$Id`"" + + # 3. Generate Detection + $DetectScript = @" +`$TargetCode = '$ProductCode' +`$TargetName = '$DisplayName' +`$Found = `$false + +if (-not [string]::IsNullOrEmpty(`$TargetCode)) { + if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\`$TargetCode") { `$Found = `$true } + elseif (Test-Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\`$TargetCode") { `$Found = `$true } + if (`$Found) { Write-Output "Detected via Key"; exit 0 } +} + +`$Paths = @("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*") +foreach (`$Path in `$Paths) { + `$Match = Get-ItemProperty `$Path -ErrorAction SilentlyContinue | Where-Object { `$_.DisplayName -like "*`$TargetName*" } | Select-Object -First 1 + if (`$Match) { Write-Output "Detected via Name"; exit 0 } +} +exit 1 +"@ + Set-Content -Path "$SourceDir\Detect.ps1" -Value $DetectScript + + $PackageName = $Id + } + + # --- FINAL PACKAGING STEP --- + Write-PackagerLog -Message "Packaging $PackageName..." + Start-Process -FilePath $IntuneUtil -ArgumentList "-c `"$SourceDir`"", "-s `"Install.ps1`"", "-o `"$OutputFolder`"", "-q" -Wait -NoNewWindow + + # Rename Output + $Original = Join-Path $OutputFolder "Install.intunewin" + $Final = Join-Path $OutputFolder "$PackageName.intunewin" + if (Test-Path $Original) { Move-Item $Original $Final -Force } + + # Copy Detect Script + Copy-Item "$SourceDir\Detect.ps1" -Destination "$OutputFolder\$PackageName-Detect.ps1" -Force + + Write-PackagerLog -Message "SUCCESS: Created $Final" + Invoke-Item $OutputFolder +} \ No newline at end of file diff --git a/Public/New-IntunePackageUI.ps1 b/Public/New-IntunePackageUI.ps1 new file mode 100644 index 0000000..46549f3 --- /dev/null +++ b/Public/New-IntunePackageUI.ps1 @@ -0,0 +1,93 @@ +function New-IntunePackageUI { + [CmdletBinding()] + param() + + # Load Windows Forms + Add-Type -AssemblyName System.Windows.Forms + Add-Type -AssemblyName System.Drawing + + # --- UI SETUP --- + $Form = New-Object System.Windows.Forms.Form + $Form.Text = "Techary Intune Packager (Lite)" + $Form.Size = New-Object System.Drawing.Size(500, 300) + $Form.StartPosition = "CenterScreen" + $Form.FormBorderStyle = "FixedDialog" + $Form.MaximizeBox = $false + + # -- App ID Field -- + $LblId = New-Object System.Windows.Forms.Label + $LblId.Text = "Application ID (e.g. 7zip.7zip):" + $LblId.Location = New-Object System.Drawing.Point(20, 20) + $LblId.Size = New-Object System.Drawing.Size(400, 20) + $Form.Controls.Add($LblId) + + $TxtId = New-Object System.Windows.Forms.TextBox + $TxtId.Location = New-Object System.Drawing.Point(20, 45) + $TxtId.Size = New-Object System.Drawing.Size(440, 25) + $Form.Controls.Add($TxtId) + + # -- Output Folder Field -- + $LblOut = New-Object System.Windows.Forms.Label + $LblOut.Text = "Output Folder:" + $LblOut.Location = New-Object System.Drawing.Point(20, 90) + $LblOut.Size = New-Object System.Drawing.Size(400, 20) + $Form.Controls.Add($LblOut) + + $TxtOut = New-Object System.Windows.Forms.TextBox + $TxtOut.Text = "C:\IntunePackages" # Default + $TxtOut.Location = New-Object System.Drawing.Point(20, 115) + $TxtOut.Size = New-Object System.Drawing.Size(350, 25) + $Form.Controls.Add($TxtOut) + + $BtnBrowse = New-Object System.Windows.Forms.Button + $BtnBrowse.Text = "..." + $BtnBrowse.Location = New-Object System.Drawing.Point(380, 114) + $BtnBrowse.Size = New-Object System.Drawing.Size(80, 27) + $BtnBrowse.Add_Click({ + $Dialog = New-Object System.Windows.Forms.FolderBrowserDialog + if ($Dialog.ShowDialog() -eq "OK") { $TxtOut.Text = $Dialog.SelectedPath } + }) + $Form.Controls.Add($BtnBrowse) + + # -- Create Button -- + $BtnRun = New-Object System.Windows.Forms.Button + $BtnRun.Text = "CREATE PACKAGE" + $BtnRun.Location = New-Object System.Drawing.Point(20, 170) + $BtnRun.Size = New-Object System.Drawing.Size(440, 50) + $BtnRun.BackColor = [System.Drawing.Color]::CornflowerBlue + $BtnRun.ForeColor = [System.Drawing.Color]::White + $BtnRun.Font = New-Object System.Drawing.Font("Segoe UI", 12, [System.Drawing.FontStyle]::Bold) + + $BtnRun.Add_Click({ + $Id = $TxtId.Text + $Out = $TxtOut.Text + + if (-not $Id) { + [System.Windows.Forms.MessageBox]::Show("Please enter an App ID.", "Error", "OK", "Warning") + return + } + + $BtnRun.Text = "Packaging..." + $BtnRun.Enabled = $false + $Form.Update() + + try { + # Call the backend function we created earlier + New-IntunePackage -Id $Id -OutputFolder $Out + + [System.Windows.Forms.MessageBox]::Show("Package Created Successfully!", "Success", "OK", "Information") + } + catch { + [System.Windows.Forms.MessageBox]::Show("Error: $_", "Failed", "OK", "Error") + } + finally { + $BtnRun.Text = "CREATE PACKAGE" + $BtnRun.Enabled = $true + } + }) + $Form.Controls.Add($BtnRun) + + # Show + $Form.ShowDialog() | Out-Null + $Form.Dispose() +} \ No newline at end of file diff --git a/Public/Test-TecharyApp.ps1 b/Public/Test-TecharyApp.ps1 new file mode 100644 index 0000000..82ebb9e --- /dev/null +++ b/Public/Test-TecharyApp.ps1 @@ -0,0 +1,42 @@ +function Test-TecharyApp { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string]$Name # App ID (e.g. "MyDPD") or Display Name + ) + + # 1. RESOLVE ID -> DISPLAY NAME + # Check if this ID exists in your Custom Catalog with a specific DisplayName mapping + $CustomApp = Get-CustomApp -Id $Name + if ($CustomApp -and $CustomApp.DisplayName) { + $Name = $CustomApp.DisplayName + } + + # 2. SEARCH REGISTRY (Classic Apps) + $Paths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + + foreach ($Path in $Paths) { + $Match = Get-ItemProperty $Path -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "*$Name*" } | + Select-Object -First 1 + + if ($Match) { + Write-Verbose "Found Registry Match: $($Match.DisplayName)" + return $true + } + } + + # 3. SEARCH MSIX (Modern Apps) + $Msix = Get-AppxPackage -Name "*$Name*" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($Msix) { + Write-Verbose "Found MSIX: $($Msix.Name)" + return $true + } + + # 4. NOT FOUND + return $false +} \ No newline at end of file diff --git a/Public/Uninstall-TecharyApp.ps1 b/Public/Uninstall-TecharyApp.ps1 new file mode 100644 index 0000000..8b4fdd6 --- /dev/null +++ b/Public/Uninstall-TecharyApp.ps1 @@ -0,0 +1,111 @@ +function Uninstall-TecharyApp { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string]$Name, + + [switch]$WhatIf + ) + + Write-PackagerLog -Message "Searching for installed application: $Name" + + # 1. SEARCH REGISTRY (Classic Apps) + $Paths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + + $App = $null + foreach ($Path in $Paths) { + $App = Get-ItemProperty $Path -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "*$Name*" } | + Select-Object -First 1 + if ($App) { break } + } + + # 2. IF NOT IN REGISTRY, CHECK MSIX (Modern Apps) + if (-not $App) { + Write-PackagerLog -Message "Not found in Registry. Checking Modern Apps (MSIX)..." + $MsixResults = Get-AppxPackage -Name "*$Name*" -ErrorAction SilentlyContinue + + if ($MsixResults) { + # FIX: Handle cases where multiple apps match (Array vs Single Object) + foreach ($Package in $MsixResults) { + Write-PackagerLog -Message "Found Modern App: $($Package.Name)" + + if ($WhatIf) { + Write-Host "[WhatIf] Would remove: $($Package.PackageFullName)" -ForegroundColor Yellow + continue + } + + try { + Remove-AppxPackage -Package $Package.PackageFullName -ErrorAction Stop + Write-PackagerLog -Message "Success: Removed $($Package.Name)" + } + catch { + Write-PackagerLog -Message "Failed to remove $($Package.Name): $_" -Severity Error + } + } + return + } + + Write-PackagerLog -Message "Application '$Name' not found on this system." -Severity Warning + return + } + + # 3. DETERMINE UNINSTALL COMMAND (Classic Apps) + $UninstallString = $null + $Type = "EXE" + + if ($App.UninstallString -match "MsiExec.exe") { + $Type = "MSI" + if ($App.UninstallString -match '{[A-F0-9-]+}') { + $Guid = $Matches[0] + $UninstallString = "msiexec.exe" + $Arguments = "/x $Guid /qn /norestart" + } + } + else { + # EXE Uninstaller logic + if ($App.QuietUninstallString) { + $RawString = $App.QuietUninstallString + } else { + $RawString = $App.UninstallString + } + + if ($RawString -match '^(?:"([^"]+)"|([^ ]+))(.*)$') { + $Exe = if ($Matches[1]) { $Matches[1] } else { $Matches[2] } + $ArgsPart = $Matches[3].Trim() + + $UninstallString = $Exe + $Arguments = $ArgsPart + + if (-not ($Arguments -match "/S|/silent|/qn|/quiet")) { + $Arguments = "$Arguments /S /silent /quiet /norestart" + } + } + } + + Write-PackagerLog -Message "Found: $($App.DisplayName) ($Type)" + Write-PackagerLog -Message "Command: $UninstallString $Arguments" + + if ($WhatIf) { + Write-Host "[WhatIf] Would execute: $UninstallString $Arguments" -ForegroundColor Yellow + return + } + + # 4. EXECUTE REMOVAL + try { + $Process = Start-Process -FilePath $UninstallString -ArgumentList $Arguments -PassThru -Wait -NoNewWindow + + if ($Process.ExitCode -eq 0 -or $Process.ExitCode -eq 3010) { + Write-PackagerLog -Message "Uninstallation Successful." + } else { + Write-PackagerLog -Message "Uninstallation finished with Exit Code: $($Process.ExitCode)" -Severity Warning + } + } + catch { + Write-PackagerLog -Message "Uninstallation Failed: $_" -Severity Error + } +} \ No newline at end of file diff --git a/Public/Write-PackagerLog.ps1 b/Public/Write-PackagerLog.ps1 new file mode 100644 index 0000000..ed2d4ee --- /dev/null +++ b/Public/Write-PackagerLog.ps1 @@ -0,0 +1,37 @@ +function Write-PackagerLog { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)][string]$Message, + [ValidateSet("Info", "Warning", "Error")][string]$Severity = "Info" + ) + + $LogPath = "$env:ProgramData\TecharyGet\InstallLogs.log" + $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + $Line = "[$Timestamp] [$Severity] $Message" + + # 1. Console Output + $Color = switch ($Severity) { "Info" {"Green"} "Warning" {"Yellow"} "Error" {"Red"} } + Write-Host $Line -ForegroundColor $Color + + # 2. File Log + if (-not (Test-Path (Split-Path $LogPath))) { New-Item -ItemType Directory (Split-Path $LogPath) -Force | Out-Null } + Add-Content -Path $LogPath -Value $Line + + # 3. ENTERPRISE EVENT LOGGING (New!) + # N-able can pick this up easily. + # Source: "TecharyGet", ID: 100 (Info), 200 (Warn), 300 (Error) + + $EventSource = "TecharyGet" + if (-not ([System.Diagnostics.EventLog]::SourceExists($EventSource))) { + # Requires Admin to create source once. + # If not admin, this skips silently to avoid crashing. + try { New-EventLog -LogName Application -Source $EventSource -ErrorAction SilentlyContinue } catch {} + } + + if ([System.Diagnostics.EventLog]::SourceExists($EventSource)) { + $EventID = switch ($Severity) { "Info" {100} "Warning" {200} "Error" {300} } + $EntryType = switch ($Severity) { "Info" {"Information"} "Warning" {"Warning"} "Error" {"Error"} } + + Write-EventLog -LogName Application -Source $EventSource -EventId $EventID -EntryType $EntryType -Message $Message + } +} \ No newline at end of file diff --git a/README.md b/README.md index fe48db8..6746164 100644 --- a/README.md +++ b/README.md @@ -1,143 +1,43 @@ -ChatGPT Image Oct 30, 2025, 09_09_28 AM +# TecharyGet -# TecharyGet PowerShell Module +**TecharyGet** is an enterprise-grade PowerShell module designed for modern software deployment. It bridges the gap between ad-hoc installations and formal Intune packaging, allowing IT Admins to deploy the latest versions of software dynamically without maintaining massive local repositories. -> **Author:** Adam Sweetapple +## ๐Ÿš€ Key Features -> **Purpose:** Install and uninstall software using custom Winget logic and external installer definitions (including EXE, MSI, ZIP, MSIX). +* **Smart Installation:** Automatically fetches the latest version of apps from GitHub (WinGet Manifests) or your Private Cloud Catalog. +* **App ID:** The app name for installation is the Winget ID which can either be found using **Winget Search ** or via the Winget PKGs repo. +* **Architecture Detection:** Automatically selects the correct installer (x64, x86, ARM64) for the target machine. +* **Enterprise Logging:** Writes detailed logs to both `C:\ProgramData\TecharyGet\InstallLogs.log` and the **Windows Event Log** (Source: `TecharyGet`) for RMM monitoring. +* **Intune Packaging:** Instantly generates `.intunewin` files with "Thin" scripts that trigger installs dynamically. +* **Smart Uninstall:** Removes apps by searching Registry (MSI/EXE), Modern Apps (MSIX), and Custom Display Names. +* **Private Catalog:** Supports a cloud-hosted `CustomApps.json` (e.g., GitHub Raw) for proprietary apps not found in public repos. -## Features +--- -* Installs apps from Winget using GitHub-hosted YAMLs +## ๐Ÿ“ฆ Installation -* Supports MSI, EXE, ZIP, and MSIX installers +1. Copy the `TecharyGet` folder to your PowerShell Modules directory: + * `C:\Program Files\WindowsPowerShell\Modules\` +--- -* Custom app support with static URLs and parameters (e.g. N-Able, myDPD) +## ๐Ÿ› ๏ธ Usage Examples -* Works with Intune deployments and SYSTEM-level context +### 1. Installing Applications +The `Install-TecharyApp` command is the primary workhorse. It attempts to find the app in the public GitHub repo first, then falls back to your Private Catalog. -* Full uninstall logic via Winget or Registry fallback +```powershell +# Install a standard app (Latest Version) +Install-TecharyApp -Id "7zip.7zip" -* Architecture-aware (x64, ARM64) - -## Available Commands -**Install an App** -```Powershell -Install-TecharyApp -AppName "7zip" -``` - -**Install with Parameters (e.g. N-Able)** -```Powershell -Install-TecharyApp -AppName "nable" -Parameters @{ - CustomerID = "123" - Token = "abcdef-12345" - CustomerName = '\"customer name\"' - ServerAddress = "nable.serveraddress.com" -} -``` -**Update TecharyGet Module** - -To get the latest TecharyGet Module, please run the following: -```Powershell -Update-TecharyGetModule -``` -**Uninstall an App** -```Powershell -Uninstall-TecharyApp -AppName "bitwarden" -``` -**List All Supported Apps** -```Powershell -Get-TecharyAppList -``` - -๐Ÿ”ง Example Output of Get-TecharyAppList -| AppKey | DisplayName | InstallerType | IsWinget | WingetID | -| ----- | ---------- | ------------- | -------- | -------- | -| 7zip | 7Zip | exe | true | 7zip.7zip | -| bitwarden | Bitwarden | exe | true | Bitwarden.Bitwarden | -| vscode | Microsoft Visual Studio Code | exe | true | Microsoft.VisualStudioCode | -| nable | N-Able RMM | exe | false | (custom) | -| powerbi | Microsoft Power BI | exe | true | Microsoft.PowerBI | - -**Show Help** -```Powershell -Help-TecharyApp +# Install a specific ID from your private catalog +Install-TecharyApp -Id "MyDPD" ``` -## AppMap Configuration +For the Nable Agent; it is slightly different, below is a example -Apps are defined in a separate file, AppMap.ps1, hosted in the GitHub Repository. - -The following structure lists the available Winget apps: -``` Powershell -"bitwarden" = @{ - DisplayName = "Bitwarden" - RepoPath = "b/Bitwarden/Bitwarden" - YamlFile = "Bitwarden.Bitwarden.installer.yaml" - PatternX64 = 'InstallerUrl:\s*(\S*/Bitwarden-Installer-\S+\.exe)' - PatternARM64 = 'InstallerUrl:\s*(\S*/Bitwarden-Installer-\S+\.exe)' - InstallerType = "exe" - ExeInstallArgs = "/allusers /S" - IsWinget = $true - WingetID = "Bitwarden.Bitwarden" -} -``` - -For custom apps that are not available in Winget are structured similar like this: -```Powershell -"mydpd" = @{ - DisplayName = "MyDPD Customer" - IsWinget = $false - DownloadUrl = "https://apis.my.dpd.co.uk/apps/download/public" - InstallerType = "exe" - ExeInstallArgs = "--Silent" -} +```powershell +Install-NableAgent ` + -CustomerID "" ` + -Token "" ` + -ServerAddress "" ``` - -## Intune Packager - -The **Intune-Packager.ps1** allows the ease of creation of Intunewin files for Intune upload and App deployment. - -To use, download the **Intune-Packager.ps1** file, right-click and Run with Powershell, you will be given the below window. - -image - - -Select your needed app on the drop-down, and click **Create IntuneWin Package**, this will create the Intunewin package in this location **"C:\IntuneApps\Output\** *i.e. C:\IntuneApps\Output\adobereader*. - -image - -For N-Able install you will see the parameters needed to create the Customer specific installer: - -image - - - - -## Notes - -* The module detects CPU architecture and installs the correct version. - -* All downloads are logged to C:\Logs\TecharyGetLogs\TecharyGet.log - -* Apps not in Winget can be defined with a static DownloadUrl and installed with logic from the module. - -* You can run winget.exe directly (e.g. for SYSTEM context via Intune) using its resolved path in C:\Program Files\WindowsApps\... - -* Hosting the AppMap.ps1 file means that we can manage all app installs from a centralised location for ALL of out customers. - -## ๐Ÿงช Tested With - -* Intune deployments (System context) - -* Windows 10/11 x64 + ARM64 - -* PowerShell 5.1 and 7+ - -## Troubleshooting - -* โ— App not found? โ†’ Make sure it's defined in AppMap.ps1 - -* โ— Duplicate key error? โ†’ Ensure there are no repeated properties in app maps (like IsWinget or WingetID) - -* โ— Winget not running in SYSTEM? โ†’ Use the direct winget.exe path resolution diff --git a/TecharyGet.psd1 b/TecharyGet.psd1 index 414d048..9b33dbc 100644 --- a/TecharyGet.psd1 +++ b/TecharyGet.psd1 @@ -1,49 +1,105 @@ @{ - # Script module file associated with this manifest - RootModule = 'TecharyGet.psm1' + # Script module or binary module file associated with this manifest. + RootModule = 'TecharyGet.psm1' - # Version of this module - ModuleVersion = '1.4' + # Version number of this module. + ModuleVersion = '2.3' # ID used to uniquely identify this module - GUID = '8d777e7e-fd28-4e34-bf9d-0c325bb81a76' + GUID = 'e9c840c8-3c3e-4246-8178-52372d807654' # Author of this module - Author = 'Adam Sweetapple' + Author = 'Adam Sweetapple' # Company or vendor of this module - CompanyName = 'Techary' + CompanyName = 'Techary' - # Copyright - Copyright = '(c) 2025 Techary. All rights reserved.' + # Copyright statement for this module + Copyright = '(c) 2026 Techary. All rights reserved.' - # Description of the module - Description = 'A PowerShell module for managing app installations and uninstalls using Winget Repo, MSI, EXE, ZIP, and MSIX sources. Supports custom logic and Intune deployment.' + # Description of the functionality provided by this module + Description = 'A PowerShell module for managing app installations and uninstalls using Winget Repo, MSI, EXE, ZIP, and MSIX sources. Supports custom logic and Intune deployment.' - # Minimum version of PowerShell required - PowerShellVersion = '5.1' + # Functions to export from this module, for best performance, do not use wildcards. + FunctionsToExport = @( + # -- Core Installation -- + 'Install-TecharyApp', + 'Uninstall-TecharyApp', + 'Test-TecharyApp', # The new Detection Logic + + # -- Specific Installers -- + 'Install-NableAgent', # The custom RMM installer + 'Get-GitHubInstaller', # Useful for manual manifest checking + + # -- Intune Packaging Tools -- + 'New-IntunePackage', # The CLI Packager (with Detect/Uninstall generation) + 'New-IntunePackageUI', # The GUI Packager + 'Show-IntunePackager', # The Wrapper for the MS Utility - # Functions to export - FunctionsToExport = "Install-TecharyApp","Uninstall-TecharyApp","Help-TecharyApp","Get-TecharyAppList","Update-TecharyGetModule" + # -- Utilities -- + 'Write-PackagerLog' + ) - # Cmdlets to export - CmdletsToExport = @() + # Cmdlets to export from this module + CmdletsToExport = @() - # Variables to export - VariablesToExport = @() + # Variables to export from this module + VariablesToExport = '*' - # Aliases to export - AliasesToExport = @() + # Aliases to export from this module + AliasesToExport = @() - # Private data to pass to PowerShell - PrivateData = @{ + # List of all modules packaged with this module + # NestedModules = @() + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess + PrivateData = @{ PSData = @{ - Tags = @('winget', 'installer', 'automation', 'techary', 'uninstall', 'intune') - ProjectUri = 'https://github.com/Techary/TecharyGet' + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('Intune', 'PackageManagement', 'Install', 'Uninstall', 'Winget', 'RMM', 'Automation') + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' } } + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.1' + + # Minimum version of the Common Language Runtime (CLR) required by this module + # CLRVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() } + diff --git a/TecharyGet.psm1 b/TecharyGet.psm1 index 749d731..4285024 100644 --- a/TecharyGet.psm1 +++ b/TecharyGet.psm1 @@ -1,463 +1,4 @@ -#region Globals -$script:folderPath = "C:\Logs\TecharyGetLogs" -$ProgressPreference = 'SilentlyContinue' -#endregion - -#region Logging -function Invoke-LogMessage { - param ([string]$Message) - $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" - $logFile = Join-Path $script:folderPath "TecharyGet.log" - Add-Content -Path $logFile -Value "[$timestamp] $Message" -} -#endregion - -#region Install App -function Install-TecharyApp { - param ( - [string]$AppName, - [hashtable]$Parameters - ) - - #If AppMap.ps1 exists, remove it to ensure we get the latest version - $AppMapexists = Test-Path "$PSScriptRoot\AppMap.ps1" - if ($AppMapexists) { - Remove-Item "$PSScriptRoot\AppMap.ps1" -Force - } - - # Download the latest AppMap.ps1 - Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Techary/TecharyGet/refs/heads/main/AppMap.ps1" -OutFile "$PSScriptRoot\AppMap.ps1" -UseBasicParsing - - if (-not $script:TecharyApps) { - . "$PSScriptRoot\AppMap.ps1" - } - - $AppKey = $AppName.ToLower() - if (-not $script:TecharyApps.ContainsKey($AppKey)) { - throw "[TecharyGet] Application '$AppName' not found in AppMap." - } - - $app = $script:TecharyApps[$AppKey] - - if (-not $app.MsiInstallArgs) { $app.MsiInstallArgs = "ALLUSERS=1 /quiet" } - if (-not $app.ExeInstallArgs) { $app.ExeInstallArgs = "/S" } - - if (-not (Test-Path $script:folderPath)) { - New-Item -Path $script:folderPath -ItemType Directory -Force | Out-Null - } - - # Handle special app logic or fallback -if ($AppKey -eq "nable") { - $required = @("CustomerID", "Token", "CustomerName", "ServerAddress") - foreach ($key in $required) { - if (-not $Parameters.ContainsKey($key)) { - throw "[Nable] Missing required parameter: $key" - } - } - - $customerID = $Parameters.CustomerID - $token = $Parameters.Token - $customerName = $Parameters.CustomerName - $serverAddress = $Parameters.ServerAddress - - # Ensure folder exists - if (-not (Test-Path $script:folderPath)) { - New-Item -Path $script:folderPath -ItemType Directory -Force | Out-Null - } - - $fileName = "Nable_RMMInstaller.exe" - $installerPath = Join-Path $script:folderPath $fileName - $downloadUrl = "https://$serverAddress/download/current/winnt/N-central/WindowsAgentSetup.exe" - - Invoke-LogMessage "[Nable] Downloading installer from: $downloadUrl" - Invoke-WebRequest -Uri $downloadUrl -OutFile $installerPath -UseBasicParsing - - $msiArgs = "/qn CUSTOMERID=$customerID CUSTOMERNAME=$customerName CUSTOMERSPECIFIC=1 REGISTRATION_TOKEN=$token SERVERPROTOCOL=HTTPS SERVERADDRESS=$serverAddress SERVERPORT=443" - $arguments = "/S /v`"$msiArgs`"" - - Invoke-LogMessage "[Nable] Installing with arguments: $arguments" - Start-Process -FilePath $installerPath -ArgumentList $arguments -Wait -NoNewWindow - - Remove-Item $installerPath -Force - Invoke-LogMessage "[Nable] Installed and cleaned up" - return -} - - - # Custom static download - if (-not $app.IsWinget -and $app.DownloadUrl) { - $arch = (Get-ComputerInfo).CSArchitecture - $suffix = if ($arch -like "*ARM*") { "arm64" } else { "x64" } - - $ext = [System.IO.Path]::GetExtension($app.DownloadUrl) - if (-not $ext) { $ext = ".exe" } - - $fileName = "${AppKey}_Installer_${suffix}${ext}" - $installerPath = Join-Path $script:folderPath $fileName - - Invoke-LogMessage "[$AppName] Downloading installer from: $($app.DownloadUrl)" - Invoke-WebRequest -Uri $app.DownloadUrl -OutFile $installerPath -UseBasicParsing - - switch ($app.InstallerType) { - "exe" { - Start-Process -FilePath $installerPath -ArgumentList $app.ExeInstallArgs -Wait -NoNewWindow - } - "msi" { - Start-Process "msiexec.exe" -ArgumentList "/i `"$installerPath`" $($app.MsiInstallArgs)" -Wait -NoNewWindow - } - "msix" { - Add-AppxProvisionedPackage -Online -PackagePath $installerPath -SkipLicense - } - default { - throw "[$AppName] Unsupported installer type for custom app." - } - } - - Invoke-LogMessage "[$AppName] Installed successfully." - Remove-Item $installerPath -Force - return - } - - # Winget-based install - Install-TecharyWingetApp ` - -AppName $AppName ` - -RepoPath $app.RepoPath ` - -YamlFileName $app.YamlFile ` - -PatternX64 $app.PatternX64 ` - -PatternARM64 $app.PatternARM64 ` - -InstallerType $app.InstallerType ` - -ExeInstallArgs $app.ExeInstallArgs ` - -MsiInstallArgs $app.MsiInstallArgs -} -#endregion - -#region Uninstall App -function Uninstall-TecharyApp { - param ( - [string]$AppName - ) - - #If AppMap.ps1 exists, remove it to ensure we get the latest version - $AppMapexists = Test-Path "$PSScriptRoot\AppMap.ps1" - if ($AppMapexists) { - Remove-Item "$PSScriptRoot\AppMap.ps1" -Force - } - - # Download the latest AppMap.ps1 - Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Techary/TecharyGet/refs/heads/main/AppMap.ps1" -OutFile "$PSScriptRoot\AppMap.ps1" -UseBasicParsing - - if (-not $script:TecharyApps) { - . "$PSScriptRoot\AppMap.ps1" - } - - $AppKey = $AppName.ToLower() - if (-not $script:TecharyApps.ContainsKey($AppKey)) { - throw "[TecharyGet] Application '$AppName' not found in AppMap." - } - - $app = $script:TecharyApps[$AppKey] - $displayName = $app.DisplayName - - # Determine if using winget - if ($app.IsWinget -and $app.WingetID) { - $arch = (Get-ComputerInfo).CSArchitecture - $wingetBasePath = if ($arch -like "*ARM*") { - "C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_*_arm64__8wekyb3d8bbwe" - } else { - "C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_*_x64__8wekyb3d8bbwe" - } - - $resolveWinget = Resolve-Path -Path $wingetBasePath -ErrorAction SilentlyContinue | Sort-Object -Descending | Select-Object -First 1 - if ($resolveWinget) { - $wingetExe = Join-Path $resolveWinget.Path "winget.exe" - } - - if (-not (Test-Path $wingetExe)) { - throw "[Uninstall] Winget executable not found." - } - - Invoke-LogMessage "[Uninstall] Uninstalling '$displayName' via winget ID: $($app.WingetID)" - & $wingetExe uninstall --id $($app.WingetID) --silent --scope machine --exact | Out-Null - Invoke-LogMessage "[Uninstall] Winget uninstall complete for $displayName" - return - } - - # Otherwise fallback to registry-based uninstall logic - $uninstallKeys = @( - "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", - "HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", - "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" - ) - - $uninstallEntry = $null - foreach ($key in $uninstallKeys) { - $uninstallEntry = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue | Where-Object { - $_.DisplayName -like "*$displayName*" - } | Select-Object -First 1 - - if ($uninstallEntry) { break } - } - - if (-not $uninstallEntry) { - throw "[Uninstall] Could not find uninstall entry for $displayName" - } - - $uninstallCommand = $uninstallEntry.UninstallString - if (-not $uninstallCommand) { - throw "[Uninstall] No uninstall command found for $displayName" - } - -# MSI uninstall via ProductCode -if ($uninstallCommand -match "msiexec\.exe.*" -or $uninstallEntry.PSChildName -match "^\{.*\}$") { - $productCode = $uninstallEntry.PSChildName - if ($productCode -match "^\{.*\}$") { - $msiArgs = "/x $productCode /qn REBOOT=ReallySuppress" - Invoke-LogMessage "[Uninstall] Executing MSI uninstall: msiexec.exe $msiArgs" - Start-Process -FilePath "msiexec.exe" -ArgumentList $msiArgs -Wait -NoNewWindow - Invoke-LogMessage "[Uninstall] MSI uninstall completed for $displayName" - return - } -} - - - # EXE uninstallers (including quoted paths and arguments) - if ($uninstallCommand -match '^(\"?[^"]+\.exe\"?)\s*(.*)$') { - $exePathRaw = $matches[1] - $args = $matches[2] - - $exePath = $exePathRaw.Trim('"').Trim() - - if (-not (Test-Path $exePath)) { - try { - $exePath = (Get-Item $exePath -ErrorAction Stop).FullName - } catch { - throw "Uninstall EXE not found: `"$exePath`"" - } - } - - Invoke-LogMessage "[Uninstall] Executing EXE uninstall: $exePath $args" - Start-Process -FilePath $exePath -ArgumentList $args -Wait -NoNewWindow - Invoke-LogMessage "[Uninstall] Uninstall completed for $displayName" - return - } - - throw "[Uninstall] Uninstall command not recognized for $displayName" -} -#endregion - -#region Install-TecharyWingetApp -function Install-TecharyWingetApp { - param ( - [string]$AppName, - [string]$RepoPath, - [string]$YamlFileName, - [string]$PatternX64, - [string]$PatternARM64, - [ValidateSet("msi", "exe", "zip", "msix")] [string]$InstallerType, - [string]$ExeInstallArgs = "/S", - [string]$MsiInstallArgs = "ALLUSERS=1 /quiet" - ) - - $arch = (Get-ComputerInfo).CSDescription - $isARM = $arch -like "*ARM*" - $suffix = if ($isARM) { "arm64" } else { "x64" } - - $versionUrl = "https://api.github.com/repos/microsoft/winget-pkgs/contents/manifests/$RepoPath" - $headers = @{ "User-Agent" = "PowerShell" } - $response = Invoke-RestMethod -Uri $versionUrl -Headers $headers - - $versions = $response | - Where-Object { $_.type -eq "dir" } | - ForEach-Object { $_.name } | - Where-Object { $_ -match '^\d+(\.\d+)*$' } - - if (-not $versions) { - throw "[$AppName] No valid version folders found in repo path: $RepoPath" -} - - $latestVersion = $versions | - Sort-Object { [version]$_ } -Descending | - Select-Object -First 1 - Invoke-LogMessage "[$AppName] Latest version detected: $latestVersion" - - $yamlUrl = "https://raw.githubusercontent.com/microsoft/winget-pkgs/refs/heads/master/manifests/$RepoPath/$latestVersion/$YamlFileName" - Invoke-LogMessage "[$AppName] YAML URL: $yamlUrl" - - try { - $yamlContent = Invoke-WebRequest -Uri $yamlUrl -UseBasicParsing - $yamlText = $yamlContent.Content - } catch { - throw "[$AppName] Failed to download YAML: $_" - } - - $installerUrl = $null - if ($isARM -and $yamlText -match $PatternARM64) { - $installerUrl = $matches[1] - } elseif ($yamlText -match $PatternX64) { - $installerUrl = $matches[1] - } else { - throw "[$AppName] Installer URL not found in YAML." - } - - # Clean the filename from the URL (removing query parameters like ?archType=x64) - $cleanInstallerUrl = $installerUrl -split '\?' | Select-Object -First 1 - $ext = [System.IO.Path]::GetExtension($cleanInstallerUrl) - - if (-not $ext) { $ext = ".exe" } - - $fileName = "${AppName}_Installer_${suffix}${ext}" - $installerPath = Join-Path $script:folderPath $fileName -` - # Now download the file using the full URL - Invoke-LogMessage "[$AppName] Downloading installer from: $installerUrl" - Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath -UseBasicParsing - - switch ($InstallerType) { - "msi" { - Start-Process "msiexec.exe" -ArgumentList "/i `"$installerPath`" $MsiInstallArgs" -Wait -NoNewWindow - } - "exe" { - Start-Process -FilePath $installerPath -ArgumentList $ExeInstallArgs -Wait -NoNewWindow - } - "msix" { - Add-AppxProvisionedPackage -Online -PackagePath $installerPath -SkipLicense - } - "zip" { - $extractPath = Join-Path $script:folderPath "$AppName-$suffix" - Expand-Archive -Path $installerPath -DestinationPath $extractPath -Force - $allMsis = Get-ChildItem -Path $extractPath -Recurse -Filter *.msi - - $preferredMsi = $allMsis | Where-Object { - $osArch = (Get-ComputerInfo).OSArchitecture - if ($osArch -like "*ARM*") { - $_.Name -match "arm64" - } else { - $_.Name -match "x64" - } - } | Select-Object -First 1 - - if (-not $preferredMsi) { - $preferredMsi = $allMsis | Select-Object -First 1 - } - - Start-Process "msiexec.exe" -ArgumentList "/i `"$($preferredMsi.FullName)`" $MsiInstallArgs" -Wait -NoNewWindow - Remove-Item -Path $extractPath -Recurse -Force - } - } - - Remove-Item $installerPath -Force - Invoke-LogMessage "[$AppName] Installed successfully." -} -#endregion - -#region Help-TecharyApp -function Help-TecharyApp { - Write-Host "" - Write-Host "TecharyApp PowerShell Module Help" -ForegroundColor Cyan - Write-Host "==================================" -ForegroundColor Cyan - Write-Host "" - Write-Host "Available Commands:" -ForegroundColor Yellow - Write-Host " Install-TecharyApp -AppName [-Parameters ]" -ForegroundColor Green - Write-Host " Uninstall-TecharyApp -AppName " -ForegroundColor Green - Write-Host " Get-TecharyAppList" -ForegroundColor Green - Write-Host " Help-TecharyApp" -ForegroundColor Green - Write-Host "" - Write-Host "Examples:" -ForegroundColor Yellow - Write-Host ' Install-TecharyApp -AppName "7zip"' - Write-Host ' Uninstall-TecharyApp -AppName "chrome"' - Write-Host ' Install-TecharyApp -AppName "nable" -Parameters @{ CustomerID="123"; Token="abc"; CustomerName="Org"; ServerAddress="control.example.com" }' - Write-Host "" - Write-Host "Tip:" -ForegroundColor Yellow - Write-Host " Use Get-TecharyAppList to see all available AppNames." - Write-Host "" -} -#endregion - -#region Get-TecharyAppList -function Get-TecharyAppList { - - #If AppMap.ps1 exists, remove it to ensure we get the latest version - $AppMapexists = Test-Path "$PSScriptRoot\AppMap.ps1" - if ($AppMapexists) { - Remove-Item "$PSScriptRoot\AppMap.ps1" -Force - } - - # Download the latest AppMap.ps1 - Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Techary/TecharyGet/refs/heads/main/AppMap.ps1" -OutFile "$PSScriptRoot\AppMap.ps1" -UseBasicParsing - - if (-not $script:TecharyApps) { - . "$PSScriptRoot\AppMap.ps1" - } - - $script:TecharyApps.Keys | Sort-Object | ForEach-Object { - $app = $script:TecharyApps[$_] - [PSCustomObject]@{ - AppKey = $_ - DisplayName = $app.DisplayName - InstallerType = $app.InstallerType - IsWinget = $app.IsWinget - WingetID = $app.WingetID - } - } -} -#endregion - -#region Update-TecharyApp -function Update-TecharyGetModule { - param ( - [string]$RepoOwner = "Techary", - [string]$RepoName = "TecharyGet", - [string]$Branch = "main", - [string]$ModuleName = "TecharyGet", - [string]$ModulePath = "$PSScriptRoot" - ) - - $localPSD = Join-Path $ModulePath "$ModuleName.psd1" - if (-not (Test-Path $localPSD)) { - Write-Host "Local .psd1 file not found at $localPSD" - return - } - - # Get local version - $localModule = Import-PowerShellDataFile -Path $localPSD - $localVersion = [version]$localModule.ModuleVersion - Write-Host "Local version: $localVersion" - - # Get remote psd1 raw content from GitHub - $remotePSDUrl = "https://raw.githubusercontent.com/$RepoOwner/$RepoName/refs/heads/$Branch/$ModuleName.psd1" - try { - $remoteText = Invoke-WebRequest -Uri $remotePSDUrl -UseBasicParsing - $tempFile = Join-Path $env:TEMP "$ModuleName.remote.psd1" - $remoteText.Content | Set-Content -Path $tempFile -Encoding UTF8 - $remoteModule = Import-PowerShellDataFile -Path $tempFile - $remoteVersion = [version]$remoteModule.ModuleVersion - Remove-Item $tempFile -Force - } catch { - Write-Host "Failed to fetch remote version: $_" - return - } - - Write-Host "Remote version: $remoteVersion" - - if ($remoteVersion -gt $localVersion) { - Write-Host "Updating module from GitHub..." - - $filesToDownload = @("$ModuleName.psm1", "$ModuleName.psd1") - foreach ($file in $filesToDownload) { - $url = "https://raw.githubusercontent.com/$RepoOwner/$RepoName/refs/heads/$Branch/$file" - $dest = Join-Path $ModulePath $file - try { - Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing - Write-Host "Updated: $file" - } catch { - Write-Warning "Failed to update ${file}: $_" - } - } - - Write-Host "Update complete." - } else { - Write-Host "Module is up to date." - } -} - -#endregion +$Root = Split-Path $MyInvocation.MyCommand.Path +Get-ChildItem -Path "$Root\Private\*.ps1" | ForEach-Object { . $_.FullName } +Get-ChildItem -Path "$Root\Public\*.ps1" | ForEach-Object { . $_.FullName } +Export-ModuleMember -Function (Get-ChildItem -Path "$Root\Public\*.ps1").BaseName