diff --git a/.github/workflows/build_pr.yml b/.github/workflows/build_pr.yml index 7e17075..3b9ccc8 100644 --- a/.github/workflows/build_pr.yml +++ b/.github/workflows/build_pr.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Install app signing certificates uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0 diff --git a/.gitignore b/.gitignore index 66850c8..d3a0610 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ DerivedData/ xcuserdata/ output/ + +# Ignore LLM skills +CLAUDE.md diff --git a/AutoPkg Wizard/AutoPkg Wizard.xcodeproj/project.pbxproj b/AutoPkg Wizard/AutoPkg Wizard.xcodeproj/project.pbxproj index fb91842..ca43744 100644 --- a/AutoPkg Wizard/AutoPkg Wizard.xcodeproj/project.pbxproj +++ b/AutoPkg Wizard/AutoPkg Wizard.xcodeproj/project.pbxproj @@ -410,7 +410,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = ""; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; @@ -423,7 +423,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = "com.grahamrpugh.AutoPkg-Wizard"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -444,7 +444,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_TEAM = ""; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; @@ -457,7 +457,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.2.0; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.grahamrpugh.AutoPkg-Wizard"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/AutoPkg Wizard/AutoPkg Wizard/Services/AutoPkgCLI.swift b/AutoPkg Wizard/AutoPkg Wizard/Services/AutoPkgCLI.swift index ec96f70..c8daecb 100644 --- a/AutoPkg Wizard/AutoPkg Wizard/Services/AutoPkgCLI.swift +++ b/AutoPkg Wizard/AutoPkg Wizard/Services/AutoPkgCLI.swift @@ -175,9 +175,11 @@ final class AutoPkgCLI { // MARK: - Override Operations - /// Create a recipe override - func makeOverride(_ recipeName: String) async throws -> String { - try await runSimple(arguments: ["make-override", recipeName]) + /// Create a recipe override, optionally with a custom output name + func makeOverride(_ recipeName: String, name: String? = nil) async throws -> String { + var args = ["make-override", recipeName] + if let name { args += ["--name", name] } + return try await runSimple(arguments: args) } /// Verify trust info for a recipe override diff --git a/AutoPkg Wizard/AutoPkg Wizard/ViewModels/OverridesViewModel.swift b/AutoPkg Wizard/AutoPkg Wizard/ViewModels/OverridesViewModel.swift index 1166aab..3f35d89 100644 --- a/AutoPkg Wizard/AutoPkg Wizard/ViewModels/OverridesViewModel.swift +++ b/AutoPkg Wizard/AutoPkg Wizard/ViewModels/OverridesViewModel.swift @@ -234,8 +234,46 @@ final class OverridesViewModel { showError = true } } + + /// Rename the override file to use `newRecipeName` as the base name, preserving the extension. + func renameOverride(_ override: AutoPkgOverride, newRecipeName: String) { + let trimmed = newRecipeName.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty, trimmed != override.recipeName else { return } + + let ext: String + if override.fileName.hasSuffix(".recipe.yaml") { ext = ".recipe.yaml" } + else if override.fileName.hasSuffix(".recipe.plist") { ext = ".recipe.plist" } + else { ext = ".recipe" } + + let expandedDir = (NSString(string: cli.overridesDirectory).expandingTildeInPath as NSString) + let newFileName = trimmed + ext + let newPath = expandedDir.appendingPathComponent(newFileName) + + do { + try FileManager.default.moveItem(atPath: override.filePath, toPath: newPath) + } catch { + errorMessage = "Failed to rename override: \(error.localizedDescription)" + showError = true + return + } + + let oldName = override.recipeName + loadOverrides() + // Re-select the renamed file + if let updated = overrides.first(where: { $0.fileName == newFileName }) { + selectOverride(updated) + } + + NotificationCenter.default.post( + name: .overrideWasRenamed, + object: nil, + userInfo: ["oldName": oldName, "newName": trimmed] + ) + NotificationCenter.default.post(name: .overridesDidChange, object: nil) + } } extension Notification.Name { static let overridesDidChange = Notification.Name("overridesDidChange") + static let overrideWasRenamed = Notification.Name("overrideWasRenamed") } diff --git a/AutoPkg Wizard/AutoPkg Wizard/ViewModels/RecipesViewModel.swift b/AutoPkg Wizard/AutoPkg Wizard/ViewModels/RecipesViewModel.swift index 17f2d60..a23346c 100644 --- a/AutoPkg Wizard/AutoPkg Wizard/ViewModels/RecipesViewModel.swift +++ b/AutoPkg Wizard/AutoPkg Wizard/ViewModels/RecipesViewModel.swift @@ -30,9 +30,25 @@ final class RecipesViewModel { /// Set of recipe names that have an existing override (stripped, lowercased for matching) var existingOverrides: Set = [] + /// Full list of override files (used to compute overridesNotInList) + var overridesList: [AutoPkgOverride] = [] /// The recipe currently having an override created var creatingOverrideFor: String? + /// Overrides whose recipe name is not present in the current recipe list. + var overridesNotInList: [AutoPkgOverride] { + overridesList.filter { !isInRecipeList($0.recipeName) } + } + + // MARK: - Make-Override Sheet State + + /// The parent recipe name for which we're about to create an override + var pendingOverrideParent: String? + /// Whether to add the (possibly renamed) override name to the recipe list after creation + var pendingOverrideAddToList: Bool = false + /// Whether the make-override naming sheet is visible + var showMakeOverrideSheet: Bool = false + // MARK: - Recipe List Management /// Strip recipe file suffixes (.recipe, .recipe.yaml, .recipe.plist) from a name. @@ -84,6 +100,11 @@ final class RecipesViewModel { guard !stripped.isEmpty, !isInRecipeList(stripped) else { return } recipeList.append(stripped) saveRecipeList() + if UserDefaults.standard.bool(forKey: "autoCreateOverride"), !hasOverride(stripped) { + pendingOverrideParent = stripped + pendingOverrideAddToList = true + showMakeOverrideSheet = true + } } func removeRecipes(at offsets: IndexSet) { @@ -108,6 +129,7 @@ final class RecipesViewModel { /// Scan the overrides directory and build the set of recipe names that have overrides. func loadOverrides() { let overrides = AutoPkgOverride.listOverrides(in: cli.overridesDirectory) + overridesList = overrides existingOverrides = Set(overrides.map { $0.recipeName.lowercased() }) } @@ -116,20 +138,47 @@ final class RecipesViewModel { existingOverrides.contains(recipeName.lowercased()) } - /// Create an override for the given recipe. - func makeOverride(_ recipeName: String) async { - creatingOverrideFor = recipeName + /// Show the naming sheet before creating an override from the recipe list button. + func requestMakeOverride(_ recipeName: String) { + pendingOverrideParent = recipeName + pendingOverrideAddToList = false + showMakeOverrideSheet = true + } + + /// Create an override for `parentRecipeName` using `overrideName` as the output name. + /// If `updateListEntry` is true, the recipe list entry for `parentRecipeName` is replaced + /// with `overrideName` (used when auto-creating on add, so the list tracks the override). + func makeOverride(parent parentRecipeName: String, overrideName: String, updateListEntry: Bool) async { + let name = overrideName.trimmingCharacters(in: .whitespacesAndNewlines) + guard !name.isEmpty else { return } + creatingOverrideFor = parentRecipeName do { - _ = try await cli.makeOverride(recipeName) + let customName = (name == parentRecipeName) ? nil : name + _ = try await cli.makeOverride(parentRecipeName, name: customName) + if updateListEntry, name != parentRecipeName, + let idx = recipeList.firstIndex(of: parentRecipeName) { + recipeList[idx] = name + saveRecipeList() + } loadOverrides() NotificationCenter.default.post(name: .overridesDidChange, object: nil) } catch { - errorMessage = "Failed to create override for \(recipeName): \(error.localizedDescription)" + errorMessage = "Failed to create override for \(parentRecipeName): \(error.localizedDescription)" showError = true } creatingOverrideFor = nil } + /// Called when an override file is renamed in the Overrides pane. + func handleOverrideRenamed(oldName: String, newName: String) { + guard oldName != newName else { return } + if let idx = recipeList.firstIndex(of: oldName) { + recipeList[idx] = newName + saveRecipeList() + } + loadOverrides() + } + // MARK: - MakeCatalogs.munki management /// If any .munki recipes are in the list, ensure MakeCatalogs.munki is diff --git a/AutoPkg Wizard/AutoPkg Wizard/Views/ArgumentsView.swift b/AutoPkg Wizard/AutoPkg Wizard/Views/ArgumentsView.swift index 74cd334..b00f37b 100644 --- a/AutoPkg Wizard/AutoPkg Wizard/Views/ArgumentsView.swift +++ b/AutoPkg Wizard/AutoPkg Wizard/Views/ArgumentsView.swift @@ -214,7 +214,11 @@ struct ArgumentsView: View { panel.allowsMultipleSelection = false panel.canChooseDirectories = false panel.canChooseFiles = true + if let lastDir = UserDefaults.standard.string(forKey: "lastSourcePackageDirectory") { + panel.directoryURL = URL(fileURLWithPath: lastDir) + } if panel.runModal() == .OK, let url = panel.url { + UserDefaults.standard.set(url.deletingLastPathComponent().path, forKey: "lastSourcePackageDirectory") viewModel.addSourcePackage(url.path) } } diff --git a/AutoPkg Wizard/AutoPkg Wizard/Views/OverridesView.swift b/AutoPkg Wizard/AutoPkg Wizard/Views/OverridesView.swift index 2fc9865..e61ee39 100644 --- a/AutoPkg Wizard/AutoPkg Wizard/Views/OverridesView.swift +++ b/AutoPkg Wizard/AutoPkg Wizard/Views/OverridesView.swift @@ -4,6 +4,8 @@ struct OverridesView: View { @State private var viewModel = OverridesViewModel() @State private var isEditing = false @State private var selectedOverrides: Set = [] + @State private var isRenamingOverride = false + @State private var renameText = "" @Environment(\.colorScheme) private var colorScheme var body: some View { @@ -46,6 +48,9 @@ struct OverridesView: View { .onReceive(NotificationCenter.default.publisher(for: .overridesDidChange)) { _ in viewModel.loadOverrides() } + .onChange(of: viewModel.selectedOverride) { _, _ in + isRenamingOverride = false + } .alert("Error", isPresented: $viewModel.showError) { Button("OK", role: .cancel) {} } message: { @@ -146,8 +151,8 @@ struct OverridesView: View { Group { if let override = viewModel.selectedOverride { VStack(alignment: .leading, spacing: 8) { - Text(override.fileName) - .font(.headline) + // Rename header + overrideNameHeader(for: override) .padding(.horizontal) .padding(.top, 8) @@ -239,6 +244,46 @@ struct OverridesView: View { } } + @ViewBuilder + private func overrideNameHeader(for override: AutoPkgOverride) -> some View { + if isRenamingOverride { + HStack { + TextField("Override name", text: $renameText) + .textFieldStyle(.roundedBorder) + .onSubmit { commitRename(override) } + Button { + commitRename(override) + } label: { + Image(systemName: "checkmark.circle.fill").foregroundStyle(.green) + } + .buttonStyle(.plain) + .disabled(renameText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + Button { + isRenamingOverride = false + } label: { + Image(systemName: "xmark.circle.fill").foregroundStyle(.secondary) + } + .buttonStyle(.plain) + } + } else { + HStack(spacing: 6) { + Text(override.recipeName) + .font(.headline) + Text(override.fileName) + .font(.caption) + .foregroundStyle(.secondary) + Button { + renameText = override.recipeName + isRenamingOverride = true + } label: { + Image(systemName: "pencil").foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .help("Rename override") + } + } + } + @ViewBuilder private func trustBadge(_ state: OverridesViewModel.TrustState) -> some View { HStack(spacing: 4) { @@ -264,6 +309,13 @@ struct OverridesView: View { } } + private func commitRename(_ override: AutoPkgOverride) { + let name = renameText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !name.isEmpty else { return } + isRenamingOverride = false + viewModel.renameOverride(override, newRecipeName: name) + } + private func deleteSelectedOverrides() { let toDelete = viewModel.overrides.filter { selectedOverrides.contains($0.id) } for override in toDelete { viewModel.deleteOverride(override) } diff --git a/AutoPkg Wizard/AutoPkg Wizard/Views/RecipesView.swift b/AutoPkg Wizard/AutoPkg Wizard/Views/RecipesView.swift index d32193f..60ebae5 100644 --- a/AutoPkg Wizard/AutoPkg Wizard/Views/RecipesView.swift +++ b/AutoPkg Wizard/AutoPkg Wizard/Views/RecipesView.swift @@ -7,7 +7,7 @@ struct RecipesView: View { var body: some View { VStack(spacing: 0) { - if viewModel.recipeList.isEmpty { + if viewModel.recipeList.isEmpty && viewModel.overridesNotInList.isEmpty { ContentUnavailableView( "No Recipes in List", systemImage: "list.bullet.rectangle", @@ -59,6 +59,17 @@ struct RecipesView: View { .onReceive(NotificationCenter.default.publisher(for: .overridesDidChange)) { _ in viewModel.loadOverrides() } + .onReceive(NotificationCenter.default.publisher(for: .overrideWasRenamed)) { note in + if let oldName = note.userInfo?["oldName"] as? String, + let newName = note.userInfo?["newName"] as? String { + viewModel.handleOverrideRenamed(oldName: oldName, newName: newName) + } + } + .sheet(isPresented: $viewModel.showMakeOverrideSheet) { + if let parent = viewModel.pendingOverrideParent { + MakeOverrideSheet(viewModel: viewModel, parentRecipeName: parent) + } + } .sheet(isPresented: $viewModel.showAddSheet) { AddRecipeSheet(viewModel: viewModel) } @@ -78,6 +89,7 @@ struct RecipesView: View { private var recipeListView: some View { VStack(spacing: 0) { List { + if !viewModel.recipeList.isEmpty { Section { ForEach(viewModel.recipeList, id: \.self) { recipe in HStack { @@ -108,7 +120,7 @@ struct RecipesView: View { .controlSize(.small) } else { Button { - Task { await viewModel.makeOverride(recipe) } + viewModel.requestMakeOverride(recipe) } label: { Image(systemName: "document.on.document") .foregroundStyle(viewModel.hasOverride(recipe) ? .green.opacity(0.6) : .secondary) @@ -165,6 +177,25 @@ struct RecipesView: View { .foregroundStyle(.tertiary) } } + } // end if !viewModel.recipeList.isEmpty + + if !viewModel.overridesNotInList.isEmpty { + Section { + ForEach(viewModel.overridesNotInList) { override in + HStack { + RecipeRow(name: override.recipeName) + Spacer() + Button("Add to List") { + viewModel.addRecipe(override.recipeName) + } + .buttonStyle(.bordered) + .controlSize(.small) + } + } + } header: { + Text("Recipe Overrides not in Recipe List") + } + } } if isEditing { @@ -601,3 +632,70 @@ struct InputValueLeaf: View { .textSelection(.enabled) } } + +// MARK: - Make Override Sheet + +struct MakeOverrideSheet: View { + @Bindable var viewModel: RecipesViewModel + let parentRecipeName: String + + @State private var overrideName: String = "" + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text("Create Recipe Override").font(.headline) + + Text("Parent recipe: \(parentRecipeName)") + .font(.subheadline) + .foregroundStyle(.secondary) + + Divider() + + VStack(alignment: .leading, spacing: 4) { + Text("Override name") + .font(.caption) + .foregroundStyle(.secondary) + TextField("Override name (e.g. MyOrg-Firefox.munki)", text: $overrideName) + .textFieldStyle(.roundedBorder) + .onSubmit { submit() } + Text("This becomes the filename and recipe list entry. Defaults to the parent recipe name.") + .font(.caption2) + .foregroundStyle(.tertiary) + } + + HStack { + Button("Cancel") { + viewModel.showMakeOverrideSheet = false + } + .keyboardShortcut(.cancelAction) + + Spacer() + + Button("Create Override") { + submit() + } + .buttonStyle(.borderedProminent) + .keyboardShortcut(.defaultAction) + .disabled(overrideName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + } + .padding(20) + .frame(width: 440) + .onAppear { + overrideName = parentRecipeName + } + } + + private func submit() { + let name = overrideName.trimmingCharacters(in: .whitespacesAndNewlines) + guard !name.isEmpty else { return } + viewModel.showMakeOverrideSheet = false + Task { + await viewModel.makeOverride( + parent: parentRecipeName, + overrideName: name, + updateListEntry: viewModel.pendingOverrideAddToList + ) + } + } +} diff --git a/AutoPkg Wizard/AutoPkg Wizard/Views/SettingsView.swift b/AutoPkg Wizard/AutoPkg Wizard/Views/SettingsView.swift index 43dc0ef..1ced53d 100644 --- a/AutoPkg Wizard/AutoPkg Wizard/Views/SettingsView.swift +++ b/AutoPkg Wizard/AutoPkg Wizard/Views/SettingsView.swift @@ -6,6 +6,7 @@ struct SettingsView: View { @State private var recipeListPath: String = AutoPkgCLI.shared.recipeListPath @State private var overridesDirectory: String = AutoPkgCLI.shared.overridesDirectory @State private var isSaved = false + @AppStorage("autoCreateOverride") private var autoCreateOverride = false @Bindable private var themeManager = SyntaxThemeManager.shared private var cli = AutoPkgCLI.shared @@ -21,7 +22,7 @@ struct SettingsView: View { aboutTab .tabItem { Label("About", systemImage: "info.circle") } } - .frame(width: 500, height: 380) + .frame(width: 500, height: 430) } // MARK: - General Tab @@ -29,18 +30,21 @@ struct SettingsView: View { private var generalTab: some View { Form { Section("AutoPkg Binary") { - HStack { - TextField("Path to autopkg", text: $autoPkgPath) - .textFieldStyle(.roundedBorder) - - Button("Browse…") { - let panel = NSOpenPanel() - panel.canChooseFiles = true - panel.canChooseDirectories = false - panel.allowsMultipleSelection = false - panel.directoryURL = URL(fileURLWithPath: "/usr/local/bin") - if panel.runModal() == .OK, let url = panel.url { - autoPkgPath = url.path + VStack(alignment: .leading, spacing: 4) { + HStack { + TextField("", text: $autoPkgPath) + .textFieldStyle(.roundedBorder) + + Button("Browse…") { + let panel = NSOpenPanel() + panel.canChooseFiles = true + panel.canChooseDirectories = false + panel.allowsMultipleSelection = false + let expanded = NSString(string: autoPkgPath).expandingTildeInPath + panel.directoryURL = URL(fileURLWithPath: expanded).deletingLastPathComponent() + if panel.runModal() == .OK, let url = panel.url { + autoPkgPath = url.path + } } } } @@ -57,35 +61,47 @@ struct SettingsView: View { } Section("Recipe List File") { - HStack { - TextField("Recipe list path", text: $recipeListPath) - .textFieldStyle(.roundedBorder) - - Button("Browse…") { - let panel = NSOpenPanel() - panel.canChooseFiles = true - panel.canChooseDirectories = false - panel.allowsMultipleSelection = false - panel.allowedContentTypes = [.plainText] - if panel.runModal() == .OK, let url = panel.url { - recipeListPath = url.path + VStack(alignment: .leading, spacing: 4) { + HStack { + TextField("", text: $recipeListPath) + .textFieldStyle(.roundedBorder) + + Button("Browse…") { + let panel = NSOpenPanel() + panel.canChooseFiles = true + panel.canChooseDirectories = false + panel.allowsMultipleSelection = false + panel.allowedContentTypes = [.plainText] + let expanded = NSString(string: recipeListPath).expandingTildeInPath + panel.directoryURL = URL(fileURLWithPath: expanded).deletingLastPathComponent() + if panel.runModal() == .OK, let url = panel.url { + recipeListPath = url.path + } } } } } + Section("Recipe Options") { + Toggle("Automatically create override when adding a recipe", isOn: $autoCreateOverride) + } + Section("Recipe Overrides Directory") { - HStack { - TextField("Overrides directory", text: $overridesDirectory) - .textFieldStyle(.roundedBorder) - - Button("Browse…") { - let panel = NSOpenPanel() - panel.canChooseFiles = false - panel.canChooseDirectories = true - panel.allowsMultipleSelection = false - if panel.runModal() == .OK, let url = panel.url { - overridesDirectory = url.path + VStack(alignment: .leading, spacing: 4) { + HStack { + TextField("", text: $overridesDirectory) + .textFieldStyle(.roundedBorder) + + Button("Browse…") { + let panel = NSOpenPanel() + panel.canChooseFiles = false + panel.canChooseDirectories = true + panel.allowsMultipleSelection = false + let expanded = NSString(string: overridesDirectory).expandingTildeInPath + panel.directoryURL = URL(fileURLWithPath: expanded) + if panel.runModal() == .OK, let url = panel.url { + overridesDirectory = url.path + } } } } @@ -103,6 +119,7 @@ struct SettingsView: View { autoPkgPath = "/usr/local/bin/autopkg" recipeListPath = NSString(string: "~/Library/AutoPkg/recipe-list.txt").expandingTildeInPath overridesDirectory = NSString(string: "~/Library/AutoPkg/RecipeOverrides").expandingTildeInPath + autoCreateOverride = false } Button("Save") { diff --git a/README.md b/README.md index 53c12f9..143c51c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ You can build a recipe list within the app by searching for recipes. Once a reci Overrides can be edited within the app, or deleted. You can also amend the autopkg run verbosity, and add pre- and post-processors which will apply to all recipes. You can also add key-value pairs, and supply paths to packages for recipes that require them. These options would also apply to all recipes, but as most would be suitable for running individual recipes in the list, you can enable or disable each one without deleting them. -Finally, you can schedule an automatic daily run of your recipe list, which creates a Launch Agent. +Finally, you can schedule an automatic daily run of your recipe list, which creates a Launch Agent. ## Requirements @@ -44,6 +44,7 @@ The landing page shows a dashboard with counts of repos, recipes, and overrides, - Manage a recipe list file (default: `~/Library/AutoPkg/recipe-list.txt`) - Add recipes from locally available recipes, GitHub search, or manual entry +- Recipe Overrides that are not in the recipe list are also listed and can be added to the list - When adding a recipe from search, the required repo is automatically added if not already installed - Drag to reorder and edit mode with multi-select for bulk removal - Run individual recipes or the entire list, with real-time streaming log output @@ -90,6 +91,14 @@ The landing page shows a dashboard with counts of repos, recipes, and overrides, - Enable/disable the schedule with automatic LaunchAgent management - View next scheduled run time, agent status, and last run time +### Settings + +- Choose an alternative path to the `autopkg` binary +- Choose an alternative Recipe List file +- Choose to automatically create an override when adding a recipe to the recipe list +- Choose an alternative Recipe Overrides directory +- Restore defaults + ## Building Build using `make` from the repo root: @@ -103,6 +112,7 @@ make clean # Remove all build artifacts ``` The `make release` target can only be run by the repo owner or in your own fork: + 1. Compiles an optimised release build via Xcode 2. Creates a macOS distribution installer package (`AutoPkgWizard-.pkg`) that installs the app into `/Applications` 3. Creates a distributable disk image (`AutoPkgWizard-.dmg`)