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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ updates to the universal artifact, while an architecture-specific installation
updates to its matching architecture. Every downloaded executable is verified
against its published SHA-256 checksum before replacement.

Homebrew-managed installations are updated with `brew upgrade fxcodex`.
`fxcodex update` and automatic executable replacement intentionally defer to
Homebrew for those installations.

Automatic checks run at most once every 24 hours and do not prevent the
requested command from running if a check fails. Configure an update policy
anchored at a minimum version with:
Expand Down
8 changes: 5 additions & 3 deletions Sources/fxcodex-cli/AppCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import FXCodexClient

@main
internal struct AppCommand: AsyncParsableCommand {
internal static let version: String = "0.1.0"
internal static let version: String = "0.1.1"
internal static let machineEncodingFailureResponse: String = """
{
"api_version": 1,
Expand Down Expand Up @@ -96,10 +96,12 @@ internal struct AppCommand: AsyncParsableCommand {
@Dependency(\.fxCodexClient) var client: FXCodexClient
guard let version = SemanticVersion(Self.version)
else { throw ValidationError("fxcodex has an invalid embedded version.") }
let executableURL: URL = currentExecutableURL()
return try await client.applyAutomaticPreferences(
version,
currentExecutableURL(),
!(try environmentSwitch(
executableURL,
!isHomebrewManagedExecutable(executableURL)
&& !(try environmentSwitch(
named: "FXCODEX_DISABLE_AUTO_UPDATE"
)
?? false)
Expand Down
9 changes: 8 additions & 1 deletion Sources/fxcodex-cli/Commands/UpdateCommand.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ArgumentParser
import Dependencies
import Foundation
import FXCodexClient

extension AppCommand {
Expand Down Expand Up @@ -38,13 +39,19 @@ extension AppCommand {
internal init() {}

internal func run() async throws {
try await self.run(executableURL: currentExecutableURL())
}

internal func run(executableURL: URL) async throws {
guard !isHomebrewManagedExecutable(executableURL)
else { throw FXCodexError.homebrewManagedUpdate }
@Dependency(\.fxCodexClient) var client: FXCodexClient
guard let currentVersion = SemanticVersion(AppCommand.version)
else { throw ValidationError("fxcodex has an invalid embedded version.") }
let result: UpdateResult = try await client.update(
currentVersion,
self.channel.value,
currentExecutableURL()
executableURL
)

if machineOutputRequested(self.json) {
Expand Down
10 changes: 10 additions & 0 deletions Sources/fxcodex-cli/Helpers/CLIUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ internal func currentExecutableURL() -> URL {
.resolvingSymlinksInPath()
}

internal func isHomebrewManagedExecutable(_ executableURL: URL) -> Bool {
let components: [String] = executableURL.standardizedFileURL
.resolvingSymlinksInPath()
.pathComponents
guard let cellarIndex = components.firstIndex(of: "Cellar")
else { return false }
return components.indices.contains(cellarIndex + 1)
&& components[cellarIndex + 1] == "fxcodex"
}

internal func machineOutputRequested(
_ localValue: Bool?
) -> Bool {
Expand Down
10 changes: 1 addition & 9 deletions Sources/fxcodex-cli/Helpers/SelfInstallationClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension DependencyValues {
static var liveValue: SelfInstallationClient {
.init(uninstall: { executableURL in
let executableURL: URL = executableURL.standardizedFileURL.resolvingSymlinksInPath()
if Self.isHomebrewManaged(executableURL) {
if isHomebrewManagedExecutable(executableURL) {
let exitCode: Int32 = try runProcess(.init(
executable: "brew",
arguments: ["uninstall", "fxcodex"],
Expand All @@ -30,14 +30,6 @@ extension DependencyValues {
return .direct
})
}

private static func isHomebrewManaged(_ executableURL: URL) -> Bool {
let components: [String] = executableURL.pathComponents
guard let cellarIndex = components.firstIndex(of: "Cellar")
else { return false }
return components.indices.contains(cellarIndex + 1)
&& components[cellarIndex + 1] == "fxcodex"
}
}

internal var _fxcodexSelfInstallation: SelfInstallationClient {
Expand Down
7 changes: 7 additions & 0 deletions Sources/fxcodex-client/Common/FXCodexError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public enum FXCodexError: Error, Equatable, LocalizedError, Sendable {
case ambiguousApplicationInstances([Int32])
case codexExecutableNotFound
case homebrewNotFound
case homebrewManagedUpdate
case invalidWorkspaceName(String)
case primaryWorkspaceMutation
case raycastBetaUnsupportedPlatform
Expand Down Expand Up @@ -38,6 +39,9 @@ public enum FXCodexError: Error, Equatable, LocalizedError, Sendable {
case .homebrewNotFound:
"homebrew_not_found"

case .homebrewManagedUpdate:
"homebrew_managed_update"

case .invalidWorkspaceName:
"invalid_workspace_name"

Expand Down Expand Up @@ -99,6 +103,9 @@ public enum FXCodexError: Error, Equatable, LocalizedError, Sendable {
case .homebrewNotFound:
"Homebrew could not be found in PATH."

case .homebrewManagedUpdate:
"This fxcodex installation is managed by Homebrew. Run 'brew upgrade fxcodex' instead."

case let .invalidWorkspaceName(name):
"Invalid workspace name '\(name)'. Use lowercase letters, numbers, and hyphens."

Expand Down
1 change: 1 addition & 0 deletions Tests/fxcodex-cli-tests/MachineResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ struct MachineResponseTests {
(.ambiguousApplicationInstances([42]), "ambiguous_application_instances"),
(.codexExecutableNotFound, "codex_executable_not_found"),
(.homebrewNotFound, "homebrew_not_found"),
(.homebrewManagedUpdate, "homebrew_managed_update"),
(.invalidWorkspaceName("Work"), "invalid_workspace_name"),
(.primaryWorkspaceMutation, "primary_workspace_mutation"),
(.raycastBetaUnsupportedPlatform, "raycast_beta_unsupported_platform"),
Expand Down
18 changes: 18 additions & 0 deletions Tests/fxcodex-cli-tests/UpdateCommandTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import ArgumentParser
import Foundation
import FXCodexClient
import Testing
@testable import FXCodexCLI

Expand Down Expand Up @@ -35,4 +37,20 @@ struct UpdateCommandTests {

#expect(command is AppCommand.UpdateCommand)
}

@Test("Defers Homebrew-managed updates to Homebrew")
func homebrewManaged() async throws {
let executableURL: URL = .init(
fileURLWithPath: "/opt/homebrew/Cellar/fxcodex/0.1.0/bin/fxcodex"
)
let command: AppCommand.UpdateCommand = try .parse([])

await #expect(throws: FXCodexError.homebrewManagedUpdate) {
try await command.run(executableURL: executableURL)
}
#expect(isHomebrewManagedExecutable(executableURL))
#expect(!isHomebrewManagedExecutable(
.init(fileURLWithPath: "/Users/example/.local/bin/fxcodex")
))
}
}