refactor: split utilities

This commit is contained in:
Peter Steinberger
2025-12-24 22:16:06 +01:00
parent 8ee62f0ac8
commit 88be2701f4
6 changed files with 236 additions and 232 deletions

View File

@@ -0,0 +1,17 @@
import Foundation
// Human-friendly age string (e.g., "2m ago").
func age(from date: Date, now: Date = .init()) -> String {
let seconds = max(0, Int(now.timeIntervalSince(date)))
let minutes = seconds / 60
let hours = minutes / 60
let days = hours / 24
if seconds < 60 { return "just now" }
if minutes == 1 { return "1 minute ago" }
if minutes < 60 { return "\(minutes)m ago" }
if hours == 1 { return "1 hour ago" }
if hours < 24 { return "\(hours)h ago" }
if days == 1 { return "yesterday" }
return "\(days)d ago"
}

View File

@@ -0,0 +1,102 @@
import Foundation
@MainActor
enum CLIInstaller {
private static func embeddedHelperURL() -> URL {
Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/Relay/clawdis")
}
static func installedLocation() -> String? {
self.installedLocation(
searchPaths: cliHelperSearchPaths,
embeddedHelper: self.embeddedHelperURL(),
fileManager: .default)
}
static func installedLocation(
searchPaths: [String],
embeddedHelper: URL,
fileManager: FileManager) -> String?
{
let embedded = embeddedHelper.resolvingSymlinksInPath()
for basePath in searchPaths {
let candidate = URL(fileURLWithPath: basePath).appendingPathComponent("clawdis").path
var isDirectory: ObjCBool = false
guard fileManager.fileExists(atPath: candidate, isDirectory: &isDirectory),
!isDirectory.boolValue
else {
continue
}
guard fileManager.isExecutableFile(atPath: candidate) else { continue }
let resolved = URL(fileURLWithPath: candidate).resolvingSymlinksInPath()
if resolved == embedded {
return candidate
}
}
return nil
}
static func isInstalled() -> Bool {
self.installedLocation() != nil
}
static func install(statusHandler: @escaping @Sendable (String) async -> Void) async {
let helper = self.embeddedHelperURL()
guard FileManager.default.isExecutableFile(atPath: helper.path) else {
await statusHandler(
"Embedded CLI missing in bundle; repackage via scripts/package-mac-app.sh " +
"(or restart-mac.sh without SKIP_GATEWAY_PACKAGE=1).")
return
}
let targets = cliHelperSearchPaths.map { "\($0)/clawdis" }
let result = await self.privilegedSymlink(source: helper.path, targets: targets)
await statusHandler(result)
}
private static func privilegedSymlink(source: String, targets: [String]) async -> String {
let escapedSource = self.shellEscape(source)
let targetList = targets.map(self.shellEscape).joined(separator: " ")
let cmds = [
"mkdir -p /usr/local/bin /opt/homebrew/bin",
targets.map { "ln -sf \(escapedSource) \($0)" }.joined(separator: "; "),
].joined(separator: "; ")
let script = """
do shell script "\(cmds)" with administrator privileges
"""
let proc = Process()
proc.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
proc.arguments = ["-e", script]
let pipe = Pipe()
proc.standardOutput = pipe
proc.standardError = pipe
do {
try proc.run()
proc.waitUntilExit()
let data = pipe.fileHandleForReading.readToEndSafely()
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if proc.terminationStatus == 0 {
return output.isEmpty ? "CLI helper linked into \(targetList)" : output
}
if output.lowercased().contains("user canceled") {
return "Install canceled"
}
return "Failed to install CLI helper: \(output)"
} catch {
return "Failed to run installer: \(error.localizedDescription)"
}
}
private static func shellEscape(_ path: String) -> String {
"'" + path.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
}
}

View File

@@ -1,237 +1,5 @@
import AppKit
import Foundation
extension ProcessInfo {
var isPreview: Bool {
self.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
var isRunningTests: Bool {
// SwiftPM tests load one or more `.xctest` bundles. With Swift Testing, `Bundle.main` is not
// guaranteed to be the `.xctest` bundle, so check all loaded bundles.
if Bundle.allBundles.contains(where: { $0.bundleURL.pathExtension == "xctest" }) { return true }
if Bundle.main.bundleURL.pathExtension == "xctest" { return true }
// Backwards-compatible fallbacks for runners that still set XCTest env vars.
return self.environment["XCTestConfigurationFilePath"] != nil
|| self.environment["XCTestBundlePath"] != nil
|| self.environment["XCTestSessionIdentifier"] != nil
}
}
enum LaunchdManager {
private static func runLaunchctl(_ args: [String]) {
let process = Process()
process.launchPath = "/bin/launchctl"
process.arguments = args
try? process.run()
}
static func startClawdis() {
let userTarget = "gui/\(getuid())/\(launchdLabel)"
self.runLaunchctl(["kickstart", "-k", userTarget])
}
static func stopClawdis() {
let userTarget = "gui/\(getuid())/\(launchdLabel)"
self.runLaunchctl(["stop", userTarget])
}
}
enum LaunchAgentManager {
private static var plistURL: URL {
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/LaunchAgents/com.steipete.clawdis.plist")
}
static func status() async -> Bool {
guard FileManager.default.fileExists(atPath: self.plistURL.path) else { return false }
let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"])
return result == 0
}
static func set(enabled: Bool, bundlePath: String) async {
if enabled {
self.writePlist(bundlePath: bundlePath)
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
_ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
_ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(launchdLabel)"])
} else {
// Disable autostart going forward but leave the current app running.
// bootout would terminate the launchd job immediately (and crash the app if launched via agent).
try? FileManager.default.removeItem(at: self.plistURL)
}
}
private static func writePlist(bundlePath: String) {
let plist = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.steipete.clawdis</string>
<key>ProgramArguments</key>
<array>
<string>\(bundlePath)/Contents/MacOS/Clawdis</string>
</array>
<key>WorkingDirectory</key>
<string>\(FileManager.default.homeDirectoryForCurrentUser.path)</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>\(CommandResolver.preferredPaths().joined(separator: ":"))</string>
</dict>
<key>StandardOutPath</key>
<string>\(LogLocator.launchdLogPath)</string>
<key>StandardErrorPath</key>
<string>\(LogLocator.launchdLogPath)</string>
</dict>
</plist>
"""
try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8)
}
@discardableResult
private static func runLaunchctl(_ args: [String]) async -> Int32 {
await Task.detached(priority: .utility) { () -> Int32 in
let process = Process()
process.launchPath = "/bin/launchctl"
process.arguments = args
process.standardOutput = Pipe()
process.standardError = Pipe()
do {
try process.run()
process.waitUntilExit()
return process.terminationStatus
} catch {
return -1
}
}.value
}
}
// Human-friendly age string (e.g., "2m ago").
func age(from date: Date, now: Date = .init()) -> String {
let seconds = max(0, Int(now.timeIntervalSince(date)))
let minutes = seconds / 60
let hours = minutes / 60
let days = hours / 24
if seconds < 60 { return "just now" }
if minutes == 1 { return "1 minute ago" }
if minutes < 60 { return "\(minutes)m ago" }
if hours == 1 { return "1 hour ago" }
if hours < 24 { return "\(hours)h ago" }
if days == 1 { return "yesterday" }
return "\(days)d ago"
}
@MainActor
enum CLIInstaller {
private static func embeddedHelperURL() -> URL {
Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/Relay/clawdis")
}
static func installedLocation() -> String? {
self.installedLocation(
searchPaths: cliHelperSearchPaths,
embeddedHelper: self.embeddedHelperURL(),
fileManager: .default)
}
static func installedLocation(
searchPaths: [String],
embeddedHelper: URL,
fileManager: FileManager) -> String?
{
let embedded = embeddedHelper.resolvingSymlinksInPath()
for basePath in searchPaths {
let candidate = URL(fileURLWithPath: basePath).appendingPathComponent("clawdis").path
var isDirectory: ObjCBool = false
guard fileManager.fileExists(atPath: candidate, isDirectory: &isDirectory),
!isDirectory.boolValue
else {
continue
}
guard fileManager.isExecutableFile(atPath: candidate) else { continue }
let resolved = URL(fileURLWithPath: candidate).resolvingSymlinksInPath()
if resolved == embedded {
return candidate
}
}
return nil
}
static func isInstalled() -> Bool {
self.installedLocation() != nil
}
static func install(statusHandler: @escaping @Sendable (String) async -> Void) async {
let helper = self.embeddedHelperURL()
guard FileManager.default.isExecutableFile(atPath: helper.path) else {
await statusHandler(
"Embedded CLI missing in bundle; repackage via scripts/package-mac-app.sh " +
"(or restart-mac.sh without SKIP_GATEWAY_PACKAGE=1).")
return
}
let targets = cliHelperSearchPaths.map { "\($0)/clawdis" }
let result = await self.privilegedSymlink(source: helper.path, targets: targets)
await statusHandler(result)
}
private static func privilegedSymlink(source: String, targets: [String]) async -> String {
let escapedSource = self.shellEscape(source)
let targetList = targets.map(self.shellEscape).joined(separator: " ")
let cmds = [
"mkdir -p /usr/local/bin /opt/homebrew/bin",
targets.map { "ln -sf \(escapedSource) \($0)" }.joined(separator: "; "),
].joined(separator: "; ")
let script = """
do shell script "\(cmds)" with administrator privileges
"""
let proc = Process()
proc.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
proc.arguments = ["-e", script]
let pipe = Pipe()
proc.standardOutput = pipe
proc.standardError = pipe
do {
try proc.run()
proc.waitUntilExit()
let data = pipe.fileHandleForReading.readToEndSafely()
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if proc.terminationStatus == 0 {
return output.isEmpty ? "CLI helper linked into \(targetList)" : output
}
if output.lowercased().contains("user canceled") {
return "Install canceled"
}
return "Failed to install CLI helper: \(output)"
} catch {
return "Failed to run installer: \(error.localizedDescription)"
}
}
private static func shellEscape(_ path: String) -> String {
"'" + path.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
}
}
enum CommandResolver {
private static let projectRootDefaultsKey = "clawdis.gatewayProjectRootPath"
private static let helperName = "clawdis"

View File

@@ -0,0 +1,78 @@
import Foundation
enum LaunchAgentManager {
private static var plistURL: URL {
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/LaunchAgents/com.steipete.clawdis.plist")
}
static func status() async -> Bool {
guard FileManager.default.fileExists(atPath: self.plistURL.path) else { return false }
let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"])
return result == 0
}
static func set(enabled: Bool, bundlePath: String) async {
if enabled {
self.writePlist(bundlePath: bundlePath)
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
_ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
_ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(launchdLabel)"])
} else {
// Disable autostart going forward but leave the current app running.
// bootout would terminate the launchd job immediately (and crash the app if launched via agent).
try? FileManager.default.removeItem(at: self.plistURL)
}
}
private static func writePlist(bundlePath: String) {
let plist = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.steipete.clawdis</string>
<key>ProgramArguments</key>
<array>
<string>\(bundlePath)/Contents/MacOS/Clawdis</string>
</array>
<key>WorkingDirectory</key>
<string>\(FileManager.default.homeDirectoryForCurrentUser.path)</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>\(CommandResolver.preferredPaths().joined(separator: ":"))</string>
</dict>
<key>StandardOutPath</key>
<string>\(LogLocator.launchdLogPath)</string>
<key>StandardErrorPath</key>
<string>\(LogLocator.launchdLogPath)</string>
</dict>
</plist>
"""
try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8)
}
@discardableResult
private static func runLaunchctl(_ args: [String]) async -> Int32 {
await Task.detached(priority: .utility) { () -> Int32 in
let process = Process()
process.launchPath = "/bin/launchctl"
process.arguments = args
process.standardOutput = Pipe()
process.standardError = Pipe()
do {
try process.run()
process.waitUntilExit()
return process.terminationStatus
} catch {
return -1
}
}.value
}
}

View File

@@ -0,0 +1,20 @@
import Foundation
enum LaunchdManager {
private static func runLaunchctl(_ args: [String]) {
let process = Process()
process.launchPath = "/bin/launchctl"
process.arguments = args
try? process.run()
}
static func startClawdis() {
let userTarget = "gui/\(getuid())/\(launchdLabel)"
self.runLaunchctl(["kickstart", "-k", userTarget])
}
static func stopClawdis() {
let userTarget = "gui/\(getuid())/\(launchdLabel)"
self.runLaunchctl(["stop", userTarget])
}
}

View File

@@ -0,0 +1,19 @@
import Foundation
extension ProcessInfo {
var isPreview: Bool {
self.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
}
var isRunningTests: Bool {
// SwiftPM tests load one or more `.xctest` bundles. With Swift Testing, `Bundle.main` is not
// guaranteed to be the `.xctest` bundle, so check all loaded bundles.
if Bundle.allBundles.contains(where: { $0.bundleURL.pathExtension == "xctest" }) { return true }
if Bundle.main.bundleURL.pathExtension == "xctest" { return true }
// Backwards-compatible fallbacks for runners that still set XCTest env vars.
return self.environment["XCTestConfigurationFilePath"] != nil
|| self.environment["XCTestBundlePath"] != nil
|| self.environment["XCTestSessionIdentifier"] != nil
}
}