hermes-swift-mac-app
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHermes Swift Mac App Skill
Hermes Swift macOS应用技能
Overview
概述
Hermes Agent is a native macOS desktop wrapper for Hermes Web UI. Built with Swift and WKWebView, it provides a standalone Mac app experience with no Electron or heavy dependencies. The app supports both local Hermes instances and remote servers via SSH tunneling.
Key features:
- Native macOS app using WKWebView (requires macOS 12+)
- Direct connection mode for local Hermes Web UI
- SSH tunnel mode for remote servers
- Clipboard integration (text and images)
- System notifications for AI responses
- Auto-update via Sparkle framework
- Signed and notarized for Gatekeeper
Hermes Agent是Hermes Web UI的原生macOS桌面包装器。它使用Swift和WKWebView构建,提供独立的Mac应用体验,无需Electron或重型依赖项。该应用支持本地Hermes实例和通过SSH隧道连接远程服务器。
核心功能:
- 基于WKWebView的原生macOS应用(要求macOS 12+)
- 本地Hermes Web UI的直接连接模式
- 远程服务器的SSH隧道模式
- 剪贴板集成(文本和图片)
- AI响应的系统通知
- 基于Sparkle框架的自动更新
- 已签名并通过Gatekeeper公证
Installation
安装
For End Users
面向终端用户
Download the latest DMG from releases:
bash
undefined从发布页面下载最新DMG:
bash
undefinedDownload Hermes-Agent-vX.X.X.dmg
下载 Hermes-Agent-vX.X.X.dmg
Open DMG and drag to Applications folder
打开DMG并拖拽到应用程序文件夹
undefinedundefinedFor Developers - Build from Source
面向开发者 - 从源码构建
bash
undefinedbash
undefinedInstall Xcode Command Line Tools if needed
如有需要,安装Xcode命令行工具
xcode-select --install
xcode-select --install
Clone and build
克隆并构建
git clone https://github.com/hermes-webui/hermes-swift-mac.git
cd hermes-swift-mac
./build.sh
The `build.sh` script:
1. Compiles the Swift code via `swift build -c release`
2. Creates the `.app` bundle structure
3. Converts `icon.png` to `AppIcon.icns`
4. Copies binaries and resources
5. Installs to `/Applications/Hermes Agent.app`git clone https://github.com/hermes-webui/hermes-swift-mac.git
cd hermes-swift-mac
./build.sh
`build.sh`脚本会执行以下操作:
1. 通过`swift build -c release`编译Swift代码
2. 创建`.app`包结构
3. 将`icon.png`转换为`AppIcon.icns`
4. 复制二进制文件和资源
5. 安装到`/Applications/Hermes Agent.app`Project Structure
项目结构
Sources/HermesAgent/
├── main.swift # Entry point, signal handling
├── AppDelegate.swift # App lifecycle, menu bar, Sparkle updater
├── BrowserWindowController.swift # WKWebView window, clipboard, notifications
├── TunnelManager.swift # SSH tunnel management
├── PreferencesWindowController.swift # Settings UI
└── SplashWindowController.swift # Launch splash screen
Package.swift # SPM manifest
build.sh # Build and install script
scripts/release.sh # Release automationSources/HermesAgent/
├── main.swift # 入口点、信号处理
├── AppDelegate.swift # 应用生命周期、菜单栏、Sparkle更新器
├── BrowserWindowController.swift # WKWebView窗口、剪贴板、通知
├── TunnelManager.swift # SSH隧道管理
├── PreferencesWindowController.swift # 设置UI
└── SplashWindowController.swift # 启动闪屏
Package.swift # SPM清单
build.sh # 构建和安装脚本
scripts/release.sh # 发布自动化脚本Configuration Modes
配置模式
Direct Mode (Local Hermes)
直接模式(本地Hermes)
For Hermes Web UI running on the same Mac:
Default settings:
- Target URL:
http://localhost:8787 - Connection Mode: Direct
swift
// In PreferencesWindowController.swift
// Default URL stored in UserDefaults
UserDefaults.standard.string(forKey: "targetURL") ?? "http://localhost:8787"适用于同一台Mac上运行的Hermes Web UI:
默认设置:
- 目标URL:
http://localhost:8787 - 连接模式:直接
swift
// 在PreferencesWindowController.swift中
// 默认URL存储在UserDefaults中
UserDefaults.standard.string(forKey: "targetURL") ?? "http://localhost:8787"SSH Tunnel Mode (Remote Hermes)
SSH隧道模式(远程Hermes)
For Hermes Web UI on a remote server:
Required settings:
- Connection Mode: SSH Tunnel
- Username: SSH user on remote server
- Host: Server hostname or IP
- Local Port: Port on Mac (default 8787)
- Remote Port: Port where Hermes runs on server (default 8787)
SSH requirements:
- Key-based authentication must work (without password)
ssh user@host - file accessible
~/.ssh/known_hosts - No password authentication support
swift
// TunnelManager.swift constructs SSH command:
ssh -o StrictHostKeyChecking=accept-new \
-o ExitOnForwardFailure=yes \
-N -L \(localPort):localhost:\(remotePort) \
\(username)@\(host)适用于运行在远程服务器上的Hermes Web UI:
必填设置:
- 连接模式:SSH隧道
- 用户名:远程服务器上的SSH用户
- 主机:服务器主机名或IP
- 本地端口:Mac上的端口(默认8787)
- 远程端口:服务器上Hermes运行的端口(默认8787)
SSH要求:
- 必须支持基于密钥的认证(无需密码即可执行)
ssh user@host - 可访问文件
~/.ssh/known_hosts - 不支持密码认证
swift
// TunnelManager.swift构造SSH命令:
ssh -o StrictHostKeyChecking=accept-new \
-o ExitOnForwardFailure=yes \
-N -L \(localPort):localhost:\(remotePort) \
\(username)@\(host)Code Examples
代码示例
Reading User Preferences
读取用户偏好设置
swift
// From PreferencesWindowController.swift
let defaults = UserDefaults.standard
// Get connection mode
let mode = defaults.string(forKey: "connectionMode") ?? "direct"
// Get target URL
let targetURL = defaults.string(forKey: "targetURL") ?? "http://localhost:8787"
// Get SSH settings (tunnel mode)
let sshUsername = defaults.string(forKey: "sshUsername") ?? ""
let sshHost = defaults.string(forKey: "sshHost") ?? ""
let localPort = defaults.integer(forKey: "localPort")
let remotePort = defaults.integer(forKey: "remotePort")swift
// 来自PreferencesWindowController.swift
let defaults = UserDefaults.standard
// 获取连接模式
let mode = defaults.string(forKey: "connectionMode") ?? "direct"
// 获取目标URL
let targetURL = defaults.string(forKey: "targetURL") ?? "http://localhost:8787"
// 获取SSH设置(隧道模式)
let sshUsername = defaults.string(forKey: "sshUsername") ?? ""
let sshHost = defaults.string(forKey: "sshHost") ?? ""
let localPort = defaults.integer(forKey: "localPort")
let remotePort = defaults.integer(forKey: "remotePort")Starting SSH Tunnel
启动SSH隧道
swift
// From TunnelManager.swift
class TunnelManager {
private var sshProcess: Process?
func startTunnel(username: String, host: String,
localPort: Int, remotePort: Int) {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
process.arguments = [
"-o", "StrictHostKeyChecking=accept-new",
"-o", "ExitOnForwardFailure=yes",
"-N",
"-L", "\(localPort):localhost:\(remotePort)",
"\(username)@\(host)"
]
try? process.run()
self.sshProcess = process
// Monitor tunnel health
monitorTunnel()
}
func stopTunnel() {
sshProcess?.terminate()
sshProcess = nil
}
}swift
// 来自TunnelManager.swift
class TunnelManager {
private var sshProcess: Process?
func startTunnel(username: String, host: String,
localPort: Int, remotePort: Int) {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
process.arguments = [
"-o", "StrictHostKeyChecking=accept-new",
"-o", "ExitOnForwardFailure=yes",
"-N",
"-L", "\(localPort):localhost:\(remotePort)",
"\(username)@\(host)"
]
try? process.run()
self.sshProcess = process
// 监控隧道状态
monitorTunnel()
}
func stopTunnel() {
sshProcess?.terminate()
sshProcess = nil
}
}Testing Connection
测试连接
swift
// From PreferencesWindowController.swift
func testConnection(url: String, completion: @escaping (Bool) -> Void) {
guard let testURL = URL(string: url) else {
completion(false)
return
}
var request = URLRequest(url: testURL)
request.timeoutInterval = 5.0
URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
completion(true)
} else {
completion(false)
}
}
}.resume()
}swift
// 来自PreferencesWindowController.swift
func testConnection(url: String, completion: @escaping (Bool) -> Void) {
guard let testURL = URL(string: url) else {
completion(false)
return
}
var request = URLRequest(url: testURL)
request.timeoutInterval = 5.0
URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
completion(true)
} else {
completion(false)
}
}
}.resume()
}Clipboard Integration
剪贴板集成
swift
// From BrowserWindowController.swift
// Paste handler for text and images
@objc func handlePaste(_ sender: Any?) {
let pasteboard = NSPasteboard.general
if let image = pasteboard.readObjects(forClasses: [NSImage.self])?.first as? NSImage,
let data = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: data),
let pngData = bitmap.representation(using: .png, properties: [:]) {
let base64 = pngData.base64EncodedString()
let js = "window.pasteImage('data:image/png;base64,\(base64)');"
webView.evaluateJavaScript(js)
} else if let string = pasteboard.string(forType: .string) {
let js = "window.pasteText(\(jsonEscape(string)));"
webView.evaluateJavaScript(js)
}
}swift
// 来自BrowserWindowController.swift
// 处理文本和图片粘贴
@objc func handlePaste(_ sender: Any?) {
let pasteboard = NSPasteboard.general
if let image = pasteboard.readObjects(forClasses: [NSImage.self])?.first as? NSImage,
let data = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: data),
let pngData = bitmap.representation(using: .png, properties: [:]) {
let base64 = pngData.base64EncodedString()
let js = "window.pasteImage('data:image/png;base64,\(base64)');"
webView.evaluateJavaScript(js)
} else if let string = pasteboard.string(forType: .string) {
let js = "window.pasteText(\(jsonEscape(string)));"
webView.evaluateJavaScript(js)
}
}System Notifications
系统通知
swift
// From BrowserWindowController.swift
// Show notification when AI response completes
func showNotification(title: String, body: String) {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: nil
)
UNUserNotificationCenter.current().add(request)
}swift
// 来自BrowserWindowController.swift
// AI响应完成时显示通知
func showNotification(title: String, body: String) {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: nil
)
UNUserNotificationCenter.current().add(request)
}Auto-Update with Sparkle
基于Sparkle的自动更新
swift
// From AppDelegate.swift
import Sparkle
class AppDelegate: NSObject, NSApplicationDelegate {
private var updaterController: SPUStandardUpdaterController?
func applicationDidFinishLaunching(_ notification: Notification) {
// Initialize Sparkle updater
updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
// Check for updates on launch
updaterController?.updater.checkForUpdatesInBackground()
}
@objc func checkForUpdates(_ sender: Any?) {
updaterController?.updater.checkForUpdates()
}
}swift
// 来自AppDelegate.swift
import Sparkle
class AppDelegate: NSObject, NSApplicationDelegate {
private var updaterController: SPUStandardUpdaterController?
func applicationDidFinishLaunching(_ notification: Notification) {
// 初始化Sparkle更新器
updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
// 启动时检查更新
updaterController?.updater.checkForUpdatesInBackground()
}
@objc func checkForUpdates(_ sender: Any?) {
updaterController?.updater.checkForUpdates()
}
}Building and Running
构建与运行
Development Build
开发构建
bash
undefinedbash
undefinedBuild in debug mode
调试模式构建
swift build
swift build
Run directly
直接运行
swift run
swift run
Run tests
运行测试
swift test
undefinedswift test
undefinedRelease Build
发布构建
bash
undefinedbash
undefinedBuild release binary
构建发布版二进制文件
swift build -c release
swift build -c release
Or use build script (compiles + installs)
或使用构建脚本(编译+安装)
./build.sh
The build script handles:
- Release compilation
- App bundle creation at `HermesAgent.app/`
- Icon conversion (PNG → ICNS)
- Binary signing (if configured)
- Installation to `/Applications/`./build.sh
构建脚本会处理:
- 发布版编译
- 在`HermesAgent.app/`创建应用包
- 图标转换(PNG → ICNS)
- 二进制签名(若已配置)
- 安装到`/Applications/`Icon Generation
图标生成
bash
undefinedbash
undefinedConvert PNG to ICNS (done by build.sh)
将PNG转换为ICNS(由build.sh自动完成)
sips -s format icns icon.png --out AppIcon.icns
sips -s format icns icon.png --out AppIcon.icns
Or manually with iconutil
或使用iconutil手动转换
mkdir icon.iconset
sips -z 16 16 icon.png --out icon.iconset/icon_16x16.png
sips -z 32 32 icon.png --out icon.iconset/icon_16x16@2x.png
sips -z 32 32 icon.png --out icon.iconset/icon_32x32.png
sips -z 64 64 icon.png --out icon.iconset/icon_32x32@2x.png
sips -z 128 128 icon.png --out icon.iconset/icon_128x128.png
sips -z 256 256 icon.png --out icon.iconset/icon_128x128@2x.png
sips -z 256 256 icon.png --out icon.iconset/icon_256x256.png
sips -z 512 512 icon.png --out icon.iconset/icon_256x256@2x.png
sips -z 512 512 icon.png --out icon.iconset/icon_512x512.png
sips -z 1024 1024 icon.png --out icon.iconset/icon_512x512@2x.png
iconutil -c icns icon.iconset
undefinedmkdir icon.iconset
sips -z 16 16 icon.png --out icon.iconset/icon_16x16.png
sips -z 32 32 icon.png --out icon.iconset/icon_16x16@2x.png
sips -z 32 32 icon.png --out icon.iconset/icon_32x32.png
sips -z 64 64 icon.png --out icon.iconset/icon_32x32@2x.png
sips -z 128 128 icon.png --out icon.iconset/icon_128x128.png
sips -z 256 256 icon.png --out icon.iconset/icon_128x128@2x.png
sips -z 256 256 icon.png --out icon.iconset/icon_256x256.png
sips -z 512 512 icon.png --out icon.iconset/icon_256x256@2x.png
sips -z 512 512 icon.png --out icon.iconset/icon_512x512.png
sips -z 1024 1024 icon.png --out icon.iconset/icon_512x512@2x.png
iconutil -c icns icon.iconset
undefinedReleasing
发布流程
Create a Release
创建发布版本
bash
undefinedbash
undefinedUse release script (pushes main, then tag separately)
使用发布脚本(先推送main分支,再单独推送标签)
scripts/release.sh v1.0.9
**Why separate pushes?** GitHub sometimes drops workflow triggers when commit and tag are pushed together. The script:
1. Pushes `main` branch
2. Waits briefly
3. Pushes the tag separately
This ensures the GitHub Actions workflow fires reliably.scripts/release.sh v1.0.9
**为何分开推送?** 当提交和标签一起推送时,GitHub有时会丢失工作流触发信号。该脚本会:
1. 推送`main`分支
2. 短暂等待
3. 单独推送标签
这能确保GitHub Actions工作流可靠触发。Manual Release Process
手动发布流程
bash
undefinedbash
undefinedTag the release
打发布标签
git tag v1.0.9
git push origin main
git push origin v1.0.9
git tag v1.0.9
git push origin main
git push origin v1.0.9
If workflow doesn't trigger, run manually:
如果工作流未触发,手动运行:
Actions → Build and Release macOS App → Run workflow → enter tag
操作 → Build and Release macOS App → 运行工作流 → 输入标签
undefinedundefinedGitHub Actions Workflow
GitHub Actions工作流
The (not shown in README but typical) would:
.github/workflows/build-release.yml- Build the app on
macos-latest - Sign with Developer ID certificate (from secrets)
- Notarize with Apple (requires Apple ID credentials)
- Create DMG with or
create-dmghdiutil - Upload DMG as release asset
.github/workflows/build-release.yml- 在环境构建应用
macos-latest - 使用开发者ID证书签名(来自密钥库)
- 通过苹果公证(需要Apple ID凭据)
- 使用或
create-dmg创建DMGhdiutil - 将DMG上传为发布资产
Common Patterns
常见模式
Checking Tunnel Status
检查隧道状态
swift
// From TunnelManager.swift
func isTunnelRunning() -> Bool {
guard let process = sshProcess else { return false }
return process.isRunning
}
func monitorTunnel() {
// Check if local port is accessible
let socket = CFSocketCreate(kCFAllocatorDefault,
PF_INET,
SOCK_STREAM,
IPPROTO_TCP,
0, nil, nil)
// ... probe localhost:localPort
}swift
// 来自TunnelManager.swift
func isTunnelRunning() -> Bool {
guard let process = sshProcess else { return false }
return process.isRunning
}
func monitorTunnel() {
// 检查本地端口是否可访问
let socket = CFSocketCreate(kCFAllocatorDefault,
PF_INET,
SOCK_STREAM,
IPPROTO_TCP,
0, nil, nil)
// ... 探测localhost:localPort
}Handling WebView Navigation
处理WebView导航
swift
// From BrowserWindowController.swift
extension BrowserWindowController: WKNavigationDelegate {
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// Open external links in Safari
if let url = navigationAction.request.url,
navigationAction.navigationType == .linkActivated {
NSWorkspace.shared.open(url)
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
}swift
// 来自BrowserWindowController.swift
extension BrowserWindowController: WKNavigationDelegate {
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// 在Safari中打开外部链接
if let url = navigationAction.request.url,
navigationAction.navigationType == .linkActivated {
NSWorkspace.shared.open(url)
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
}Global Keyboard Shortcut
全局键盘快捷键
swift
// From AppDelegate.swift
func applicationDidFinishLaunching(_ notification: Notification) {
// Register ⌘⇧H to bring app forward
NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in
if event.modifierFlags.contains([.command, .shift]),
event.charactersIgnoringModifiers == "h" {
NSApp.activate(ignoringOtherApps: true)
}
}
}swift
// 来自AppDelegate.swift
func applicationDidFinishLaunching(_ notification: Notification) {
// 注册⌘⇧H快捷键以唤起应用
NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in
if event.modifierFlags.contains([.command, .shift]),
event.charactersIgnoringModifiers == "h" {
NSApp.activate(ignoringOtherApps: true)
}
}
}Troubleshooting
故障排除
Connection Error on Launch
启动时连接错误
Symptom: Blank page or "Unable to connect" error
Solutions:
- Ensure Hermes Web UI is running:
bash
cd ~/hermes-webui-public bash start.sh - Check Target URL in Preferences (⌘,)
- Use Test Connection button to verify
- Check Console.app for errors from "Hermes Agent"
症状: 空白页面或"无法连接"错误
解决方案:
- 确保Hermes Web UI正在运行:
bash
cd ~/hermes-webui-public bash start.sh - 在偏好设置(⌘,)中检查目标URL
- 使用测试连接按钮验证
- 在Console.app中查看"Hermes Agent"的错误日志
SSH Tunnel Fails Immediately
SSH隧道立即失败
Symptom: Status shows "Disconnected" right after connecting
Solutions:
- Test SSH key auth works:
bash
ssh user@your-server # Should connect without password - Set up SSH keys if needed:
bash
ssh-keygen -t ed25519 ssh-copy-id user@your-server - Verify remote port is correct (where Hermes actually runs)
- Check isn't corrupted
~/.ssh/known_hosts
症状: 连接后状态立即显示"已断开"
解决方案:
- 测试SSH密钥认证是否正常:
bash
ssh user@your-server # 应无需密码即可连接 - 如有需要,设置SSH密钥:
bash
ssh-keygen -t ed25519 ssh-copy-id user@your-server - 验证远程端口是否正确(Hermes实际运行的端口)
- 检查是否损坏
~/.ssh/known_hosts
Voice Input Not Working
语音输入无法工作
Symptom: Microphone button doesn't respond
Solutions:
- Grant microphone permission:
- System Settings → Privacy & Security → Microphone
- Enable "Hermes Agent"
- Restart app after granting permission
- Check Info.plist includes
NSMicrophoneUsageDescription
症状: 麦克风按钮无响应
解决方案:
- 授予麦克风权限:
- 系统设置 → 隐私与安全性 → 麦克风
- 启用"Hermes Agent"
- 授予权限后重启应用
- 检查Info.plist是否包含
NSMicrophoneUsageDescription
Gatekeeper Blocks App
Gatekeeper阻止应用
Symptom: "App can't be opened because it is from an unidentified developer"
Solutions:
- Download latest release (v1.0.4+) — these are signed and notarized
- If building from source without signing:
bash
xattr -cr /Applications/Hermes\ Agent.app - Right-click app → Open (first time only)
症状: "无法打开应用,因为它来自未识别的开发者"
解决方案:
- 下载最新版本(v1.0.4+)——这些版本已签名并通过公证
- 如果从源码构建且未签名:
bash
xattr -cr /Applications/Hermes\ Agent.app - 右键点击应用 → 打开(仅首次需要)
Blurry App Icon
应用图标模糊
Symptom: Icon looks pixelated in Dock after building
Solution:
bash
undefined症状: 构建后Dock中的图标显示像素化
解决方案:
bash
undefinedRefresh icon cache
刷新图标缓存
killall Dock
undefinedkillall Dock
undefinedPort Already in Use
端口已被占用
Symptom: "Port forwarding failed" when starting tunnel
Solutions:
bash
undefined症状: 启动隧道时显示"端口转发失败"
解决方案:
bash
undefinedFind what's using the port
查找占用端口的进程
lsof -i :8787
lsof -i :8787
Kill the process or change Local Port in Preferences
终止进程或在偏好设置中修改本地端口
undefinedundefinedEnvironment Variables
环境变量
The app doesn't use environment variables directly, but for development:
bash
undefined应用本身不直接使用环境变量,但开发时可使用:
bash
undefinedEnable WebKit debug logging
启用WebKit调试日志
export WEBKIT_DEBUG=1
export WEBKIT_DEBUG=1
Sparkle update feed (set in Info.plist normally)
Sparkle更新源(通常在Info.plist中设置)
export SPARKLE_APPCAST_URL=https://example.com/appcast.xml
undefinedexport SPARKLE_APPCAST_URL=https://example.com/appcast.xml
undefinedTesting
测试
bash
undefinedbash
undefinedRun all tests
运行所有测试
swift test
swift test
Run specific test
运行特定测试
swift test --filter TunnelManagerTests
swift test --filter TunnelManagerTests
Run with verbose output
带详细输出运行测试
swift test --verbose
undefinedswift test --verbose
undefinedExample Test
测试示例
swift
// From Tests/HermesAgentTests/TunnelManagerTests.swift
import XCTest
@testable import HermesAgent
final class TunnelManagerTests: XCTestCase {
func testTunnelCreation() {
let manager = TunnelManager()
XCTAssertFalse(manager.isTunnelRunning())
manager.startTunnel(
username: "test",
host: "localhost",
localPort: 8787,
remotePort: 8787
)
// Wait briefly for process to start
sleep(1)
XCTAssertTrue(manager.isTunnelRunning())
manager.stopTunnel()
XCTAssertFalse(manager.isTunnelRunning())
}
}swift
// 来自Tests/HermesAgentTests/TunnelManagerTests.swift
import XCTest
@testable import HermesAgent
final class TunnelManagerTests: XCTestCase {
func testTunnelCreation() {
let manager = TunnelManager()
XCTAssertFalse(manager.isTunnelRunning())
manager.startTunnel(
username: "test",
host: "localhost",
localPort: 8787,
remotePort: 8787
)
// 短暂等待进程启动
sleep(1)
XCTAssertTrue(manager.isTunnelRunning())
manager.stopTunnel()
XCTAssertFalse(manager.isTunnelRunning())
}
}Dependencies
依赖项
Managed via Swift Package Manager in :
Package.swiftswift
// Package.swift
dependencies: [
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.3.0")
],
targets: [
.executableTarget(
name: "HermesAgent",
dependencies: [
.product(name: "Sparkle", package: "Sparkle")
]
)
]Runtime dependencies:
- macOS 12 (Monterey) or later
- Xcode Command Line Tools (for building)
- SSH (provided by macOS)
No external dependencies for users — fully self-contained app.
通过中的Swift Package Manager管理:
Package.swiftswift
// Package.swift
dependencies: [
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.3.0")
],
targets: [
.executableTarget(
name: "HermesAgent",
dependencies: [
.product(name: "Sparkle", package: "Sparkle")
]
)
]运行时依赖:
- macOS 12(Monterey)或更高版本
- Xcode命令行工具(用于构建)
- SSH(由macOS提供)
终端用户无需额外依赖——应用完全独立。