Add an .engine RuntimeEvent source so host-tier issues (devices, packages, attach, spawn-gating, instruments, hookpacks, repl, itrace, editor) flow into the existing event stream instead of stdout. HookPackLibrary takes a callback for decode errors, and ITraceDecoder returns collected panic strings so ProcessNode can yield them as engine events.
84 lines
2.5 KiB
Swift
84 lines
2.5 KiB
Swift
import Foundation
|
|
import Observation
|
|
|
|
@Observable
|
|
@MainActor
|
|
public final class HookPackLibrary {
|
|
public private(set) var packs: [HookPack] = []
|
|
|
|
@ObservationIgnored public var onError: ((String) -> Void)?
|
|
|
|
private let directory: URL
|
|
|
|
public init(directory: URL) {
|
|
self.directory = directory
|
|
}
|
|
|
|
public func reload() {
|
|
packs = discover()
|
|
}
|
|
|
|
public func pack(withId id: String) -> HookPack? {
|
|
packs.first { $0.id == id }
|
|
}
|
|
|
|
public func descriptors() -> [InstrumentDescriptor] {
|
|
packs.map { pack in
|
|
let packID = pack.id
|
|
let icon = pack.resolvedIcon
|
|
let displayName = pack.manifest.name
|
|
let initialFeatures = Dictionary(
|
|
uniqueKeysWithValues: pack.manifest.features.map {
|
|
($0.id, FeatureState(enabled: $0.enabledByDefault, value: $0.schema.defaultValue))
|
|
}
|
|
)
|
|
|
|
return InstrumentDescriptor(
|
|
id: "hook-pack:\(packID)",
|
|
kind: .hookPack,
|
|
sourceIdentifier: packID,
|
|
displayName: displayName,
|
|
icon: icon,
|
|
compatibility: pack.manifest.compatibility,
|
|
makeInitialConfigJSON: {
|
|
HookPackConfig(packId: packID, features: initialFeatures).encode()
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
private func discover() -> [HookPack] {
|
|
let fm = FileManager.default
|
|
guard
|
|
let contents = try? fm.contentsOfDirectory(
|
|
at: directory,
|
|
includingPropertiesForKeys: [.isDirectoryKey],
|
|
options: [.skipsHiddenFiles]
|
|
)
|
|
else {
|
|
return []
|
|
}
|
|
|
|
var result: [HookPack] = []
|
|
for url in contents {
|
|
guard
|
|
let rv = try? url.resourceValues(forKeys: [.isDirectoryKey]),
|
|
rv.isDirectory == true
|
|
else { continue }
|
|
|
|
let manifestURL = url.appendingPathComponent("manifest.json")
|
|
guard let data = try? Data(contentsOf: manifestURL) else { continue }
|
|
|
|
do {
|
|
let manifest = try JSONDecoder().decode(HookPackManifest.self, from: data)
|
|
let id = url.lastPathComponent
|
|
result.append(HookPack(id: id, manifest: manifest, folderURL: url))
|
|
} catch {
|
|
onError?("Failed to decode hook-pack manifest at \(manifestURL.path): \(error)")
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|