Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
DerivedData/
xcuserdata/
output/

# Ignore LLM skills
CLAUDE.md
8 changes: 4 additions & 4 deletions AutoPkg Wizard/AutoPkg Wizard.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = "";
Expand All @@ -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;
Expand All @@ -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)";
Expand Down
8 changes: 5 additions & 3 deletions AutoPkg Wizard/AutoPkg Wizard/Services/AutoPkgCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions AutoPkg Wizard/AutoPkg Wizard/ViewModels/OverridesViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
59 changes: 54 additions & 5 deletions AutoPkg Wizard/AutoPkg Wizard/ViewModels/RecipesViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,25 @@ final class RecipesViewModel {

/// Set of recipe names that have an existing override (stripped, lowercased for matching)
var existingOverrides: Set<String> = []
/// 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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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() })
}

Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions AutoPkg Wizard/AutoPkg Wizard/Views/ArgumentsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
56 changes: 54 additions & 2 deletions AutoPkg Wizard/AutoPkg Wizard/Views/OverridesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ struct OverridesView: View {
@State private var viewModel = OverridesViewModel()
@State private var isEditing = false
@State private var selectedOverrides: Set<String> = []
@State private var isRenamingOverride = false
@State private var renameText = ""
@Environment(\.colorScheme) private var colorScheme

var body: some View {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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) {
Expand All @@ -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) }
Expand Down
Loading
Loading