From cfc58fbcb4b700f7ec7d5f555705ff3d06dfa077 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:27:57 +0000 Subject: [PATCH 01/38] Delete AppMap.ps1 --- AppMap.ps1 | 589 ----------------------------------------------------- 1 file changed, 589 deletions(-) delete mode 100644 AppMap.ps1 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 - - -} From 806f558f9ac91a1caae5c693651513dc0cbd7a38 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:28:07 +0000 Subject: [PATCH 02/38] Delete Intune-Packager.ps1 --- Intune-Packager.ps1 | 176 -------------------------------------------- 1 file changed, 176 deletions(-) delete mode 100644 Intune-Packager.ps1 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() From f8d2b4e04095e75a5ea28c61e2d6f67debfc91b2 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:30:12 +0000 Subject: [PATCH 03/38] Delete TecharyGet.psd1 --- TecharyGet.psd1 | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 TecharyGet.psd1 diff --git a/TecharyGet.psd1 b/TecharyGet.psd1 deleted file mode 100644 index 414d048..0000000 --- a/TecharyGet.psd1 +++ /dev/null @@ -1,49 +0,0 @@ -@{ - # Script module file associated with this manifest - RootModule = 'TecharyGet.psm1' - - # Version of this module - ModuleVersion = '1.4' - - # ID used to uniquely identify this module - GUID = '8d777e7e-fd28-4e34-bf9d-0c325bb81a76' - - # Author of this module - Author = 'Adam Sweetapple' - - # Company or vendor of this module - CompanyName = 'Techary' - - # Copyright - Copyright = '(c) 2025 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.' - - # Minimum version of PowerShell required - PowerShellVersion = '5.1' - - # Functions to export - FunctionsToExport = "Install-TecharyApp","Uninstall-TecharyApp","Help-TecharyApp","Get-TecharyAppList","Update-TecharyGetModule" - - # Cmdlets to export - CmdletsToExport = @() - - # Variables to export - VariablesToExport = @() - - # Aliases to export - AliasesToExport = @() - - # Private data to pass to PowerShell - PrivateData = @{ - - PSData = @{ - Tags = @('winget', 'installer', 'automation', 'techary', 'uninstall', 'intune') - ProjectUri = 'https://github.com/Techary/TecharyGet' - } - } -} - - - From 987470a51de711f8f4858b7bb8f303025534296d Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:30:19 +0000 Subject: [PATCH 04/38] Delete TecharyGet.psm1 --- TecharyGet.psm1 | 463 ------------------------------------------------ 1 file changed, 463 deletions(-) delete mode 100644 TecharyGet.psm1 diff --git a/TecharyGet.psm1 b/TecharyGet.psm1 deleted file mode 100644 index 749d731..0000000 --- a/TecharyGet.psm1 +++ /dev/null @@ -1,463 +0,0 @@ -#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 From 4004e2342e14d3e08a44bc3121d4a74825b432d8 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:32:57 +0000 Subject: [PATCH 05/38] Add files via upload --- AppPackager.psd1 | 133 ++++++++++++++++++++++++++++++ AppPackager.psm1 | 4 + Private/CustomApps.json | 23 ++++++ Private/Get-CustomApp.ps1 | 67 +++++++++++++++ Public/Get-GitHubInstaller.ps1 | 114 +++++++++++++++++++++++++ Public/Install-AppPackage.ps1 | 103 +++++++++++++++++++++++ Public/Install-NableAgent.ps1 | 80 ++++++++++++++++++ Public/Install-SmartApp.ps1 | 68 +++++++++++++++ Public/Invoke-PackagerCleanup.ps1 | 12 +++ Public/New-IntunePackage.ps1 | 70 ++++++++++++++++ Public/New-IntunePackageUI.ps1 | 93 +++++++++++++++++++++ Public/Uninstall-SmartApp.ps1 | 111 +++++++++++++++++++++++++ Public/Write-PackagerLog.ps1 | 37 +++++++++ 13 files changed, 915 insertions(+) create mode 100644 AppPackager.psd1 create mode 100644 AppPackager.psm1 create mode 100644 Private/CustomApps.json create mode 100644 Private/Get-CustomApp.ps1 create mode 100644 Public/Get-GitHubInstaller.ps1 create mode 100644 Public/Install-AppPackage.ps1 create mode 100644 Public/Install-NableAgent.ps1 create mode 100644 Public/Install-SmartApp.ps1 create mode 100644 Public/Invoke-PackagerCleanup.ps1 create mode 100644 Public/New-IntunePackage.ps1 create mode 100644 Public/New-IntunePackageUI.ps1 create mode 100644 Public/Uninstall-SmartApp.ps1 create mode 100644 Public/Write-PackagerLog.ps1 diff --git a/AppPackager.psd1 b/AppPackager.psd1 new file mode 100644 index 0000000..7463d6e --- /dev/null +++ b/AppPackager.psd1 @@ -0,0 +1,133 @@ +# +# Module manifest for module 'AppPackager' +# +# Generated by: asweetapple +# +# Generated on: 04/02/2026 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'AppPackager.psm1' + +# Version number of this module. +ModuleVersion = '0.0.1' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '9f3ce1a8-1494-4b4a-b885-0c1112fc234e' + +# Author of this module +Author = 'asweetapple' + +# Company or vendor of this module +CompanyName = 'Unknown' + +# Copyright statement for this module +Copyright = '(c) asweetapple. All rights reserved.' + +# Description of the functionality provided by this module +# Description = '' + +# Minimum version of the PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# 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 = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = 'Get-GitHubInstaller', 'Install-AppPackage', 'Write-PackagerLog', + 'Invoke-PackagerCleanup', 'Install-GitHubApp' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = '*' + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = '*' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # 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 = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/AppPackager.psm1 b/AppPackager.psm1 new file mode 100644 index 0000000..4285024 --- /dev/null +++ b/AppPackager.psm1 @@ -0,0 +1,4 @@ +$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 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..d35a27f --- /dev/null +++ b/Public/Get-GitHubInstaller.ps1 @@ -0,0 +1,114 @@ +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" } + Write-PackagerLog -Message "System Architecture: $SysArch" + + # 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 + $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 + Write-PackagerLog -Message "Latest Version Found: $LatestVersion" + + # 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." } + + Write-PackagerLog -Message "Parsing Manifest: $($InstallerFile.name)" + $YamlContent = Invoke-RestMethod -Uri $InstallerFile.download_url + + # --- PARSING LOGIC (Fixed Regex) --- + $Blocks = $YamlContent -split 'Architecture:\s*' + + $SelectedUrl = $null + $SelectedArgs = $null + $SelectedType = "exe" + + # Search Blocks for Match + foreach ($Block in $Blocks) { + if ([string]::IsNullOrWhiteSpace($Block)) { continue } + $BlockArch = $Block.Split("`r`n")[0].Trim() + + if ($BlockArch -eq $SysArch) { + # We found our block! + if ($Block -match 'InstallerUrl:\s*["'']?([^"''\r\n]+)["'']?') { $SelectedUrl = $Matches[1].Trim() } + if ($Block -match 'InstallerType:\s*([a-zA-Z0-9]+)') { $SelectedType = $Matches[1].Trim() } + + # FIX: Greedy Regex to capture arguments with quotes (like /v"/qn") + if ($Block -match 'Silent:\s*(.+)') { + # Remove leading/trailing quotes from the YAML value itself, but keep internal quotes + $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') + } + elseif ($Block -match 'SilentWithProgress:\s*(.+)') { + $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') + } + break + } + } + + # Fallback: Scan Whole File if missing + if (-not $SelectedUrl) { + if ($YamlContent -match 'InstallerUrl:\s*["'']?([^"''\r\n]+)["'']?') { $SelectedUrl = $Matches[1].Trim() } + } + + if (-not $SelectedArgs) { + # FIX: Greedy Global Regex + if ($YamlContent -match 'Silent:\s*(.+)') { + $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') + Write-PackagerLog -Message "Found Global Arguments." + } + elseif ($YamlContent -match 'SilentWithProgress:\s*(.+)') { + $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') + } + } + + if (-not $SelectedUrl) { throw "Could not find an installer URL." } + + # --- 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..." + $UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + Invoke-WebRequest -Uri $SelectedUrl -OutFile $FullPath -UseBasicParsing -UserAgent $UserAgent + + return [PSCustomObject]@{ + Name = $Id; InstallerPath = $FullPath; FileName = $FileName; SilentArgs = $SelectedArgs; InstallerType = $SelectedType + } + } + catch { + Write-PackagerLog -Message "GitHub Scraping Failed: $_" -Severity Error + throw $_ + } +} \ No newline at end of file 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..7ba9d31 --- /dev/null +++ b/Public/Install-NableAgent.ps1 @@ -0,0 +1,80 @@ +function Install-NableAgent { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)][string]$CustomerID, + [Parameter(Mandatory=$true)][string]$Token, + [Parameter(Mandatory=$true)][string]$CustomerName, + [Parameter(Mandatory=$true)][string]$ServerAddress, + [int]$TimeoutSeconds = 1200 # 20 minutes + ) + + $Name = "N-able RMM Agent" + $DownloadUrl = "https://$ServerAddress/download/current/winnt/N-central/WindowsAgentSetup.exe" + $DownloadPath = "$env:TEMP\AppPackager" + $FileName = "Nable_RMMInstaller.exe" + $InstallerPath = Join-Path $DownloadPath $FileName + + Write-PackagerLog -Message "Starting N-able Deployment for Customer: $CustomerName" + + try { + # 1. Download + if (Test-Path $DownloadPath) { Remove-Item "$DownloadPath\*" -Recurse -Force -ErrorAction SilentlyContinue } + New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null + + Write-PackagerLog -Message "Downloading installer from: $DownloadUrl" + + # We use a spoofed UserAgent just in case N-able blocks scripts, though usually not required here. + $UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + Invoke-WebRequest -Uri $DownloadUrl -OutFile $InstallerPath -UseBasicParsing -UserAgent $UserAgent + + if (-not (Test-Path $InstallerPath)) { throw "Download failed. File not found." } + + # 2. Build Arguments (Standard InstallShield format) + # /S = Silent for the wrapper + # /v = Pass arguments to internal MSI + # We quote the MSI arguments carefully so Install-AppPackage regex keeps them together. + + $MsiArgs = "/qn CUSTOMERID=$CustomerID CUSTOMERNAME=$CustomerName CUSTOMERSPECIFIC=1 REGISTRATION_TOKEN=$Token SERVERPROTOCOL=HTTPS SERVERADDRESS=$ServerAddress SERVERPORT=443" + + # NOTE: We construct the final string. Install-AppPackage's Regex will see "/v"..." " as one argument. + $FinalArgs = "/S /v`"$MsiArgs`"" + + # 3. Install (Using your module's executor) + Install-AppPackage -Name $Name -FilePath $InstallerPath -Arguments $FinalArgs + + # 4. Cleanup Installer immediately + Remove-Item $InstallerPath -Force -ErrorAction SilentlyContinue + + # 5. Validation Loop (The "Is it actually working?" check) + 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" + + if ($Service1 -and $Service2 -and $FileCheck) { + Write-PackagerLog -Message "Validation Successful: N-able services are running." + return # Success! + } + + # 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)." + } + + Write-PackagerLog -Message "Waiting for services to start..." + Start-Sleep -Seconds 10 + } + + } + catch { + Write-PackagerLog -Message "N-able Deployment Failed: $_" -Severity Error + throw $_ + } +} \ No newline at end of file diff --git a/Public/Install-SmartApp.ps1 b/Public/Install-SmartApp.ps1 new file mode 100644 index 0000000..8f92967 --- /dev/null +++ b/Public/Install-SmartApp.ps1 @@ -0,0 +1,68 @@ +$ProgressPreference = 'SilentlyContinue' + +function Install-SmartApp { + [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..32df464 --- /dev/null +++ b/Public/New-IntunePackage.ps1 @@ -0,0 +1,70 @@ +function New-IntunePackage { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string]$Id, # e.g. "Dell.CommandUpdate" + + [string]$OutputFolder = "C:\IntunePackages" + ) + + # --- 1. SETUP --- + $PackagerTemp = "$env:TEMP\IntunePackager_Working\$Id" + $SourceDir = "$PackagerTemp\Source" + $IntuneUtil = "$env:TEMP\IntuneWinAppUtil.exe" + + # Clean Workspace + if (Test-Path $PackagerTemp) { Remove-Item $PackagerTemp -Recurse -Force -ErrorAction SilentlyContinue } + New-Item -Path $SourceDir -ItemType Directory -Force | Out-Null + + # Ensure Output Dir + if (-not (Test-Path $OutputFolder)) { New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null } + + # Ensure Intune Utility + if (-not (Test-Path $IntuneUtil)) { + Write-PackagerLog -Message "Downloading IntuneWinAppUtil.exe..." + Invoke-WebRequest "https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool/raw/master/IntuneWinAppUtil.exe" -OutFile $IntuneUtil + } + + # --- 2. GENERATE INSTALL.PS1 (The One-Liner) --- + $InstallScript = @" +# Install Trigger for $Id +`$ErrorActionPreference = 'Stop' + +# Try to load the module if not already loaded (Assumes it is installed on the PC) +Import-Module TecharyGet -ErrorAction SilentlyContinue + +Write-Host "Triggering Install for: $Id" +Install-SmartApp -Id "$Id" +"@ + Set-Content -Path "$SourceDir\Install.ps1" -Value $InstallScript + + # --- 3. GENERATE UNINSTALL.PS1 (The One-Liner) --- + $UninstallScript = @" +# Uninstall Trigger for $Id +`$ErrorActionPreference = 'Stop' + +Import-Module TecharyGet -ErrorAction SilentlyContinue + +Write-Host "Triggering Uninstall for: $Id" +Uninstall-SmartApp -Name "$Id" +"@ + Set-Content -Path "$SourceDir\Uninstall.ps1" -Value $UninstallScript + + # --- 4. PACKAGE IT --- + Write-PackagerLog -Message "Packaging scripts into .intunewin..." + + $Process = Start-Process -FilePath $IntuneUtil ` + -ArgumentList "-c `"$SourceDir`"", "-s `"Install.ps1`"", "-o `"$OutputFolder`"", "-q" ` + -PassThru -Wait -NoNewWindow + + if ($Process.ExitCode -eq 0) { + $Original = Join-Path $OutputFolder "Install.intunewin" + $Final = Join-Path $OutputFolder "$Id.intunewin" + if (Test-Path $Original) { Move-Item $Original $Final -Force } + + Write-PackagerLog -Message "SUCCESS: Package created at $Final" + Invoke-Item $OutputFolder + } else { + Write-PackagerLog -Message "Packaging Failed with Exit Code $($Process.ExitCode)" -Severity Error + } +} \ 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/Uninstall-SmartApp.ps1 b/Public/Uninstall-SmartApp.ps1 new file mode 100644 index 0000000..83609e7 --- /dev/null +++ b/Public/Uninstall-SmartApp.ps1 @@ -0,0 +1,111 @@ +function Uninstall-SmartApp { + [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 From a010d5e0621fed7133497cdde2505d905908c985 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:35:36 +0000 Subject: [PATCH 06/38] Revise README.md for TecharyGet module details Updated the README.md to include comprehensive module features and usage examples. --- README.md | 224 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 128 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index fe48db8..e8fae0d 100644 --- a/README.md +++ b/README.md @@ -1,143 +1,175 @@ -ChatGPT Image Oct 30, 2025, 09_09_28 AM +Here is a professional, comprehensive `README.md` file for your module. It covers all the advanced features we built (Cloud Catalog, Intune Packaging, Smart Uninstall, etc.). -# TecharyGet PowerShell Module +You can copy-paste this directly into the root of your project. -> **Author:** Adam Sweetapple +--- -> **Purpose:** Install and uninstall software using custom Winget logic and external installer definitions (including EXE, MSI, ZIP, MSIX). +```markdown +# TecharyGet -## Features +**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. -* Installs apps from Winget using GitHub-hosted YAMLs +## 🚀 Key Features -* Supports MSI, EXE, ZIP, and MSIX installers +* **Smart Installation:** Automatically fetches the latest version of apps from GitHub (WinGet Manifests) or your Private Cloud Catalog. +* **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. -* Custom app support with static URLs and parameters (e.g. N-Able, myDPD) +--- -* Works with Intune deployments and SYSTEM-level context +## 📦 Installation -* Full uninstall logic via Winget or Registry fallback +1. Copy the `TecharyGet` folder to your PowerShell Modules directory: + * `C:\Program Files\WindowsPowerShell\Modules\` +2. Import the module: + ```powershell + Import-Module TecharyGet -Force + ``` -* Architecture-aware (x64, ARM64) +--- -## Available Commands -**Install an App** -```Powershell -Install-TecharyApp -AppName "7zip" -``` +## 🛠️ Usage Examples -**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** +### 1. Installing Applications +The `Install-SmartApp` command is the primary workhorse. It attempts to find the app in the public GitHub repo first, then falls back to your Private Catalog. + +```powershell +# Install a standard app (Latest Version) +Install-SmartApp -Id "7zip.7zip" + +# Install a specific ID from your private catalog +Install-SmartApp -Id "MyDPD" + +# Install a complex app (e.g. Dell Command Update) - Handles Exit Code 4 automatically +Install-SmartApp -Id "Dell.CommandUpdate" -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 + +### 2. Uninstalling Applications + +The uninstaller intelligently searches the Registry (HKLM/HKCU) and Appx packages. + +```powershell +# Uninstall by Display Name +Uninstall-SmartApp -Name "Google Chrome" + +# Uninstall using a Custom ID (looks up real name in JSON) +Uninstall-SmartApp -Name "MyDPD" + +# Test run (Safe Mode) +Uninstall-SmartApp -Name "Windows365" -WhatIf + ``` -🔧 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 +### 3. Creating Intune Packages + +You can generate ready-to-upload `.intunewin` files that contain "Thin" scripts (One-Liners). These scripts assume `TecharyGet` is installed on the target machine and trigger a live download/install. + +**GUI Method:** + +```powershell +New-IntunePackageUI + ``` -## AppMap Configuration - -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" -} +**CLI Method:** + +```powershell +New-IntunePackage -Id "Dell.CommandUpdate" -OutputFolder "C:\IntunePackages" + ``` -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" -} +* **Install Command:** `powershell.exe -ExecutionPolicy Bypass -File Install.ps1` +* **Uninstall Command:** `powershell.exe -ExecutionPolicy Bypass -File Uninstall.ps1` + +### 4. Special N-able Agent Deployment + +N-able requires dynamic parameters (Token, CustomerID). Use the dedicated function: + +```powershell +Install-NableAgent -CustomerID "123" ` + -Token "abc-123-xyz" ` + -CustomerName "ClientA" ` + -ServerAddress "rmm.example.com" + ``` -## Intune Packager +--- + +## ☁️ Private Catalog Configuration -The **Intune-Packager.ps1** allows the ease of creation of Intunewin files for Intune upload and App deployment. +For apps that are not in the public WinGet repo (e.g., licensed software, internal tools), use the **Private Catalog**. -To use, download the **Intune-Packager.ps1** file, right-click and Run with Powershell, you will be given the below window. +### 1. The JSON Structure -image +Create a `CustomApps.json` file: +```json +[ + { + "Id": "MyDPD", + "Url": "[https://www.mydpd.co.uk/downloads/MyDPD_Installer.exe](https://www.mydpd.co.uk/downloads/MyDPD_Installer.exe)", + "SilentArgs": "/S /v/qn", + "InstallerType": "exe", + "DisplayName": "MyDPD Desktop Client" + }, + { + "Id": "InternalTool", + "Url": "[https://storage.mycompany.com/tools/internal.msi](https://storage.mycompany.com/tools/internal.msi)", + "SilentArgs": "/qb", + "InstallerType": "msi", + "DisplayName": "Techary Internal Tool v2" + } +] -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 +### 2. Hosting (Cloud Sync) -For N-Able install you will see the parameters needed to create the Customer specific installer: +Host this file on a raw URL (e.g., GitHub Raw, Azure Blob). The module automatically caches this file locally and syncs it every 60 minutes. -image +**To configure the URL:** +Edit `Private\Get-CustomApp.ps1` and update the `$CloudUrl` variable: +```powershell +$CloudUrl = "[https://raw.githubusercontent.com/Techary/TecharyGet/BETA/TecharyGet/Private/CustomApps.json](https://raw.githubusercontent.com/Techary/TecharyGet/BETA/TecharyGet/Private/CustomApps.json)" +``` +--- -## Notes +## 🔍 Logging & Telemetry -* The module detects CPU architecture and installs the correct version. +The module provides full observability for RMM tools (N-able, Datto, NinjaOne). -* All downloads are logged to C:\Logs\TecharyGetLogs\TecharyGet.log +* **Log File:** `C:\ProgramData\TecharyGet\InstallLogs.log` +* **Event Log:** Windows Logs -> Application +* **Source:** `TecharyGet` +* **Event IDs:** +* `100`: Information (Started, Success) +* `200`: Warning (Reboot Required, Retry) +* `300`: Error (Download Failed, Install Failed) -* 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 +## ⚠️ Troubleshooting -* PowerShell 5.1 and 7+ +**"Access Denied" on Download:** +The module uses User-Agent spoofing (Chrome/120) to bypass Akamai/Vendor blocks (e.g., Dell, Adobe). -## Troubleshooting +**Exit Code 4 (Dell):** +The module treats Exit Code `4` as "Success (Reboot Required)" specifically for Dell installers, preventing false failure reports. -* ❗ App not found? → Make sure it's defined in AppMap.ps1 +**"WindowsApp" Uninstall Failures:** +Store Apps often have internal names different from their display names. Use `Get-StartApps` to find the internal ID (e.g., `MicrosoftCorporationII.Windows365`). -* ❗ 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 +``` From 75da1dd2bba0d521a4497379bed7332766541daf Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:36:02 +0000 Subject: [PATCH 07/38] Revise README by removing outdated sections Removed introductory text and several sections about uninstalling applications, creating Intune packages, special N-able agent deployment, private catalog configuration, logging, telemetry, and troubleshooting. --- README.md | 135 ------------------------------------------------------ 1 file changed, 135 deletions(-) diff --git a/README.md b/README.md index e8fae0d..fa5bce0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,3 @@ -Here is a professional, comprehensive `README.md` file for your module. It covers all the advanced features we built (Cloud Catalog, Intune Packaging, Smart Uninstall, etc.). - -You can copy-paste this directly into the root of your project. - ---- - -```markdown # TecharyGet **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. @@ -45,131 +38,3 @@ Install-SmartApp -Id "MyDPD" # Install a complex app (e.g. Dell Command Update) - Handles Exit Code 4 automatically Install-SmartApp -Id "Dell.CommandUpdate" - -``` - -### 2. Uninstalling Applications - -The uninstaller intelligently searches the Registry (HKLM/HKCU) and Appx packages. - -```powershell -# Uninstall by Display Name -Uninstall-SmartApp -Name "Google Chrome" - -# Uninstall using a Custom ID (looks up real name in JSON) -Uninstall-SmartApp -Name "MyDPD" - -# Test run (Safe Mode) -Uninstall-SmartApp -Name "Windows365" -WhatIf - -``` - -### 3. Creating Intune Packages - -You can generate ready-to-upload `.intunewin` files that contain "Thin" scripts (One-Liners). These scripts assume `TecharyGet` is installed on the target machine and trigger a live download/install. - -**GUI Method:** - -```powershell -New-IntunePackageUI - -``` - -**CLI Method:** - -```powershell -New-IntunePackage -Id "Dell.CommandUpdate" -OutputFolder "C:\IntunePackages" - -``` - -* **Install Command:** `powershell.exe -ExecutionPolicy Bypass -File Install.ps1` -* **Uninstall Command:** `powershell.exe -ExecutionPolicy Bypass -File Uninstall.ps1` - -### 4. Special N-able Agent Deployment - -N-able requires dynamic parameters (Token, CustomerID). Use the dedicated function: - -```powershell -Install-NableAgent -CustomerID "123" ` - -Token "abc-123-xyz" ` - -CustomerName "ClientA" ` - -ServerAddress "rmm.example.com" - -``` - ---- - -## ☁️ Private Catalog Configuration - -For apps that are not in the public WinGet repo (e.g., licensed software, internal tools), use the **Private Catalog**. - -### 1. The JSON Structure - -Create a `CustomApps.json` file: - -```json -[ - { - "Id": "MyDPD", - "Url": "[https://www.mydpd.co.uk/downloads/MyDPD_Installer.exe](https://www.mydpd.co.uk/downloads/MyDPD_Installer.exe)", - "SilentArgs": "/S /v/qn", - "InstallerType": "exe", - "DisplayName": "MyDPD Desktop Client" - }, - { - "Id": "InternalTool", - "Url": "[https://storage.mycompany.com/tools/internal.msi](https://storage.mycompany.com/tools/internal.msi)", - "SilentArgs": "/qb", - "InstallerType": "msi", - "DisplayName": "Techary Internal Tool v2" - } -] - -``` - -### 2. Hosting (Cloud Sync) - -Host this file on a raw URL (e.g., GitHub Raw, Azure Blob). The module automatically caches this file locally and syncs it every 60 minutes. - -**To configure the URL:** -Edit `Private\Get-CustomApp.ps1` and update the `$CloudUrl` variable: - -```powershell -$CloudUrl = "[https://raw.githubusercontent.com/Techary/TecharyGet/BETA/TecharyGet/Private/CustomApps.json](https://raw.githubusercontent.com/Techary/TecharyGet/BETA/TecharyGet/Private/CustomApps.json)" - -``` - ---- - -## 🔍 Logging & Telemetry - -The module provides full observability for RMM tools (N-able, Datto, NinjaOne). - -* **Log File:** `C:\ProgramData\TecharyGet\InstallLogs.log` -* **Event Log:** Windows Logs -> Application -* **Source:** `TecharyGet` -* **Event IDs:** -* `100`: Information (Started, Success) -* `200`: Warning (Reboot Required, Retry) -* `300`: Error (Download Failed, Install Failed) - - - - - ---- - -## ⚠️ Troubleshooting - -**"Access Denied" on Download:** -The module uses User-Agent spoofing (Chrome/120) to bypass Akamai/Vendor blocks (e.g., Dell, Adobe). - -**Exit Code 4 (Dell):** -The module treats Exit Code `4` as "Success (Reboot Required)" specifically for Dell installers, preventing false failure reports. - -**"WindowsApp" Uninstall Failures:** -Store Apps often have internal names different from their display names. Use `Get-StartApps` to find the internal ID (e.g., `MicrosoftCorporationII.Windows365`). - -``` - -``` From b28813c00406cc8e9e385e2a3942c330a74d7c40 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:43:28 +0000 Subject: [PATCH 08/38] Update README to streamline installation and usage Removed import instructions and simplified usage examples. --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index fa5bce0..381ccd3 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,6 @@ 1. Copy the `TecharyGet` folder to your PowerShell Modules directory: * `C:\Program Files\WindowsPowerShell\Modules\` -2. Import the module: - ```powershell - Import-Module TecharyGet -Force - ``` - --- ## 🛠️ Usage Examples @@ -35,6 +30,3 @@ Install-SmartApp -Id "7zip.7zip" # Install a specific ID from your private catalog Install-SmartApp -Id "MyDPD" - -# Install a complex app (e.g. Dell Command Update) - Handles Exit Code 4 automatically -Install-SmartApp -Id "Dell.CommandUpdate" From b6503aba87a612fc516b6b71ce35c024bdb74a1d Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:10:49 +0000 Subject: [PATCH 09/38] Add Install-TecharyApp function for app installation --- Public/Install-SmartApp.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Public/Install-SmartApp.ps1 b/Public/Install-SmartApp.ps1 index 8f92967..6329d9e 100644 --- a/Public/Install-SmartApp.ps1 +++ b/Public/Install-SmartApp.ps1 @@ -1,6 +1,6 @@ $ProgressPreference = 'SilentlyContinue' -function Install-SmartApp { +function Install-TecharyApp { [CmdletBinding()] param( [Parameter(Mandatory=$true)] @@ -65,4 +65,5 @@ function Install-SmartApp { Install-AppPackage -Name $Pkg.Name -FilePath $Pkg.InstallerPath -Arguments $Args Invoke-PackagerCleanup -Paths "$env:TEMP\AppPackager" -Force -} \ No newline at end of file + +} From a722e949ac60550647faa5cd4606a6d9e9b4f8da Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:11:23 +0000 Subject: [PATCH 10/38] Rename Install-SmartApp.ps1 to Install-TecharyApp.ps1 --- Public/{Install-SmartApp.ps1 => Install-TecharyApp.ps1} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Public/{Install-SmartApp.ps1 => Install-TecharyApp.ps1} (100%) diff --git a/Public/Install-SmartApp.ps1 b/Public/Install-TecharyApp.ps1 similarity index 100% rename from Public/Install-SmartApp.ps1 rename to Public/Install-TecharyApp.ps1 From d59103c626acd5e7360cfcf115e41fb4e4d22845 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:11:47 +0000 Subject: [PATCH 11/38] Add Uninstall-TecharyApp function for app removal --- Public/{Uninstall-SmartApp.ps1 => Uninstall-TecharyApp.ps1} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename Public/{Uninstall-SmartApp.ps1 => Uninstall-TecharyApp.ps1} (96%) diff --git a/Public/Uninstall-SmartApp.ps1 b/Public/Uninstall-TecharyApp.ps1 similarity index 96% rename from Public/Uninstall-SmartApp.ps1 rename to Public/Uninstall-TecharyApp.ps1 index 83609e7..20d0c70 100644 --- a/Public/Uninstall-SmartApp.ps1 +++ b/Public/Uninstall-TecharyApp.ps1 @@ -1,4 +1,4 @@ -function Uninstall-SmartApp { +function Uninstall-TecharyApp { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] @@ -108,4 +108,5 @@ function Uninstall-SmartApp { catch { Write-PackagerLog -Message "Uninstallation Failed: $_" -Severity Error } -} \ No newline at end of file + +} From 0592b692a71207a7cb9af20f1912e6c16eece537 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:12:29 +0000 Subject: [PATCH 12/38] Rename Install-SmartApp to Install-TecharyApp --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 381ccd3..ae4da47 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,11 @@ ## 🛠️ Usage Examples ### 1. Installing Applications -The `Install-SmartApp` command is the primary workhorse. It attempts to find the app in the public GitHub repo first, then falls back to your Private Catalog. +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. ```powershell # Install a standard app (Latest Version) -Install-SmartApp -Id "7zip.7zip" +Install-TecharyApp -Id "7zip.7zip" # Install a specific ID from your private catalog -Install-SmartApp -Id "MyDPD" +Install-TecharyApp -Id "MyDPD" From f094a9898830e21c4e8a6990a4ae977f7f8b9474 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:14:22 +0000 Subject: [PATCH 13/38] Update Install-TecharyApp function for MSI fallback --- Public/Install-TecharyApp.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Public/Install-TecharyApp.ps1 b/Public/Install-TecharyApp.ps1 index 6329d9e..5b7362e 100644 --- a/Public/Install-TecharyApp.ps1 +++ b/Public/Install-TecharyApp.ps1 @@ -60,10 +60,11 @@ function Install-TecharyApp { # MSI Fallback Logic $Args = $Pkg.SilentArgs if ([string]::IsNullOrWhiteSpace($Args) -and ($Pkg.InstallerPath -match ".msi$" -or $Pkg.InstallerType -eq "msi")) { - $Args = "/qb /norestart" + $Args = "ALLUSERS=1 /quiet /norestart" } Install-AppPackage -Name $Pkg.Name -FilePath $Pkg.InstallerPath -Arguments $Args Invoke-PackagerCleanup -Paths "$env:TEMP\AppPackager" -Force } + From dfd243ccc3f5da29c5c69d429e54d2ed3f5b0b56 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:15:21 +0000 Subject: [PATCH 14/38] Refactor TecharyGet.psm1 for consistency --- AppPackager.psm1 => TecharyGet.psm1 | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename AppPackager.psm1 => TecharyGet.psm1 (100%) diff --git a/AppPackager.psm1 b/TecharyGet.psm1 similarity index 100% rename from AppPackager.psm1 rename to TecharyGet.psm1 From ed4e88eb46fe3228f16223409e80085ba3f365de Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:19:06 +0000 Subject: [PATCH 15/38] Update module manifest for TecharyGet --- AppPackager.psd1 => TecharyGet.psd1 | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) rename AppPackager.psd1 => TecharyGet.psd1 (91%) diff --git a/AppPackager.psd1 b/TecharyGet.psd1 similarity index 91% rename from AppPackager.psd1 rename to TecharyGet.psd1 index 7463d6e..3aa76c0 100644 --- a/AppPackager.psd1 +++ b/TecharyGet.psd1 @@ -1,7 +1,7 @@ # -# Module manifest for module 'AppPackager' +# Module manifest for module 'TecharyGet' # -# Generated by: asweetapple +# Generated by: Adam Sweetapple # # Generated on: 04/02/2026 # @@ -9,10 +9,10 @@ @{ # Script module or binary module file associated with this manifest. -RootModule = 'AppPackager.psm1' +RootModule = 'TecharyGet.psm1' # Version number of this module. -ModuleVersion = '0.0.1' +ModuleVersion = '0.1' # Supported PSEditions # CompatiblePSEditions = @() @@ -21,13 +21,13 @@ ModuleVersion = '0.0.1' GUID = '9f3ce1a8-1494-4b4a-b885-0c1112fc234e' # Author of this module -Author = 'asweetapple' +Author = 'Adam Sweetapple' # Company or vendor of this module -CompanyName = 'Unknown' +CompanyName = 'Techary' # Copyright statement for this module -Copyright = '(c) asweetapple. All rights reserved.' +Copyright = '(c) Adam Sweetapple. All rights reserved.' # Description of the functionality provided by this module # Description = '' @@ -131,3 +131,4 @@ PrivateData = @{ } + From 4d9c0fe1e2715b8c433ca1df92057f049b106022 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:22:59 +0000 Subject: [PATCH 16/38] Update TecharyGet module manifest with new functions Added new functions for managing Techary applications. --- TecharyGet.psd1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TecharyGet.psd1 b/TecharyGet.psd1 index 3aa76c0..9e3097f 100644 --- a/TecharyGet.psd1 +++ b/TecharyGet.psd1 @@ -70,7 +70,9 @@ Copyright = '(c) Adam Sweetapple. All rights reserved.' # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = 'Get-GitHubInstaller', 'Install-AppPackage', 'Write-PackagerLog', - 'Invoke-PackagerCleanup', 'Install-GitHubApp' + 'Invoke-PackagerCleanup', 'Install-GitHubApp', 'Install-TecharyApp', + 'New-IntunePackage', 'New-IntunePackageUI', 'Uninstall-TecharyGet', + 'Install-NableAgent', 'Invoke-PackagerCleanup', 'Write-PackagerLog' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = '*' @@ -132,3 +134,4 @@ PrivateData = @{ } + From 315ba936b053f19dc8edbffaf02c72a9086cf597 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:52:10 +0000 Subject: [PATCH 17/38] Update TecharyGet.psd1 --- TecharyGet.psd1 | 182 +++++++++++++++++++----------------------------- 1 file changed, 73 insertions(+), 109 deletions(-) diff --git a/TecharyGet.psd1 b/TecharyGet.psd1 index 9e3097f..9f0c8fe 100644 --- a/TecharyGet.psd1 +++ b/TecharyGet.psd1 @@ -1,137 +1,101 @@ -# -# Module manifest for module 'TecharyGet' -# -# Generated by: Adam Sweetapple -# -# Generated on: 04/02/2026 -# - @{ + # Script module or binary module file associated with this manifest. + RootModule = 'TecharyGet.psm1' -# Script module or binary module file associated with this manifest. -RootModule = 'TecharyGet.psm1' - -# Version number of this module. -ModuleVersion = '0.1' - -# Supported PSEditions -# CompatiblePSEditions = @() - -# ID used to uniquely identify this module -GUID = '9f3ce1a8-1494-4b4a-b885-0c1112fc234e' - -# Author of this module -Author = 'Adam Sweetapple' - -# Company or vendor of this module -CompanyName = 'Techary' - -# Copyright statement for this module -Copyright = '(c) Adam Sweetapple. All rights reserved.' - -# Description of the functionality provided by this module -# Description = '' - -# Minimum version of the PowerShell engine required by this module -# PowerShellVersion = '' - -# Name of the PowerShell host required by this module -# PowerShellHostName = '' + # Version number of this module. + ModuleVersion = '0.1' -# Minimum version of the PowerShell host required by this module -# PowerShellHostVersion = '' + # ID used to uniquely identify this module + GUID = 'e9c840c8-3c3e-4246-8178-52372d807654' -# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# DotNetFrameworkVersion = '' + # Author of this module + Author = 'Adam Sweetapple' -# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# ClrVersion = '' + # Company or vendor of this module + CompanyName = 'Techary' -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' + # Copyright statement for this module + Copyright = '(c) 2026 Techary. All rights reserved.' -# Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() + # 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.' -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() + # 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 -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() + # -- 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 -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() + # -- Utilities -- + 'Write-PackagerLog' + ) -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() + # Cmdlets to export from this module + CmdletsToExport = @() -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() + # Variables to export from this module + VariablesToExport = '*' -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = 'Get-GitHubInstaller', 'Install-AppPackage', 'Write-PackagerLog', - 'Invoke-PackagerCleanup', 'Install-GitHubApp', 'Install-TecharyApp', - 'New-IntunePackage', 'New-IntunePackageUI', 'Uninstall-TecharyGet', - 'Install-NableAgent', 'Invoke-PackagerCleanup', 'Write-PackagerLog' + # Aliases to export from this module + AliasesToExport = @() -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = '*' + # List of all modules packaged with this module + # NestedModules = @() -# Variables to export from this module -VariablesToExport = '*' + # List of all files packaged with this module + # FileList = @() -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = '*' + # Private data to pass to the module specified in RootModule/ModuleToProcess + PrivateData = @{ + PSData = @{ + # 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 = '' -# DSC resources to export from this module -# DscResourcesToExport = @() + # A URL to the main website for this project. + # ProjectUri = '' -# List of all modules packaged with this module -# ModuleList = @() + # A URL to an icon representing this module. + # IconUri = '' -# List of all files packaged with this module -# FileList = @() + # ReleaseNotes of this module + # ReleaseNotes = '' + } + } -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.1' - PSData = @{ + # Minimum version of the Common Language Runtime (CLR) required by this module + # CLRVersion = '' - # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' - # A URL to the license for this module. - # LicenseUri = '' + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() - # A URL to the main website for this project. - # ProjectUri = '' + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() - # A URL to an icon representing this module. - # IconUri = '' + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() - # ReleaseNotes of this module - # ReleaseNotes = '' - - # Prerelease string of this module - # Prerelease = '' - - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false - - # External dependent modules of this module - # ExternalModuleDependencies = @() - - } # End of PSData hashtable - -} # End of PrivateData hashtable - -# HelpInfo URI of this module -# HelpInfoURI = '' - -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() } - - - From 230a31dfbfcae3eb5a2e909bfe1a3bfcaf3fec1a Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:53:06 +0000 Subject: [PATCH 18/38] Delete Public/Get-GitHubInstaller.ps1 --- Public/Get-GitHubInstaller.ps1 | 114 --------------------------------- 1 file changed, 114 deletions(-) delete mode 100644 Public/Get-GitHubInstaller.ps1 diff --git a/Public/Get-GitHubInstaller.ps1 b/Public/Get-GitHubInstaller.ps1 deleted file mode 100644 index d35a27f..0000000 --- a/Public/Get-GitHubInstaller.ps1 +++ /dev/null @@ -1,114 +0,0 @@ -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" } - Write-PackagerLog -Message "System Architecture: $SysArch" - - # 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 - $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 - Write-PackagerLog -Message "Latest Version Found: $LatestVersion" - - # 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." } - - Write-PackagerLog -Message "Parsing Manifest: $($InstallerFile.name)" - $YamlContent = Invoke-RestMethod -Uri $InstallerFile.download_url - - # --- PARSING LOGIC (Fixed Regex) --- - $Blocks = $YamlContent -split 'Architecture:\s*' - - $SelectedUrl = $null - $SelectedArgs = $null - $SelectedType = "exe" - - # Search Blocks for Match - foreach ($Block in $Blocks) { - if ([string]::IsNullOrWhiteSpace($Block)) { continue } - $BlockArch = $Block.Split("`r`n")[0].Trim() - - if ($BlockArch -eq $SysArch) { - # We found our block! - if ($Block -match 'InstallerUrl:\s*["'']?([^"''\r\n]+)["'']?') { $SelectedUrl = $Matches[1].Trim() } - if ($Block -match 'InstallerType:\s*([a-zA-Z0-9]+)') { $SelectedType = $Matches[1].Trim() } - - # FIX: Greedy Regex to capture arguments with quotes (like /v"/qn") - if ($Block -match 'Silent:\s*(.+)') { - # Remove leading/trailing quotes from the YAML value itself, but keep internal quotes - $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') - } - elseif ($Block -match 'SilentWithProgress:\s*(.+)') { - $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') - } - break - } - } - - # Fallback: Scan Whole File if missing - if (-not $SelectedUrl) { - if ($YamlContent -match 'InstallerUrl:\s*["'']?([^"''\r\n]+)["'']?') { $SelectedUrl = $Matches[1].Trim() } - } - - if (-not $SelectedArgs) { - # FIX: Greedy Global Regex - if ($YamlContent -match 'Silent:\s*(.+)') { - $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') - Write-PackagerLog -Message "Found Global Arguments." - } - elseif ($YamlContent -match 'SilentWithProgress:\s*(.+)') { - $SelectedArgs = $Matches[1].Trim().Trim("'").Trim('"') - } - } - - if (-not $SelectedUrl) { throw "Could not find an installer URL." } - - # --- 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..." - $UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - Invoke-WebRequest -Uri $SelectedUrl -OutFile $FullPath -UseBasicParsing -UserAgent $UserAgent - - return [PSCustomObject]@{ - Name = $Id; InstallerPath = $FullPath; FileName = $FileName; SilentArgs = $SelectedArgs; InstallerType = $SelectedType - } - } - catch { - Write-PackagerLog -Message "GitHub Scraping Failed: $_" -Severity Error - throw $_ - } -} \ No newline at end of file From f2b0eb3a7f84caae0f6fbd52a69efdacb9855bdb Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:53:15 +0000 Subject: [PATCH 19/38] Delete Public/Install-AppPackage.ps1 --- Public/Install-AppPackage.ps1 | 103 ---------------------------------- 1 file changed, 103 deletions(-) delete mode 100644 Public/Install-AppPackage.ps1 diff --git a/Public/Install-AppPackage.ps1 b/Public/Install-AppPackage.ps1 deleted file mode 100644 index 8fbf4c8..0000000 --- a/Public/Install-AppPackage.ps1 +++ /dev/null @@ -1,103 +0,0 @@ -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 From 686b9b16636bc9583a2a01ce3567facea1181181 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:53:23 +0000 Subject: [PATCH 20/38] Delete Public/Install-NableAgent.ps1 --- Public/Install-NableAgent.ps1 | 80 ----------------------------------- 1 file changed, 80 deletions(-) delete mode 100644 Public/Install-NableAgent.ps1 diff --git a/Public/Install-NableAgent.ps1 b/Public/Install-NableAgent.ps1 deleted file mode 100644 index 7ba9d31..0000000 --- a/Public/Install-NableAgent.ps1 +++ /dev/null @@ -1,80 +0,0 @@ -function Install-NableAgent { - [CmdletBinding()] - param ( - [Parameter(Mandatory=$true)][string]$CustomerID, - [Parameter(Mandatory=$true)][string]$Token, - [Parameter(Mandatory=$true)][string]$CustomerName, - [Parameter(Mandatory=$true)][string]$ServerAddress, - [int]$TimeoutSeconds = 1200 # 20 minutes - ) - - $Name = "N-able RMM Agent" - $DownloadUrl = "https://$ServerAddress/download/current/winnt/N-central/WindowsAgentSetup.exe" - $DownloadPath = "$env:TEMP\AppPackager" - $FileName = "Nable_RMMInstaller.exe" - $InstallerPath = Join-Path $DownloadPath $FileName - - Write-PackagerLog -Message "Starting N-able Deployment for Customer: $CustomerName" - - try { - # 1. Download - if (Test-Path $DownloadPath) { Remove-Item "$DownloadPath\*" -Recurse -Force -ErrorAction SilentlyContinue } - New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null - - Write-PackagerLog -Message "Downloading installer from: $DownloadUrl" - - # We use a spoofed UserAgent just in case N-able blocks scripts, though usually not required here. - $UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - Invoke-WebRequest -Uri $DownloadUrl -OutFile $InstallerPath -UseBasicParsing -UserAgent $UserAgent - - if (-not (Test-Path $InstallerPath)) { throw "Download failed. File not found." } - - # 2. Build Arguments (Standard InstallShield format) - # /S = Silent for the wrapper - # /v = Pass arguments to internal MSI - # We quote the MSI arguments carefully so Install-AppPackage regex keeps them together. - - $MsiArgs = "/qn CUSTOMERID=$CustomerID CUSTOMERNAME=$CustomerName CUSTOMERSPECIFIC=1 REGISTRATION_TOKEN=$Token SERVERPROTOCOL=HTTPS SERVERADDRESS=$ServerAddress SERVERPORT=443" - - # NOTE: We construct the final string. Install-AppPackage's Regex will see "/v"..." " as one argument. - $FinalArgs = "/S /v`"$MsiArgs`"" - - # 3. Install (Using your module's executor) - Install-AppPackage -Name $Name -FilePath $InstallerPath -Arguments $FinalArgs - - # 4. Cleanup Installer immediately - Remove-Item $InstallerPath -Force -ErrorAction SilentlyContinue - - # 5. Validation Loop (The "Is it actually working?" check) - 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" - - if ($Service1 -and $Service2 -and $FileCheck) { - Write-PackagerLog -Message "Validation Successful: N-able services are running." - return # Success! - } - - # 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)." - } - - Write-PackagerLog -Message "Waiting for services to start..." - Start-Sleep -Seconds 10 - } - - } - catch { - Write-PackagerLog -Message "N-able Deployment Failed: $_" -Severity Error - throw $_ - } -} \ No newline at end of file From 182e62ed5cbee76a0113f8255c3800b5c6487c15 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:53:33 +0000 Subject: [PATCH 21/38] Delete Public/Install-TecharyApp.ps1 --- Public/Install-TecharyApp.ps1 | 70 ----------------------------------- 1 file changed, 70 deletions(-) delete mode 100644 Public/Install-TecharyApp.ps1 diff --git a/Public/Install-TecharyApp.ps1 b/Public/Install-TecharyApp.ps1 deleted file mode 100644 index 5b7362e..0000000 --- a/Public/Install-TecharyApp.ps1 +++ /dev/null @@ -1,70 +0,0 @@ -$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 = "ALLUSERS=1 /quiet /norestart" - } - - Install-AppPackage -Name $Pkg.Name -FilePath $Pkg.InstallerPath -Arguments $Args - Invoke-PackagerCleanup -Paths "$env:TEMP\AppPackager" -Force - -} - From 9958f1a431102fd0467206054a18c2074e374ae1 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:53:41 +0000 Subject: [PATCH 22/38] Delete Public/Invoke-PackagerCleanup.ps1 --- Public/Invoke-PackagerCleanup.ps1 | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 Public/Invoke-PackagerCleanup.ps1 diff --git a/Public/Invoke-PackagerCleanup.ps1 b/Public/Invoke-PackagerCleanup.ps1 deleted file mode 100644 index 7b27a39..0000000 --- a/Public/Invoke-PackagerCleanup.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -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" - } - } - } -} From 28b91e2d95f37f045c03d3422476f20daedde305 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:53:51 +0000 Subject: [PATCH 23/38] Delete Public/New-IntunePackage.ps1 --- Public/New-IntunePackage.ps1 | 70 ------------------------------------ 1 file changed, 70 deletions(-) delete mode 100644 Public/New-IntunePackage.ps1 diff --git a/Public/New-IntunePackage.ps1 b/Public/New-IntunePackage.ps1 deleted file mode 100644 index 32df464..0000000 --- a/Public/New-IntunePackage.ps1 +++ /dev/null @@ -1,70 +0,0 @@ -function New-IntunePackage { - [CmdletBinding()] - param ( - [Parameter(Mandatory=$true)] - [string]$Id, # e.g. "Dell.CommandUpdate" - - [string]$OutputFolder = "C:\IntunePackages" - ) - - # --- 1. SETUP --- - $PackagerTemp = "$env:TEMP\IntunePackager_Working\$Id" - $SourceDir = "$PackagerTemp\Source" - $IntuneUtil = "$env:TEMP\IntuneWinAppUtil.exe" - - # Clean Workspace - if (Test-Path $PackagerTemp) { Remove-Item $PackagerTemp -Recurse -Force -ErrorAction SilentlyContinue } - New-Item -Path $SourceDir -ItemType Directory -Force | Out-Null - - # Ensure Output Dir - if (-not (Test-Path $OutputFolder)) { New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null } - - # Ensure Intune Utility - if (-not (Test-Path $IntuneUtil)) { - Write-PackagerLog -Message "Downloading IntuneWinAppUtil.exe..." - Invoke-WebRequest "https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool/raw/master/IntuneWinAppUtil.exe" -OutFile $IntuneUtil - } - - # --- 2. GENERATE INSTALL.PS1 (The One-Liner) --- - $InstallScript = @" -# Install Trigger for $Id -`$ErrorActionPreference = 'Stop' - -# Try to load the module if not already loaded (Assumes it is installed on the PC) -Import-Module TecharyGet -ErrorAction SilentlyContinue - -Write-Host "Triggering Install for: $Id" -Install-SmartApp -Id "$Id" -"@ - Set-Content -Path "$SourceDir\Install.ps1" -Value $InstallScript - - # --- 3. GENERATE UNINSTALL.PS1 (The One-Liner) --- - $UninstallScript = @" -# Uninstall Trigger for $Id -`$ErrorActionPreference = 'Stop' - -Import-Module TecharyGet -ErrorAction SilentlyContinue - -Write-Host "Triggering Uninstall for: $Id" -Uninstall-SmartApp -Name "$Id" -"@ - Set-Content -Path "$SourceDir\Uninstall.ps1" -Value $UninstallScript - - # --- 4. PACKAGE IT --- - Write-PackagerLog -Message "Packaging scripts into .intunewin..." - - $Process = Start-Process -FilePath $IntuneUtil ` - -ArgumentList "-c `"$SourceDir`"", "-s `"Install.ps1`"", "-o `"$OutputFolder`"", "-q" ` - -PassThru -Wait -NoNewWindow - - if ($Process.ExitCode -eq 0) { - $Original = Join-Path $OutputFolder "Install.intunewin" - $Final = Join-Path $OutputFolder "$Id.intunewin" - if (Test-Path $Original) { Move-Item $Original $Final -Force } - - Write-PackagerLog -Message "SUCCESS: Package created at $Final" - Invoke-Item $OutputFolder - } else { - Write-PackagerLog -Message "Packaging Failed with Exit Code $($Process.ExitCode)" -Severity Error - } -} \ No newline at end of file From c1c62bf6c83e8f66a199e93ee73ee14e17e3bb1d Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:53:57 +0000 Subject: [PATCH 24/38] Delete Public/New-IntunePackageUI.ps1 --- Public/New-IntunePackageUI.ps1 | 93 ---------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 Public/New-IntunePackageUI.ps1 diff --git a/Public/New-IntunePackageUI.ps1 b/Public/New-IntunePackageUI.ps1 deleted file mode 100644 index 46549f3..0000000 --- a/Public/New-IntunePackageUI.ps1 +++ /dev/null @@ -1,93 +0,0 @@ -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 From 292bad8054492f5b935778770251588993e1ad15 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:54:05 +0000 Subject: [PATCH 25/38] Delete Public/Uninstall-TecharyApp.ps1 --- Public/Uninstall-TecharyApp.ps1 | 112 -------------------------------- 1 file changed, 112 deletions(-) delete mode 100644 Public/Uninstall-TecharyApp.ps1 diff --git a/Public/Uninstall-TecharyApp.ps1 b/Public/Uninstall-TecharyApp.ps1 deleted file mode 100644 index 20d0c70..0000000 --- a/Public/Uninstall-TecharyApp.ps1 +++ /dev/null @@ -1,112 +0,0 @@ -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 - } - -} From d6f4a83b20eb6713299f6012382511cb4e0771dc Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:54:11 +0000 Subject: [PATCH 26/38] Delete Public/Write-PackagerLog.ps1 --- Public/Write-PackagerLog.ps1 | 37 ------------------------------------ 1 file changed, 37 deletions(-) delete mode 100644 Public/Write-PackagerLog.ps1 diff --git a/Public/Write-PackagerLog.ps1 b/Public/Write-PackagerLog.ps1 deleted file mode 100644 index ed2d4ee..0000000 --- a/Public/Write-PackagerLog.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -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 From ddf7ffeb2b1946cb198951b679234b7d8213aeaf Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:54:50 +0000 Subject: [PATCH 27/38] Add aa.txt with initial content 'aa' --- Public/aa.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 Public/aa.txt diff --git a/Public/aa.txt b/Public/aa.txt new file mode 100644 index 0000000..e61ef7b --- /dev/null +++ b/Public/aa.txt @@ -0,0 +1 @@ +aa From 533b85fe1f85d9a2993f41234fec38497de1a818 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:55:31 +0000 Subject: [PATCH 28/38] Add files via upload --- Public/Get-GitHubInstaller.ps1 | 117 +++++++++++++++++++++ Public/Install-AppPackage.ps1 | 103 +++++++++++++++++++ Public/Install-NableAgent.ps1 | 80 +++++++++++++++ Public/Install-TecharyApp.ps1 | 68 ++++++++++++ Public/Invoke-PackagerCleanup.ps1 | 12 +++ Public/New-IntunePackage.ps1 | 165 ++++++++++++++++++++++++++++++ Public/New-IntunePackageUI.ps1 | 93 +++++++++++++++++ Public/Test-TecharyApp.ps1 | 42 ++++++++ Public/Uninstall-TecharyApp.ps1 | 111 ++++++++++++++++++++ Public/Write-PackagerLog.ps1 | 37 +++++++ 10 files changed, 828 insertions(+) create mode 100644 Public/Get-GitHubInstaller.ps1 create mode 100644 Public/Install-AppPackage.ps1 create mode 100644 Public/Install-NableAgent.ps1 create mode 100644 Public/Install-TecharyApp.ps1 create mode 100644 Public/Invoke-PackagerCleanup.ps1 create mode 100644 Public/New-IntunePackage.ps1 create mode 100644 Public/New-IntunePackageUI.ps1 create mode 100644 Public/Test-TecharyApp.ps1 create mode 100644 Public/Uninstall-TecharyApp.ps1 create mode 100644 Public/Write-PackagerLog.ps1 diff --git a/Public/Get-GitHubInstaller.ps1 b/Public/Get-GitHubInstaller.ps1 new file mode 100644 index 0000000..747828f --- /dev/null +++ b/Public/Get-GitHubInstaller.ps1 @@ -0,0 +1,117 @@ +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"' } + + # --- 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 $_ + } +} \ No newline at end of file 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..7ba9d31 --- /dev/null +++ b/Public/Install-NableAgent.ps1 @@ -0,0 +1,80 @@ +function Install-NableAgent { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)][string]$CustomerID, + [Parameter(Mandatory=$true)][string]$Token, + [Parameter(Mandatory=$true)][string]$CustomerName, + [Parameter(Mandatory=$true)][string]$ServerAddress, + [int]$TimeoutSeconds = 1200 # 20 minutes + ) + + $Name = "N-able RMM Agent" + $DownloadUrl = "https://$ServerAddress/download/current/winnt/N-central/WindowsAgentSetup.exe" + $DownloadPath = "$env:TEMP\AppPackager" + $FileName = "Nable_RMMInstaller.exe" + $InstallerPath = Join-Path $DownloadPath $FileName + + Write-PackagerLog -Message "Starting N-able Deployment for Customer: $CustomerName" + + try { + # 1. Download + if (Test-Path $DownloadPath) { Remove-Item "$DownloadPath\*" -Recurse -Force -ErrorAction SilentlyContinue } + New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null + + Write-PackagerLog -Message "Downloading installer from: $DownloadUrl" + + # We use a spoofed UserAgent just in case N-able blocks scripts, though usually not required here. + $UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + Invoke-WebRequest -Uri $DownloadUrl -OutFile $InstallerPath -UseBasicParsing -UserAgent $UserAgent + + if (-not (Test-Path $InstallerPath)) { throw "Download failed. File not found." } + + # 2. Build Arguments (Standard InstallShield format) + # /S = Silent for the wrapper + # /v = Pass arguments to internal MSI + # We quote the MSI arguments carefully so Install-AppPackage regex keeps them together. + + $MsiArgs = "/qn CUSTOMERID=$CustomerID CUSTOMERNAME=$CustomerName CUSTOMERSPECIFIC=1 REGISTRATION_TOKEN=$Token SERVERPROTOCOL=HTTPS SERVERADDRESS=$ServerAddress SERVERPORT=443" + + # NOTE: We construct the final string. Install-AppPackage's Regex will see "/v"..." " as one argument. + $FinalArgs = "/S /v`"$MsiArgs`"" + + # 3. Install (Using your module's executor) + Install-AppPackage -Name $Name -FilePath $InstallerPath -Arguments $FinalArgs + + # 4. Cleanup Installer immediately + Remove-Item $InstallerPath -Force -ErrorAction SilentlyContinue + + # 5. Validation Loop (The "Is it actually working?" check) + 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" + + if ($Service1 -and $Service2 -and $FileCheck) { + Write-PackagerLog -Message "Validation Successful: N-able services are running." + return # Success! + } + + # 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)." + } + + Write-PackagerLog -Message "Waiting for services to start..." + Start-Sleep -Seconds 10 + } + + } + catch { + Write-PackagerLog -Message "N-able Deployment Failed: $_" -Severity Error + throw $_ + } +} \ No newline at end of file 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 From 30ee674a0d263c8b68c91a05d8e71d1aa10d0802 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:42:04 +0000 Subject: [PATCH 29/38] Update TecharyGet.psd1 --- TecharyGet.psd1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TecharyGet.psd1 b/TecharyGet.psd1 index 9f0c8fe..e48bbce 100644 --- a/TecharyGet.psd1 +++ b/TecharyGet.psd1 @@ -3,7 +3,7 @@ RootModule = 'TecharyGet.psm1' # Version number of this module. - ModuleVersion = '0.1' + ModuleVersion = '2.0' # ID used to uniquely identify this module GUID = 'e9c840c8-3c3e-4246-8178-52372d807654' @@ -99,3 +99,4 @@ # Format files (.ps1xml) to be loaded when importing this module # FormatsToProcess = @() } + From f3293557984869340a9e22bfa56c0f76733f2701 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:14:48 +0000 Subject: [PATCH 30/38] Delete Public/aa.txt --- Public/aa.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Public/aa.txt diff --git a/Public/aa.txt b/Public/aa.txt deleted file mode 100644 index e61ef7b..0000000 --- a/Public/aa.txt +++ /dev/null @@ -1 +0,0 @@ -aa From b58ab70d62483dc957571260897b98e4408b5dd9 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:19:47 +0000 Subject: [PATCH 31/38] Update TecharyGet.psd1 --- TecharyGet.psd1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TecharyGet.psd1 b/TecharyGet.psd1 index e48bbce..196d6fc 100644 --- a/TecharyGet.psd1 +++ b/TecharyGet.psd1 @@ -3,7 +3,7 @@ RootModule = 'TecharyGet.psm1' # Version number of this module. - ModuleVersion = '2.0' + ModuleVersion = '2.1' # ID used to uniquely identify this module GUID = 'e9c840c8-3c3e-4246-8178-52372d807654' @@ -100,3 +100,4 @@ # FormatsToProcess = @() } + From 3f033ade665e49de29e75c960036f52493f2f3ed Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:52:10 +0000 Subject: [PATCH 32/38] Update TecharyGet.psd1 --- TecharyGet.psd1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TecharyGet.psd1 b/TecharyGet.psd1 index 196d6fc..33425a3 100644 --- a/TecharyGet.psd1 +++ b/TecharyGet.psd1 @@ -3,7 +3,7 @@ RootModule = 'TecharyGet.psm1' # Version number of this module. - ModuleVersion = '2.1' + ModuleVersion = '2.2' # ID used to uniquely identify this module GUID = 'e9c840c8-3c3e-4246-8178-52372d807654' @@ -101,3 +101,4 @@ } + From c53aa2592f30c1f9d5968ab152d77cbe4668b4a1 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:06:22 +0000 Subject: [PATCH 33/38] Refactor Install-NableAgent function Updated Install-NableAgent function to improve parameter handling and validation logic. --- Public/Install-NableAgent.ps1 | 113 +++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 43 deletions(-) diff --git a/Public/Install-NableAgent.ps1 b/Public/Install-NableAgent.ps1 index 7ba9d31..2662fef 100644 --- a/Public/Install-NableAgent.ps1 +++ b/Public/Install-NableAgent.ps1 @@ -1,80 +1,107 @@ function Install-NableAgent { [CmdletBinding()] param ( - [Parameter(Mandatory=$true)][string]$CustomerID, - [Parameter(Mandatory=$true)][string]$Token, - [Parameter(Mandatory=$true)][string]$CustomerName, - [Parameter(Mandatory=$true)][string]$ServerAddress, - [int]$TimeoutSeconds = 1200 # 20 minutes + [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" - $DownloadUrl = "https://$ServerAddress/download/current/winnt/N-central/WindowsAgentSetup.exe" - $DownloadPath = "$env:TEMP\AppPackager" + $DownloadPath = "C:\Temp\AppPackager" $FileName = "Nable_RMMInstaller.exe" $InstallerPath = Join-Path $DownloadPath $FileName - - Write-PackagerLog -Message "Starting N-able Deployment for Customer: $CustomerName" + $DownloadUrl = "https://$ServerAddress/download/current/winnt/N-central/WindowsAgentSetup.exe" try { - # 1. Download - if (Test-Path $DownloadPath) { Remove-Item "$DownloadPath\*" -Recurse -Force -ErrorAction SilentlyContinue } - New-Item -ItemType Directory -Path $DownloadPath -Force | Out-Null + 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" - - # We use a spoofed UserAgent just in case N-able blocks scripts, though usually not required here. - $UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - Invoke-WebRequest -Uri $DownloadUrl -OutFile $InstallerPath -UseBasicParsing -UserAgent $UserAgent - - if (-not (Test-Path $InstallerPath)) { throw "Download failed. File not found." } - - # 2. Build Arguments (Standard InstallShield format) - # /S = Silent for the wrapper - # /v = Pass arguments to internal MSI - # We quote the MSI arguments carefully so Install-AppPackage regex keeps them together. - - $MsiArgs = "/qn CUSTOMERID=$CustomerID CUSTOMERNAME=$CustomerName CUSTOMERSPECIFIC=1 REGISTRATION_TOKEN=$Token SERVERPROTOCOL=HTTPS SERVERADDRESS=$ServerAddress SERVERPORT=443" - - # NOTE: We construct the final string. Install-AppPackage's Regex will see "/v"..." " as one argument. - $FinalArgs = "/S /v`"$MsiArgs`"" - - # 3. Install (Using your module's executor) - Install-AppPackage -Name $Name -FilePath $InstallerPath -Arguments $FinalArgs - - # 4. Cleanup Installer immediately - Remove-Item $InstallerPath -Force -ErrorAction SilentlyContinue - - # 5. Validation Loop (The "Is it actually working?" check) + + 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" - if ($Service1 -and $Service2 -and $FileCheck) { + $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." - return # Success! + 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)." + 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 $_ } -} \ No newline at end of file +} From ec8c1783de3647648c95506d3e480a982bb8b50f Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:07:39 +0000 Subject: [PATCH 34/38] Update TecharyGet.psd1 --- TecharyGet.psd1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TecharyGet.psd1 b/TecharyGet.psd1 index 33425a3..9b33dbc 100644 --- a/TecharyGet.psd1 +++ b/TecharyGet.psd1 @@ -3,7 +3,7 @@ RootModule = 'TecharyGet.psm1' # Version number of this module. - ModuleVersion = '2.2' + ModuleVersion = '2.3' # ID used to uniquely identify this module GUID = 'e9c840c8-3c3e-4246-8178-52372d807654' @@ -102,3 +102,4 @@ + From f20deadbd1ea232ecd0713af8c2f7c432059fe3a Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:40:15 +0000 Subject: [PATCH 35/38] Update README with Nable Agent installation example Added installation instructions for Nable Agent. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index ae4da47..b7fb81f 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,13 @@ Install-TecharyApp -Id "7zip.7zip" # Install a specific ID from your private catalog Install-TecharyApp -Id "MyDPD" +``` + +For the Nable Agent; it is slightly different, below is a example + +```powershell +Install-NableAgent ` + -CustomerID "" ` + -Token "" ` + -ServerAddress "" +``` From fe90072526fc6b310d962433df87cdda56076762 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:14:54 +0000 Subject: [PATCH 36/38] Update Get-GitHubInstaller.ps1 --- Public/Get-GitHubInstaller.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Public/Get-GitHubInstaller.ps1 b/Public/Get-GitHubInstaller.ps1 index 747828f..86e5b08 100644 --- a/Public/Get-GitHubInstaller.ps1 +++ b/Public/Get-GitHubInstaller.ps1 @@ -88,6 +88,9 @@ function Get-GitHubInstaller { # 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" } + # --- DOWNLOAD --- $UriObj = [System.Uri]$SelectedUrl $RealExtension = [System.IO.Path]::GetExtension($UriObj.LocalPath).ToLower() @@ -114,4 +117,5 @@ function Get-GitHubInstaller { Write-PackagerLog -Message "GitHub Scraping Failed: $_" -Severity Error throw $_ } -} \ No newline at end of file + +} From cbac32dda0eda89f137e5651cb6daa2308285898 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:11:58 +0000 Subject: [PATCH 37/38] Update Get-GitHubInstaller.ps1 --- Public/Get-GitHubInstaller.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Public/Get-GitHubInstaller.ps1 b/Public/Get-GitHubInstaller.ps1 index 86e5b08..c028dd0 100644 --- a/Public/Get-GitHubInstaller.ps1 +++ b/Public/Get-GitHubInstaller.ps1 @@ -91,6 +91,9 @@ function Get-GitHubInstaller { # 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() @@ -117,5 +120,4 @@ function Get-GitHubInstaller { Write-PackagerLog -Message "GitHub Scraping Failed: $_" -Severity Error throw $_ } - } From 11db0ca44a9851e14740eb52e40862e7c2e0f961 Mon Sep 17 00:00:00 2001 From: TecharyAdam <86409453+TecharyAdam@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:27:39 +0000 Subject: [PATCH 38/38] Update README with App ID details Added information about the App ID for installation. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b7fb81f..6746164 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ ## 🚀 Key Features * **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.