← back to Exo
EXO: add CLI flags for root install/uninstall
3a9baeb9db469975f9c760960bf0d1fcb187d2ce · 2026-02-03 22:49:47 +0000 · Jake Hillion
The macOS app required user interaction via AppleScript prompts to
install or uninstall network configuration components, making automated
deployments difficult.
Added --install and --uninstall command line flags that execute the
network setup scripts directly when running as root, bypassing GUI
prompts. Created a new main.swift entry point that parses CLI arguments
and delegates to NetworkSetupHelper's new direct execution methods.
This enables headless installation via `sudo EXO --install` for
automated deployment scenarios while preserving the existing GUI
behavior when launched normally.
Test plan:
- Deployed to a machine that didn't have the content installed. Got
blocked on the popup and EXO never launched.
- Relaunched EXO, confirmed it still never starts because of the popup.
- Ran `sudo /Applications/EXO.app/Contents/MacOS/EXO --install`
- Launched EXO - the API started as expected.
- Ran `sudo /Applications/EXO.app/Contents/MacOS/EXO --uninstall`
- Launched EXO - got the popup.
Files touched
M app/EXO/EXO/EXOApp.swiftM app/EXO/EXO/Services/NetworkSetupHelper.swiftA app/EXO/EXO/main.swift
Diff
commit 3a9baeb9db469975f9c760960bf0d1fcb187d2ce
Author: Jake Hillion <jake@hillion.co.uk>
Date: Tue Feb 3 22:49:47 2026 +0000
EXO: add CLI flags for root install/uninstall
The macOS app required user interaction via AppleScript prompts to
install or uninstall network configuration components, making automated
deployments difficult.
Added --install and --uninstall command line flags that execute the
network setup scripts directly when running as root, bypassing GUI
prompts. Created a new main.swift entry point that parses CLI arguments
and delegates to NetworkSetupHelper's new direct execution methods.
This enables headless installation via `sudo EXO --install` for
automated deployment scenarios while preserving the existing GUI
behavior when launched normally.
Test plan:
- Deployed to a machine that didn't have the content installed. Got
blocked on the popup and EXO never launched.
- Relaunched EXO, confirmed it still never starts because of the popup.
- Ran `sudo /Applications/EXO.app/Contents/MacOS/EXO --install`
- Launched EXO - the API started as expected.
- Ran `sudo /Applications/EXO.app/Contents/MacOS/EXO --uninstall`
- Launched EXO - got the popup.
---
app/EXO/EXO/EXOApp.swift | 1 -
app/EXO/EXO/Services/NetworkSetupHelper.swift | 55 +++++++++++++++++
app/EXO/EXO/main.swift | 85 +++++++++++++++++++++++++++
3 files changed, 140 insertions(+), 1 deletion(-)
diff --git a/app/EXO/EXO/EXOApp.swift b/app/EXO/EXO/EXOApp.swift
index 7669c408..3aff58c3 100644
--- a/app/EXO/EXO/EXOApp.swift
+++ b/app/EXO/EXO/EXOApp.swift
@@ -14,7 +14,6 @@ import SwiftUI
import UserNotifications
import os.log
-@main
struct EXOApp: App {
@StateObject private var controller: ExoProcessController
@StateObject private var stateService: ClusterStateService
diff --git a/app/EXO/EXO/Services/NetworkSetupHelper.swift b/app/EXO/EXO/Services/NetworkSetupHelper.swift
index 82cb82d4..5428ee8e 100644
--- a/app/EXO/EXO/Services/NetworkSetupHelper.swift
+++ b/app/EXO/EXO/Services/NetworkSetupHelper.swift
@@ -288,6 +288,61 @@ enum NetworkSetupHelper {
"""
}
+ /// Direct install without GUI (requires root).
+ /// Returns true on success, false on failure.
+ static func installDirectly() -> Bool {
+ let script = makeInstallerScript()
+ return runShellDirectly(script)
+ }
+
+ /// Direct uninstall without GUI (requires root).
+ /// Returns true on success, false on failure.
+ static func uninstallDirectly() -> Bool {
+ let script = makeUninstallScript()
+ return runShellDirectly(script)
+ }
+
+ /// Run a shell script directly via Process (no AppleScript, requires root).
+ /// Returns true on success, false on failure.
+ private static func runShellDirectly(_ script: String) -> Bool {
+ let process = Process()
+ process.executableURL = URL(fileURLWithPath: "/bin/bash")
+ process.arguments = ["-c", script]
+
+ let outputPipe = Pipe()
+ let errorPipe = Pipe()
+ process.standardOutput = outputPipe
+ process.standardError = errorPipe
+
+ do {
+ try process.run()
+ process.waitUntilExit()
+
+ let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
+ let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
+
+ if let output = String(data: outputData, encoding: .utf8), !output.isEmpty {
+ print(output)
+ }
+ if let errorOutput = String(data: errorData, encoding: .utf8), !errorOutput.isEmpty {
+ fputs(errorOutput, stderr)
+ }
+
+ if process.terminationStatus == 0 {
+ logger.info("Shell script completed successfully")
+ return true
+ } else {
+ logger.error("Shell script failed with exit code \(process.terminationStatus)")
+ return false
+ }
+ } catch {
+ logger.error(
+ "Failed to run shell script: \(error.localizedDescription, privacy: .public)")
+ fputs("Error: \(error.localizedDescription)\n", stderr)
+ return false
+ }
+ }
+
private static func runShellAsAdmin(_ script: String) throws {
let escapedScript =
script
diff --git a/app/EXO/EXO/main.swift b/app/EXO/EXO/main.swift
new file mode 100644
index 00000000..9383981f
--- /dev/null
+++ b/app/EXO/EXO/main.swift
@@ -0,0 +1,85 @@
+//
+// main.swift
+// EXO
+//
+// Created by Jake Hillion on 2026-02-03.
+//
+
+import Foundation
+
+/// Command line options for the EXO app
+enum CLICommand {
+ case install
+ case uninstall
+ case help
+ case none
+}
+
+/// Parse command line arguments to determine the CLI command
+func parseArguments() -> CLICommand {
+ let args = CommandLine.arguments
+ if args.contains("--help") || args.contains("-h") {
+ return .help
+ }
+ if args.contains("--install") {
+ return .install
+ }
+ if args.contains("--uninstall") {
+ return .uninstall
+ }
+ return .none
+}
+
+/// Print usage information
+func printUsage() {
+ let programName = (CommandLine.arguments.first as NSString?)?.lastPathComponent ?? "EXO"
+ print(
+ """
+ Usage: \(programName) [OPTIONS]
+
+ Options:
+ --install Install EXO network configuration (requires root)
+ --uninstall Uninstall EXO network configuration (requires root)
+ --help, -h Show this help message
+
+ When run without options, starts the normal GUI application.
+
+ Examples:
+ sudo \(programName) --install Install network components as root
+ sudo \(programName) --uninstall Remove network components as root
+ """)
+}
+
+/// Check if running as root
+func isRunningAsRoot() -> Bool {
+ return getuid() == 0
+}
+
+// Main entry point
+let command = parseArguments()
+
+switch command {
+case .help:
+ printUsage()
+ exit(0)
+
+case .install:
+ if !isRunningAsRoot() {
+ fputs("Error: --install requires root privileges. Run with sudo.\n", stderr)
+ exit(1)
+ }
+ let success = NetworkSetupHelper.installDirectly()
+ exit(success ? 0 : 1)
+
+case .uninstall:
+ if !isRunningAsRoot() {
+ fputs("Error: --uninstall requires root privileges. Run with sudo.\n", stderr)
+ exit(1)
+ }
+ let success = NetworkSetupHelper.uninstallDirectly()
+ exit(success ? 0 : 1)
+
+case .none:
+ // Start normal GUI application
+ EXOApp.main()
+}
← 01b86a9e feat: add uncertainty visualization with token-level logprob
·
back to Exo
·
Add Qwen3-Coder-Next model cards (#1367) ffe6396c →