├── .gitignore ├── LICENSE ├── README.md ├── RecipeBuilder.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── WorkspaceSettings.xcsettings │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── mikael.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── WorkspaceSettings.xcsettings ├── xcshareddata │ └── xcschemes │ │ └── RecipeBuilder.xcscheme └── xcuserdata │ └── mikael.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── RecipeBuilder ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── RecipeBuilder_icon-1024.png │ │ ├── RecipeBuilder_icon-128.png │ │ ├── RecipeBuilder_icon-16.png │ │ ├── RecipeBuilder_icon-256.png │ │ ├── RecipeBuilder_icon-257.png │ │ ├── RecipeBuilder_icon-32.png │ │ ├── RecipeBuilder_icon-33.png │ │ ├── RecipeBuilder_icon-512.png │ │ ├── RecipeBuilder_icon-513.png │ │ └── RecipeBuilder_icon-64.png │ ├── Contents.json │ └── Warning.iconset │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png ├── Base.lproj │ └── MainMenu.xib ├── Buttons.swift ├── Buttons3rdParty.swift ├── CheckAndVerify.swift ├── Functions.swift ├── Info.plist ├── JamfUploaderHelpTexts.swift ├── RecipeBuilder.entitlements ├── UserButtons.swift ├── XMLtoYaml.swift ├── XMLtoYamlSnippets.swift └── YamlToXML.swift └── images ├── recipeBuildericon.png └── recipebuilderinterface.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mikael Löfgren 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | # RecipeBuilder
3 | - Click and create recipes (Choose File - New to get started) 4 | - Open recipes for editing (it always open recipes as a copy, so save and replace when done) 5 | - Shows processor-info as helptext when you click a processor button, sometimes with additional extra note 6 | - Finalize, lint xml and remove XML comments 7 | - Save and auto-pkg run from app 8 | - Save and open in external editor 9 | - Search recipes content to find processors usage 10 | - Open autopkg cache folder 11 | - Create your own buttons 12 | - Check and verify recipes files 13 | - Switch mode between XML and YAML 14 | - Batch convert XML to YAML or YAML to XML 15 | - Set Autopkg Trustinfo 16 | 17 | 18 | System requirements: macOS 11 or later
19 | Dependencies: Highlightr and Yams
20 | Icon: vecteezy.com
21 | 22 | Download: https://github.com/mikaellofgren/RecipeBuilder/releases 23 | 24 |
25 | 26 | # User buttons
27 | In folder "~/Library/Application Support/RecipeBuilder" 28 | you create folders with name 1,2,3..up to 10. You can skip folders, but folder 1 is required if you 29 | want buttons to autostart, otherwise you have to enable them. 30 | In every folder create title.txt and add text to the file for the button name. 31 | Keep it below 26 characters for best result. 32 | 33 | Add output.txt for the output text. 34 | 35 | And help.txt for the Note text your reading right now (optional). 36 | To easy open "~/Library/Application Support/RecipeBuilder", choose File options - Open user buttons folder 37 | Click the Enable and Reload button to create the first "demo" button. 38 | Use that button to reload if you trying out your new buttons. -------------------------------------------------------------------------------- /RecipeBuilder.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7300825A245308B100F4909F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73008259245308B100F4909F /* AppDelegate.swift */; }; 11 | 7300825C245308B600F4909F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7300825B245308B600F4909F /* Assets.xcassets */; }; 12 | 7300825F245308B600F4909F /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7300825D245308B600F4909F /* MainMenu.xib */; }; 13 | 73008268245309F200F4909F /* Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73008267245309F200F4909F /* Buttons.swift */; }; 14 | 733DE0042D160A9800F5CB2D /* XMLtoYaml.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733DE0032D160A8C00F5CB2D /* XMLtoYaml.swift */; }; 15 | 733DE0072D197D0D00F5CB2D /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = 733DE0062D197D0D00F5CB2D /* Yams */; }; 16 | 733DE00D2D1D596B00F5CB2D /* YamlToXML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733DE00C2D1D596B00F5CB2D /* YamlToXML.swift */; }; 17 | 733DE00F2D1D863400F5CB2D /* XMLtoYamlSnippets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 733DE00E2D1D863400F5CB2D /* XMLtoYamlSnippets.swift */; }; 18 | 734EF16724B2582200C203C9 /* UserButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 734EF16624B2582200C203C9 /* UserButtons.swift */; }; 19 | 7352335927CBF82C004FAA8C /* Buttons3rdParty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7352335827CBF82C004FAA8C /* Buttons3rdParty.swift */; }; 20 | 7352335B27D2981C004FAA8C /* JamfUploaderHelpTexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7352335A27D2981C004FAA8C /* JamfUploaderHelpTexts.swift */; }; 21 | 73631C1024587182008A6491 /* Highlightr in Frameworks */ = {isa = PBXBuildFile; productRef = 73631C0F24587182008A6491 /* Highlightr */; }; 22 | 73A9CE74247A5D2600627DA8 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73A9CE73247A5D2600627DA8 /* Functions.swift */; }; 23 | 73B2E40D27D51F9800CFEA88 /* CheckAndVerify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B2E40C27D51F9800CFEA88 /* CheckAndVerify.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 73008256245308B100F4909F /* RecipeBuilder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RecipeBuilder.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | 73008259245308B100F4909F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 29 | 7300825B245308B600F4909F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | 7300825E245308B600F4909F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 31 | 73008260245308B600F4909F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | 73008261245308B600F4909F /* RecipeBuilder.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RecipeBuilder.entitlements; sourceTree = ""; }; 33 | 73008267245309F200F4909F /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = ""; }; 34 | 733DE0032D160A8C00F5CB2D /* XMLtoYaml.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLtoYaml.swift; sourceTree = ""; }; 35 | 733DE00C2D1D596B00F5CB2D /* YamlToXML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YamlToXML.swift; sourceTree = ""; }; 36 | 733DE00E2D1D863400F5CB2D /* XMLtoYamlSnippets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLtoYamlSnippets.swift; sourceTree = ""; }; 37 | 734EF16624B2582200C203C9 /* UserButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserButtons.swift; sourceTree = ""; }; 38 | 7352335827CBF82C004FAA8C /* Buttons3rdParty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons3rdParty.swift; sourceTree = ""; }; 39 | 7352335A27D2981C004FAA8C /* JamfUploaderHelpTexts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JamfUploaderHelpTexts.swift; sourceTree = ""; }; 40 | 73A9CE73247A5D2600627DA8 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 41 | 73B2E40C27D51F9800CFEA88 /* CheckAndVerify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckAndVerify.swift; sourceTree = ""; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFrameworksBuildPhase section */ 45 | 73008253245308B100F4909F /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | 733DE0072D197D0D00F5CB2D /* Yams in Frameworks */, 50 | 73631C1024587182008A6491 /* Highlightr in Frameworks */, 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXFrameworksBuildPhase section */ 55 | 56 | /* Begin PBXGroup section */ 57 | 7300824D245308B100F4909F = { 58 | isa = PBXGroup; 59 | children = ( 60 | 73008258245308B100F4909F /* RecipeBuilder */, 61 | 73008257245308B100F4909F /* Products */, 62 | ); 63 | sourceTree = ""; 64 | }; 65 | 73008257245308B100F4909F /* Products */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 73008256245308B100F4909F /* RecipeBuilder.app */, 69 | ); 70 | name = Products; 71 | sourceTree = ""; 72 | }; 73 | 73008258245308B100F4909F /* RecipeBuilder */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 73008259245308B100F4909F /* AppDelegate.swift */, 77 | 7352335A27D2981C004FAA8C /* JamfUploaderHelpTexts.swift */, 78 | 734EF16624B2582200C203C9 /* UserButtons.swift */, 79 | 73008267245309F200F4909F /* Buttons.swift */, 80 | 7352335827CBF82C004FAA8C /* Buttons3rdParty.swift */, 81 | 73A9CE73247A5D2600627DA8 /* Functions.swift */, 82 | 733DE00C2D1D596B00F5CB2D /* YamlToXML.swift */, 83 | 733DE0032D160A8C00F5CB2D /* XMLtoYaml.swift */, 84 | 733DE00E2D1D863400F5CB2D /* XMLtoYamlSnippets.swift */, 85 | 73B2E40C27D51F9800CFEA88 /* CheckAndVerify.swift */, 86 | 7300825B245308B600F4909F /* Assets.xcassets */, 87 | 7300825D245308B600F4909F /* MainMenu.xib */, 88 | 73008260245308B600F4909F /* Info.plist */, 89 | 73008261245308B600F4909F /* RecipeBuilder.entitlements */, 90 | ); 91 | path = RecipeBuilder; 92 | sourceTree = ""; 93 | }; 94 | /* End PBXGroup section */ 95 | 96 | /* Begin PBXNativeTarget section */ 97 | 73008255245308B100F4909F /* RecipeBuilder */ = { 98 | isa = PBXNativeTarget; 99 | buildConfigurationList = 73008264245308B600F4909F /* Build configuration list for PBXNativeTarget "RecipeBuilder" */; 100 | buildPhases = ( 101 | 73008252245308B100F4909F /* Sources */, 102 | 73008253245308B100F4909F /* Frameworks */, 103 | 73008254245308B100F4909F /* Resources */, 104 | ); 105 | buildRules = ( 106 | ); 107 | dependencies = ( 108 | ); 109 | name = RecipeBuilder; 110 | packageProductDependencies = ( 111 | 73631C0F24587182008A6491 /* Highlightr */, 112 | 733DE0062D197D0D00F5CB2D /* Yams */, 113 | ); 114 | productName = autopkgRecipeBuilder; 115 | productReference = 73008256245308B100F4909F /* RecipeBuilder.app */; 116 | productType = "com.apple.product-type.application"; 117 | }; 118 | /* End PBXNativeTarget section */ 119 | 120 | /* Begin PBXProject section */ 121 | 7300824E245308B100F4909F /* Project object */ = { 122 | isa = PBXProject; 123 | attributes = { 124 | BuildIndependentTargetsInParallel = YES; 125 | LastSwiftUpdateCheck = 1130; 126 | LastUpgradeCheck = 1620; 127 | ORGANIZATIONNAME = "Mikael Löfgren"; 128 | TargetAttributes = { 129 | 73008255245308B100F4909F = { 130 | CreatedOnToolsVersion = 11.3.1; 131 | }; 132 | }; 133 | }; 134 | buildConfigurationList = 73008251245308B100F4909F /* Build configuration list for PBXProject "RecipeBuilder" */; 135 | compatibilityVersion = "Xcode 9.3"; 136 | developmentRegion = en; 137 | hasScannedForEncodings = 0; 138 | knownRegions = ( 139 | en, 140 | Base, 141 | ); 142 | mainGroup = 7300824D245308B100F4909F; 143 | packageReferences = ( 144 | 73631C0E24587182008A6491 /* XCRemoteSwiftPackageReference "Highlightr" */, 145 | 733DE0052D197D0D00F5CB2D /* XCRemoteSwiftPackageReference "Yams" */, 146 | ); 147 | productRefGroup = 73008257245308B100F4909F /* Products */; 148 | projectDirPath = ""; 149 | projectRoot = ""; 150 | targets = ( 151 | 73008255245308B100F4909F /* RecipeBuilder */, 152 | ); 153 | }; 154 | /* End PBXProject section */ 155 | 156 | /* Begin PBXResourcesBuildPhase section */ 157 | 73008254245308B100F4909F /* Resources */ = { 158 | isa = PBXResourcesBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | 7300825C245308B600F4909F /* Assets.xcassets in Resources */, 162 | 7300825F245308B600F4909F /* MainMenu.xib in Resources */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXResourcesBuildPhase section */ 167 | 168 | /* Begin PBXSourcesBuildPhase section */ 169 | 73008252245308B100F4909F /* Sources */ = { 170 | isa = PBXSourcesBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | 7300825A245308B100F4909F /* AppDelegate.swift in Sources */, 174 | 73008268245309F200F4909F /* Buttons.swift in Sources */, 175 | 733DE0042D160A9800F5CB2D /* XMLtoYaml.swift in Sources */, 176 | 733DE00D2D1D596B00F5CB2D /* YamlToXML.swift in Sources */, 177 | 73B2E40D27D51F9800CFEA88 /* CheckAndVerify.swift in Sources */, 178 | 7352335927CBF82C004FAA8C /* Buttons3rdParty.swift in Sources */, 179 | 7352335B27D2981C004FAA8C /* JamfUploaderHelpTexts.swift in Sources */, 180 | 734EF16724B2582200C203C9 /* UserButtons.swift in Sources */, 181 | 73A9CE74247A5D2600627DA8 /* Functions.swift in Sources */, 182 | 733DE00F2D1D863400F5CB2D /* XMLtoYamlSnippets.swift in Sources */, 183 | ); 184 | runOnlyForDeploymentPostprocessing = 0; 185 | }; 186 | /* End PBXSourcesBuildPhase section */ 187 | 188 | /* Begin PBXVariantGroup section */ 189 | 7300825D245308B600F4909F /* MainMenu.xib */ = { 190 | isa = PBXVariantGroup; 191 | children = ( 192 | 7300825E245308B600F4909F /* Base */, 193 | ); 194 | name = MainMenu.xib; 195 | sourceTree = ""; 196 | }; 197 | /* End PBXVariantGroup section */ 198 | 199 | /* Begin XCBuildConfiguration section */ 200 | 73008262245308B600F4909F /* Debug */ = { 201 | isa = XCBuildConfiguration; 202 | buildSettings = { 203 | ALWAYS_SEARCH_USER_PATHS = NO; 204 | CLANG_ANALYZER_NONNULL = YES; 205 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 206 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 207 | CLANG_CXX_LIBRARY = "libc++"; 208 | CLANG_ENABLE_MODULES = YES; 209 | CLANG_ENABLE_OBJC_ARC = YES; 210 | CLANG_ENABLE_OBJC_WEAK = YES; 211 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 212 | CLANG_WARN_BOOL_CONVERSION = YES; 213 | CLANG_WARN_COMMA = YES; 214 | CLANG_WARN_CONSTANT_CONVERSION = YES; 215 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 216 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 217 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 218 | CLANG_WARN_EMPTY_BODY = YES; 219 | CLANG_WARN_ENUM_CONVERSION = YES; 220 | CLANG_WARN_INFINITE_RECURSION = YES; 221 | CLANG_WARN_INT_CONVERSION = YES; 222 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 223 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 224 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 225 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 226 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 227 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 228 | CLANG_WARN_STRICT_PROTOTYPES = YES; 229 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 230 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 231 | CLANG_WARN_UNREACHABLE_CODE = YES; 232 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 233 | COPY_PHASE_STRIP = NO; 234 | DEAD_CODE_STRIPPING = YES; 235 | DEBUG_INFORMATION_FORMAT = dwarf; 236 | ENABLE_HARDENED_RUNTIME = YES; 237 | ENABLE_STRICT_OBJC_MSGSEND = YES; 238 | ENABLE_TESTABILITY = YES; 239 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 240 | GCC_C_LANGUAGE_STANDARD = gnu11; 241 | GCC_DYNAMIC_NO_PIC = NO; 242 | GCC_NO_COMMON_BLOCKS = YES; 243 | GCC_OPTIMIZATION_LEVEL = 0; 244 | GCC_PREPROCESSOR_DEFINITIONS = ( 245 | "DEBUG=1", 246 | "$(inherited)", 247 | ); 248 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 249 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 250 | GCC_WARN_UNDECLARED_SELECTOR = YES; 251 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 252 | GCC_WARN_UNUSED_FUNCTION = YES; 253 | GCC_WARN_UNUSED_VARIABLE = YES; 254 | MACOSX_DEPLOYMENT_TARGET = 10.14; 255 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 256 | MTL_FAST_MATH = YES; 257 | ONLY_ACTIVE_ARCH = YES; 258 | SDKROOT = macosx; 259 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 260 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 261 | }; 262 | name = Debug; 263 | }; 264 | 73008263245308B600F4909F /* Release */ = { 265 | isa = XCBuildConfiguration; 266 | buildSettings = { 267 | ALWAYS_SEARCH_USER_PATHS = NO; 268 | CLANG_ANALYZER_NONNULL = YES; 269 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 270 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 271 | CLANG_CXX_LIBRARY = "libc++"; 272 | CLANG_ENABLE_MODULES = YES; 273 | CLANG_ENABLE_OBJC_ARC = YES; 274 | CLANG_ENABLE_OBJC_WEAK = YES; 275 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 276 | CLANG_WARN_BOOL_CONVERSION = YES; 277 | CLANG_WARN_COMMA = YES; 278 | CLANG_WARN_CONSTANT_CONVERSION = YES; 279 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 280 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 281 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 282 | CLANG_WARN_EMPTY_BODY = YES; 283 | CLANG_WARN_ENUM_CONVERSION = YES; 284 | CLANG_WARN_INFINITE_RECURSION = YES; 285 | CLANG_WARN_INT_CONVERSION = YES; 286 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 287 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 288 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 289 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 290 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 291 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 292 | CLANG_WARN_STRICT_PROTOTYPES = YES; 293 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 294 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 295 | CLANG_WARN_UNREACHABLE_CODE = YES; 296 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 297 | COPY_PHASE_STRIP = NO; 298 | DEAD_CODE_STRIPPING = YES; 299 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 300 | ENABLE_HARDENED_RUNTIME = YES; 301 | ENABLE_NS_ASSERTIONS = NO; 302 | ENABLE_STRICT_OBJC_MSGSEND = YES; 303 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 304 | GCC_C_LANGUAGE_STANDARD = gnu11; 305 | GCC_NO_COMMON_BLOCKS = YES; 306 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 307 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 308 | GCC_WARN_UNDECLARED_SELECTOR = YES; 309 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 310 | GCC_WARN_UNUSED_FUNCTION = YES; 311 | GCC_WARN_UNUSED_VARIABLE = YES; 312 | MACOSX_DEPLOYMENT_TARGET = 10.14; 313 | MTL_ENABLE_DEBUG_INFO = NO; 314 | MTL_FAST_MATH = YES; 315 | SDKROOT = macosx; 316 | SWIFT_COMPILATION_MODE = wholemodule; 317 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 318 | }; 319 | name = Release; 320 | }; 321 | 73008265245308B600F4909F /* Debug */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 325 | CODE_SIGN_ENTITLEMENTS = RecipeBuilder/RecipeBuilder.entitlements; 326 | CODE_SIGN_IDENTITY = "Apple Development"; 327 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 328 | CODE_SIGN_STYLE = Automatic; 329 | COMBINE_HIDPI_IMAGES = YES; 330 | DEAD_CODE_STRIPPING = YES; 331 | DEVELOPMENT_TEAM = F489D96499; 332 | ENABLE_HARDENED_RUNTIME = YES; 333 | INFOPLIST_FILE = "$(SRCROOT)/RecipeBuilder/Info.plist"; 334 | INFOPLIST_KEY_CFBundleDisplayName = RecipeBuilder; 335 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 336 | LD_RUNPATH_SEARCH_PATHS = ( 337 | "$(inherited)", 338 | "@executable_path/../Frameworks", 339 | ); 340 | MACOSX_DEPLOYMENT_TARGET = 11.0; 341 | MARKETING_VERSION = 1.05; 342 | PRODUCT_BUNDLE_IDENTIFIER = se.mikaellofgren.RecipeBuilder; 343 | PRODUCT_NAME = "$(TARGET_NAME)"; 344 | PROVISIONING_PROFILE_SPECIFIER = ""; 345 | SWIFT_VERSION = 5.0; 346 | }; 347 | name = Debug; 348 | }; 349 | 73008266245308B600F4909F /* Release */ = { 350 | isa = XCBuildConfiguration; 351 | buildSettings = { 352 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 353 | CODE_SIGN_ENTITLEMENTS = RecipeBuilder/RecipeBuilder.entitlements; 354 | CODE_SIGN_IDENTITY = "-"; 355 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 356 | CODE_SIGN_STYLE = Automatic; 357 | COMBINE_HIDPI_IMAGES = YES; 358 | DEAD_CODE_STRIPPING = YES; 359 | DEVELOPMENT_TEAM = F489D96499; 360 | ENABLE_HARDENED_RUNTIME = YES; 361 | INFOPLIST_FILE = "$(SRCROOT)/RecipeBuilder/Info.plist"; 362 | INFOPLIST_KEY_CFBundleDisplayName = RecipeBuilder; 363 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 364 | LD_RUNPATH_SEARCH_PATHS = ( 365 | "$(inherited)", 366 | "@executable_path/../Frameworks", 367 | ); 368 | MACOSX_DEPLOYMENT_TARGET = 11.0; 369 | MARKETING_VERSION = 1.05; 370 | PRODUCT_BUNDLE_IDENTIFIER = se.mikaellofgren.RecipeBuilder; 371 | PRODUCT_NAME = "$(TARGET_NAME)"; 372 | PROVISIONING_PROFILE_SPECIFIER = ""; 373 | SWIFT_VERSION = 5.0; 374 | }; 375 | name = Release; 376 | }; 377 | /* End XCBuildConfiguration section */ 378 | 379 | /* Begin XCConfigurationList section */ 380 | 73008251245308B100F4909F /* Build configuration list for PBXProject "RecipeBuilder" */ = { 381 | isa = XCConfigurationList; 382 | buildConfigurations = ( 383 | 73008262245308B600F4909F /* Debug */, 384 | 73008263245308B600F4909F /* Release */, 385 | ); 386 | defaultConfigurationIsVisible = 0; 387 | defaultConfigurationName = Release; 388 | }; 389 | 73008264245308B600F4909F /* Build configuration list for PBXNativeTarget "RecipeBuilder" */ = { 390 | isa = XCConfigurationList; 391 | buildConfigurations = ( 392 | 73008265245308B600F4909F /* Debug */, 393 | 73008266245308B600F4909F /* Release */, 394 | ); 395 | defaultConfigurationIsVisible = 0; 396 | defaultConfigurationName = Release; 397 | }; 398 | /* End XCConfigurationList section */ 399 | 400 | /* Begin XCRemoteSwiftPackageReference section */ 401 | 733DE0052D197D0D00F5CB2D /* XCRemoteSwiftPackageReference "Yams" */ = { 402 | isa = XCRemoteSwiftPackageReference; 403 | repositoryURL = "https://github.com/jpsim/Yams.git"; 404 | requirement = { 405 | kind = upToNextMajorVersion; 406 | minimumVersion = 5.1.3; 407 | }; 408 | }; 409 | 73631C0E24587182008A6491 /* XCRemoteSwiftPackageReference "Highlightr" */ = { 410 | isa = XCRemoteSwiftPackageReference; 411 | repositoryURL = "https://github.com/helje5/Highlightr.git"; 412 | requirement = { 413 | kind = upToNextMajorVersion; 414 | minimumVersion = 3.0.0; 415 | }; 416 | }; 417 | /* End XCRemoteSwiftPackageReference section */ 418 | 419 | /* Begin XCSwiftPackageProductDependency section */ 420 | 733DE0062D197D0D00F5CB2D /* Yams */ = { 421 | isa = XCSwiftPackageProductDependency; 422 | package = 733DE0052D197D0D00F5CB2D /* XCRemoteSwiftPackageReference "Yams" */; 423 | productName = Yams; 424 | }; 425 | 73631C0F24587182008A6491 /* Highlightr */ = { 426 | isa = XCSwiftPackageProductDependency; 427 | package = 73631C0E24587182008A6491 /* XCRemoteSwiftPackageReference "Highlightr" */; 428 | productName = Highlightr; 429 | }; 430 | /* End XCSwiftPackageProductDependency section */ 431 | }; 432 | rootObject = 7300824E245308B100F4909F /* Project object */; 433 | } 434 | -------------------------------------------------------------------------------- /RecipeBuilder.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RecipeBuilder.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RecipeBuilder.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RecipeBuilder.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "d6664d9d6d3791ee76cab980eddff700b5ba91415e6a27a99334e67f0c0af1e6", 3 | "pins" : [ 4 | { 5 | "identity" : "highlightr", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/helje5/Highlightr.git", 8 | "state" : { 9 | "revision" : "628094ff81cdebf31da6bf31be4b4ac758c127a4", 10 | "version" : "3.0.0" 11 | } 12 | }, 13 | { 14 | "identity" : "yams", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/jpsim/Yams.git", 17 | "state" : { 18 | "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d", 19 | "version" : "5.1.3" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /RecipeBuilder.xcodeproj/project.xcworkspace/xcuserdata/mikael.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder.xcodeproj/project.xcworkspace/xcuserdata/mikael.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /RecipeBuilder.xcodeproj/project.xcworkspace/xcuserdata/mikael.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | IssueFilterStyle 12 | ShowActiveSchemeOnly 13 | LiveSourceIssuesEnabled 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /RecipeBuilder.xcodeproj/xcshareddata/xcschemes/RecipeBuilder.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 44 | 46 | 52 | 53 | 54 | 55 | 61 | 63 | 69 | 70 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /RecipeBuilder.xcodeproj/xcuserdata/mikael.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /RecipeBuilder.xcodeproj/xcuserdata/mikael.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | RecipeBuilder.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | autopkgRecipeBuilder.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 73008255245308B100F4909F 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /RecipeBuilder/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // 4 | // Created by Mikael Löfgren on 2024-12-27 5 | // Copyright © 2024 Mikael Löfgren. All rights reserved. 6 | // 7 | 8 | import Cocoa 9 | import AppKit 10 | import Highlightr 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { 13 | var searchSelectedRecipe = "" 14 | var saveAndOpenExternalEditor = "false" 15 | var selectedExternalEditor = "BBEdit" 16 | var trustInfo = "" 17 | 18 | var recipeDirectlyFileName: String = "" 19 | 20 | @IBOutlet weak var window: NSWindow! 21 | 22 | @IBOutlet var processorsView: NSView! 23 | @IBOutlet var processorsView3rdParty: NSView! 24 | @IBOutlet weak var buttonView: NSView! 25 | @IBOutlet var recipeIdentifierTextField: NSTextField! 26 | @IBOutlet var appPKGTextField: NSTextField! 27 | @IBOutlet var outputTextField: NSTextView! 28 | @IBOutlet var recipeFormatPopup: NSPopUpButton! 29 | @IBOutlet var openFile: NSMenuItem! 30 | @IBOutlet var saveDocument: NSMenuItem! 31 | @IBOutlet var newDocument: NSMenuItem! 32 | @IBOutlet var matchingRecipes: NSPopUpButton! 33 | @IBOutlet var searchField: NSSearchField! 34 | @IBOutlet var fileOptions: NSPopUpButton! 35 | @IBOutlet var finalizeMenuItem: NSMenuItem! 36 | @IBOutlet var trustInfoTrue: NSMenuItem! 37 | @IBOutlet var trustInfoFalse: NSMenuItem! 38 | @IBOutlet var bbedit: NSMenuItem! 39 | @IBOutlet var sublimeText: NSMenuItem! 40 | @IBOutlet var textMate: NSMenuItem! 41 | @IBOutlet var visualStudioCode: NSMenuItem! 42 | 43 | @IBOutlet var modeSwitch: NSSwitch! 44 | @IBOutlet var spinner: NSProgressIndicator! 45 | @IBOutlet var logWindow: NSPanel! 46 | @IBOutlet var logTextView: NSTextView! 47 | @IBOutlet var helpPopover: NSPopover! 48 | @IBOutlet var helpPopoverText: NSTextView! 49 | @IBOutlet weak var enableAndReloadButton: NSButton! 50 | 51 | @IBAction func getIdentifierTextValue(_ sender: NSTextField) { 52 | getIdentifierTextFieldsValues () 53 | } 54 | 55 | @IBAction func setAppPKGvalue(_ sender: NSTextField) { 56 | getAppPkgTextFieldsValues () 57 | } 58 | 59 | @IBAction func startSearch(_ sender: NSSearchField) { 60 | searchString = sender.stringValue 61 | 62 | if searchString == "" { 63 | appDelegate().matchingRecipes.removeAllItems() 64 | appDelegate().matchingRecipes.addItem(withTitle: "Matching recipes") 65 | appDelegate().matchingRecipes.selectItem(at: 0) 66 | return 67 | } else { 68 | getAutopkgPlistValues () 69 | appDelegate().matchingRecipes.removeAllItems() 70 | appDelegate().matchingRecipes.addItem(withTitle: "Searching...") 71 | appDelegate().matchingRecipes.selectItem(at: 0) 72 | appDelegate().spinner.isHidden=false 73 | self.spinner.startAnimation(self) 74 | } 75 | 76 | } 77 | 78 | @IBAction func selectedRecipe(_ sender: NSPopUpButton) { 79 | searchSelectedRecipe = matchingRecipes.titleOfSelectedItem! 80 | let searchSelectedFileURL = URL(fileURLWithPath: appDelegate().searchSelectedRecipe) 81 | openFileInExternalEditor(fileURL: searchSelectedFileURL) 82 | } 83 | 84 | @IBAction func newDocumentAction(_ sender: NSMenuItem) { 85 | createNewDocument () 86 | } 87 | 88 | @IBAction func openFileAction(_ sender: NSMenuItem) { 89 | openRecipe () 90 | } 91 | 92 | func application(_ sender: NSApplication, openFile recipeDirectlyFileName: String) -> Bool { 93 | self.recipeDirectlyFileName = recipeDirectlyFileName 94 | openRecipeDirectly () 95 | return true 96 | } 97 | 98 | @IBAction func saveDocumentAction(_ sender: NSMenuItem) { 99 | saveRecipe () 100 | } 101 | 102 | @IBAction func finalizeDoc(_ sender: NSMenuItem) { 103 | finalizeDocument () 104 | } 105 | 106 | @IBAction func verifyRecipes(_ sender: NSMenuItem) { 107 | startCheckAndVerify () 108 | } 109 | 110 | @IBAction func autopkgRun(_ sender: NSMenuItem) { 111 | appDelegate().spinner.isHidden=false 112 | self.spinner.startAnimation(self) 113 | autoPkgRunner () 114 | } 115 | 116 | @IBAction func openAutoPkgCacheFolder(_ sender: NSMenuItem) { 117 | openAutoPkgCache () 118 | } 119 | 120 | 121 | @IBAction func openUserButtonsFolder(_ sender: NSMenuItem) { 122 | openUserButtons () 123 | } 124 | 125 | @IBAction func trustInfoTrue(_ sender: NSMenuItem) { 126 | setTrustInfo(to: true) 127 | trustInfo = sender.title 128 | toggleTrustInfo() 129 | } 130 | 131 | @IBAction func trustInfoFalse(_ sender: NSMenuItem) { 132 | setTrustInfo(to: false) 133 | trustInfo = sender.title.lowercased() 134 | toggleTrustInfo() 135 | } 136 | 137 | @IBAction func batchXMLtoYaml(_ sender: NSMenuItem) { 138 | selectFolderAndProcessRecipesToYaml() 139 | } 140 | 141 | @IBAction func batchYamltoXML(_ sender: NSMenuItem) { 142 | selectFolderAndProcessRecipesToXML() 143 | } 144 | 145 | @IBAction func openExternalBBEdit(_ sender: NSMenuItem) { 146 | saveAndOpenExternalEditor = "true" 147 | selectedExternalEditor = sender.title 148 | toggleExternalEditor () 149 | saveRecipe () 150 | } 151 | 152 | @IBAction func openExternalSublime(_ sender: NSMenuItem) { 153 | saveAndOpenExternalEditor = "true" 154 | selectedExternalEditor = sender.title 155 | toggleExternalEditor () 156 | saveRecipe () 157 | } 158 | 159 | @IBAction func openExternalTextMate(_ sender: NSMenuItem) { 160 | saveAndOpenExternalEditor = "true" 161 | selectedExternalEditor = sender.title 162 | toggleExternalEditor () 163 | saveRecipe () 164 | } 165 | 166 | @IBAction func openExternalVisualStudioCode (_ sender: NSMenuItem) { 167 | saveAndOpenExternalEditor = "true" 168 | selectedExternalEditor = sender.title 169 | toggleExternalEditor () 170 | saveRecipe () 171 | } 172 | 173 | @IBAction func modeSwitch(_ sender: Any) { 174 | yamlModeStatusSwitcher () 175 | } 176 | 177 | @objc func appDMGVersionerAction (sender: NSButton) { 178 | appDMGVersioner () 179 | helpPopover.close() 180 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 181 | writePopOvertext(processor: "AppDmgVersioner", extraHelpText: "Name of app found at the root of the disk image. This does not search recursively for a matching app. If you need to specify a path, use Versioner instead.") 182 | } 183 | 184 | @objc func appPkgCreatorAction (sender: NSButton) { 185 | appPkgCreator () 186 | helpPopover.close() 187 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 188 | writePopOvertext(processor: "AppPkgCreator", extraHelpText: """ 189 | This is defaults values that can be removed, unless you want another paths 190 | Arguments 191 | 192 | pkg_path 193 | %RECIPE_CACHE_DIR%/%app_name%-%version%.pkg 194 | app_path 195 | %RECIPE_CACHE_DIR%/%pathname%/*.app 196 | 197 | 198 | Often used in pkg.recipe 199 | """) 200 | } 201 | 202 | @objc func codeSignatureVerifierAppAction(sender: NSButton) { 203 | codeSignatureVerifierApp () 204 | helpPopover.close() 205 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 206 | writePopOvertext(processor: "CodeSignatureVerifier", extraHelpText: "Used with download.recipes") 207 | } 208 | 209 | @objc func codeSignatureVerifierPkgAction(sender: NSButton) { 210 | codeSignatureVerifierPKG () 211 | helpPopover.close() 212 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 213 | writePopOvertext(processor: "CodeSignatureVerifier", extraHelpText: "Used with download.recipes") 214 | } 215 | 216 | @objc func copierAction(sender: NSButton) { 217 | copier () 218 | helpPopover.close() 219 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 220 | writePopOvertext(processor: "Copier", extraHelpText: "") 221 | } 222 | 223 | @objc func deprecationWarningAction(sender: NSButton) { 224 | deprecationWarning () 225 | helpPopover.close() 226 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 227 | writePopOvertext(processor: "DeprecationWarning", extraHelpText: "This processor outputs a warning that the recipe has been deprecated.") 228 | } 229 | 230 | @objc func dmgCreatorAction(sender: NSButton) { 231 | dmgCreator () 232 | helpPopover.close() 233 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 234 | writePopOvertext(processor: "DmgCreator", extraHelpText: "") 235 | } 236 | 237 | @objc func endOfCheckPhaseAction(sender: NSButton) { 238 | endOfCheckPhase () 239 | helpPopover.close() 240 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 241 | writePopOvertext(processor: "EndOfCheckPhase", extraHelpText: "Recommended to be used directly after processsor URLDownloader") 242 | } 243 | 244 | @objc func fileCreatorAction(sender: NSButton) { 245 | fileCreator () 246 | helpPopover.close() 247 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 248 | writePopOvertext(processor: "FileCreator", extraHelpText: "") 249 | } 250 | 251 | @objc func fileFinderAction(sender: NSButton) { 252 | fileFinder () 253 | helpPopover.close() 254 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 255 | writePopOvertext(processor: "FileFinder", extraHelpText: "") 256 | } 257 | 258 | @objc func fileMoverAction(sender: NSButton) { 259 | fileMover () 260 | helpPopover.close() 261 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 262 | writePopOvertext(processor: "FileMover", extraHelpText: "") 263 | } 264 | 265 | @objc func flatPkgPackerAction(sender: NSButton) { 266 | flatPkgPacker () 267 | helpPopover.close() 268 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 269 | writePopOvertext(processor: "FlatPkgPacker", extraHelpText: "") 270 | } 271 | 272 | @objc func flatPkgUnpackerAction(sender: NSButton) { 273 | flatPkgUnpacker () 274 | helpPopover.close() 275 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 276 | writePopOvertext(processor: "FlatPkgUnpacker", extraHelpText: "") 277 | } 278 | 279 | @objc func gitHubReleasesInfoProviderAction(sender: NSButton) { 280 | gitHubReleasesInfoProvider () 281 | helpPopover.close() 282 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 283 | writePopOvertext(processor: "GitHubReleasesInfoProvider", extraHelpText: """ 284 | Add as Input key to use the %GITHUB_REPO% variable 285 | GITHUB_REPO 286 | MagerValp/AutoDMG 287 | """) 288 | } 289 | 290 | @objc func installerAction(sender: NSButton) { 291 | installer () 292 | helpPopover.close() 293 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 294 | writePopOvertext(processor: "Installer", extraHelpText: "Used with install.recipes") 295 | } 296 | 297 | @objc func installFromDMGAction(sender: NSButton) { 298 | installFromDMG () 299 | helpPopover.close() 300 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 301 | writePopOvertext(processor: "InstallFromDMG", extraHelpText: "Used with install.recipes") 302 | } 303 | 304 | @objc func munkiCatalogBuilderAction(sender: NSButton) { 305 | munkiCatalogBuilder () 306 | helpPopover.close() 307 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 308 | writePopOvertext(processor: "MunkiCatalogBuilder", extraHelpText: "") 309 | } 310 | 311 | @objc func munkiImporterAction(sender: NSButton) { 312 | munkiImporter () 313 | helpPopover.close() 314 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 315 | writePopOvertext(processor: "MunkiImporter", extraHelpText: "") 316 | } 317 | 318 | @objc func munkiInfoCreatorAction(sender: NSButton) { 319 | munkiInfoCreator () 320 | helpPopover.close() 321 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 322 | writePopOvertext(processor: "MunkiInfoCreator", extraHelpText: "") 323 | } 324 | 325 | @objc func MunkiInstallsItemsCreatorAction(sender: NSButton) { 326 | munkiInstallsItemsCreator () 327 | helpPopover.close() 328 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 329 | writePopOvertext(processor: "MunkiInstallsItemsCreator", extraHelpText: "The faux_root path must include the installs_items_path %RECIPE_CACHE_DIR%/Applications/%NAME%.app. MunkiPkginfoMerger needed afterwards") 330 | } 331 | 332 | @objc func munkiPkginfoMergerAction(sender: NSButton) { 333 | munkiPkginfoMerger () 334 | helpPopover.close() 335 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 336 | writePopOvertext(processor: "MunkiPkginfoMerger", extraHelpText: "") 337 | } 338 | 339 | @objc func packageRequiredAction(sender: NSButton) { 340 | packageRequired () 341 | helpPopover.close() 342 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 343 | writePopOvertext(processor: "PackageRequired", extraHelpText: "") 344 | } 345 | 346 | @objc func pathDeleterAction(sender: NSButton) { 347 | pathDeleter () 348 | helpPopover.close() 349 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 350 | writePopOvertext(processor: "PathDeleter", extraHelpText: "") 351 | } 352 | 353 | @objc func pkgCopierAction(sender: NSButton) { 354 | pkgCopier () 355 | helpPopover.close() 356 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 357 | writePopOvertext(processor: "PkgCopier", extraHelpText: "") 358 | } 359 | 360 | @objc func pkgCreatorAction(sender: NSButton) { 361 | pkgCreator () 362 | helpPopover.close() 363 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 364 | writePopOvertext(processor: "PkgCreator", extraHelpText: "Add your reverse domain for the id key") 365 | } 366 | 367 | @objc func pkgExtractorAction(sender: NSButton) { 368 | pkgExtractor () 369 | helpPopover.close() 370 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 371 | writePopOvertext(processor: "PkgExtractor", extraHelpText: """ 372 | You probably should use FlatPkgUnpacker instead. 373 | Bundle-style pkg is the old pkg format and rare. 374 | """) 375 | } 376 | 377 | @objc func pkgInfoCreatorAction(sender: NSButton) { 378 | pkgInfoCreator () 379 | helpPopover.close() 380 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 381 | writePopOvertext(processor: "PkgInfoCreator", extraHelpText: """ 382 | pkgroot and version are required, but likely they are set earlier from another processor like PkgRootCreator 383 | """) 384 | } 385 | 386 | @objc func pkgPayloadUnpackerAction(sender: NSButton) { 387 | pkgPayloadUnpacker () 388 | helpPopover.close() 389 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 390 | writePopOvertext(processor: "PkgPayloadUnpacker", extraHelpText: "") 391 | } 392 | 393 | @objc func pkgRootCreatorAction(sender: NSButton) { 394 | pkgRootCreator () 395 | helpPopover.close() 396 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 397 | writePopOvertext(processor: "PkgRootCreator", extraHelpText: "Deletes whole directory don't use %RECIPE_CACHE_DIR% instead use %RECIPE_CACHE_DIR%/%NAME%") 398 | } 399 | 400 | @objc func plistEditorAction(sender: NSButton) { 401 | plistEditor () 402 | helpPopover.close() 403 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 404 | writePopOvertext(processor: "PlistEditor", extraHelpText: "") 405 | } 406 | 407 | @objc func plistReaderAction(sender: NSButton) { 408 | plistReader () 409 | helpPopover.close() 410 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 411 | writePopOvertext(processor: "PlistReader", extraHelpText: "") 412 | } 413 | 414 | @objc func sparkleUpdateInfoProviderAction(sender: NSButton) { 415 | sparkleUpdateInfoProvider () 416 | helpPopover.close() 417 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 418 | writePopOvertext(processor: "SparkleUpdateInfoProvider", extraHelpText: """ 419 | Use before URLDownloader to get the %DOWNLOAD_URL% thats passed to URLDownloader 420 | Replace the input key 421 | DOWNLOAD_URL 422 | with 423 | SPARKLE_FEED_URL 424 | and provide the the Sparkle feed URL as the string instead 425 | """) 426 | } 427 | 428 | @objc func stopProcessingIfAction(sender: NSButton) { 429 | stopProcessingIf () 430 | helpPopover.close() 431 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 432 | writePopOvertext(processor: "StopProcessingIf", extraHelpText: "") 433 | } 434 | 435 | @objc func symlinkerAction(sender: NSButton) { 436 | symlinker () 437 | helpPopover.close() 438 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 439 | writePopOvertext(processor: "Symlinker", extraHelpText: "") 440 | } 441 | 442 | @objc func unarchiverAction(sender: NSButton) { 443 | unarchiver () 444 | helpPopover.close() 445 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 446 | writePopOvertext(processor: "Unarchiver", extraHelpText: "") 447 | } 448 | 449 | @objc func urlDownloaderAction(sender: NSButton) { 450 | urlDownloader () 451 | helpPopover.close() 452 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 453 | writePopOvertext(processor: "URLDownloader", extraHelpText: "") 454 | } 455 | 456 | @objc func urlTextSearcherAction(sender: NSButton) { 457 | urlTextSearcher () 458 | helpPopover.close() 459 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 460 | writePopOvertext(processor: "URLTextSearcher", extraHelpText: "Often used before URLDownloader get the \"real\" download URL thats passed to URLDownloader") 461 | } 462 | 463 | @objc func versionerAction(sender: NSButton) { 464 | versioner () 465 | helpPopover.close() 466 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 467 | writePopOvertext(processor: "Versioner", extraHelpText: """ 468 | Remove 469 | plist_version_key 470 | CFBundleVersion 471 | if you only want CFBundleShortVersionString 472 | """) 473 | } 474 | 475 | // 3rd party buttons 476 | @objc func JamfAccountUploaderAction(sender: NSButton) { 477 | JamfAccountUploader() 478 | helpPopover.close() 479 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 480 | writePopOvertextJamfUploader(processor: "JamfAccountUploader", extraHelpText: JamfAccountUploaderHelp) 481 | } 482 | 483 | @objc func JamfCategoryUploaderAction(sender: NSButton) { 484 | JamfCategoryUploader () 485 | helpPopover.close() 486 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 487 | writePopOvertextJamfUploader(processor: "JamfCategoryUploader", extraHelpText: JamfCategoryUploaderHelp) 488 | //writePopOvertext(processor: "", extraHelpText: JamfCategoryUploaderHelp) 489 | } 490 | 491 | @objc func JamfClassicAPIObjectUploaderAction(sender: NSButton) { 492 | JamfClassicAPIObjectUploader() 493 | helpPopover.close() 494 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 495 | writePopOvertextJamfUploader(processor: "JamfClassicAPIObjectUploader", extraHelpText: JamfClassicAPIObjectUploaderHelp) 496 | } 497 | 498 | @objc func JamfComputerGroupDeleterAction(sender: NSButton) { 499 | JamfComputerGroupDeleter() 500 | helpPopover.close() 501 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 502 | writePopOvertextJamfUploader(processor: "JamfComputerGroupDeleter", extraHelpText: JamfComputerGroupDeleterHelp) 503 | } 504 | 505 | @objc func JamfComputerGroupUploaderAction(sender: NSButton) { 506 | JamfComputerGroupUploader () 507 | helpPopover.close() 508 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 509 | writePopOvertextJamfUploader(processor: "JamfComputerGroupUploader", extraHelpText: JamfComputerGroupUploaderHelp) 510 | //writePopOvertext(processor: "", extraHelpText: JamfComputerGroupUploaderHelp) 511 | } 512 | 513 | @objc func JamfComputerProfileUploaderAction(sender: NSButton) { 514 | JamfComputerProfileUploader () 515 | helpPopover.close() 516 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 517 | writePopOvertextJamfUploader(processor: "JamfComputerProfileUploader", extraHelpText: JamfComputerProfileUploaderHelp) 518 | //writePopOvertext(processor: "", extraHelpText: JamfComputerProfileUploaderHelp) 519 | } 520 | 521 | @objc func JamfDockItemUploaderAction(sender: NSButton) { 522 | JamfDockItemUploader () 523 | helpPopover.close() 524 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 525 | writePopOvertextJamfUploader(processor: "JamfDockItemUploader", extraHelpText: JamfDockItemUploaderHelp) 526 | //writePopOvertext(processor: "", extraHelpText: JamfDockItemUploaderHelp) 527 | } 528 | 529 | @objc func JamfExtensionAttributeUploaderAction(sender: NSButton) { 530 | JamfExtensionAttributeUploader () 531 | helpPopover.close() 532 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 533 | writePopOvertextJamfUploader(processor: "JamfExtensionAttributeUploader", extraHelpText: JamfExtensionAttributeUploaderHelp) 534 | //writePopOvertext(processor: "", extraHelpText: JamfExtensionAttributeUploaderHelp) 535 | } 536 | 537 | @objc func JamfIconUploaderAction(sender: NSButton) { 538 | JamfIconUploader() 539 | helpPopover.close() 540 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 541 | writePopOvertextJamfUploader(processor: "JamfIconUploader", extraHelpText: JamfIconUploaderHelp) 542 | } 543 | 544 | @objc func JamfMacAppUploaderAction(sender: NSButton) { 545 | JamfMacAppUploader () 546 | helpPopover.close() 547 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 548 | writePopOvertextJamfUploader(processor: "JamfMacAppUploader", extraHelpText: JamfMacAppUploaderHelp) 549 | } 550 | 551 | @objc func JamfMobileDeviceGroupUploaderAction(sender: NSButton) { 552 | JamfMobileDeviceGroupUploader() 553 | helpPopover.close() 554 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 555 | writePopOvertextJamfUploader(processor: "JamfMobileDeviceGroupUploader", extraHelpText: JamfMobileDeviceGroupUploaderHelp) 556 | } 557 | 558 | @objc func JamfMobileDeviceProfileUploaderAction(sender: NSButton) { 559 | JamfMobileDeviceProfileUploader() 560 | helpPopover.close() 561 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 562 | writePopOvertextJamfUploader(processor: "JamfMobileDeviceProfileUploader", extraHelpText: JamfMobileDeviceProfileUploaderHelp) 563 | } 564 | 565 | @objc func JamfPackageCleanerAction(sender: NSButton) { 566 | JamfPackageCleaner() 567 | helpPopover.close() 568 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 569 | writePopOvertextJamfUploader(processor: "JamfPackageCleaner", extraHelpText: JamfPackageCleanerHelp) 570 | } 571 | 572 | @objc func JamfPackageRecalculatorAction(sender: NSButton) { 573 | JamfPackageRecalculator() 574 | helpPopover.close() 575 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 576 | writePopOvertextJamfUploader(processor: "JamfPackageRecalculator", extraHelpText: JamfPackageRecalculatorHelp) 577 | } 578 | 579 | @objc func JamfPackageUploaderAction(sender: NSButton) { 580 | JamfPackageUploader () 581 | helpPopover.close() 582 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 583 | writePopOvertextJamfUploader(processor: "JamfPackageUploader", extraHelpText: JamfPackageUploaderHelp) 584 | //writePopOvertext(processor: "", extraHelpText: JamfPackageUploaderHelp) 585 | } 586 | 587 | @objc func JamfPatchCheckerAction(sender: NSButton) { 588 | JamfPatchChecker() 589 | helpPopover.close() 590 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 591 | writePopOvertextJamfUploader(processor: "JamfPatchChecker", extraHelpText: JamfPatchCheckerHelp) 592 | } 593 | 594 | @objc func JamfPatchUploaderAction(sender: NSButton) { 595 | JamfPatchUploader () 596 | helpPopover.close() 597 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 598 | writePopOvertextJamfUploader(processor: "JamfPatchUploader", extraHelpText: JamfPatchUploaderHelp) 599 | //writePopOvertext(processor: "", extraHelpText: JamfPatchUploaderHelp) 600 | } 601 | 602 | @objc func JamfPkgMetadataUploaderAction(sender: NSButton) { 603 | JamfPkgMetadataUploader() 604 | helpPopover.close() 605 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 606 | writePopOvertextJamfUploader(processor: "JamfPkgMetadataUploader", extraHelpText: JamfPkgMetadataUploaderHelp) 607 | } 608 | 609 | @objc func JamfPolicyDeleterAction(sender: NSButton) { 610 | JamfPolicyDeleter () 611 | helpPopover.close() 612 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 613 | writePopOvertextJamfUploader(processor: "JamfPolicyDeleter", extraHelpText: JamfPolicyDeleterHelp) 614 | //writePopOvertext(processor: "", extraHelpText: JamfPolicyDeleterHelp) 615 | } 616 | 617 | @objc func JamfPolicyLogFlusherAction(sender: NSButton) { 618 | JamfPolicyLogFlusher () 619 | helpPopover.close() 620 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 621 | writePopOvertextJamfUploader(processor: "JamfPolicyLogFlusher", extraHelpText: JamfPolicyLogFlusherHelp) 622 | //writePopOvertext(processor: "", extraHelpText: JamfPolicyLogFlusherHelp) 623 | } 624 | 625 | @objc func JamfPolicyUploaderAction(sender: NSButton) { 626 | JamfPolicyUploader () 627 | helpPopover.close() 628 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 629 | writePopOvertextJamfUploader(processor: "JamfPolicyUploader", extraHelpText: JamfPolicyUploaderHelp) 630 | //writePopOvertext(processor: "", extraHelpText: JamfPolicyUploaderHelp) 631 | } 632 | 633 | @objc func JamfScriptUploaderAction(sender: NSButton) { 634 | JamfScriptUploader () 635 | helpPopover.close() 636 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 637 | writePopOvertextJamfUploader(processor: "JamfScriptUploader", extraHelpText: JamfScriptUploaderHelp) 638 | //writePopOvertext(processor: "", extraHelpText: JamfScriptUploaderHelp) 639 | } 640 | 641 | @objc func JamfSoftwareRestrictionUploaderAction(sender: NSButton) { 642 | JamfSoftwareRestrictionUploader () 643 | helpPopover.close() 644 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 645 | writePopOvertextJamfUploader(processor: "JamfSoftwareRestrictionUploader", extraHelpText: JamfSoftwareRestrictionUploaderHelp) 646 | //writePopOvertext(processor: "", extraHelpText: JamfSoftwareRestrictionUploaderHelp) 647 | } 648 | 649 | @objc func JamfUploaderSlackerAction(sender: NSButton) { 650 | JamfUploaderSlacker () 651 | helpPopover.close() 652 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 653 | writePopOvertextJamfUploader(processor: "JamfUploaderSlacker", extraHelpText: JamfUploaderSlackerHelp) 654 | //writePopOvertext(processor: "", extraHelpText: JamfUploaderSlackerHelp) 655 | } 656 | 657 | @objc func JamfUploaderTeamsNotifierAction(sender: NSButton) { 658 | JamfUploaderTeamsNotifier () 659 | helpPopover.close() 660 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 661 | writePopOvertextJamfUploader(processor: "JamfUploaderTeamsNotifier", extraHelpText: JamfUploaderTeamsNotifierHelp) 662 | //writePopOvertext(processor: "", extraHelpText: JamfUploaderTeamsNotifierHelp) 663 | } 664 | 665 | 666 | @IBAction func enableAndReloadAction(_ sender: NSButton) { 667 | getAllUserButtons () 668 | } 669 | 670 | @objc func buttonAction1(sender: NSButton!) { 671 | output = output1 672 | writeOutputUserButtons () 673 | helpPopover.close() 674 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 675 | writePopOvertextUserButtons (helpText: help1) 676 | } 677 | 678 | @objc func buttonAction2(sender: NSButton!) { 679 | output = output2 680 | writeOutputUserButtons () 681 | helpPopover.close() 682 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 683 | writePopOvertextUserButtons (helpText: help2) 684 | } 685 | 686 | @objc func buttonAction3(sender: NSButton!) { 687 | output = output3 688 | writeOutputUserButtons () 689 | helpPopover.close() 690 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 691 | writePopOvertextUserButtons (helpText: help3) 692 | } 693 | 694 | @objc func buttonAction4(sender: NSButton!) { 695 | output = output4 696 | writeOutputUserButtons () 697 | helpPopover.close() 698 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 699 | writePopOvertextUserButtons (helpText: help4) 700 | } 701 | 702 | @objc func buttonAction5(sender: NSButton!) { 703 | output = output5 704 | writeOutputUserButtons () 705 | helpPopover.close() 706 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 707 | writePopOvertextUserButtons (helpText: help5) 708 | } 709 | 710 | @objc func buttonAction6(sender: NSButton!) { 711 | output = output6 712 | writeOutputUserButtons () 713 | helpPopover.close() 714 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 715 | writePopOvertextUserButtons (helpText: help6) 716 | } 717 | 718 | @objc func buttonAction7(sender: NSButton!) { 719 | output = output7 720 | writeOutputUserButtons () 721 | helpPopover.close() 722 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 723 | writePopOvertextUserButtons (helpText: help7) 724 | } 725 | 726 | @objc func buttonAction8(sender: NSButton!) { 727 | output = output8 728 | writeOutputUserButtons () 729 | helpPopover.close() 730 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 731 | writePopOvertextUserButtons (helpText: help8) 732 | } 733 | 734 | @objc func buttonAction9(sender: NSButton!) { 735 | output = output9 736 | writeOutputUserButtons () 737 | helpPopover.close() 738 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 739 | writePopOvertextUserButtons (helpText: help9) 740 | 741 | } 742 | 743 | @objc func buttonAction10(sender: NSButton!) { 744 | output = output10 745 | writeOutputUserButtons () 746 | helpPopover.close() 747 | helpPopover.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxX) 748 | writePopOvertextUserButtons (helpText: help10) 749 | } 750 | 751 | func showHelpPopoverinTextField() { 752 | guard let textField = outputTextField else { return } 753 | let textFieldFrame = textField.window?.convertToScreen(textField.convert(textField.bounds, to: nil)) ?? .zero 754 | // Show the popover anchored to the text field 755 | // Use the full bounds of the text field for the anchor 756 | let anchorRect = NSRect( 757 | x: textFieldFrame.origin.x - 1000, // Align to the left edge 758 | y: textFieldFrame.origin.y - 1320, // Optional vertical offset 759 | width: textFieldFrame.width, // Full width of the text field 760 | height: textFieldFrame.height // Full height of the text field 761 | ) 762 | 763 | 764 | // Set up the popover text 765 | helpPopoverText.string = """ 766 | Getting Started 767 | 1. Set Up Your New Recipe 768 | Enter the Identifier, Recipe Format 769 | and App/Package Name in the respective fields. 770 | 771 | 2. Create the New Recipe: 772 | Select File > New (Command-N) 773 | 774 | 3. Choose Your Format: 775 | Use the XML/Yaml switch to toggle between the two supported formats.(Command-Y) 776 | 777 | ----- 778 | To Open an existing Recipe file 779 | Select File > Open 780 | """ 781 | let attributedText = [ NSAttributedString.Key.font: NSFont(name: "Menlo", size: 12.0)! ] 782 | let processorInfoAttributed = NSAttributedString(string: helpPopoverText.string, attributes: attributedText) 783 | appDelegate().helpPopoverText.string = "" 784 | appDelegate().helpPopoverText.textStorage?.insert(NSAttributedString(attributedString: processorInfoAttributed), at: 0) 785 | helpPopover.show(relativeTo: anchorRect, of: textField, preferredEdge: .maxY) 786 | //maxY: Displays the popover below the rectangle, with the arrow pointing upwards. 787 | //.minY: Displays the popover above the rectangle, with the arrow pointing downwards. 788 | //.maxX = Displays the popover to left 789 | // minX: Displays the popover to the left 790 | } 791 | 792 | func applicationDidFinishLaunching(_ aNotification: Notification) { 793 | outputTextField.isAutomaticQuoteSubstitutionEnabled = false 794 | outputTextField.font = NSFont(name: "Menlo", size: 12) 795 | createAllDefaultButtons () 796 | createAll3rdPartyButtons () 797 | checkThatAutopkgExist () 798 | checkForUserButtonsAndEnable () 799 | showHelpPopoverinTextField() 800 | getTrustInfo() 801 | } 802 | 803 | 804 | func applicationWillTerminate(_ aNotification: Notification) { 805 | // Insert code here to tear down your application 806 | 807 | } 808 | 809 | } 810 | -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "RecipeBuilder_icon-16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "RecipeBuilder_icon-33.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "RecipeBuilder_icon-32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "RecipeBuilder_icon-64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "RecipeBuilder_icon-128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "RecipeBuilder_icon-256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "RecipeBuilder_icon-257.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "RecipeBuilder_icon-512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "RecipeBuilder_icon-513.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "RecipeBuilder_icon-1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-1024.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-128.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-16.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-256.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-257.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-257.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-32.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-33.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-512.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-513.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-513.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/AppIcon.appiconset/RecipeBuilder_icon-64.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/Warning.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_128x128.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/Warning.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/Warning.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_16x16.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/Warning.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/Warning.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_256x256.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/Warning.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/Warning.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_32x32.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/Warning.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/Warning.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_512x512.png -------------------------------------------------------------------------------- /RecipeBuilder/Assets.xcassets/Warning.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/RecipeBuilder/Assets.xcassets/Warning.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /RecipeBuilder/Buttons3rdParty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Buttons3rdParty.swift 3 | // 4 | // Created by Mikael Löfgren on 2024-12-27 5 | // Copyright © 2024 Mikael Löfgren. All rights reserved. 6 | // 7 | 8 | import Cocoa 9 | import AppKit 10 | import Foundation 11 | import Highlightr 12 | 13 | func createAll3rdPartyButtons () { 14 | createButtonJamfAccountUploader() 15 | createButtonJamfCategoryUploader() 16 | createButtonJamfClassicAPIObjectUploader() 17 | createButtonJamfComputerGroupDeleter() 18 | createButtonJamfComputerGroupUploader() 19 | createButtonJamfComputerProfileUploader() 20 | createButtonJamfDockItemUploader() 21 | createButtonJamfExtensionAttributeUploader() 22 | createButtonJamfIconUploader() 23 | createButtonJamfMacAppUploader() 24 | createButtonJamfMobileDeviceGroupUploader() 25 | createButtonJamfMobileDeviceProfileUploader() 26 | createButtonJamfPackageCleaner() 27 | createButtonJamfPackageRecalculator() 28 | createButtonJamfPackageUploader() 29 | createButtonJamfPatchChecker() 30 | createButtonJamfPatchUploader() 31 | createButtonJamfPkgMetadataUploader() 32 | createButtonJamfPolicyDeleter() 33 | createButtonJamfPolicyLogFlusher() 34 | createButtonJamfPolicyUploader() 35 | createButtonJamfScriptUploader() 36 | createButtonJamfSoftwareRestrictionUploader() 37 | createButtonJamfUploaderSlacker() 38 | createButtonJamfUploaderTeamsNotifier() 39 | } 40 | 41 | // Functions to create the buttons 42 | // 21 pixels between every button 43 | // https://github.com/autopkg/grahampugh-recipes/blob/main/JamfUploaderProcessors/READMEs/JamfCategoryUploader.md 44 | func createButtonJamfAccountUploader() { 45 | let button = NSButton(frame: NSRect(x: 17, y: 855, width: 191, height: 17)) 46 | button.title = "JamfAccountUploader" 47 | button.bezelStyle = .inline 48 | button.setButtonType(.momentaryPushIn) 49 | button.isBordered = true 50 | button.font = .boldSystemFont(ofSize: 11) 51 | button.toolTip = "Upload an account to Jamf" 52 | button.action = #selector(appDelegate().JamfAccountUploaderAction) 53 | appDelegate().processorsView3rdParty.addSubview(button) 54 | } 55 | 56 | func createButtonJamfCategoryUploader() { 57 | let button = NSButton(frame: NSRect(x: 17, y: 834, width: 191, height: 17)) 58 | button.title = "JamfCategoryUploader" 59 | button.bezelStyle = .inline 60 | button.setButtonType(.momentaryPushIn) 61 | button.isBordered = true 62 | button.font = .boldSystemFont(ofSize: 11) 63 | button.toolTip = "Upload a category to Jamf" 64 | button.action = #selector(appDelegate().JamfCategoryUploaderAction) 65 | appDelegate().processorsView3rdParty.addSubview(button) 66 | } 67 | 68 | func createButtonJamfClassicAPIObjectUploader() { 69 | let button = NSButton(frame: NSRect(x: 17, y: 813, width: 191, height: 17)) 70 | button.title = "JamfClassicAPIObjectUploader" 71 | button.bezelStyle = .inline 72 | button.setButtonType(.momentaryPushIn) 73 | button.isBordered = true 74 | button.font = .boldSystemFont(ofSize: 11) 75 | button.toolTip = "Upload Classic API objects to Jamf" 76 | button.action = #selector(appDelegate().JamfClassicAPIObjectUploaderAction) 77 | appDelegate().processorsView3rdParty.addSubview(button) 78 | } 79 | 80 | func createButtonJamfComputerGroupDeleter() { 81 | let button = NSButton(frame: NSRect(x: 17, y: 792, width: 191, height: 17)) 82 | button.title = "JamfComputerGroupDeleter" 83 | button.bezelStyle = .inline 84 | button.setButtonType(.momentaryPushIn) 85 | button.isBordered = true 86 | button.font = .boldSystemFont(ofSize: 11) 87 | button.toolTip = "Delete a computer group in Jamf" 88 | button.action = #selector(appDelegate().JamfComputerGroupDeleterAction) 89 | appDelegate().processorsView3rdParty.addSubview(button) 90 | } 91 | 92 | func createButtonJamfComputerGroupUploader() { 93 | let button = NSButton(frame: NSRect(x: 17, y: 771, width: 191, height: 17)) 94 | button.title = "JamfComputerGroupUploader" 95 | button.bezelStyle = .inline 96 | button.setButtonType(.momentaryPushIn) 97 | button.isBordered = true 98 | button.font = .boldSystemFont(ofSize: 11) 99 | button.toolTip = "Upload a computer group to Jamf" 100 | button.action = #selector(appDelegate().JamfComputerGroupUploaderAction) 101 | appDelegate().processorsView3rdParty.addSubview(button) 102 | } 103 | 104 | func createButtonJamfComputerProfileUploader() { 105 | let button = NSButton(frame: NSRect(x: 17, y: 750, width: 191, height: 17)) 106 | button.title = "JamfComputerProfileUploader" 107 | button.bezelStyle = .inline 108 | button.setButtonType(.momentaryPushIn) 109 | button.isBordered = true 110 | button.font = .boldSystemFont(ofSize: 11) 111 | button.toolTip = "Upload a computer profile to Jamf" 112 | button.action = #selector(appDelegate().JamfComputerProfileUploaderAction) 113 | appDelegate().processorsView3rdParty.addSubview(button) 114 | } 115 | 116 | func createButtonJamfDockItemUploader() { 117 | let button = NSButton(frame: NSRect(x: 17, y: 729, width: 191, height: 17)) 118 | button.title = "JamfDockItemUploader" 119 | button.bezelStyle = .inline 120 | button.setButtonType(.momentaryPushIn) 121 | button.isBordered = true 122 | button.font = .boldSystemFont(ofSize: 11) 123 | button.toolTip = "Upload a dock item to Jamf" 124 | button.action = #selector(appDelegate().JamfDockItemUploaderAction) 125 | appDelegate().processorsView3rdParty.addSubview(button) 126 | } 127 | 128 | func createButtonJamfExtensionAttributeUploader() { 129 | let button = NSButton(frame: NSRect(x: 17, y: 708, width: 191, height: 17)) 130 | button.title = "JamfExtensionAttributeUploader" 131 | button.bezelStyle = .inline 132 | button.setButtonType(.momentaryPushIn) 133 | button.isBordered = true 134 | button.font = .boldSystemFont(ofSize: 11) 135 | button.toolTip = "Upload an extension attribute to Jamf" 136 | button.action = #selector(appDelegate().JamfExtensionAttributeUploaderAction) 137 | appDelegate().processorsView3rdParty.addSubview(button) 138 | } 139 | 140 | func createButtonJamfIconUploader() { 141 | let button = NSButton(frame: NSRect(x: 17, y: 687, width: 191, height: 17)) 142 | button.title = "JamfIconUploader" 143 | button.bezelStyle = .inline 144 | button.setButtonType(.momentaryPushIn) 145 | button.isBordered = true 146 | button.font = .boldSystemFont(ofSize: 11) 147 | button.toolTip = "Upload an icon to Jamf" 148 | button.action = #selector(appDelegate().JamfIconUploaderAction) 149 | appDelegate().processorsView3rdParty.addSubview(button) 150 | } 151 | 152 | func createButtonJamfMacAppUploader() { 153 | let button = NSButton(frame: NSRect(x: 17, y: 666, width: 191, height: 17)) 154 | button.title = "JamfMacAppUploader" 155 | button.bezelStyle = .inline 156 | button.setButtonType(.momentaryPushIn) 157 | button.isBordered = true 158 | button.font = .boldSystemFont(ofSize: 11) 159 | button.toolTip = "Upload a Mac app to Jamf" 160 | button.action = #selector(appDelegate().JamfMacAppUploaderAction) 161 | appDelegate().processorsView3rdParty.addSubview(button) 162 | } 163 | 164 | func createButtonJamfMobileDeviceGroupUploader() { 165 | let button = NSButton(frame: NSRect(x: 17, y: 645, width: 191, height: 17)) 166 | button.title = "JamfMobileDeviceGroupUploader" 167 | button.bezelStyle = .inline 168 | button.setButtonType(.momentaryPushIn) 169 | button.isBordered = true 170 | button.font = .boldSystemFont(ofSize: 11) 171 | button.toolTip = "Upload a mobile device group to Jamf" 172 | button.action = #selector(appDelegate().JamfMobileDeviceGroupUploaderAction) 173 | appDelegate().processorsView3rdParty.addSubview(button) 174 | } 175 | 176 | func createButtonJamfMobileDeviceProfileUploader() { 177 | let button = NSButton(frame: NSRect(x: 17, y: 624, width: 191, height: 17)) 178 | button.title = "JamfMobileDeviceProfileUpload" 179 | button.bezelStyle = .inline 180 | button.setButtonType(.momentaryPushIn) 181 | button.isBordered = true 182 | button.font = .boldSystemFont(ofSize: 11) 183 | button.toolTip = "Upload a mobile device profile to Jamf" 184 | button.action = #selector(appDelegate().JamfMobileDeviceProfileUploaderAction) 185 | appDelegate().processorsView3rdParty.addSubview(button) 186 | } 187 | 188 | func createButtonJamfPackageCleaner() { 189 | let button = NSButton(frame: NSRect(x: 17, y: 603, width: 191, height: 17)) 190 | button.title = "JamfPackageCleaner" 191 | button.bezelStyle = .inline 192 | button.setButtonType(.momentaryPushIn) 193 | button.isBordered = true 194 | button.font = .boldSystemFont(ofSize: 11) 195 | button.toolTip = "Clean up old packages in Jamf" 196 | button.action = #selector(appDelegate().JamfPackageCleanerAction) 197 | appDelegate().processorsView3rdParty.addSubview(button) 198 | } 199 | 200 | func createButtonJamfPackageRecalculator() { 201 | let button = NSButton(frame: NSRect(x: 17, y: 582, width: 191, height: 17)) 202 | button.title = "JamfPackageRecalculator" 203 | button.bezelStyle = .inline 204 | button.setButtonType(.momentaryPushIn) 205 | button.isBordered = true 206 | button.font = .boldSystemFont(ofSize: 11) 207 | button.toolTip = "Recalculate package information in Jamf" 208 | button.action = #selector(appDelegate().JamfPackageRecalculatorAction) 209 | appDelegate().processorsView3rdParty.addSubview(button) 210 | } 211 | 212 | func createButtonJamfPackageUploader() { 213 | let button = NSButton(frame: NSRect(x: 17, y: 561, width: 191, height: 17)) 214 | button.title = "JamfPackageUploader" 215 | button.bezelStyle = .inline 216 | button.setButtonType(.momentaryPushIn) 217 | button.isBordered = true 218 | button.font = .boldSystemFont(ofSize: 11) 219 | button.toolTip = "Upload a package to Jamf" 220 | button.action = #selector(appDelegate().JamfPackageUploaderAction) 221 | appDelegate().processorsView3rdParty.addSubview(button) 222 | } 223 | 224 | func createButtonJamfPatchChecker() { 225 | let button = NSButton(frame: NSRect(x: 17, y: 540, width: 191, height: 17)) 226 | button.title = "JamfPatchChecker" 227 | button.bezelStyle = .inline 228 | button.setButtonType(.momentaryPushIn) 229 | button.isBordered = true 230 | button.font = .boldSystemFont(ofSize: 11) 231 | button.toolTip = "Check for patch updates in Jamf" 232 | button.action = #selector(appDelegate().JamfPatchCheckerAction) 233 | appDelegate().processorsView3rdParty.addSubview(button) 234 | } 235 | 236 | func createButtonJamfPatchUploader() { 237 | let button = NSButton(frame: NSRect(x: 17, y: 519, width: 191, height: 17)) 238 | button.title = "JamfPatchUploader" 239 | button.bezelStyle = .inline 240 | button.setButtonType(.momentaryPushIn) 241 | button.isBordered = true 242 | button.font = .boldSystemFont(ofSize: 11) 243 | button.toolTip = "Upload patch definitions to Jamf" 244 | button.action = #selector(appDelegate().JamfPatchUploaderAction) 245 | appDelegate().processorsView3rdParty.addSubview(button) 246 | } 247 | 248 | func createButtonJamfPkgMetadataUploader() { 249 | let button = NSButton(frame: NSRect(x: 17, y: 498, width: 191, height: 17)) 250 | button.title = "JamfPkgMetadataUploader" 251 | button.bezelStyle = .inline 252 | button.setButtonType(.momentaryPushIn) 253 | button.isBordered = true 254 | button.font = .boldSystemFont(ofSize: 11) 255 | button.toolTip = "Upload pkg Metadata to Jamf" 256 | button.action = #selector(appDelegate().JamfPkgMetadataUploaderAction) 257 | appDelegate().processorsView3rdParty.addSubview(button) 258 | } 259 | 260 | func createButtonJamfPolicyDeleter() { 261 | let button = NSButton(frame: NSRect(x: 17, y: 477, width: 191, height: 17)) 262 | button.title = "JamfPolicyDeleter" 263 | button.bezelStyle = .inline 264 | button.setButtonType(.momentaryPushIn) 265 | button.isBordered = true 266 | button.font = .boldSystemFont(ofSize: 11) 267 | button.toolTip = "Delete policies in Jamf" 268 | button.action = #selector(appDelegate().JamfPolicyDeleterAction) 269 | appDelegate().processorsView3rdParty.addSubview(button) 270 | } 271 | 272 | func createButtonJamfPolicyLogFlusher() { 273 | let button = NSButton(frame: NSRect(x: 17, y: 456, width: 191, height: 17)) 274 | button.title = "JamfPolicyLogFlusher" 275 | button.bezelStyle = .inline 276 | button.setButtonType(.momentaryPushIn) 277 | button.isBordered = true 278 | button.font = .boldSystemFont(ofSize: 11) 279 | button.toolTip = "Flush policy logs in Jamf" 280 | button.action = #selector(appDelegate().JamfPolicyLogFlusherAction) 281 | appDelegate().processorsView3rdParty.addSubview(button) 282 | } 283 | 284 | func createButtonJamfPolicyUploader() { 285 | let button = NSButton(frame: NSRect(x: 17, y: 435, width: 191, height: 17)) 286 | button.title = "JamfPolicyUploader" 287 | button.bezelStyle = .inline 288 | button.setButtonType(.momentaryPushIn) 289 | button.isBordered = true 290 | button.font = .boldSystemFont(ofSize: 11) 291 | button.toolTip = "Upload a policy to Jamf" 292 | button.action = #selector(appDelegate().JamfPolicyUploaderAction) 293 | appDelegate().processorsView3rdParty.addSubview(button) 294 | } 295 | 296 | func createButtonJamfScriptUploader() { 297 | let button = NSButton(frame: NSRect(x: 17, y: 414, width: 191, height: 17)) 298 | button.title = "JamfScriptUploader" 299 | button.bezelStyle = .inline 300 | button.setButtonType(.momentaryPushIn) 301 | button.isBordered = true 302 | button.font = .boldSystemFont(ofSize: 11) 303 | button.toolTip = "Upload a script to Jamf" 304 | button.action = #selector(appDelegate().JamfScriptUploaderAction) 305 | appDelegate().processorsView3rdParty.addSubview(button) 306 | } 307 | 308 | func createButtonJamfSoftwareRestrictionUploader() { 309 | let button = NSButton(frame: NSRect(x: 17, y: 393, width: 191, height: 17)) 310 | button.title = "JamfSoftwareRestrictionUpload" 311 | button.bezelStyle = .inline 312 | button.setButtonType(.momentaryPushIn) 313 | button.isBordered = true 314 | button.font = .boldSystemFont(ofSize: 11) 315 | button.toolTip = "Upload software restrictions to Jamf" 316 | button.action = #selector(appDelegate().JamfSoftwareRestrictionUploaderAction) 317 | appDelegate().processorsView3rdParty.addSubview(button) 318 | } 319 | 320 | func createButtonJamfUploaderSlacker() { 321 | let button = NSButton(frame: NSRect(x: 17, y: 372, width: 191, height: 17)) 322 | button.title = "JamfUploaderSlacker" 323 | button.bezelStyle = .inline 324 | button.setButtonType(.momentaryPushIn) 325 | button.isBordered = true 326 | button.font = .boldSystemFont(ofSize: 11) 327 | button.toolTip = "Send notifications to Slack" 328 | button.action = #selector(appDelegate().JamfUploaderSlackerAction) 329 | appDelegate().processorsView3rdParty.addSubview(button) 330 | } 331 | 332 | func createButtonJamfUploaderTeamsNotifier() { 333 | let button = NSButton(frame: NSRect(x: 17, y: 351, width: 191, height: 17)) 334 | button.title = "JamfUploaderTeamsNotifier" 335 | button.bezelStyle = .inline 336 | button.setButtonType(.momentaryPushIn) 337 | button.isBordered = true 338 | button.font = .boldSystemFont(ofSize: 11) 339 | button.toolTip = "Send notifications to Microsoft Teams" 340 | button.action = #selector(appDelegate().JamfUploaderTeamsNotifierAction) 341 | appDelegate().processorsView3rdParty.addSubview(button) 342 | } 343 | 344 | 345 | // Functions called from button trigger/action (write output etc) 346 | func JamfAccountUploader() { 347 | output = """ 348 | 349 | Arguments 350 | 351 | account_name 352 | %ACCOUNT_NAME% 353 | account_type 354 | user 355 | account_template 356 | /path/to/XML 357 | 358 | Processor 359 | com.github.grahampugh.jamf-upload.processors/JamfAccountUploader 360 | 361 | """ 362 | writeOutput() 363 | } 364 | 365 | func JamfCategoryUploader () { 366 | output = """ 367 | 368 | Arguments 369 | 370 | category_name 371 | %CATEGORY% 372 | 373 | Processor 374 | com.github.grahampugh.jamf-upload.processors/JamfCategoryUploader 375 | 376 | """ 377 | writeOutput () 378 | } 379 | 380 | func JamfClassicAPIObjectUploader() { 381 | output = """ 382 | 383 | Arguments 384 | 385 | object_name 386 | %OBJECT_NAME% 387 | object_type 388 | %OBJECT_TYPE% 389 | object_template 390 | /path/to/template.xml 391 | 392 | Processor 393 | com.github.grahampugh.jamf-upload.processors/JamfClassicAPIObjectUploader 394 | 395 | """ 396 | writeOutput() 397 | } 398 | 399 | func JamfComputerGroupDeleter() { 400 | output = """ 401 | 402 | Arguments 403 | 404 | computergroup_name 405 | %GROUP_NAME% 406 | 407 | Processor 408 | com.github.grahampugh.jamf-upload.processors/JamfComputerGroupDeleter 409 | 410 | """ 411 | writeOutput() 412 | } 413 | 414 | func JamfComputerGroupUploader () { 415 | output = """ 416 | 417 | Arguments 418 | 419 | computergroup_name 420 | %GROUP_NAME% 421 | computergroup_template 422 | /path/to/template.xml 423 | 424 | Processor 425 | com.github.grahampugh.jamf-upload.processors/JamfComputerGroupUploader 426 | 427 | """ 428 | writeOutput () 429 | } 430 | 431 | func JamfComputerProfileUploader () { 432 | output = """ 433 | 434 | Arguments 435 | 436 | profile_name 437 | %PROFILE_NAME% 438 | mobileconfig 439 | /path/to/mobileconfig 440 | identifier 441 | Configuration Profile payload identifier 442 | profile_category 443 | Category to assign to the profile 444 | organization 445 | Organization to assign to the profile 446 | profile_description 447 | Description to assign to the profile 448 | profile_computergroup 449 | Device group that will be scoped to the profile 450 | 451 | Processor 452 | com.github.grahampugh.jamf-upload.processors/JamfComputerProfileUploader 453 | 454 | """ 455 | writeOutput () 456 | } 457 | 458 | func JamfDockItemUploader () { 459 | output = """ 460 | 461 | Arguments 462 | 463 | dock_item_name 464 | %NAME% 465 | dock_item_type 466 | App 467 | dock_item_path 468 | file:///Applications/AppName.app/ 469 | replace_dock_item 470 | False 471 | 472 | Processor 473 | com.github.grahampugh.jamf-upload.processors/JamfDockItemUploader 474 | 475 | """ 476 | writeOutput () 477 | } 478 | 479 | func JamfExtensionAttributeUploader () { 480 | output = """ 481 | 482 | Arguments 483 | 484 | ea_name 485 | %EXTENSION_ATTRIBUTE_NAME% 486 | ea_script_path 487 | %EXTENSION_ATTRIBUTE_SCRIPT% 488 | 489 | Processor 490 | com.github.grahampugh.jamf-upload.processors/JamfExtensionAttributeUploader 491 | 492 | """ 493 | writeOutput () 494 | } 495 | 496 | func JamfIconUploader() { 497 | output = """ 498 | 499 | Arguments 500 | 501 | icon_file 502 | /path/to/icon.png 503 | 504 | Processor 505 | com.github.grahampugh.jamf-upload.processors/JamfIconUploader 506 | 507 | """ 508 | writeOutput() 509 | } 510 | 511 | func JamfMacAppUploader () { 512 | output = """ 513 | 514 | Arguments 515 | 516 | macapp_name 517 | Mac App Store app name 518 | macapp_template 519 | /path/to/template.xml 520 | 521 | Processor 522 | com.github.grahampugh.jamf-upload.processors/JamfMacAppUploader 523 | 524 | """ 525 | writeOutput () 526 | } 527 | 528 | 529 | func JamfMobileDeviceGroupUploader() { 530 | output = """ 531 | 532 | Arguments 533 | 534 | mobiledevicegroup_name 535 | %GROUP_NAME% 536 | mobiledevicegroup_template 537 | /path/to/template.xml 538 | 539 | Processor 540 | com.github.grahampugh.jamf-upload.processors/JamfMobileDeviceGroupUploader 541 | 542 | """ 543 | writeOutput() 544 | } 545 | 546 | func JamfMobileDeviceProfileUploader() { 547 | output = """ 548 | 549 | Arguments 550 | 551 | profile_name 552 | %PROFILE_NAME% 553 | mobileconfig 554 | /path/to/mobileconfig 555 | identifier 556 | Configuration Profile payload identifier 557 | profile_category 558 | Category to assign to the profile 559 | organization 560 | Organization to assign to the profile 561 | profile_description 562 | Description to assign to the profile 563 | profile_mobiledevicegroup 564 | Device group that will be scoped to the profile 565 | 566 | Processor 567 | com.github.grahampugh.jamf-upload.processors/JamfMobileDeviceProfileUploader 568 | 569 | """ 570 | writeOutput() 571 | } 572 | 573 | func JamfPackageCleaner() { 574 | output = """ 575 | 576 | Arguments 577 | 578 | pkg_name_match 579 | %NAME%- 580 | versions_to_keep 581 | 5 582 | 583 | Processor 584 | com.github.grahampugh.jamf-upload.processors/JamfPackageCleaner 585 | 586 | """ 587 | writeOutput() 588 | } 589 | 590 | func JamfPackageRecalculator() { 591 | output = """ 592 | 593 | Processor 594 | com.github.grahampugh.jamf-upload.processors/JamfPackageRecalculator 595 | 596 | """ 597 | writeOutput() 598 | } 599 | 600 | func JamfPackageUploader () { 601 | output = """ 602 | 603 | Arguments 604 | 605 | pkg_category 606 | %CATEGORY% 607 | 608 | Processor 609 | com.github.grahampugh.jamf-upload.processors/JamfPackageUploader 610 | 611 | """ 612 | writeOutput () 613 | } 614 | 615 | func JamfPatchChecker() { 616 | output = """ 617 | 618 | Arguments 619 | 620 | patch_softwaretitle 621 | Name of the patch softwaretitle (e.g. 'Mozilla Firefox') used in Jamf. 622 | 623 | Processor 624 | com.github.grahampugh.jamf-upload.processors/JamfPatchChecker 625 | 626 | """ 627 | writeOutput() 628 | } 629 | 630 | 631 | func JamfPatchUploader () { 632 | output = """ 633 | 634 | Arguments 635 | 636 | patch_softwaretitle 637 | %NAME% 638 | patch_name 639 | %PATCH_NAME% 640 | patch_template 641 | PatchTemplate-selfservice.xml 642 | patch_icon_policy_name 643 | Install Latest %NAME% 644 | 645 | Processor 646 | com.github.grahampugh.jamf-upload.processors/JamfPatchUploader 647 | 648 | """ 649 | writeOutput () 650 | } 651 | 652 | 653 | func JamfPkgMetadataUploader() { 654 | output = """ 655 | 656 | Arguments 657 | 658 | pkg_display_name 659 | Package display name 660 | pkg_category 661 | Package category 662 | pkg_info 663 | Package info field 664 | pkg_notes 665 | Package notes field 666 | pkg_priority 667 | Package priority. Default=10 668 | reboot_required 669 | Default='False' 670 | os_requirements 671 | Package OS requirement 672 | required_processor 673 | Package required processor. Acceptable values are 'x86' or 'None' 674 | send_notification 675 | Whether to send a notification when a package is installed. Default='False' 676 | replace_pkg_metadata 677 | Overwrite existing package metadata and continue if True 678 | 679 | Processor 680 | com.github.grahampugh.jamf-upload.processors/JamfPkgMetadataUploader 681 | 682 | """ 683 | writeOutput() 684 | } 685 | 686 | func JamfPolicyDeleter() { 687 | output = """ 688 | 689 | Arguments 690 | 691 | policy_name 692 | %POLICY_NAME% 693 | 694 | Processor 695 | com.github.grahampugh.jamf-upload.processors/JamfPolicyDeleter 696 | 697 | """ 698 | writeOutput () 699 | } 700 | 701 | func JamfPolicyLogFlusher() { 702 | output = """ 703 | 704 | Arguments 705 | 706 | policy_name 707 | %POLICY_NAME% 708 | logflush_interval 709 | Zero Days 710 | 711 | Processor 712 | com.github.grahampugh.jamf-upload.processors/JamfPolicyLogFlusher 713 | 714 | """ 715 | writeOutput () 716 | } 717 | 718 | 719 | func JamfPolicyUploader () { 720 | output = """ 721 | 722 | Arguments 723 | 724 | icon 725 | %SELF_SERVICE_ICON% 726 | policy_name 727 | %POLICY_NAME% 728 | policy_template 729 | %POLICY_TEMPLATE% 730 | 731 | Processor 732 | com.github.grahampugh.jamf-upload.processors/JamfPolicyUploader 733 | 734 | """ 735 | writeOutput () 736 | } 737 | 738 | func JamfScriptUploader () { 739 | output = """ 740 | 741 | Arguments 742 | 743 | script_path 744 | /path/to/script.sh 745 | script_category 746 | %SCRIPT_CATEGORY% 747 | script_priority 748 | After 749 | 750 | Processor 751 | com.github.grahampugh.jamf-upload.processors/JamfScriptUploader 752 | 753 | """ 754 | writeOutput () 755 | } 756 | 757 | func JamfSoftwareRestrictionUploader () { 758 | output = """ 759 | 760 | Arguments 761 | 762 | restriction_name 763 | Name of Restriction 764 | restriction_template 765 | /path/to/template.xml 766 | 767 | Processor 768 | com.github.grahampugh.jamf-upload.processors/JamfSoftwareRestrictionUploader 769 | 770 | """ 771 | writeOutput () 772 | } 773 | 774 | func JamfUploaderSlacker () { 775 | output = """ 776 | 777 | Arguments 778 | 779 | NAME 780 | Name of the application 781 | slack_webhook_url 782 | https://slack.webhook.url 783 | 784 | Processor 785 | com.github.grahampugh.jamf-upload.processors/JamfUploaderSlacker 786 | 787 | """ 788 | writeOutput () 789 | } 790 | 791 | func JamfUploaderTeamsNotifier () { 792 | output = """ 793 | 794 | Arguments 795 | 796 | NAME 797 | Name of the application 798 | teams_webhook_url 799 | https://teams.webhook.url 800 | 801 | Processor 802 | com.github.grahampugh.jamf-upload.processors/JamfUploaderTeamsNotifier 803 | 804 | """ 805 | writeOutput () 806 | } 807 | -------------------------------------------------------------------------------- /RecipeBuilder/CheckAndVerify.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckAndVerify.swift 3 | // 4 | // Created by Mikael Löfgren on 2024-12-27 5 | // Copyright © 2024 Mikael Löfgren. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import Cocoa 10 | 11 | // Inspired by: https://github.com/homebysix/pre-commit-macadmin/blob/main/pre_commit_hooks/check_autopkg_recipes.py 12 | 13 | func returnProcessVersion (processName: String) -> String { 14 | var returnVersion = "" 15 | 16 | struct ProcessVersions { 17 | var name: String 18 | var version: String 19 | } 20 | 21 | var processandversions: [ProcessVersions] 22 | processandversions = [ 23 | ProcessVersions(name:"AppDmgVersioner", version:"0"), // no version needed? 24 | ProcessVersions(name:"AppPkgCreator", version:"1.0"), 25 | ProcessVersions(name:"CodeSignatureVerifier", version:"0.3.1"), 26 | ProcessVersions(name:"Copier", version:"0"), // no version needed? 27 | ProcessVersions(name:"CURLTextSearcher", version:"0.5.1"), 28 | ProcessVersions(name:"DeprecationWarning", version:"1.1"), 29 | ProcessVersions(name:"DmgCreator", version:"0"), // no version needed? 30 | ProcessVersions(name:"EndOfCheckPhase", version:"0.1.0"), 31 | ProcessVersions(name:"FileCreator", version:"0"), // no version needed? 32 | ProcessVersions(name:"FileFinder", version:"0.2.3"), 33 | ProcessVersions(name:"FileMover", version:"0.2.9"), 34 | ProcessVersions(name:"FlatPkgPacker", version:"0.2.4"), 35 | ProcessVersions(name:"FlatPkgUnpacker", version:"0.1.0"), 36 | ProcessVersions(name:"GitHubReleasesInfoProvider", version:"0.5.0"), 37 | ProcessVersions(name:"Installer", version:"0.4.0"), 38 | ProcessVersions(name:"InstallFromDMG", version:"0.4.0"), 39 | ProcessVersions(name:"MunkiCatalogBuilder", version:"0.1.0"), 40 | ProcessVersions(name:"MunkiImporter", version:"0.1.0"), 41 | ProcessVersions(name:"MunkiInfoCreator", version:"0"), // no version needed? 42 | ProcessVersions(name:"MunkiInstallsItemsCreator", version:"0.1.0"), 43 | ProcessVersions(name:"MunkiPkginfoMerger", version:"0.1.0"), 44 | ProcessVersions(name:"MunkiSetDefaultCatalog", version:"0.4.2"), 45 | ProcessVersions(name:"PackageRequired", version:"0.5.1"), 46 | ProcessVersions(name:"PathDeleter", version:"0.1.0"), 47 | ProcessVersions(name:"PkgCopier", version:"0.1.0"), 48 | ProcessVersions(name:"PkgCreator", version:"0"),// no version needed? 49 | ProcessVersions(name:"PkgExtractor", version:"0.1.0"), 50 | ProcessVersions(name:"PkgInfoCreator", version:"0"),// no version needed? 51 | ProcessVersions(name:"PkgPayloadUnpacker", version:"0.1.0"), 52 | ProcessVersions(name:"PkgRootCreator", version:"0"),// no version needed? 53 | ProcessVersions(name:"PlistEditor", version:"0.1.0"), 54 | ProcessVersions(name:"PlistReader", version:"0.2.5"), 55 | ProcessVersions(name:"SparkleUpdateInfoProvider", version:"0.1.0"), 56 | ProcessVersions(name:"StopProcessingIf", version:"0.1.0"), 57 | ProcessVersions(name:"Symlinker", version:"0.1.0"), 58 | ProcessVersions(name:"Unarchiver", version:"0.1.0"), 59 | ProcessVersions(name:"URLDownloader", version:"0"), // no version needed? 60 | ProcessVersions(name:"URLTextSearcher", version:"0.2.9"), 61 | ProcessVersions(name:"Versioner", version:"0.1.0") 62 | ] 63 | 64 | let nameArray = processandversions.filter{$0.name.hasPrefix("\(processName)")}.map{$0.version} 65 | 66 | if !nameArray.isEmpty { 67 | returnVersion = nameArray[0] 68 | } 69 | return returnVersion 70 | } 71 | 72 | // Not in use 73 | //struct InputValues:Codable { 74 | //var Name:String 75 | //private enum CodingKeys : String, CodingKey { 76 | // case Name = "NAME" 77 | //} 78 | //} 79 | 80 | struct ProcessValues:Codable { 81 | var Processor:String 82 | private enum CodingKeys : String, CodingKey { 83 | case Processor 84 | } 85 | } 86 | 87 | struct RecipePlistConfig:Codable { 88 | var Description: String? 89 | var Identifier: String 90 | var MinimumVersion: String? 91 | var ParentRecipe: String? 92 | //var Input: InputValues? 93 | var Process: [ProcessValues]? 94 | 95 | private enum CodingKeys : String, CodingKey { 96 | case Description = "Description" 97 | case Identifier = "Identifier" 98 | case MinimumVersion = "MinimumVersion" 99 | case ParentRecipe = "ParentRecipe" 100 | //case Input = "Input" 101 | case Process 102 | } 103 | } 104 | 105 | 106 | struct Recipe { 107 | let identifier: String 108 | let path: String 109 | } 110 | 111 | var recipesArraySet: [Recipe] = [] 112 | var recipesArray: [String] = [] 113 | var checkResults = "" 114 | 115 | func checkDuplicates(array: [String]) -> String { 116 | var encountered = Set() 117 | var duplicate = Set() 118 | var result = "" 119 | 120 | for value in array { 121 | if encountered.contains(value) { 122 | // Add duplicate to the set 123 | duplicate.insert(value) 124 | } 125 | else { 126 | // Add value to the set 127 | encountered.insert(value) 128 | } 129 | } 130 | 131 | for all in duplicate { 132 | result += "\n⚠️ Duplicated identifier\nIdentifier: \(all)\n" 133 | for recipePath in recipesArraySet { 134 | if recipePath.identifier == all { 135 | // Append the filepath to results 136 | result += "File: \(recipePath.path)\n" 137 | } 138 | } 139 | } 140 | return result 141 | } 142 | 143 | 144 | 145 | func startCheckAndVerify () { 146 | 147 | var defaultPath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/AutoPkg/").path 148 | 149 | if !FileManager.default.fileExists(atPath: defaultPath) { defaultPath = FileManager.default.homeDirectoryForCurrentUser.path } 150 | 151 | let dialog = NSOpenPanel(); 152 | dialog.message = "Choose a folder to verify .recipes files from" 153 | dialog.directoryURL = NSURL.fileURL(withPath: defaultPath, isDirectory: true) 154 | dialog.showsResizeIndicator = true; 155 | dialog.showsHiddenFiles = true; 156 | dialog.canCreateDirectories = false; 157 | dialog.canChooseDirectories = true; 158 | dialog.canChooseFiles = false; 159 | dialog.allowsMultipleSelection = false; 160 | 161 | 162 | 163 | 164 | if (dialog.runModal() == NSApplication.ModalResponse.OK) { 165 | let result = dialog.url 166 | 167 | if (result != nil) { 168 | defaultPath = result!.path 169 | } 170 | } else { 171 | // User clicked on "Cancel" 172 | appDelegate().fileOptions.selectItem(at: 0) 173 | appDelegate().spinner.isHidden=true 174 | return 175 | } 176 | 177 | let recipeRepoDir = defaultPath 178 | if !FileManager.default.fileExists(atPath: recipeRepoDir) { 179 | print("Path doesnt exist at: \(recipeRepoDir)") 180 | appDelegate().fileOptions.selectItem(at: 0) 181 | appDelegate().spinner.isHidden=true 182 | return } 183 | 184 | 185 | let enumerator = FileManager.default.enumerator(atPath: recipeRepoDir) 186 | let filePaths = enumerator?.allObjects as! [String] 187 | var recipeFilePaths = filePaths.filter{$0.hasSuffix(".recipe")} 188 | var recipeYamlFilePaths = filePaths.filter{$0.hasSuffix(".recipe.yaml")} 189 | let totalRecipes = recipeFilePaths.count + recipeYamlFilePaths.count 190 | 191 | if recipeFilePaths.isEmpty { 192 | appDelegate().fileOptions.selectItem(at: 0) 193 | appDelegate().spinner.isHidden=true 194 | return 195 | } 196 | 197 | let startText = "Start checking \(totalRecipes) recipes files..." 198 | appDelegate().spinner.isHidden=false 199 | appDelegate().spinner.startAnimation(appDelegate) 200 | appDelegate().logWindow.orderFront(Any?.self) 201 | highlightr!.theme.codeFont = NSFont(name: "Menlo", size: 12) 202 | let highlightedCode = highlightr!.highlight(startText, as: "bash")! 203 | appDelegate().logTextView.string = "" 204 | appDelegate().logTextView.textStorage?.insert(NSAttributedString(attributedString: highlightedCode), at: 0) 205 | 206 | DispatchQueue.global(qos: .userInteractive).async { 207 | 208 | 209 | for recipes in recipeFilePaths { 210 | let path = "\(recipeRepoDir)/\(recipes)" 211 | 212 | func parseRecipe() -> RecipePlistConfig { 213 | let url = URL(fileURLWithPath: path) 214 | guard let data = try? Data(contentsOf: url), 215 | let preferences = try? PropertyListDecoder().decode(RecipePlistConfig.self, from: data) 216 | else { 217 | // If problem parsing file set Identifier to Empty 218 | return RecipePlistConfig(Identifier: "Empty") 219 | } 220 | 221 | return preferences 222 | } 223 | var minimumVersion = "MISSING" 224 | if parseRecipe().MinimumVersion != nil { 225 | // Value from plist 226 | minimumVersion = parseRecipe().MinimumVersion! 227 | } 228 | 229 | func checkProcessors () { 230 | var processArray: [String] = [] 231 | if parseRecipe().Process != nil { 232 | for all in parseRecipe().Process! { 233 | 234 | // Value of process minimum (minimum version of autopkg required to run this recipe) 235 | // and value from plist should be higher or equal 236 | let processVersion = returnProcessVersion(processName: all.Processor) 237 | 238 | // if the vaules are Ascending 1..2..3 or same then ok 239 | let versionCheck = processVersion.compare(minimumVersion, options: .numeric) 240 | let ascending = versionCheck == ComparisonResult.orderedAscending 241 | let orderedSame = versionCheck == ComparisonResult.orderedSame 242 | //var descending = versionCheck == ComparisonResult.orderedDescending 243 | 244 | if ascending == false && orderedSame == false { 245 | checkResults += "\n⚠️ Processor: \(all.Processor) needs MinimumVersion: \(processVersion), now version: \(minimumVersion) is set in:\nFile: \(path)\n" 246 | 247 | // print(all.Processor) 248 | // print("Processor version:\(processVersion)") 249 | // print("Plist version:\(minimumVersion)") 250 | } 251 | 252 | // Check deprecated processors 253 | if all.Processor == "CURLDownloader" { 254 | checkResults += "\n⚠️ Processor: \(all.Processor) is deprecated\nFile: \(path)\n" 255 | } 256 | if all.Processor == "BrewCaskInfoProvider" { 257 | checkResults += "\n⚠️ Processor: \(all.Processor) is deprecated\nFile: \(path)\n" 258 | } 259 | // Warn if any superclass processors 260 | if all.Processor == "URLGetter" { 261 | checkResults += "\n⚠️ Processor: \(all.Processor) is a superclass processor\nFile: \(path)\n" 262 | } 263 | processArray.append(all.Processor) 264 | } 265 | } 266 | 267 | 268 | // Check EndofCheckPhase is after URLDownloader using Array with Index 269 | let urlDownloader = processArray.firstIndex(where: {$0 == "URLDownloader"}) 270 | let endOfCheckPhase = processArray.firstIndex(where: {$0 == "EndOfCheckPhase"}) 271 | if urlDownloader != nil && endOfCheckPhase != nil { 272 | // Convert to Int 273 | let urlDownloaderInt = Int(urlDownloader!) 274 | let endOfCheckPhaseInt = Int(endOfCheckPhase!) 275 | 276 | if urlDownloaderInt > endOfCheckPhaseInt { 277 | checkResults += "\n⚠️ Processor: URLDownloader should be used before Processor: EndOfCheckPhase\nFile: \(path)\n" 278 | } 279 | 280 | if urlDownloaderInt + 1 < endOfCheckPhaseInt { 281 | checkResults += "\n⚠️ Processor: EndOfCheckPhase is recommended to be directly after Processor: URLDownloader\nFile: \(path)\n" 282 | } 283 | 284 | } else { 285 | if urlDownloader != nil && endOfCheckPhase == nil { 286 | checkResults += "\n⚠️ Processor: URLDownloader exist but missing Processor: EndOfCheckPhase\nFile: \(path)\n" 287 | } 288 | 289 | } 290 | } 291 | 292 | checkProcessors () 293 | 294 | if parseRecipe().Identifier != "Empty" { 295 | // Add Identifier to Array so we can check for duplicates 296 | recipesArraySet.append(Recipe(identifier: parseRecipe().Identifier, path: path)) 297 | 298 | if parseRecipe().ParentRecipe != nil { 299 | // If ParentRecipe is the same as Identifier then show warning 300 | if parseRecipe().ParentRecipe == parseRecipe().Identifier { 301 | checkResults += "\n🛑 ParentRecipe is the same as Identifier\nFile: \(path)\n" 302 | } 303 | } 304 | } else { 305 | // We get this error: Failed to set posix_spawn_file_actions for fd -1 at index 0 with errno 9 306 | // if we process to many files like ~3000 with a shell command 307 | let plutil = shell("/usr/bin/plutil \"\(path)\"").replacingOccurrences(of: ":", with: "\nError:", options: [.regularExpression, .caseInsensitive]) 308 | checkResults += "\n🛑 Error reading file\nFile: \(plutil)\n" 309 | } 310 | } 311 | 312 | if !recipeYamlFilePaths.isEmpty { 313 | for yamlRecipes in recipeYamlFilePaths { 314 | let path = "\(recipeRepoDir)/\(yamlRecipes)" 315 | var yamlID = "" 316 | if freopen(path, "r", stdin) == nil { 317 | perror(path) 318 | } 319 | while let line = readLine() { 320 | if line.contains("Identifier:") { 321 | yamlID = line.replacingOccurrences(of: "Identifier: ", with: "", options: [.regularExpression, .caseInsensitive]) 322 | recipesArraySet.append(Recipe(identifier: yamlID, path: path)) 323 | } 324 | 325 | if line.contains("ParentRecipe:") { 326 | let yamlParentRecipe = line.replacingOccurrences(of: "ParentRecipe: ", with: "", options: [.regularExpression, .caseInsensitive]) 327 | 328 | if yamlParentRecipe == yamlID { 329 | checkResults += "\n🛑 ParentRecipe is the same as Identifier\nFile: \(path)\n" 330 | } 331 | } 332 | 333 | if line.contains("MinimumVersion:") { 334 | var yamlMinimumVersion: String 335 | yamlMinimumVersion = line.replacingOccurrences(of: "MinimumVersion: ", with: "", options: [.regularExpression, .caseInsensitive]) 336 | yamlMinimumVersion = yamlMinimumVersion.replacingOccurrences(of: "\"", with: "", options: [.regularExpression, .caseInsensitive]) 337 | yamlMinimumVersion = yamlMinimumVersion.replacingOccurrences(of: "'", with: "", options: [.regularExpression, .caseInsensitive]) 338 | let yamlMinimumVersionDouble = Double(yamlMinimumVersion) ?? 0 339 | 340 | if yamlMinimumVersionDouble < 2.3 { 341 | checkResults += "\n⚠️ MinimumVersion is lower then 2.3\nFile: \(path)\n" 342 | } 343 | } 344 | } 345 | } 346 | } 347 | 348 | DispatchQueue.main.async { 349 | // Back on the main thread 350 | recipesArray = recipesArraySet.map({ $0.identifier }) 351 | 352 | checkResults += checkDuplicates(array: recipesArray) 353 | 354 | if checkResults.isEmpty { 355 | checkResults += ("✅ No problems found\n") 356 | } 357 | 358 | checkResults += """ 359 | -------------------------------------------- 360 | Done checking \(totalRecipes) recipes files\n 361 | """ 362 | appDelegate().logWindow.orderFront(Any?.self) 363 | highlightr!.theme.codeFont = NSFont(name: "Menlo", size: 12) 364 | let highlightedCode = highlightr!.highlight(checkResults, as: "bash")! 365 | appDelegate().logTextView.string = "" 366 | appDelegate().logTextView.textStorage?.insert(NSAttributedString(attributedString: highlightedCode), at: 0) 367 | appDelegate().fileOptions.selectItem(at: 0) 368 | appDelegate().spinner.isHidden=true 369 | // Reset all values 370 | recipeFilePaths.removeAll() 371 | recipeYamlFilePaths.removeAll() 372 | recipesArray.removeAll() 373 | recipesArraySet.removeAll() 374 | checkResults = "" 375 | 376 | } 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /RecipeBuilder/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeName 11 | RecipeBuilder text document 12 | CFBundleTypeRole 13 | Editor 14 | LSHandlerRank 15 | Alternate 16 | LSItemContentTypes 17 | 18 | com.apple.property-list 19 | com.apple.xml-property-list 20 | public.xml 21 | 22 | 23 | 24 | CFBundleExecutable 25 | $(EXECUTABLE_NAME) 26 | CFBundleIconFile 27 | 28 | CFBundleIdentifier 29 | $(PRODUCT_BUNDLE_IDENTIFIER) 30 | CFBundleInfoDictionaryVersion 31 | 6.0 32 | CFBundleName 33 | $(PRODUCT_NAME) 34 | CFBundlePackageType 35 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 36 | CFBundleShortVersionString 37 | $(MARKETING_VERSION) 38 | CFBundleVersion 39 | 1 40 | LSApplicationCategoryType 41 | public.app-category.utilities 42 | LSMinimumSystemVersion 43 | $(MACOSX_DEPLOYMENT_TARGET) 44 | NSHumanReadableCopyright 45 | Copyright © 2025 Mikael Löfgren. All rights reserved. 46 | NSMainNibFile 47 | MainMenu 48 | NSPrincipalClass 49 | NSApplication 50 | NSSupportsAutomaticTermination 51 | 52 | NSSupportsSuddenTermination 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /RecipeBuilder/JamfUploaderHelpTexts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JamfUploaderHelpTexts.swift 3 | // 4 | // Created by Mikael Löfgren on 2024-12-27 5 | // Copyright © 2024 Mikael Löfgren. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | var JamfAccountUploaderHelp = "" 11 | var JamfCategoryUploaderHelp = "" 12 | var JamfClassicAPIObjectUploaderHelp = "" 13 | var JamfComputerGroupDeleterHelp = "" 14 | var JamfComputerGroupUploaderHelp = "" 15 | var JamfComputerProfileUploaderHelp = "" 16 | var JamfDockItemUploaderHelp = "" 17 | var JamfExtensionAttributeUploaderHelp = "" 18 | var JamfIconUploaderHelp = "" 19 | var JamfMacAppUploaderHelp = "" 20 | var JamfMobileDeviceGroupUploaderHelp = "" 21 | var JamfMobileDeviceProfileUploaderHelp = "" 22 | var JamfPackageCleanerHelp = "" 23 | var JamfPackageRecalculatorHelp = "" 24 | var JamfPackageUploaderHelp = "" 25 | var JamfPatchCheckerHelp = "" 26 | var JamfPatchUploaderHelp = "" 27 | var JamfPkgMetadataUploaderHelp = "" 28 | var JamfPolicyDeleterHelp = "" 29 | var JamfPolicyLogFlusherHelp = "" 30 | var JamfPolicyUploaderHelp = "" 31 | var JamfScriptUploaderHelp = "" 32 | var JamfSoftwareRestrictionUploaderHelp = "" 33 | var JamfUploaderSlackerHelp = "" 34 | var JamfUploaderTeamsNotifierHelp = "" 35 | -------------------------------------------------------------------------------- /RecipeBuilder/RecipeBuilder.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /RecipeBuilder/UserButtons.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserButtons.swift 3 | // 4 | // Created by Mikael Löfgren on 2024-12-27 5 | // Copyright © 2024 Mikael Löfgren. All rights reserved. 6 | // 7 | 8 | 9 | import Cocoa 10 | import AppKit 11 | import Foundation 12 | 13 | 14 | var allButtonsFolders = "" 15 | let recipeBuilderButtonsFolder = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support/RecipeBuilder/").path 16 | var titleFileString = "" 17 | var outputFileString = "" 18 | var helpFileString = "" 19 | var titleFile = URL(fileURLWithPath: "") 20 | var outputFile = URL(fileURLWithPath: "") 21 | var helpFile = URL(fileURLWithPath: "") 22 | var title1 = "" 23 | var output1 = "" 24 | var help1 = "" 25 | 26 | var title2 = "" 27 | var output2 = "" 28 | var help2 = "" 29 | 30 | var title3 = "" 31 | var output3 = "" 32 | var help3 = "" 33 | 34 | var title4 = "" 35 | var output4 = "" 36 | var help4 = "" 37 | 38 | var title5 = "" 39 | var output5 = "" 40 | var help5 = "" 41 | 42 | var title6 = "" 43 | var output6 = "" 44 | var help6 = "" 45 | 46 | var title7 = "" 47 | var output7 = "" 48 | var help7 = "" 49 | 50 | var title8 = "" 51 | var output8 = "" 52 | var help8 = "" 53 | 54 | var title9 = "" 55 | var output9 = "" 56 | var help9 = "" 57 | 58 | var title10 = "" 59 | var output10 = "" 60 | var help10 = "" 61 | 62 | func createButton1 (title: String) { 63 | let button = NSButton(frame: NSRect(x: 17, y: 258, width: 191, height: 17)) 64 | button.title = title 65 | button.bezelStyle = NSButton.BezelStyle.inline 66 | button.setButtonType(NSButton.ButtonType.momentaryPushIn) 67 | button.isBordered = true 68 | button.font = .boldSystemFont(ofSize: 11) 69 | button.action = #selector(appDelegate().buttonAction1) 70 | appDelegate().buttonView.addSubview(button) 71 | } 72 | 73 | 74 | func createButton2 (title: String) { 75 | let button = NSButton(frame: NSRect(x: 17, y: 237, width: 191, height: 17)) 76 | button.title = title 77 | button.bezelStyle = NSButton.BezelStyle.inline 78 | button.setButtonType(NSButton.ButtonType.momentaryPushIn) 79 | button.isBordered = true 80 | button.font = .boldSystemFont(ofSize: 11) 81 | button.action = #selector(appDelegate().buttonAction2) 82 | appDelegate().buttonView.addSubview(button) 83 | } 84 | 85 | 86 | func createButton3 (title: String) { 87 | let button = NSButton(frame: NSRect(x: 17, y: 216, width: 191, height: 17)) 88 | button.title = title 89 | button.bezelStyle = NSButton.BezelStyle.inline 90 | button.setButtonType(NSButton.ButtonType.momentaryPushIn) 91 | button.isBordered = true 92 | button.font = .boldSystemFont(ofSize: 11) 93 | button.action = #selector(appDelegate().buttonAction3) 94 | appDelegate().buttonView.addSubview(button) 95 | } 96 | 97 | 98 | func createButton4 (title: String) { 99 | let button = NSButton(frame: NSRect(x: 17, y: 195, width: 191, height: 17)) 100 | button.title = title 101 | button.bezelStyle = NSButton.BezelStyle.inline 102 | button.setButtonType(NSButton.ButtonType.momentaryPushIn) 103 | button.isBordered = true 104 | button.font = .boldSystemFont(ofSize: 11) 105 | button.action = #selector(appDelegate().buttonAction4) 106 | appDelegate().buttonView.addSubview(button) 107 | } 108 | 109 | 110 | func createButton5 (title: String) { 111 | let button = NSButton(frame: NSRect(x: 17, y: 174, width: 191, height: 17)) 112 | button.title = title 113 | button.bezelStyle = NSButton.BezelStyle.inline 114 | button.setButtonType(NSButton.ButtonType.momentaryPushIn) 115 | button.isBordered = true 116 | button.font = .boldSystemFont(ofSize: 11) 117 | button.action = #selector(appDelegate().buttonAction5) 118 | appDelegate().buttonView.addSubview(button) 119 | } 120 | 121 | 122 | func createButton6 (title: String) { 123 | let button = NSButton(frame: NSRect(x: 17, y: 153, width: 191, height: 17)) 124 | button.title = title 125 | button.bezelStyle = NSButton.BezelStyle.inline 126 | button.setButtonType(NSButton.ButtonType.momentaryPushIn) 127 | button.isBordered = true 128 | button.font = .boldSystemFont(ofSize: 11) 129 | button.action = #selector(appDelegate().buttonAction6) 130 | appDelegate().buttonView.addSubview(button) 131 | } 132 | 133 | 134 | func createButton7 (title: String) { 135 | let button = NSButton(frame: NSRect(x: 17, y: 132, width: 191, height: 17)) 136 | button.title = title 137 | button.bezelStyle = NSButton.BezelStyle.inline 138 | button.setButtonType(NSButton.ButtonType.momentaryPushIn) 139 | button.isBordered = true 140 | button.font = .boldSystemFont(ofSize: 11) 141 | button.action = #selector(appDelegate().buttonAction7) 142 | appDelegate().buttonView.addSubview(button) 143 | } 144 | 145 | 146 | func createButton8 (title: String) { 147 | let button = NSButton(frame: NSRect(x: 17, y: 111, width: 191, height: 17)) 148 | button.title = title 149 | button.bezelStyle = NSButton.BezelStyle.inline 150 | button.setButtonType(NSButton.ButtonType.momentaryPushIn) 151 | button.isBordered = true 152 | button.font = .boldSystemFont(ofSize: 11) 153 | button.action = #selector(appDelegate().buttonAction8) 154 | appDelegate().buttonView.addSubview(button) 155 | } 156 | 157 | 158 | func createButton9 (title: String) { 159 | let button = NSButton(frame: NSRect(x: 17, y: 90, width: 191, height: 17)) 160 | button.title = title 161 | button.bezelStyle = NSButton.BezelStyle.inline 162 | button.setButtonType(NSButton.ButtonType.momentaryPushIn) 163 | button.isBordered = true 164 | button.font = .boldSystemFont(ofSize: 11) 165 | button.action = #selector(appDelegate().buttonAction9) 166 | appDelegate().buttonView.addSubview(button) 167 | } 168 | 169 | func createButton10 (title: String) { 170 | let button = NSButton(frame: NSRect(x: 17, y: 69, width: 191, height: 17)) 171 | button.title = title 172 | button.bezelStyle = NSButton.BezelStyle.inline 173 | button.setButtonType(NSButton.ButtonType.momentaryPushIn) 174 | button.isBordered = true 175 | button.font = .boldSystemFont(ofSize: 11) 176 | button.action = #selector(appDelegate().buttonAction10) 177 | appDelegate().buttonView.addSubview(button) 178 | } 179 | 180 | 181 | func buttonWarning (infomessage: String) { 182 | let warning = NSAlert() 183 | warning.icon = NSImage(named: "Warning") 184 | warning.addButton(withTitle: "OK") 185 | warning.messageText = "Button is missing required info" 186 | warning.alertStyle = NSAlert.Style.warning 187 | warning.informativeText = """ 188 | \(infomessage) 189 | """ 190 | warning.runModal() 191 | } 192 | 193 | func checkForUserButtonsAndEnable () { 194 | titleFileString = "\(recipeBuilderButtonsFolder)/1/title.txt" 195 | outputFileString = "\(recipeBuilderButtonsFolder)/1/output.txt" 196 | helpFileString = "\(recipeBuilderButtonsFolder)/1/help.txt" 197 | 198 | // If folder one is missing file, dont enable 199 | if FileManager.default.fileExists(atPath: titleFileString ) && FileManager.default.fileExists(atPath: outputFileString ) { 200 | getAllUserButtons () 201 | } 202 | 203 | } 204 | 205 | 206 | func openUserButtons () { 207 | if FileManager.default.fileExists(atPath: recipeBuilderButtonsFolder) { 208 | NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: recipeBuilderButtonsFolder) 209 | } else { 210 | return } 211 | appDelegate().fileOptions.selectItem(at: 0) 212 | } 213 | 214 | 215 | 216 | func getAllUserButtons () { 217 | 218 | if FileManager.default.fileExists(atPath: recipeBuilderButtonsFolder) { 219 | } else { 220 | do { 221 | try FileManager.default.createDirectory(atPath: recipeBuilderButtonsFolder, withIntermediateDirectories: true, attributes: nil) 222 | } catch { 223 | print(error) 224 | } 225 | } 226 | 227 | if FileManager.default.fileExists(atPath: "\(recipeBuilderButtonsFolder)/1") { 228 | } else { 229 | do { 230 | try FileManager.default.createDirectory(atPath: "\(recipeBuilderButtonsFolder)/1", withIntermediateDirectories: true, attributes: nil) 231 | let writeTitle = "Insert empty key and string" 232 | let writeOutput = """ 233 | 234 | 235 | """ 236 | let writeHelp = """ 237 | Welcome to UserButtons! 238 | 239 | In folder \"\(recipeBuilderButtonsFolder)\" 240 | you create folders with name 1,2,3..up to 10. You can skip folders, but folder 1 is required if you 241 | want buttons to autostart, otherwise you have to enable them. 242 | In every folder create title.txt and add text to the file for the button name. 243 | Keep it below 26 characters for best result. 244 | 245 | Add output.txt for the output text. 246 | 247 | And help.txt for the Note text your reading right now (optional). 248 | To easy open \"\(recipeBuilderButtonsFolder)\", choose File options - Open user buttons folder 249 | Click the Enable and reload button to create the first "demo" button. 250 | Use that button to reload if you trying out your new buttons. 251 | """ 252 | titleFile = URL(fileURLWithPath: "\(recipeBuilderButtonsFolder)/1/title.txt") 253 | outputFile = URL(fileURLWithPath: "\(recipeBuilderButtonsFolder)/1/output.txt") 254 | helpFile = URL(fileURLWithPath: "\(recipeBuilderButtonsFolder)/1/help.txt") 255 | try writeTitle.write(to: titleFile, atomically: true, encoding: String.Encoding.utf8) 256 | try writeOutput.write(to: outputFile, atomically: true, encoding: String.Encoding.utf8) 257 | try writeHelp.write(to: helpFile, atomically: true, encoding: String.Encoding.utf8) 258 | } catch { 259 | print(error) 260 | } 261 | } 262 | 263 | // Remove all the buttons and add them again if they exist 264 | for subview in appDelegate().buttonView.subviews { 265 | if subview is NSButton { 266 | subview.removeFromSuperview() 267 | appDelegate().buttonView.addSubview(appDelegate().enableAndReloadButton) 268 | } 269 | } 270 | 271 | 272 | let documentsPath = recipeBuilderButtonsFolder 273 | let url = URL(fileURLWithPath: documentsPath) 274 | let fileManager = FileManager.default 275 | let enumerator: FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: url.path)! 276 | while let subFolders = enumerator.nextObject() as? String { 277 | 278 | let folderName = subFolders 279 | 280 | switch folderName { 281 | case let folderName where folderName == "1": 282 | titleFileString = "\(recipeBuilderButtonsFolder)/1/title.txt" 283 | outputFileString = "\(recipeBuilderButtonsFolder)/1/output.txt" 284 | helpFileString = "\(recipeBuilderButtonsFolder)/1/help.txt" 285 | titleFile = URL(fileURLWithPath: titleFileString) 286 | outputFile = URL(fileURLWithPath: outputFileString) 287 | helpFile = URL(fileURLWithPath: helpFileString) 288 | 289 | if FileManager.default.fileExists(atPath: titleFileString ) { 290 | do { 291 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8) 292 | if title != "" { 293 | title1 = title 294 | createButton1(title: title1) 295 | } else { 296 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty") 297 | } 298 | } catch { 299 | print(error) 300 | } 301 | } 302 | 303 | if FileManager.default.fileExists(atPath: outputFileString) { 304 | do { 305 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8) 306 | if output != "" { 307 | output1 = output 308 | } else { 309 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.") 310 | } 311 | } catch { 312 | print(error) 313 | } 314 | } 315 | 316 | if FileManager.default.fileExists(atPath: helpFileString) { 317 | do { 318 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8) 319 | help1 = help 320 | } catch { 321 | print(error) 322 | } 323 | } 324 | 325 | 326 | case let folderName where folderName == "2": 327 | titleFileString = "\(recipeBuilderButtonsFolder)/2/title.txt" 328 | outputFileString = "\(recipeBuilderButtonsFolder)/2/output.txt" 329 | helpFileString = "\(recipeBuilderButtonsFolder)/2/help.txt" 330 | titleFile = URL(fileURLWithPath: titleFileString) 331 | outputFile = URL(fileURLWithPath: outputFileString) 332 | helpFile = URL(fileURLWithPath: helpFileString) 333 | 334 | if FileManager.default.fileExists(atPath: titleFileString ) { 335 | do { 336 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8) 337 | if title != "" { 338 | title2 = title 339 | createButton2(title: title2) 340 | } else { 341 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty") 342 | } 343 | } catch { 344 | print(error) 345 | } 346 | } 347 | 348 | if FileManager.default.fileExists(atPath: outputFileString) { 349 | do { 350 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8) 351 | if output != "" { 352 | output2 = output 353 | } else { 354 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.") 355 | } 356 | } catch { 357 | print(error) 358 | } 359 | } 360 | 361 | if FileManager.default.fileExists(atPath: helpFileString) { 362 | do { 363 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8) 364 | help2 = help 365 | } catch { 366 | print(error) 367 | } 368 | } 369 | 370 | case let folderName where folderName == "3": 371 | titleFileString = "\(recipeBuilderButtonsFolder)/3/title.txt" 372 | outputFileString = "\(recipeBuilderButtonsFolder)/3/output.txt" 373 | helpFileString = "\(recipeBuilderButtonsFolder)/3/help.txt" 374 | titleFile = URL(fileURLWithPath: titleFileString) 375 | outputFile = URL(fileURLWithPath: outputFileString) 376 | helpFile = URL(fileURLWithPath: helpFileString) 377 | 378 | if FileManager.default.fileExists(atPath: titleFileString ) { 379 | do { 380 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8) 381 | if title != "" { 382 | title3 = title 383 | createButton3(title: title3) 384 | } else { 385 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty") 386 | } 387 | } catch { 388 | print(error) 389 | } 390 | } 391 | 392 | if FileManager.default.fileExists(atPath: outputFileString) { 393 | do { 394 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8) 395 | if output != "" { 396 | output3 = output 397 | } else { 398 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.") 399 | } 400 | } catch { 401 | print(error) 402 | } 403 | } 404 | 405 | if FileManager.default.fileExists(atPath: helpFileString) { 406 | do { 407 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8) 408 | help3 = help 409 | } catch { 410 | print(error) 411 | } 412 | } 413 | 414 | case let folderName where folderName == "4": 415 | titleFileString = "\(recipeBuilderButtonsFolder)/4/title.txt" 416 | outputFileString = "\(recipeBuilderButtonsFolder)/4/output.txt" 417 | helpFileString = "\(recipeBuilderButtonsFolder)/4/help.txt" 418 | titleFile = URL(fileURLWithPath: titleFileString) 419 | outputFile = URL(fileURLWithPath: outputFileString) 420 | helpFile = URL(fileURLWithPath: helpFileString) 421 | 422 | if FileManager.default.fileExists(atPath: titleFileString ) { 423 | do { 424 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8) 425 | if title != "" { 426 | title4 = title 427 | createButton4(title: title4) 428 | } else { 429 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty") 430 | } 431 | } catch { 432 | print(error) 433 | } 434 | } 435 | 436 | if FileManager.default.fileExists(atPath: outputFileString) { 437 | do { 438 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8) 439 | if output != "" { 440 | output4 = output 441 | } else { 442 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.") 443 | } 444 | } catch { 445 | print(error) 446 | } 447 | } 448 | 449 | if FileManager.default.fileExists(atPath: helpFileString) { 450 | do { 451 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8) 452 | help4 = help 453 | } catch { 454 | print(error) 455 | } 456 | } 457 | 458 | case let folderName where folderName == "5": 459 | titleFileString = "\(recipeBuilderButtonsFolder)/5/title.txt" 460 | outputFileString = "\(recipeBuilderButtonsFolder)/5/output.txt" 461 | helpFileString = "\(recipeBuilderButtonsFolder)/5/help.txt" 462 | titleFile = URL(fileURLWithPath: titleFileString) 463 | outputFile = URL(fileURLWithPath: outputFileString) 464 | helpFile = URL(fileURLWithPath: helpFileString) 465 | 466 | if FileManager.default.fileExists(atPath: titleFileString ) { 467 | do { 468 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8) 469 | if title != "" { 470 | title5 = title 471 | createButton5(title: title5) 472 | } else { 473 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty") 474 | } 475 | } catch { 476 | print(error) 477 | } 478 | } 479 | 480 | if FileManager.default.fileExists(atPath: outputFileString) { 481 | do { 482 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8) 483 | if output != "" { 484 | output5 = output 485 | } else { 486 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.") 487 | } 488 | } catch { 489 | print(error) 490 | } 491 | } 492 | 493 | if FileManager.default.fileExists(atPath: helpFileString) { 494 | do { 495 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8) 496 | help5 = help 497 | } catch { 498 | print(error) 499 | } 500 | } 501 | 502 | case let folderName where folderName == "6": 503 | titleFileString = "\(recipeBuilderButtonsFolder)/6/title.txt" 504 | outputFileString = "\(recipeBuilderButtonsFolder)/6/output.txt" 505 | helpFileString = "\(recipeBuilderButtonsFolder)/6/help.txt" 506 | titleFile = URL(fileURLWithPath: titleFileString) 507 | outputFile = URL(fileURLWithPath: outputFileString) 508 | helpFile = URL(fileURLWithPath: helpFileString) 509 | 510 | if FileManager.default.fileExists(atPath: titleFileString ) { 511 | do { 512 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8) 513 | if title != "" { 514 | title6 = title 515 | createButton6(title: title6) 516 | } else { 517 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty") 518 | } 519 | } catch { 520 | print(error) 521 | } 522 | } 523 | 524 | if FileManager.default.fileExists(atPath: outputFileString) { 525 | do { 526 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8) 527 | if output != "" { 528 | output6 = output 529 | } else { 530 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.") 531 | } 532 | } catch { 533 | print(error) 534 | } 535 | } 536 | 537 | if FileManager.default.fileExists(atPath: helpFileString) { 538 | do { 539 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8) 540 | help6 = help 541 | } catch { 542 | print(error) 543 | } 544 | } 545 | 546 | case let folderName where folderName == "7": 547 | titleFileString = "\(recipeBuilderButtonsFolder)/7/title.txt" 548 | outputFileString = "\(recipeBuilderButtonsFolder)/7/output.txt" 549 | helpFileString = "\(recipeBuilderButtonsFolder)/7/help.txt" 550 | titleFile = URL(fileURLWithPath: titleFileString) 551 | outputFile = URL(fileURLWithPath: outputFileString) 552 | helpFile = URL(fileURLWithPath: helpFileString) 553 | 554 | if FileManager.default.fileExists(atPath: titleFileString ) { 555 | do { 556 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8) 557 | if title != "" { 558 | title7 = title 559 | createButton7(title: title7) 560 | } else { 561 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty") 562 | } 563 | } catch { 564 | print(error) 565 | } 566 | } 567 | 568 | if FileManager.default.fileExists(atPath: outputFileString) { 569 | do { 570 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8) 571 | if output != "" { 572 | output7 = output 573 | } else { 574 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.") 575 | } 576 | } catch { 577 | print(error) 578 | } 579 | } 580 | 581 | if FileManager.default.fileExists(atPath: helpFileString) { 582 | do { 583 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8) 584 | help7 = help 585 | } catch { 586 | print(error) 587 | } 588 | } 589 | 590 | case let folderName where folderName == "8": 591 | titleFileString = "\(recipeBuilderButtonsFolder)/8/title.txt" 592 | outputFileString = "\(recipeBuilderButtonsFolder)/8/output.txt" 593 | helpFileString = "\(recipeBuilderButtonsFolder)/8/help.txt" 594 | titleFile = URL(fileURLWithPath: titleFileString) 595 | outputFile = URL(fileURLWithPath: outputFileString) 596 | helpFile = URL(fileURLWithPath: helpFileString) 597 | 598 | if FileManager.default.fileExists(atPath: titleFileString ) { 599 | do { 600 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8) 601 | if title != "" { 602 | title8 = title 603 | createButton8(title: title8) 604 | } else { 605 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty") 606 | } 607 | } catch { 608 | print(error) 609 | } 610 | } 611 | 612 | if FileManager.default.fileExists(atPath: outputFileString) { 613 | do { 614 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8) 615 | if output != "" { 616 | output8 = output 617 | } else { 618 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.") 619 | } 620 | } catch { 621 | print(error) 622 | } 623 | } 624 | 625 | if FileManager.default.fileExists(atPath: helpFileString) { 626 | do { 627 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8) 628 | help8 = help 629 | } catch { 630 | print(error) 631 | } 632 | } 633 | 634 | case let folderName where folderName == "9": 635 | titleFileString = "\(recipeBuilderButtonsFolder)/9/title.txt" 636 | outputFileString = "\(recipeBuilderButtonsFolder)/9/output.txt" 637 | helpFileString = "\(recipeBuilderButtonsFolder)/9/help.txt" 638 | titleFile = URL(fileURLWithPath: titleFileString) 639 | outputFile = URL(fileURLWithPath: outputFileString) 640 | helpFile = URL(fileURLWithPath: helpFileString) 641 | 642 | if FileManager.default.fileExists(atPath: titleFileString ) { 643 | do { 644 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8) 645 | if title != "" { 646 | title9 = title 647 | createButton9(title: title9) 648 | } else { 649 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty") 650 | } 651 | } catch { 652 | print(error) 653 | } 654 | } 655 | 656 | if FileManager.default.fileExists(atPath: outputFileString) { 657 | do { 658 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8) 659 | if output != "" { 660 | output9 = output 661 | } else { 662 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.") 663 | } 664 | } catch { 665 | print(error) 666 | } 667 | } 668 | 669 | if FileManager.default.fileExists(atPath: helpFileString) { 670 | do { 671 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8) 672 | help9 = help 673 | } catch { 674 | print(error) 675 | } 676 | } 677 | 678 | case let folderName where folderName == "10": 679 | titleFileString = "\(recipeBuilderButtonsFolder)/10/title.txt" 680 | outputFileString = "\(recipeBuilderButtonsFolder)/10/output.txt" 681 | helpFileString = "\(recipeBuilderButtonsFolder)/10/help.txt" 682 | titleFile = URL(fileURLWithPath: titleFileString) 683 | outputFile = URL(fileURLWithPath: outputFileString) 684 | helpFile = URL(fileURLWithPath: helpFileString) 685 | 686 | if FileManager.default.fileExists(atPath: titleFileString ) { 687 | do { 688 | let title = try String(contentsOf: titleFile, encoding: String.Encoding.utf8) 689 | if title != "" { 690 | title10 = title 691 | createButton10(title: title10) 692 | } else { 693 | buttonWarning (infomessage: "File: \"\(titleFileString)\" seems empty") 694 | } 695 | } catch { 696 | print(error) 697 | } 698 | } 699 | 700 | if FileManager.default.fileExists(atPath: outputFileString) { 701 | do { 702 | let output = try String(contentsOf: outputFile, encoding: String.Encoding.utf8) 703 | if output != "" { 704 | output10 = output 705 | } else { 706 | buttonWarning (infomessage: "File: \"\(outputFileString)\" seems empty.\nThis want output anything.") 707 | } 708 | } catch { 709 | print(error) 710 | } 711 | } 712 | 713 | if FileManager.default.fileExists(atPath: helpFileString) { 714 | do { 715 | let help = try String(contentsOf: helpFile, encoding: String.Encoding.utf8) 716 | help10 = help 717 | } catch { 718 | print(error) 719 | } 720 | } 721 | default: break 722 | } 723 | } 724 | 725 | } 726 | -------------------------------------------------------------------------------- /RecipeBuilder/XMLtoYaml.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Yams 3 | import AppKit 4 | 5 | 6 | // Function to parse XML into Dictionary 7 | func xmlToDictionary(xmlString: String) -> [String: Any]? { 8 | guard let data = xmlString.data(using: .utf8) else { 9 | print("Error converting XML string to Data") 10 | outputToLogView(logString: "⚠️ Error converting XML string to Data\n") 11 | return nil 12 | } 13 | 14 | do { 15 | let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) 16 | if var dictionary = plist as? [String: Any] { 17 | // Handle Process array 18 | if let processArray = dictionary["Process"] as? [[String: Any]] { 19 | let modifiedProcessArray = processArray.map { processStep -> [String: Any] in 20 | var modifiedStep = processStep 21 | 22 | // Only create Arguments if there's a Comment or if Arguments already exists 23 | if let comment = modifiedStep["Comment"] as? String { 24 | var arguments = modifiedStep["Arguments"] as? [String: Any] ?? [:] 25 | arguments["Comment"] = comment 26 | modifiedStep["Arguments"] = arguments 27 | modifiedStep.removeValue(forKey: "Comment") 28 | } 29 | 30 | return modifiedStep 31 | } 32 | dictionary["Process"] = modifiedProcessArray 33 | } 34 | return dictionary 35 | } else { 36 | print("Error: Expected dictionary but got something else") 37 | outputToLogView(logString: "⚠️ Error: Expected dictionary but got something else\n") 38 | return nil 39 | } 40 | } catch { 41 | print("Error parsing XML: \(error)") 42 | outputToLogView(logString: "⚠️ Error parsing XML: \(error)\n") 43 | return nil 44 | } 45 | } 46 | 47 | 48 | // Function to sanitize values for YAML compatibility 49 | func sanitizeValueForYAML(_ value: Any, key: String? = nil) -> Any { 50 | if let number = value as? NSNumber { 51 | if number === kCFBooleanTrue || number === kCFBooleanFalse { 52 | return number.boolValue 53 | } else { 54 | return number.intValue 55 | } 56 | } else if let string = value as? String { 57 | let nameOfKey = key ?? "" 58 | 59 | // Sub function check if string most likely are a regex 60 | func isLikelyRegex(_ string: String) -> Bool { 61 | let regexIndicators = ["[", "]", "(", ")", "{", "}", "\\", ".", "+", "*", "?", "^", "$", "|"] 62 | return regexIndicators.contains { string.contains($0) } 63 | } 64 | 65 | // Single quote all regex 66 | if (nameOfKey == "re_pattern" || nameOfKey == "asset_regex") && isLikelyRegex(string) && 67 | !(string.hasPrefix("'") && !string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) && !string.contains("'") { 68 | return "'\(string)'" 69 | } 70 | 71 | // Sub function check if string only number 72 | func onlyNumbers(_ string: String) -> Bool { 73 | return string.range(of: "^[0-9]+$", options: .regularExpression) != nil 74 | } 75 | 76 | // Single quote all numbers 77 | if onlyNumbers(string) && 78 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) { 79 | return "'\(string)'" 80 | } 81 | 82 | // Single quote if requirement contains : 83 | if nameOfKey == "requirement" && string.contains(":") && 84 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) { 85 | return "'\(string)'" 86 | } 87 | 88 | // Single quote if Developer ID Installer: 89 | if string.contains("Developer ID Installer:") && 90 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) { 91 | return "'\(string)'" 92 | } 93 | 94 | // Single quote if empty string and key find 95 | if nameOfKey == "find" && string == " " || string == ":" && 96 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) { 97 | return "'\(string)'" 98 | } 99 | 100 | // Single quote if empty string 101 | if string.isEmpty && 102 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) { 103 | return "'\(string)'" 104 | } 105 | 106 | // Single quote if key is split_on 107 | if nameOfKey == "split_on" && 108 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) { 109 | return "'\(string)'" 110 | } 111 | 112 | // If not key is 'requirement' single quote 113 | if nameOfKey != "requirement" && string.contains("%") && 114 | !(string.hasPrefix("'") && string.hasSuffix("'") && !string.dropFirst().dropLast().contains("'")) && 115 | !(string.hasPrefix("\"") && string.hasSuffix("\"") && !string.dropFirst().dropLast().contains("\"")) && 116 | !(string.hasPrefix("(") && string.hasSuffix(")") && !string.dropFirst().dropLast().contains("()")) && 117 | !string.contains("\n") && !string.contains("\r") { 118 | return "'\(string)'" 119 | } 120 | 121 | // Cleanup any warning messages or Comments messages 122 | if nameOfKey == "warning_message" || nameOfKey == "Comment" || nameOfKey == "comment" { 123 | let sanitizedString = string 124 | .trimmingCharacters(in: .whitespacesAndNewlines) 125 | .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) 126 | .replacingOccurrences(of: ": ", with: " ", options: .regularExpression) 127 | .replacingOccurrences(of: "\"", with: "", options: .regularExpression) 128 | return sanitizedString 129 | } 130 | 131 | /* Warn if string contains : - Seems not needed 132 | if string.contains(":") && 133 | !string.hasPrefix("\"") && !string.hasSuffix("\"") && 134 | !string.hasPrefix("'") && !string.hasSuffix("'") && 135 | !string.hasPrefix("(") && !string.hasSuffix(")") && 136 | !string.contains("://") { 137 | outputToLogView(logString: "⚠️ String(s) contains : might need to be single quoted:\n") 138 | outputToLogView(logString: "\(string)\n") 139 | } 140 | 141 | // Warn if string contains " 142 | if string.contains("\"") && 143 | !(string.hasPrefix("\"") && string.hasSuffix("\"")) && 144 | !(string.hasPrefix("'") && string.hasSuffix("'")) { 145 | outputToLogView(logString: "⚠️ String(s) contains double quotes but may not be properly enclosed:\n") 146 | outputToLogView(logString: "\(string)\n") 147 | } 148 | */ 149 | 150 | // If Codesignature values are empty, we set a empty requried array value [] 151 | if (nameOfKey == "expected_authority_names" || nameOfKey == "codesign_additional_arguments") && string.isEmpty { 152 | outputToLogView(logString: "⚠️ Key '\(key!)' has an empty value, inserting default empty array []\n") 153 | return "[]" 154 | } 155 | 156 | // Fix multiline Description 157 | if nameOfKey == "Description" && (string.contains("\n") || string.contains("\r")) && !string.hasPrefix(" ") { 158 | let indentedString = string.split(separator: "\n").map { line in 159 | return " \(line.trimmingCharacters(in: .whitespaces))" 160 | }.joined(separator: "\n") 161 | return indentedString 162 | } 163 | 164 | // Fix multiline file_content 165 | if nameOfKey == "file_content" || nameOfKey == "installcheck_script" || nameOfKey == "uninstall_script" || nameOfKey == "additional_install_actions" || nameOfKey == "postinstall_script" || nameOfKey == "postuninstall_script"{ 166 | // Check if the first line starts with "|" 167 | let trimmedString = string.trimmingCharacters(in: .whitespacesAndNewlines) 168 | if !trimmedString.hasPrefix("|") { 169 | // Add "|" only if it doesn't already exist 170 | let lines = string.split(separator: "\n", omittingEmptySubsequences: false) 171 | let updatedString = "|" + (lines.isEmpty ? "" : "\n") + lines.map { " \($0)" }.joined(separator: "\n") 172 | return updatedString 173 | } else if !string.hasPrefix("|") { 174 | // Ensure the first line starts with exactly " |" 175 | let lines = string.split(separator: "\n", omittingEmptySubsequences: false) 176 | let updatedString = "|" + (lines.count > 1 ? "\n" : "") + lines.dropFirst().map { " \($0)" }.joined(separator: "\n") 177 | return updatedString 178 | } 179 | } 180 | 181 | return string 182 | } else if let array = value as? [Any] { 183 | // If arrays is empty 184 | if array.isEmpty { 185 | outputToLogView(logString: "⚠️ Key '\(key ?? "")' contains an empty array, ensuring default empty array []\n") 186 | return "[]" // Replace with default empty array 187 | } 188 | // End arrays is empty 189 | return array.map { sanitizeValueForYAML($0, key: key) } 190 | } else if value is NSNull { 191 | return "" 192 | } else if let dict = value as? [String: Any] { 193 | if dict.isEmpty { 194 | return "{}" 195 | } 196 | return sanitizeDictionaryForYAML(dict) 197 | } else { 198 | return "\(value)" 199 | } 200 | } 201 | 202 | 203 | // Function to reorder the keys in a Process dictionary 204 | func reorderProcessKeys(_ processStep: [String: Any]) -> [String: Any] { 205 | var orderedStep = [String: Any]() 206 | 207 | // Always put Processor first 208 | if let processor = processStep["Processor"] { 209 | orderedStep["Processor"] = processor 210 | } 211 | 212 | // Only include Arguments if it exists and is not empty 213 | if let arguments = processStep["Arguments"] as? [String: Any], !arguments.isEmpty { 214 | orderedStep["Arguments"] = arguments 215 | } 216 | 217 | return orderedStep 218 | } 219 | 220 | 221 | // Function to sanitize and reorder the Process section for YAML 222 | func formatProcessForYAML(dictionary: [String: Any]) -> [String: Any] { 223 | var modifiedDict = dictionary 224 | if let processArray = dictionary["Process"] as? [[String: Any]] { 225 | let formattedProcessArray = processArray.map { processStep -> [String: Any] in 226 | var sanitizedStep = sanitizeDictionaryForYAML(processStep) 227 | 228 | // Move any remaining Comments to Arguments 229 | if let comment = sanitizedStep["Comment"] as? String { 230 | var arguments = sanitizedStep["Arguments"] as? [String: Any] ?? [:] 231 | arguments["Comment"] = comment 232 | sanitizedStep["Arguments"] = arguments 233 | sanitizedStep.removeValue(forKey: "Comment") 234 | } 235 | 236 | // Remove Arguments if it's empty 237 | if let arguments = sanitizedStep["Arguments"] as? [String: Any], arguments.isEmpty { 238 | sanitizedStep.removeValue(forKey: "Arguments") 239 | } 240 | 241 | return reorderProcessKeys(sanitizedStep) 242 | } 243 | modifiedDict["Process"] = formattedProcessArray 244 | } 245 | return sanitizeDictionaryForYAML(modifiedDict) 246 | } 247 | 248 | 249 | // Function to sanitize an entire dictionary 250 | func sanitizeDictionaryForYAML(_ dictionary: [String: Any]) -> [String: Any] { 251 | var sanitizedDict = [String: Any]() 252 | for (key, value) in dictionary { 253 | if let dictValue = value as? [String: Any] { 254 | sanitizedDict[key] = sanitizeDictionaryForYAML(dictValue) 255 | } else if let arrayValue = value as? [Any] { 256 | if arrayValue.isEmpty { 257 | outputToLogView(logString: "⚠️ Key '\(key)' contains an empty array, ensuring default empty array []\n") 258 | sanitizedDict[key] = [] // Replace with default empty array 259 | } else { 260 | sanitizedDict[key] = arrayValue.map { sanitizeValueForYAML($0, key: key) } 261 | } 262 | } else { 263 | sanitizedDict[key] = sanitizeValueForYAML(value, key: key) 264 | } 265 | } 266 | return sanitizedDict 267 | } 268 | 269 | 270 | // Custom function to convert Dictionary to YAML with specific key order 271 | func dictionaryToOrderedYAML(dict: [String: Any]) -> String? { 272 | do { 273 | var yamlString = "" 274 | 275 | // Ensure these keys come first 276 | let orderedKeys = ["Comment", "Description", "Identifier", "ParentRecipe", "MinimumVersion", "Input"] 277 | 278 | for key in orderedKeys { 279 | if let value = dict[key], !(value is String && (value as! String).isEmpty) { 280 | if key == "Description", let descriptionValue = value as? String { 281 | if descriptionValue.contains("\n") || descriptionValue.contains("\r") && !descriptionValue.contains(":") { 282 | let descriptionValuetrimmed = descriptionValue.trimmingCharacters(in: CharacterSet(charactersIn: "\"")) 283 | yamlString += "\(key): |\n\(descriptionValuetrimmed)\n" 284 | } else { 285 | let yamlFragment = try Yams.dump(object: [key: value]) 286 | yamlString += yamlFragment 287 | } 288 | } else { 289 | let yamlFragment = try Yams.dump(object: [key: value]) 290 | yamlString += yamlFragment.replacingOccurrences(of: "'''", with: "'") 291 | } 292 | } 293 | } 294 | 295 | // Add newline before Input section if it exists 296 | if yamlString.contains("Input:") { 297 | yamlString = yamlString.replacingOccurrences(of: "Input:", with: "\nInput:") 298 | } 299 | 300 | // Handle the Process key separately to ensure it comes last 301 | var processString = "\nProcess:\n" 302 | if let processArray = dict["Process"] as? [[String: Any]] { 303 | for processStep in processArray { 304 | if let processor = processStep["Processor"] as? String { 305 | processString += "- Processor: \(processor)\n" 306 | } 307 | 308 | if let arguments = processStep["Arguments"] as? [String: Any] { 309 | processString += " Arguments:\n" 310 | for (argKey, argValue) in arguments { 311 | let sanitizedValue = sanitizeValueForYAML(argValue, key: argKey) 312 | if argKey == "pkginfo", let pkginfoDict = argValue as? [String: Any] { 313 | // Special handling for pkginfo section 314 | processString += " pkginfo:\n" 315 | processString += formatPkginfoSection(pkginfoDict, indent: 6) 316 | } else if let arrayValue = sanitizedValue as? [Any] { 317 | processString += " \(argKey):\n" 318 | if arrayValue.isEmpty { 319 | processString += " []\n" 320 | } else { 321 | for item in arrayValue { 322 | if let str = item as? String { 323 | processString += " - \(str)\n" 324 | } else if let nestedDict = item as? [String: Any] { 325 | processString += " -\n" 326 | for (nestedKey, nestedValue) in nestedDict { 327 | processString += " \(nestedKey): \(nestedValue)\n" 328 | } 329 | } 330 | } 331 | } 332 | } else if let dictValue = sanitizedValue as? [String: Any] { 333 | if dictValue.isEmpty { 334 | processString += " \(argKey): {}\n" 335 | } else { 336 | processString += " \(argKey):\n" 337 | for (dictKey, dictItem) in dictValue { 338 | if let nestedArray = dictItem as? [[String: Any]] { 339 | processString += " \(dictKey):\n" 340 | if nestedArray.isEmpty { 341 | processString += " []\n" 342 | } else { 343 | for item in nestedArray { 344 | processString += " -\n" 345 | for (k, v) in item { 346 | processString += " \(k): \(v)\n" 347 | } 348 | } 349 | } 350 | } else { 351 | processString += " \(dictKey): \(dictItem)\n" 352 | } 353 | } 354 | } 355 | } else { 356 | processString += " \(argKey): \(sanitizedValue)\n" 357 | } 358 | } 359 | } 360 | processString += "\n" 361 | } 362 | } else { 363 | processString += " []" 364 | } 365 | 366 | // Add any remaining keys that were not explicitly handled 367 | for (key, value) in dict { 368 | if !orderedKeys.contains(key) && key != "Process" && key != "TopComment" && !(value is String && (value as! String).isEmpty) { 369 | let yamlFragment = try Yams.dump(object: [key: value]) 370 | yamlString += yamlFragment 371 | } 372 | } 373 | 374 | // Append the Process section at the end 375 | yamlString += processString 376 | 377 | return yamlString 378 | } catch { 379 | print("Error converting Dictionary to YAML: \(error)") 380 | outputToLogView(logString: "⚠️ Error converting Dictionary to YAML: \(error)\n") 381 | return nil 382 | } 383 | } 384 | 385 | // Helper function to format pkginfo sections 386 | func formatPkginfoSection(_ pkginfo: [String: Any], indent: Int) -> String { 387 | var result = "" 388 | let indentStr = String(repeating: " ", count: indent) 389 | 390 | // Define the order of keys we want to process first 391 | let orderedKeys = ["name", "display_name", "description", "version", "catalogs", "category", "developer"] 392 | 393 | // Process ordered keys first 394 | for key in orderedKeys { 395 | if let value = pkginfo[key] { 396 | if let arrayValue = value as? [Any] { 397 | result += "\(indentStr)\(key):\n" 398 | if arrayValue.isEmpty { 399 | result += "\(indentStr) []\n" 400 | } else { 401 | for item in arrayValue { 402 | result += "\(indentStr) - \(item)\n" 403 | } 404 | } 405 | } else { 406 | result += "\(indentStr)\(key): \(value)\n" 407 | } 408 | } 409 | } 410 | 411 | // Process remaining keys 412 | for (key, value) in pkginfo { 413 | if !orderedKeys.contains(key) { 414 | if let arrayValue = value as? [Any] { 415 | result += "\(indentStr)\(key):\n" 416 | if arrayValue.isEmpty { 417 | result += "\(indentStr) []\n" 418 | } else { 419 | for item in arrayValue { 420 | result += "\(indentStr) - \(item)\n" 421 | } 422 | } 423 | } else { 424 | result += "\(indentStr)\(key): \(value)\n" 425 | } 426 | } 427 | } 428 | 429 | return result 430 | } 431 | 432 | 433 | // Function to process XML and output YAML 434 | func xmlToFormattedYAML(xmlString: String) -> String? { 435 | guard let dictionary = xmlToDictionary(xmlString: xmlString) else { 436 | return nil 437 | } 438 | let formattedDict = formatProcessForYAML(dictionary: dictionary) 439 | return dictionaryToOrderedYAML(dict: formattedDict) 440 | } 441 | 442 | 443 | // Function to clean some remaining nested arrays 444 | func cleanRemainingArrays(_ yamlString: String) -> String { 445 | let targetPatterns = [ 446 | "catalogs:", 447 | "supported_architectures:", 448 | "requires:", 449 | "update_for:" 450 | ] 451 | 452 | let lines = yamlString.split(separator: "\n", omittingEmptySubsequences: false) 453 | var processedLines = [String]() 454 | 455 | for line in lines { 456 | var updatedLine = String(line) 457 | 458 | // Check if the line matches any of the target patterns 459 | if targetPatterns.contains(where: { updatedLine.trimmingCharacters(in: .whitespaces).hasPrefix($0) }) { 460 | // Remove escaped single quotes and create Yaml Array 461 | updatedLine = updatedLine.replacingOccurrences(of: "\\'", with: "") 462 | updatedLine = updatedLine.replacingOccurrences(of: "[", with: "\n - ") 463 | updatedLine = updatedLine.replacingOccurrences(of: "]", with: "") 464 | } 465 | 466 | processedLines.append(updatedLine) 467 | } 468 | 469 | return processedLines.joined(separator: "\n") 470 | } 471 | 472 | // Main function to convert XML to YAML and output to UI 473 | func XMLtoYaml() { 474 | let wholeDocument = (appDelegate().outputTextField.textStorage as NSAttributedString?)!.string 475 | if wholeDocument.isEmpty { return } 476 | 477 | // Clear Set of error message 478 | loggedMessages.removeAll() 479 | 480 | if var yamlOutput = xmlToFormattedYAML(xmlString: wholeDocument) { 481 | /*/ Ensure Process header exists even if empty 482 | if !yamlOutput.contains("Process:") { 483 | yamlOutput += "\nProcess: []\n" 484 | } 485 | */ 486 | 487 | // Workaround for some nested arrays that not get "real" YAML format 488 | yamlOutput = cleanRemainingArrays(yamlOutput) 489 | 490 | appDelegate().outputTextField.string = "" 491 | highlightr!.setTheme(to: "xcode") 492 | highlightr!.theme.codeFont = NSFont(name: "Menlo", size: 12) 493 | let highlightedCode = highlightr!.highlight(yamlOutput, as: "yaml")! 494 | appDelegate().outputTextField.textStorage?.insert(highlightedCode, at: 0) 495 | 496 | // Function to highlight logged messages in the text field 497 | func highlightLoggedMessages() { 498 | // Convert Set to Array 499 | let loggedMessagesArray = Array(loggedMessages) 500 | 501 | // Get the entire text in the NSTextView 502 | guard let wholeDocument = appDelegate().outputTextField.string as NSString? else { return } 503 | 504 | // Iterate over each logged message and apply the highlighting 505 | for string in loggedMessagesArray { 506 | let range = wholeDocument.range(of: string) 507 | 508 | // Ensure the identifier exists in the text 509 | if range.location != NSNotFound { 510 | // Replace the matched text with attributed text 511 | let attributedReplaceText = NSAttributedString(string: string, attributes: yellowBackgroundAttributes) 512 | appDelegate().outputTextField.textStorage?.replaceCharacters(in: range, with: attributedReplaceText) 513 | } 514 | } 515 | } 516 | 517 | // Highlight the logged messages in the text view 518 | highlightLoggedMessages() 519 | 520 | // If we run multiple then we need a check for this 521 | if loggedMessages.isEmpty { 522 | outputToLogView(logString: "✅ No problems found\n") 523 | } 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /RecipeBuilder/XMLtoYamlSnippets.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Yams 3 | import AppKit 4 | 5 | // Snippets functions 6 | // Function to convert XML snippet to Dictionary 7 | func xmlSnippetToDictionary(xmlString: String) -> [String: Any]? { 8 | guard let data = xmlString.data(using: .utf8) else { 9 | print("Error converting XML string to Data") 10 | outputToLogView (logString: "\n⚠️ Error converting XML string to Data\n") 11 | return nil 12 | } 13 | 14 | do { 15 | let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) 16 | if let dictionary = plist as? [String: Any] { 17 | return dictionary 18 | } else { 19 | print("Error: Expected dictionary but got something else") 20 | outputToLogView (logString: "\n⚠️ Error: Expected dictionary but got something else\n") 21 | return nil 22 | } 23 | } catch { 24 | print("Error parsing XML: \(error)") 25 | outputToLogView (logString: "\n⚠️ Error parsing XML: \(error)\n") 26 | return nil 27 | } 28 | } 29 | 30 | // Function to make dictionary values YAML-compatible 31 | func makeYAMLCompatibleSnippet(_ value: Any) -> Any { 32 | if let dict = value as? [String: Any] { 33 | var yamlCompatibleDict = [String: Any]() 34 | for (key, value) in dict { 35 | yamlCompatibleDict[key] = makeYAMLCompatibleSnippet(value) 36 | } 37 | return yamlCompatibleDict 38 | } else if let array = value as? [Any] { 39 | return array.map { makeYAMLCompatibleSnippet($0) } 40 | } else { 41 | return String(describing: value) 42 | } 43 | } 44 | 45 | // Function to reorder the keys in the dictionary to ensure `Processor` comes first 46 | func reorderProcessKeysSnippet(_ processStep: [String: Any]) -> [String: Any] { 47 | var orderedStep = [String: Any]() 48 | // Ensure Processor comes first 49 | if let processor = processStep["Processor"] as? String { 50 | orderedStep["Processor"] = processor 51 | } 52 | // Then Arguments 53 | if let arguments = processStep["Arguments"] as? [String: Any] { 54 | orderedStep["Arguments"] = arguments 55 | } 56 | // Add any other keys 57 | for (key, value) in processStep where key != "Processor" && key != "Arguments" { 58 | orderedStep[key] = value 59 | } 60 | return orderedStep 61 | } 62 | 63 | // Custom function to convert Dictionary to Ordered YAML 64 | func dictionaryToOrderedYAMLSnippet(dict: [String: Any]) -> String { 65 | var yamlString = "" 66 | 67 | // Ensure Processor comes first 68 | if let processor = dict["Processor"] { 69 | let yamlFragment = (try? Yams.dump(object: ["Processor": processor])) ?? "" 70 | yamlString += yamlFragment 71 | } 72 | 73 | // Then Arguments 74 | if let arguments = dict["Arguments"] { 75 | let yamlFragment = (try? Yams.dump(object: ["Arguments": arguments])) ?? "" 76 | yamlString += yamlFragment 77 | } 78 | 79 | // Add any other keys 80 | for (key, value) in dict where key != "Processor" && key != "Arguments" { 81 | let yamlFragment = (try? Yams.dump(object: [key: value])) ?? "" 82 | yamlString += yamlFragment 83 | } 84 | 85 | return yamlString 86 | } 87 | 88 | // Function to convert XML `` snippet into a YAML list 89 | func xmlDictSnippetToYAMLList(xmlSnippet: String) -> String? { 90 | guard let parsedDict = xmlSnippetToDictionary(xmlString: xmlSnippet) else { 91 | return nil 92 | } 93 | 94 | // Reorder the parsed dictionary to ensure `Processor` comes first 95 | let reorderedDict = reorderProcessKeysSnippet(parsedDict) 96 | 97 | // Wrap the dictionary in an array for YAML list formatting 98 | let yamlCompatibleObject = makeYAMLCompatibleSnippet([reorderedDict]) 99 | 100 | if let list = yamlCompatibleObject as? [[String: Any]] { 101 | var yamlString = "" 102 | for item in list { 103 | let itemYAML = dictionaryToOrderedYAMLSnippet(dict: item) 104 | yamlString += "- " + itemYAML.replacingOccurrences(of: "\n", with: "\n ").trimmingCharacters(in: .whitespacesAndNewlines) + "\n\n" 105 | } 106 | return yamlString 107 | } 108 | return nil 109 | } 110 | 111 | -------------------------------------------------------------------------------- /RecipeBuilder/YamlToXML.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Yams 3 | import AppKit 4 | 5 | // Yaml to XML Functions 6 | // Function to convert Dictionary to XML (Apple plist format) 7 | func dictionaryToXML(dict: [String: Any]) -> String? { 8 | do { 9 | let data = try PropertyListSerialization.data(fromPropertyList: dict, format: .xml, options: 0) 10 | return String(data: data, encoding: .utf8) 11 | } catch { 12 | print("Error converting Dictionary to XML: \(error)") 13 | outputToLogView (logString: "⚠️ Error converting Dictionary to XML: \(error)\n") 14 | return nil 15 | } 16 | } 17 | 18 | 19 | // Function to parse YAML into Dictionary 20 | func yamlToDictionary(yamlString: String) -> [String: Any]? { 21 | do { 22 | if let dictionary = try Yams.load(yaml: yamlString) as? [String: Any] { 23 | return dictionary 24 | } else { 25 | print("Error: YAML did not convert to Dictionary") 26 | outputToLogView (logString: "⚠️ Error: YAML did not convert to Dictionary\n") 27 | return nil 28 | } 29 | } catch { 30 | print("Error parsing YAML: \(error)") 31 | outputToLogView (logString: "⚠️ Error parsing YAML: \(error)\n") 32 | return nil 33 | } 34 | } 35 | 36 | 37 | // Function to convert YAML to XML 38 | func yamlToXML(yamlString: String) -> String? { 39 | if let dictionary = yamlToDictionary(yamlString: yamlString) { 40 | return dictionaryToXML(dict: dictionary) 41 | } 42 | return nil 43 | } 44 | 45 | 46 | // Output the converted Yaml as XML 47 | func YamlToXML() { 48 | var yamlDocument = (appDelegate().outputTextField.textStorage as NSAttributedString?)!.string 49 | if yamlDocument == "" {return } 50 | 51 | // Clear Set of error message 52 | loggedMessages.removeAll() 53 | 54 | // Replace empty array from YAML with empty value for XML [] if existing 55 | yamlDocument = yamlDocument.replacingOccurrences( 56 | of: #"(?m)(?<=\bProcess:)\s*(?=\n\s*\n|\z)"#, 57 | with: " []", 58 | options: [.regularExpression, .caseInsensitive] 59 | ) 60 | if let xmlOutput = yamlToXML(yamlString: yamlDocument) { 61 | appDelegate().outputTextField.string = "" 62 | highlightr!.setTheme(to: "xcode") 63 | highlightr!.theme.codeFont = NSFont(name: "Menlo", size: 12) 64 | let highlightedCode = highlightr!.highlight(xmlOutput, as: "xml")! 65 | appDelegate().outputTextField.textStorage?.insert(highlightedCode, at: 0) 66 | } 67 | 68 | // If we run multiple then we need a check for this 69 | if loggedMessages.isEmpty { 70 | outputToLogView(logString: "✅ No problems found\n") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /images/recipeBuildericon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/images/recipeBuildericon.png -------------------------------------------------------------------------------- /images/recipebuilderinterface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaellofgren/RecipeBuilder/c485cf9e1a502700c4e76518c1987611fa0421c7/images/recipebuilderinterface.png --------------------------------------------------------------------------------