Skip to content
Open
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
7 changes: 5 additions & 2 deletions Scripts/package_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,10 @@ if [[ "$SIGNING_MODE" == "adhoc" ]]; then
AUTO_CHECKS=false
fi
WIDGET_BUNDLE_ID="${BUNDLE_ID}.widget"
APP_GROUP_ID="group.com.steipete.codexbar"
APP_TEAM_ID="${APP_TEAM_ID:-Y5PE65HELJ}"
APP_GROUP_ID="${APP_TEAM_ID}.com.steipete.codexbar"
if [[ "$BUNDLE_ID" == *".debug"* ]]; then
APP_GROUP_ID="group.com.steipete.codexbar.debug"
APP_GROUP_ID="${APP_TEAM_ID}.com.steipete.codexbar.debug"
fi
ENTITLEMENTS_DIR="$ROOT/.build/entitlements"
APP_ENTITLEMENTS="${ENTITLEMENTS_DIR}/CodexBar.entitlements"
Expand Down Expand Up @@ -197,6 +198,7 @@ cat > "$APP/Contents/Info.plist" <<PLIST
<key>SUEnableAutomaticChecks</key><${AUTO_CHECKS}/>
<key>CodexBuildTimestamp</key><string>${BUILD_TIMESTAMP}</string>
<key>CodexGitCommit</key><string>${GIT_COMMIT}</string>
<key>CodexBarTeamID</key><string>${APP_TEAM_ID}</string>
</dict>
</plist>
PLIST
Expand Down Expand Up @@ -292,6 +294,7 @@ if [[ -n "$(resolve_binary_path "CodexBarWidget" "${ARCH_LIST[0]}")" ]]; then
<key>CFBundleShortVersionString</key><string>${MARKETING_VERSION}</string>
<key>CFBundleVersion</key><string>${BUILD_NUMBER}</string>
<key>LSMinimumSystemVersion</key><string>14.0</string>
<key>CodexBarTeamID</key><string>${APP_TEAM_ID}</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key><string>com.apple.widgetkit-extension</string>
Expand Down
16 changes: 15 additions & 1 deletion Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ enum MenuBarMetricPreference: String, CaseIterable, Identifiable {
@MainActor
@Observable
final class SettingsStore {
static let sharedDefaults = UserDefaults(suiteName: "group.com.steipete.codexbar")
static let sharedDefaults = AppGroupSupport.sharedDefaults()
static let mergedOverviewProviderLimit = 3
static let isRunningTests: Bool = {
let env = ProcessInfo.processInfo.environment
Expand Down Expand Up @@ -124,6 +124,20 @@ final class SettingsStore {
copilotTokenStore: any CopilotTokenStoring = KeychainCopilotTokenStore(),
tokenAccountStore: any ProviderTokenAccountStoring = FileTokenAccountStore())
{
let appGroupID = AppGroupSupport.currentGroupID()
let appGroupMigration = AppGroupSupport.migrateLegacyDataIfNeeded(standardDefaults: userDefaults)
let sharedDefaultsAvailable = Self.sharedDefaults != nil
if !Self.isRunningTests {
CodexBarLog.logger(LogCategories.settings).info(
"App group resolved",
metadata: [
"groupID": appGroupID,
"sharedDefaultsAvailable": sharedDefaultsAvailable ? "1" : "0",
"migrationStatus": appGroupMigration.status.rawValue,
"migratedSnapshot": appGroupMigration.copiedSnapshot ? "1" : "0",
])
}

let hasStoredOpenAIWebAccessPreference = userDefaults.object(forKey: "openAIWebAccessEnabled") != nil
let hadExistingConfig = (try? configStore.load()) != nil
let legacyStores = CodexBarConfigMigrator.LegacyStores(
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/StatusItemController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,8 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
// Status items for individual providers are now created lazily in updateVisibility()
super.init()
self.wireBindings()
self.updateIcons()
self.updateVisibility()
self.updateIcons()
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleDebugReplayNotification(_:)),
Expand Down
210 changes: 210 additions & 0 deletions Sources/CodexBarCore/AppGroupSupport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import Foundation
#if os(macOS)
import Security
#endif

public enum AppGroupSupport {
public static let defaultTeamID = "Y5PE65HELJ"
public static let teamIDInfoKey = "CodexBarTeamID"
public static let legacyReleaseGroupID = "group.com.steipete.codexbar"
public static let legacyDebugGroupID = "group.com.steipete.codexbar.debug"
public static let widgetSnapshotFilename = "widget-snapshot.json"
public static let migrationVersion = 1
public static let migrationVersionKey = "appGroupMigrationVersion"

public struct MigrationResult: Sendable {
public enum Status: String, Sendable {
case alreadyCompleted
case targetUnavailable
case noChangesNeeded
case migrated
}

public let status: Status
public let copiedSnapshot: Bool

public init(status: Status, copiedSnapshot: Bool = false) {
self.status = status
self.copiedSnapshot = copiedSnapshot
}
}

public static func currentGroupID(for bundleID: String? = Bundle.main.bundleIdentifier) -> String {
self.currentGroupID(teamID: self.resolvedTeamID(), bundleID: bundleID)
}

static func currentGroupID(teamID: String, bundleID: String?) -> String {
let base = "\(teamID).com.steipete.codexbar"
return self.isDebugBundleID(bundleID) ? "\(base).debug" : base
}

public static func resolvedTeamID(bundle: Bundle = .main) -> String {
self.resolvedTeamID(
infoDictionaryOverride: bundle.infoDictionary,
bundleURLOverride: bundle.bundleURL)
}

static func resolvedTeamID(
infoDictionaryOverride: [String: Any]?,
bundleURLOverride: URL?) -> String
{
if let teamID = self.codeSignatureTeamID(bundleURL: bundleURLOverride) {
return teamID
}
if let teamID = infoDictionaryOverride?[self.teamIDInfoKey] as? String,
!teamID.isEmpty
{
return teamID
}
return self.defaultTeamID
}

public static func legacyGroupID(for bundleID: String? = Bundle.main.bundleIdentifier) -> String {
self.isDebugBundleID(bundleID) ? self.legacyDebugGroupID : self.legacyReleaseGroupID
}

public static func sharedDefaults(
bundleID: String? = Bundle.main.bundleIdentifier,
fileManager: FileManager = .default)
-> UserDefaults?
{
guard self.currentContainerURL(bundleID: bundleID, fileManager: fileManager) != nil else { return nil }
return UserDefaults(suiteName: self.currentGroupID(for: bundleID))
}

public static func currentContainerURL(
bundleID: String? = Bundle.main.bundleIdentifier,
fileManager: FileManager = .default)
-> URL?
{
#if os(macOS)
fileManager.containerURL(forSecurityApplicationGroupIdentifier: self.currentGroupID(for: bundleID))
#else
nil
#endif
}

public static func snapshotURL(
bundleID: String? = Bundle.main.bundleIdentifier,
fileManager: FileManager = .default,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser)
-> URL
{
if let container = self.currentContainerURL(bundleID: bundleID, fileManager: fileManager) {
return container.appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)
}

let directory = self.localFallbackDirectory(fileManager: fileManager, homeDirectory: homeDirectory)
return directory.appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)
}

public static func localFallbackDirectory(
fileManager: FileManager = .default,
homeDirectory _: URL = FileManager.default.homeDirectoryForCurrentUser)
-> URL
{
let base = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? fileManager.temporaryDirectory
let directory = base.appendingPathComponent("CodexBar", isDirectory: true)
try? fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
return directory
}

public static func legacyContainerCandidateURL(
bundleID: String? = Bundle.main.bundleIdentifier,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser)
-> URL
{
homeDirectory
.appendingPathComponent("Library", isDirectory: true)
.appendingPathComponent("Group Containers", isDirectory: true)
.appendingPathComponent(self.legacyGroupID(for: bundleID), isDirectory: true)
}

public static func migrateLegacyDataIfNeeded(
bundleID: String? = Bundle.main.bundleIdentifier,
standardDefaults: UserDefaults = .standard,
fileManager: FileManager = .default,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser,
currentDefaultsOverride: UserDefaults? = nil,
currentSnapshotURLOverride: URL? = nil,
legacySnapshotURLOverride: URL? = nil)
-> MigrationResult
{
if standardDefaults.integer(forKey: self.migrationVersionKey) >= self.migrationVersion {
return MigrationResult(status: .alreadyCompleted)
}

guard currentDefaultsOverride ?? self.sharedDefaults(bundleID: bundleID, fileManager: fileManager) != nil else {
return MigrationResult(status: .targetUnavailable)
}

let currentSnapshotURL = currentSnapshotURLOverride
?? self.currentContainerURL(bundleID: bundleID, fileManager: fileManager)?
.appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)
let legacySnapshotURL = legacySnapshotURLOverride
?? self.legacyContainerCandidateURL(bundleID: bundleID, homeDirectory: homeDirectory)
.appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)

let copiedSnapshot = {
guard let currentSnapshotURL else { return false }
guard !fileManager.fileExists(atPath: currentSnapshotURL.path),
fileManager.fileExists(atPath: legacySnapshotURL.path)
else {
return false
}
do {
try fileManager.createDirectory(
at: currentSnapshotURL.deletingLastPathComponent(),
withIntermediateDirectories: true)
try fileManager.copyItem(at: legacySnapshotURL, to: currentSnapshotURL)
return true
} catch {
return false
}
}()

let result = if copiedSnapshot {
MigrationResult(status: .migrated, copiedSnapshot: true)
} else {
MigrationResult(status: .noChangesNeeded)
}

standardDefaults.set(self.migrationVersion, forKey: self.migrationVersionKey)
return result
}

private static func isDebugBundleID(_ bundleID: String?) -> Bool {
guard let bundleID, !bundleID.isEmpty else { return false }
return bundleID.contains(".debug")
}

private static func codeSignatureTeamID(bundleURL: URL?) -> String? {
#if os(macOS)
guard let bundleURL else { return nil }

var staticCode: SecStaticCode?
guard SecStaticCodeCreateWithPath(bundleURL as CFURL, SecCSFlags(), &staticCode) == errSecSuccess,
let code = staticCode
else {
return nil
}

var infoCF: CFDictionary?
guard SecCodeCopySigningInformation(
code,
SecCSFlags(rawValue: kSecCSSigningInformation),
&infoCF) == errSecSuccess,
let info = infoCF as? [String: Any],
let teamID = info[kSecCodeInfoTeamIdentifier as String] as? String,
!teamID.isEmpty
else {
return nil
}
return teamID
#else
_ = bundleURL
return nil
#endif
}
}
5 changes: 1 addition & 4 deletions Sources/CodexBarCore/KeychainAccessGate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import SweetCookieKit

public enum KeychainAccessGate {
private static let flagKey = "debugDisableKeychainAccess"
private static let appGroupID = "group.com.steipete.codexbar"
@TaskLocal private static var taskOverrideValue: Bool?
private nonisolated(unsafe) static var overrideValue: Bool?

Expand All @@ -19,9 +18,7 @@ public enum KeychainAccessGate {
#endif
if let overrideValue { return overrideValue }
if UserDefaults.standard.bool(forKey: Self.flagKey) { return true }
if let shared = UserDefaults(suiteName: Self.appGroupID),
shared.bool(forKey: Self.flagKey)
{
if let shared = AppGroupSupport.sharedDefaults(), shared.bool(forKey: Self.flagKey) {
return true
}
return false
Expand Down
42 changes: 10 additions & 32 deletions Sources/CodexBarCore/WidgetSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,16 @@ public struct WidgetSnapshot: Codable, Sendable {
}

public enum WidgetSnapshotStore {
public static let appGroupID = "group.com.steipete.codexbar"
private static let filename = "widget-snapshot.json"
private static let filename = AppGroupSupport.widgetSnapshotFilename

public static func load(bundleID: String? = Bundle.main.bundleIdentifier) -> WidgetSnapshot? {
guard let url = self.snapshotURL(bundleID: bundleID) else { return nil }
let url = self.snapshotURL(bundleID: bundleID)
guard let data = try? Data(contentsOf: url) else { return nil }
return try? self.decoder.decode(WidgetSnapshot.self, from: data)
}

public static func save(_ snapshot: WidgetSnapshot, bundleID: String? = Bundle.main.bundleIdentifier) {
guard let url = self.snapshotURL(bundleID: bundleID) else { return }
let url = self.snapshotURL(bundleID: bundleID)
do {
let data = try self.encoder.encode(snapshot)
try data.write(to: url, options: [.atomic])
Expand All @@ -133,32 +132,12 @@ public enum WidgetSnapshotStore {
}
}

private static func snapshotURL(bundleID: String?) -> URL? {
let fm = FileManager.default
let groupID = self.groupID(for: bundleID)
#if os(macOS)
if let groupID, let container = fm.containerURL(forSecurityApplicationGroupIdentifier: groupID) {
return container.appendingPathComponent(self.filename, isDirectory: false)
}
#endif

let base = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? fm.temporaryDirectory
let dir = base.appendingPathComponent("CodexBar", isDirectory: true)
try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
return dir.appendingPathComponent(self.filename, isDirectory: false)
private static func snapshotURL(bundleID: String?) -> URL {
AppGroupSupport.snapshotURL(bundleID: bundleID)
}

public static func appGroupID(for bundleID: String?) -> String? {
self.groupID(for: bundleID)
}

private static func groupID(for bundleID: String?) -> String? {
guard let bundleID, !bundleID.isEmpty else { return self.appGroupID }
if bundleID.contains(".debug") {
return "group.com.steipete.codexbar.debug"
}
return self.appGroupID
AppGroupSupport.currentGroupID(for: bundleID)
}

private static var encoder: JSONEncoder {
Expand All @@ -178,7 +157,7 @@ public enum WidgetSelectionStore {
private static let selectedProviderKey = "widgetSelectedProvider"

public static func loadSelectedProvider(bundleID: String? = Bundle.main.bundleIdentifier) -> UsageProvider? {
guard let defaults = self.sharedDefaults(bundleID: bundleID) else { return nil }
let defaults = self.sharedDefaults(bundleID: bundleID)
guard let raw = defaults.string(forKey: self.selectedProviderKey) else { return nil }
return UsageProvider(rawValue: raw)
}
Expand All @@ -187,12 +166,11 @@ public enum WidgetSelectionStore {
_ provider: UsageProvider,
bundleID: String? = Bundle.main.bundleIdentifier)
{
guard let defaults = self.sharedDefaults(bundleID: bundleID) else { return }
let defaults = self.sharedDefaults(bundleID: bundleID)
defaults.set(provider.rawValue, forKey: self.selectedProviderKey)
}

private static func sharedDefaults(bundleID: String?) -> UserDefaults? {
guard let groupID = WidgetSnapshotStore.appGroupID(for: bundleID) else { return nil }
return UserDefaults(suiteName: groupID)
private static func sharedDefaults(bundleID: String?) -> UserDefaults {
AppGroupSupport.sharedDefaults(bundleID: bundleID) ?? .standard
}
}
Loading