Add command palette entries to install/uninstall cmux CLI in PATH (#626)

Adds CmuxCLIPathInstaller with symlink management at /usr/local/bin/cmux,
exposed via Cmd+Shift+P command palette (VS Code style). Install entry
shown when CLI is not in PATH, uninstall entry shown when it is.
Falls back to osascript admin privilege escalation when /usr/local/bin
is not user-writable. Uses attributesOfItem instead of fileExists to
correctly handle dangling symlinks from relocated app bundles.

Closes https://github.com/manaflow-ai/cmux/issues/618
This commit is contained in:
Lawrence Chen 2026-02-27 01:53:13 -08:00 committed by GitHub
parent 978341b228
commit 49e93e4b4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 503 additions and 0 deletions

View file

@ -253,6 +253,321 @@ enum WorkspaceShortcutMapper {
}
}
struct CmuxCLIPathInstaller {
struct InstallOutcome {
let usedAdministratorPrivileges: Bool
let destinationURL: URL
let sourceURL: URL
}
struct UninstallOutcome {
let usedAdministratorPrivileges: Bool
let destinationURL: URL
let removedExistingEntry: Bool
}
enum InstallerError: LocalizedError {
case bundledCLIMissing(expectedPath: String)
case destinationParentNotDirectory(path: String)
case destinationIsDirectory(path: String)
case installVerificationFailed(path: String)
case uninstallVerificationFailed(path: String)
case privilegedCommandFailed(message: String)
var errorDescription: String? {
switch self {
case .bundledCLIMissing(let expectedPath):
return "Bundled cmux CLI was not found at \(expectedPath)."
case .destinationParentNotDirectory(let path):
return "Expected \(path) to be a directory."
case .destinationIsDirectory(let path):
return "\(path) is a directory. Remove or rename it and try again."
case .installVerificationFailed(let path):
return "Installed symlink at \(path) did not point to the bundled cmux CLI."
case .uninstallVerificationFailed(let path):
return "Failed to remove \(path)."
case .privilegedCommandFailed(let message):
return "Administrator action failed: \(message)"
}
}
}
typealias PrivilegedInstallHandler = (_ sourceURL: URL, _ destinationURL: URL) throws -> Void
typealias PrivilegedUninstallHandler = (_ destinationURL: URL) throws -> Void
let fileManager: FileManager
let destinationURL: URL
private let bundledCLIURLProvider: () -> URL?
private let expectedBundledCLIPath: String
private let privilegedInstaller: PrivilegedInstallHandler
private let privilegedUninstaller: PrivilegedUninstallHandler
init(
fileManager: FileManager = .default,
destinationURL: URL = URL(fileURLWithPath: "/usr/local/bin/cmux"),
bundledCLIURLProvider: @escaping () -> URL? = {
CmuxCLIPathInstaller.defaultBundledCLIURL()
},
expectedBundledCLIPath: String = CmuxCLIPathInstaller.defaultBundledCLIExpectedPath(),
privilegedInstaller: PrivilegedInstallHandler? = nil,
privilegedUninstaller: PrivilegedUninstallHandler? = nil
) {
self.fileManager = fileManager
self.destinationURL = destinationURL
self.bundledCLIURLProvider = bundledCLIURLProvider
self.expectedBundledCLIPath = expectedBundledCLIPath
self.privilegedInstaller = privilegedInstaller ?? Self.installWithAdministratorPrivileges(sourceURL:destinationURL:)
self.privilegedUninstaller = privilegedUninstaller ?? Self.uninstallWithAdministratorPrivileges(destinationURL:)
}
var destinationPath: String {
destinationURL.path
}
func install() throws -> InstallOutcome {
let sourceURL = try resolveBundledCLIURL()
do {
try installWithoutAdministratorPrivileges(sourceURL: sourceURL)
return InstallOutcome(
usedAdministratorPrivileges: false,
destinationURL: destinationURL,
sourceURL: sourceURL
)
} catch {
guard Self.isPermissionDenied(error) else { throw error }
try ensureDestinationIsNotDirectory()
try privilegedInstaller(sourceURL, destinationURL)
try verifyInstalledSymlinkTarget(sourceURL: sourceURL)
return InstallOutcome(
usedAdministratorPrivileges: true,
destinationURL: destinationURL,
sourceURL: sourceURL
)
}
}
func uninstall() throws -> UninstallOutcome {
do {
let removedExistingEntry = try uninstallWithoutAdministratorPrivileges()
return UninstallOutcome(
usedAdministratorPrivileges: false,
destinationURL: destinationURL,
removedExistingEntry: removedExistingEntry
)
} catch {
guard Self.isPermissionDenied(error) else { throw error }
try ensureDestinationIsNotDirectory()
let removedExistingEntry = destinationEntryExists()
try privilegedUninstaller(destinationURL)
if destinationEntryExists() {
throw InstallerError.uninstallVerificationFailed(path: destinationURL.path)
}
return UninstallOutcome(
usedAdministratorPrivileges: true,
destinationURL: destinationURL,
removedExistingEntry: removedExistingEntry
)
}
}
func isInstalled() -> Bool {
guard let sourceURL = bundledCLIURLProvider()?.standardizedFileURL else { return false }
guard let installedTargetURL = symlinkDestinationURL() else { return false }
return installedTargetURL == sourceURL
}
private func resolveBundledCLIURL() throws -> URL {
guard let sourceURL = bundledCLIURLProvider()?.standardizedFileURL else {
throw InstallerError.bundledCLIMissing(expectedPath: expectedBundledCLIPath)
}
var isDirectory: ObjCBool = false
guard fileManager.fileExists(atPath: sourceURL.path, isDirectory: &isDirectory), !isDirectory.boolValue else {
throw InstallerError.bundledCLIMissing(expectedPath: sourceURL.path)
}
return sourceURL
}
private func installWithoutAdministratorPrivileges(sourceURL: URL) throws {
try ensureDestinationParentDirectoryExists()
try ensureDestinationIsNotDirectory()
if destinationEntryExists() {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.createSymbolicLink(at: destinationURL, withDestinationURL: sourceURL)
try verifyInstalledSymlinkTarget(sourceURL: sourceURL)
}
@discardableResult
private func uninstallWithoutAdministratorPrivileges() throws -> Bool {
try ensureDestinationIsNotDirectory()
let existed = destinationEntryExists()
if existed {
try fileManager.removeItem(at: destinationURL)
}
if destinationEntryExists() {
throw InstallerError.uninstallVerificationFailed(path: destinationURL.path)
}
return existed
}
/// Check if the destination path has any filesystem entry (including dangling symlinks).
/// `FileManager.fileExists` follows symlinks, so a dangling symlink returns false.
private func destinationEntryExists() -> Bool {
(try? fileManager.attributesOfItem(atPath: destinationURL.path)) != nil
}
private func verifyInstalledSymlinkTarget(sourceURL: URL) throws {
guard let installedTargetURL = symlinkDestinationURL(),
installedTargetURL == sourceURL.standardizedFileURL else {
throw InstallerError.installVerificationFailed(path: destinationURL.path)
}
}
private func symlinkDestinationURL() -> URL? {
guard fileManager.fileExists(atPath: destinationURL.path) else { return nil }
guard let destinationPath = try? fileManager.destinationOfSymbolicLink(atPath: destinationURL.path) else {
return nil
}
return URL(
fileURLWithPath: destinationPath,
relativeTo: destinationURL.deletingLastPathComponent()
).standardizedFileURL
}
private func ensureDestinationParentDirectoryExists() throws {
let parentURL = destinationURL.deletingLastPathComponent()
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: parentURL.path, isDirectory: &isDirectory) {
guard isDirectory.boolValue else {
throw InstallerError.destinationParentNotDirectory(path: parentURL.path)
}
return
}
try fileManager.createDirectory(at: parentURL, withIntermediateDirectories: true)
}
private func ensureDestinationIsNotDirectory() throws {
guard let values = try resourceValuesIfFileExists(
at: destinationURL,
keys: [.isDirectoryKey, .isSymbolicLinkKey]
) else {
return
}
if values.isDirectory == true, values.isSymbolicLink != true {
throw InstallerError.destinationIsDirectory(path: destinationURL.path)
}
}
private func resourceValuesIfFileExists(
at url: URL,
keys: Set<URLResourceKey>
) throws -> URLResourceValues? {
do {
return try url.resourceValues(forKeys: keys)
} catch {
let nsError = error as NSError
if nsError.domain == NSCocoaErrorDomain && nsError.code == NSFileReadNoSuchFileError {
return nil
}
if nsError.domain == NSPOSIXErrorDomain,
POSIXErrorCode(rawValue: Int32(nsError.code)) == .ENOENT {
return nil
}
throw error
}
}
private static func defaultBundledCLIURL(bundle: Bundle = .main) -> URL? {
bundle.resourceURL?.appendingPathComponent("bin/cmux", isDirectory: false)
}
private static func defaultBundledCLIExpectedPath(bundle: Bundle = .main) -> String {
bundle.bundleURL
.appendingPathComponent("Contents/Resources/bin/cmux", isDirectory: false)
.path
}
private static func installWithAdministratorPrivileges(sourceURL: URL, destinationURL: URL) throws {
let destinationPath = destinationURL.path
let parentPath = destinationURL.deletingLastPathComponent().path
let command = "/bin/mkdir -p \(shellQuoted(parentPath)) && " +
"/bin/rm -f \(shellQuoted(destinationPath)) && " +
"/bin/ln -s \(shellQuoted(sourceURL.path)) \(shellQuoted(destinationPath))"
try runPrivilegedShellCommand(command)
}
private static func uninstallWithAdministratorPrivileges(destinationURL: URL) throws {
let command = "/bin/rm -f \(shellQuoted(destinationURL.path))"
try runPrivilegedShellCommand(command)
}
private static func runPrivilegedShellCommand(_ command: String) throws {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
process.arguments = [
"-e", "on run argv",
"-e", "do shell script (item 1 of argv) with administrator privileges",
"-e", "end run",
command
]
let stdout = Pipe()
let stderr = Pipe()
process.standardOutput = stdout
process.standardError = stderr
try process.run()
process.waitUntilExit()
guard process.terminationStatus == 0 else {
let stderrText = String(
data: stderr.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let stdoutText = String(
data: stdout.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let details = stderrText.isEmpty ? stdoutText : stderrText
let message = details.isEmpty
? "osascript exited with status \(process.terminationStatus)."
: details
throw InstallerError.privilegedCommandFailed(message: message)
}
}
private static func shellQuoted(_ value: String) -> String {
"'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
}
private static func isPermissionDenied(_ error: Error) -> Bool {
isPermissionDenied(error as NSError)
}
private static func isPermissionDenied(_ error: NSError) -> Bool {
if error.domain == NSPOSIXErrorDomain,
let code = POSIXErrorCode(rawValue: Int32(error.code)),
code == .EACCES || code == .EPERM || code == .EROFS {
return true
}
if error.domain == NSCocoaErrorDomain {
switch error.code {
case NSFileWriteNoPermissionError, NSFileReadNoPermissionError, NSFileWriteVolumeReadOnlyError:
return true
default:
break
}
}
if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError {
return isPermissionDenied(underlying)
}
return false
}
}
private extension NSScreen {
var cmuxDisplayID: UInt32? {
let key = NSDeviceDescriptionKey("NSScreenNumber")
@ -3467,6 +3782,79 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
updateController.attemptUpdate()
}
func isCmuxCLIInstalledInPATH() -> Bool {
CmuxCLIPathInstaller().isInstalled()
}
@objc func installCmuxCLIInPath(_ sender: Any?) {
let installer = CmuxCLIPathInstaller()
do {
let outcome = try installer.install()
var informativeText = """
Created symlink:
\(outcome.destinationURL.path) -> \(outcome.sourceURL.path)
"""
if outcome.usedAdministratorPrivileges {
informativeText += "\n\nAdministrator privileges were required to write to /usr/local/bin."
}
presentCLIPathAlert(
title: "cmux CLI Installed",
informativeText: informativeText,
style: .informational
)
} catch {
presentCLIPathAlert(
title: "Couldn't Install cmux CLI",
informativeText: error.localizedDescription,
style: .warning
)
}
}
@objc func uninstallCmuxCLIInPath(_ sender: Any?) {
let installer = CmuxCLIPathInstaller()
do {
let outcome = try installer.uninstall()
let prefix = outcome.removedExistingEntry
? "Removed \(outcome.destinationURL.path)."
: "No cmux CLI symlink was found at \(outcome.destinationURL.path)."
var informativeText = prefix
if outcome.usedAdministratorPrivileges {
informativeText += "\n\nAdministrator privileges were required to modify /usr/local/bin."
}
presentCLIPathAlert(
title: "cmux CLI Uninstalled",
informativeText: informativeText,
style: .informational
)
} catch {
presentCLIPathAlert(
title: "Couldn't Uninstall cmux CLI",
informativeText: error.localizedDescription,
style: .warning
)
}
}
private func presentCLIPathAlert(
title: String,
informativeText: String,
style: NSAlert.Style
) {
let alert = NSAlert()
alert.alertStyle = style
alert.messageText = title
alert.informativeText = informativeText
alert.addButton(withTitle: "OK")
if let window = NSApp.keyWindow ?? NSApp.mainWindow {
alert.beginSheetModal(for: window, completionHandler: nil)
} else {
_ = alert.runModal()
}
}
@objc func restartSocketListener(_ sender: Any?) {
guard tabManager != nil else {
NSSound.beep()

View file

@ -3504,6 +3504,24 @@ struct ContentView: View {
keywords: ["create", "new", "window"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.installCLI",
title: constant("Shell Command: Install 'cmux' in PATH"),
subtitle: constant("CLI"),
keywords: ["install", "cli", "path", "shell", "command", "symlink"],
when: { _ in !(AppDelegate.shared?.isCmuxCLIInstalledInPATH() ?? false) }
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.uninstallCLI",
title: constant("Shell Command: Uninstall 'cmux' from PATH"),
subtitle: constant("CLI"),
keywords: ["uninstall", "remove", "cli", "path", "shell", "command", "symlink"],
when: { _ in AppDelegate.shared?.isCmuxCLIInstalledInPATH() ?? false }
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.newTerminalTab",
@ -4021,6 +4039,12 @@ struct ContentView: View {
registry.register(commandId: "palette.newWindow") {
AppDelegate.shared?.openNewMainWindow(nil)
}
registry.register(commandId: "palette.installCLI") {
AppDelegate.shared?.installCmuxCLIInPath(nil)
}
registry.register(commandId: "palette.uninstallCLI") {
AppDelegate.shared?.uninstallCmuxCLIInPath(nil)
}
registry.register(commandId: "palette.newTerminalTab") {
tabManager.newSurface()
}

View file

@ -240,3 +240,94 @@ final class SocketControlPasswordStoreTests: XCTestCase {
XCTAssertEqual(try SocketControlPasswordStore.loadPassword(fileURL: fileURL), "legacy-secret")
}
}
final class CmuxCLIPathInstallerTests: XCTestCase {
func testInstallAndUninstallRoundTripWithoutAdministratorPrivileges() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-cli-installer-tests-\(UUID().uuidString)", isDirectory: true)
try fileManager.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: root) }
let bundledCLIURL = root
.appendingPathComponent("cmux.app/Contents/Resources/bin/cmux", isDirectory: false)
try fileManager.createDirectory(
at: bundledCLIURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try "#!/bin/sh\necho cmux\n".write(to: bundledCLIURL, atomically: true, encoding: .utf8)
let destinationURL = root.appendingPathComponent("usr/local/bin/cmux", isDirectory: false)
var privilegedInstallCallCount = 0
var privilegedUninstallCallCount = 0
let installer = CmuxCLIPathInstaller(
fileManager: fileManager,
destinationURL: destinationURL,
bundledCLIURLProvider: { bundledCLIURL },
expectedBundledCLIPath: bundledCLIURL.path,
privilegedInstaller: { _, _ in privilegedInstallCallCount += 1 },
privilegedUninstaller: { _ in privilegedUninstallCallCount += 1 }
)
let installOutcome = try installer.install()
XCTAssertFalse(installOutcome.usedAdministratorPrivileges)
XCTAssertEqual(privilegedInstallCallCount, 0)
XCTAssertTrue(installer.isInstalled())
XCTAssertEqual(
try fileManager.destinationOfSymbolicLink(atPath: destinationURL.path),
bundledCLIURL.path
)
let uninstallOutcome = try installer.uninstall()
XCTAssertFalse(uninstallOutcome.usedAdministratorPrivileges)
XCTAssertTrue(uninstallOutcome.removedExistingEntry)
XCTAssertEqual(privilegedUninstallCallCount, 0)
XCTAssertFalse(fileManager.fileExists(atPath: destinationURL.path))
XCTAssertFalse(installer.isInstalled())
}
func testInstallFallsBackToAdministratorFlowWhenDestinationIsNotWritable() throws {
let fileManager = FileManager.default
let root = fileManager.temporaryDirectory
.appendingPathComponent("cmux-cli-installer-tests-\(UUID().uuidString)", isDirectory: true)
try fileManager.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: root) }
let bundledCLIURL = root
.appendingPathComponent("cmux.app/Contents/Resources/bin/cmux", isDirectory: false)
try fileManager.createDirectory(
at: bundledCLIURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try "#!/bin/sh\necho cmux\n".write(to: bundledCLIURL, atomically: true, encoding: .utf8)
let destinationURL = root.appendingPathComponent("usr/local/bin/cmux", isDirectory: false)
let destinationDir = destinationURL.deletingLastPathComponent()
try fileManager.createDirectory(at: destinationDir, withIntermediateDirectories: true)
try fileManager.setAttributes([.posixPermissions: 0o555], ofItemAtPath: destinationDir.path)
defer {
try? fileManager.setAttributes([.posixPermissions: 0o755], ofItemAtPath: destinationDir.path)
}
var privilegedInstallCallCount = 0
let installer = CmuxCLIPathInstaller(
fileManager: fileManager,
destinationURL: destinationURL,
bundledCLIURLProvider: { bundledCLIURL },
expectedBundledCLIPath: bundledCLIURL.path,
privilegedInstaller: { sourceURL, privilegedDestinationURL in
privilegedInstallCallCount += 1
XCTAssertEqual(sourceURL.standardizedFileURL, bundledCLIURL.standardizedFileURL)
XCTAssertEqual(privilegedDestinationURL.standardizedFileURL, destinationURL.standardizedFileURL)
try fileManager.setAttributes([.posixPermissions: 0o755], ofItemAtPath: destinationDir.path)
try fileManager.createSymbolicLink(at: privilegedDestinationURL, withDestinationURL: sourceURL)
}
)
let installOutcome = try installer.install()
XCTAssertTrue(installOutcome.usedAdministratorPrivileges)
XCTAssertEqual(privilegedInstallCallCount, 1)
XCTAssertTrue(installer.isInstalled())
}
}