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
69 changes: 69 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: CI

on:
push:
branches: ["**"]
pull_request:

# 同一分支后推的提交取消正在跑的旧 run,省 runner 时间。
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
typescript-sdk:
name: TypeScript SDK · test + build
runs-on: ubuntu-latest
defaults:
run:
working-directory: sdks/typescript
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: sdks/typescript/package-lock.json

- name: Install dependencies
run: npm install

- name: Typecheck (SDK + examples)
run: npm run typecheck

- name: Test (build + node --test)
run: npm test

macos-app:
name: macOS · build + package
runs-on: macos-15
defaults:
run:
working-directory: ahakeyconfig-mac
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # package_app.sh 用 git rev-list --count 算 build 号

- name: Toolchain
run: swift --version

- name: Compile-check all targets (debug)
run: swift build

- name: Package "AhaKey Studio.app" (release, ad-hoc signed)
run: |
chmod +x scripts/package_app.sh
scripts/package_app.sh

- name: Zip app bundle
working-directory: ahakeyconfig-mac/dist
run: ditto -c -k --sequesterRsrc --keepParent "AhaKey Studio.app" "AhaKey-Studio-macOS.zip"

- name: Upload app artifact
uses: actions/upload-artifact@v4
with:
name: AhaKey-Studio-macOS
path: ahakeyconfig-mac/dist/AhaKey-Studio-macOS.zip
if-no-files-found: error
21 changes: 19 additions & 2 deletions ahakeyconfig-mac/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,30 @@ let package = Package(
products: [
.executable(name: "AhaKeyConfig", targets: ["AhaKeyConfig"]),
.executable(name: "ahakeyconfig-agent", targets: ["AhaKeyConfigAgent"]),
.executable(name: "PluginShowcase", targets: ["PluginShowcase"]),
.library(name: "AhaKeyPluginKit", targets: ["AhaKeyPluginKit"]),
],

targets: [
.target(
name: "AhaKeyPluginKit",
path: "Sources/AhaKeyPluginKit"
),
.executableTarget(
name: "Plugin",
dependencies: ["AhaKeyPluginKit"],
path: "Sources/AhaKeyPlugin"
),
.executableTarget(
name: "PluginShowcase",
dependencies: ["AhaKeyPluginKit"],
path: "Sources/AhaKeyPluginShowcase"
),
.executableTarget(
name: "AhaKeyConfig",
dependencies: ["AhaKeyPluginKit"],
path: "Sources",
exclude: ["Agent"],
exclude: ["Agent", "AhaKeyPlugin", "AhaKeyPluginKit", "AhaKeyPluginShowcase"],
// 与 scripts/build.sh 中 Info.plist 一致。嵌入 __info_plist 段后 TCC 可识别。
// Debug 使用单独 plist:系统在「隐私与安全性」列表中显示为「AhaKey Studio(调试)」,与正式包区分。
linkerSettings: [
Expand Down
36 changes: 32 additions & 4 deletions ahakeyconfig-mac/Sources/Agent/AhaKeyAgent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,19 @@ final class AhaKeyAgent: NSObject, CBCentralManagerDelegate, CBPeripheralDelegat
)
}

func startSocketListener() {
@discardableResult
func startSocketListener() -> Bool {
if Self.hasLiveSocket(at: socketPath) {
emit("已有 Agent 在监听 Unix socket: \(socketPath)")
return false
}

startWatchdog()
// 清理旧 socket
// 清理没有监听进程的残留 socket
unlink(socketPath)

let fd = socket(AF_UNIX, SOCK_STREAM, 0)
guard fd >= 0 else { emit("socket() 失败"); return }
guard fd >= 0 else { emit("socket() 失败"); return false }

var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
Expand All @@ -212,7 +218,7 @@ final class AhaKeyAgent: NSObject, CBCentralManagerDelegate, CBPeripheralDelegat
bind(fd, sockPtr, socklen_t(MemoryLayout<sockaddr_un>.size))
}
}
guard bindResult == 0 else { emit("bind() 失败: \(errno)"); close(fd); return }
guard bindResult == 0 else { emit("bind() 失败: \(errno)"); close(fd); return false }

listen(fd, 5)
emit("监听 Unix socket: \(socketPath)")
Expand All @@ -224,6 +230,7 @@ final class AhaKeyAgent: NSObject, CBCentralManagerDelegate, CBPeripheralDelegat
self?.handleClient(clientFd)
}
}
return true
}

// MARK: - 看门狗
Expand Down Expand Up @@ -252,6 +259,27 @@ final class AhaKeyAgent: NSObject, CBCentralManagerDelegate, CBPeripheralDelegat

// MARK: - Socket handling

private static func hasLiveSocket(at path: String) -> Bool {
let fd = socket(AF_UNIX, SOCK_STREAM, 0)
guard fd >= 0 else { return false }
defer { close(fd) }

var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
path.withCString { ptr in
withUnsafeMutablePointer(to: &addr.sun_path) { sunPath in
let buf = UnsafeMutableRawPointer(sunPath).assumingMemoryBound(to: CChar.self)
strcpy(buf, ptr)
}
}

return withUnsafePointer(to: &addr) { ptr in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in
connect(fd, sockPtr, socklen_t(MemoryLayout<sockaddr_un>.size)) == 0
}
}
}

/// 单个客户端的处理:读一包请求,按 JSON 或旧版纯数字分发。
///
/// 协议:
Expand Down
4 changes: 3 additions & 1 deletion ahakeyconfig-mac/Sources/Agent/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ agent.onLog = { msg in
let ts = ISO8601DateFormatter().string(from: Date())
print("[\(ts)] \(msg)")
}
agent.startSocketListener()
guard agent.startSocketListener() else {
exit(EXIT_FAILURE)
}

// 保持运行
RunLoop.main.run()
47 changes: 47 additions & 0 deletions ahakeyconfig-mac/Sources/AhaKeyPlugin/Plugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import AhaKeyPluginKit
import Foundation

// AhaKey Plugin demo executable —— 把 `AhaKeyPluginKit` 当 SDK 走一遍 manager 流程:
// 1. 扫描 `~/Library/Application Support/AhaKeyConfig/plugins/`
// (或环境变量 `AHAKEY_PLUGINS_DIR` 指定的目录)
// 2. 按 manifest 拉起每个插件 + 握手
// 3. 列出加载结果
// 4. 停一会儿(让插件有机会反向调用 host)
// 5. 全部 shutdown
//
// 真实主 app 直接 `import AhaKeyPluginKit` 使用 `PluginManager`,不依赖这个 executable。
// 这里只是给 `swift run Plugin` 一个能跑的入口,便于本地验证骨架。

@main
struct PluginDemoMain {
static func main() async {
let manager = PluginManager()
let count = await manager.loadAll()

let plugins = await manager.allLoaded()
if plugins.isEmpty {
FileHandle.standardError.write(Data(
"""
[plugin-demo] no plugins loaded. \
drop a plugin.json into \(PluginManager.defaultPluginsRoot.path) \
or set AHAKEY_PLUGINS_DIR=<path> and rerun.

""".utf8
))
} else {
FileHandle.standardError.write(Data(
"[plugin-demo] loaded \(count) plugin(s):\n".utf8
))
for p in plugins {
let reported = p.initialize?.name ?? "<no name>"
FileHandle.standardError.write(Data(
" - \(p.manifest.id) v\(p.manifest.version) [plugin says: \(reported)]\n".utf8
))
}
}

// 给插件 1 秒做反向 RPC 之类的事,再 shutdown。
try? await Task.sleep(nanoseconds: 1_000_000_000)
await manager.unloadAll()
}
}
Loading
Loading