fix(macos): validate embedded CLI helper
This commit is contained in:
@@ -105,8 +105,10 @@ struct OnboardingView: View {
|
||||
}
|
||||
|
||||
private var buttonTitle: String { self.currentPage == self.pageCount - 1 ? "Finish" : "Next" }
|
||||
private let devLinkCommand =
|
||||
"ln -sf /Applications/Clawdis.app/Contents/Resources/Relay/clawdis /usr/local/bin/clawdis"
|
||||
private var devLinkCommand: String {
|
||||
let bundlePath = Bundle.main.bundlePath
|
||||
return "ln -sf '\(bundlePath)/Contents/Resources/Relay/clawdis' /usr/local/bin/clawdis"
|
||||
}
|
||||
|
||||
private struct LocalGatewayProbe: Equatable {
|
||||
let port: Int
|
||||
|
||||
@@ -133,18 +133,38 @@ func age(from date: Date, now: Date = .init()) -> String {
|
||||
|
||||
@MainActor
|
||||
enum CLIInstaller {
|
||||
static func installedLocation() -> String? {
|
||||
let fm = FileManager.default
|
||||
private static func embeddedHelperURL() -> URL {
|
||||
Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/Relay/clawdis")
|
||||
}
|
||||
|
||||
for basePath in cliHelperSearchPaths {
|
||||
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 fm.fileExists(atPath: candidate, isDirectory: &isDirectory), !isDirectory.boolValue else {
|
||||
guard fileManager.fileExists(atPath: candidate, isDirectory: &isDirectory),
|
||||
!isDirectory.boolValue
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
if fm.isExecutableFile(atPath: candidate) {
|
||||
guard fileManager.isExecutableFile(atPath: candidate) else { continue }
|
||||
|
||||
let resolved = URL(fileURLWithPath: candidate).resolvingSymlinksInPath()
|
||||
if resolved == embedded {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
@@ -157,9 +177,11 @@ enum CLIInstaller {
|
||||
}
|
||||
|
||||
static func install(statusHandler: @escaping @Sendable (String) async -> Void) async {
|
||||
let helper = Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/Relay/clawdis")
|
||||
let helper = self.embeddedHelperURL()
|
||||
guard FileManager.default.isExecutableFile(atPath: helper.path) else {
|
||||
await statusHandler("Helper missing in bundle; rebuild via scripts/package-mac-app.sh")
|
||||
await statusHandler(
|
||||
"Embedded CLI missing in bundle; repackage via scripts/package-mac-app.sh " +
|
||||
"(or restart-mac.sh without SKIP_GATEWAY_PACKAGE=1).")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
43
apps/macos/Tests/ClawdisIPCTests/CLIInstallerTests.swift
Normal file
43
apps/macos/Tests/ClawdisIPCTests/CLIInstallerTests.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Clawdis
|
||||
|
||||
@Suite(.serialized)
|
||||
@MainActor
|
||||
struct CLIInstallerTests {
|
||||
@Test func installedLocationOnlyAcceptsEmbeddedHelper() throws {
|
||||
let fm = FileManager.default
|
||||
let root = fm.temporaryDirectory.appendingPathComponent(
|
||||
"clawdis-cli-installer-\(UUID().uuidString)")
|
||||
defer { try? fm.removeItem(at: root) }
|
||||
|
||||
let embedded = root.appendingPathComponent("Relay/clawdis")
|
||||
try fm.createDirectory(at: embedded.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
fm.createFile(atPath: embedded.path, contents: Data())
|
||||
try fm.setAttributes([.posixPermissions: 0o755], ofItemAtPath: embedded.path)
|
||||
|
||||
let binDir = root.appendingPathComponent("bin")
|
||||
try fm.createDirectory(at: binDir, withIntermediateDirectories: true)
|
||||
let link = binDir.appendingPathComponent("clawdis")
|
||||
try fm.createSymbolicLink(at: link, withDestinationURL: embedded)
|
||||
|
||||
let found = CLIInstaller.installedLocation(
|
||||
searchPaths: [binDir.path],
|
||||
embeddedHelper: embedded,
|
||||
fileManager: fm)
|
||||
#expect(found == link.path)
|
||||
|
||||
try fm.removeItem(at: link)
|
||||
let other = root.appendingPathComponent("Other/clawdis")
|
||||
try fm.createDirectory(at: other.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
fm.createFile(atPath: other.path, contents: Data())
|
||||
try fm.setAttributes([.posixPermissions: 0o755], ofItemAtPath: other.path)
|
||||
try fm.createSymbolicLink(at: link, withDestinationURL: other)
|
||||
|
||||
let rejected = CLIInstaller.installedLocation(
|
||||
searchPaths: [binDir.path],
|
||||
embeddedHelper: embedded,
|
||||
fileManager: fm)
|
||||
#expect(rejected == nil)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import Testing
|
||||
@testable import Clawdis
|
||||
|
||||
@Suite struct GatewayDiscoveryModelTests {
|
||||
@Suite
|
||||
@MainActor
|
||||
struct GatewayDiscoveryModelTests {
|
||||
@Test func localGatewayMatchesLanHost() {
|
||||
let local = GatewayDiscoveryModel.LocalIdentity(
|
||||
hostTokens: ["studio"],
|
||||
|
||||
@@ -16,6 +16,7 @@ import Testing
|
||||
server: [:],
|
||||
features: [:],
|
||||
snapshot: snapshot,
|
||||
canvashosturl: nil,
|
||||
policy: [:])
|
||||
|
||||
let mapped = MacGatewayChatTransport.mapPushToTransportEvent(.snapshot(hello))
|
||||
|
||||
@@ -6,41 +6,56 @@ import Testing
|
||||
@MainActor
|
||||
struct MasterDiscoveryMenuSmokeTests {
|
||||
@Test func inlineListBuildsBodyWhenEmpty() {
|
||||
let discovery = MasterDiscoveryModel()
|
||||
let discovery = GatewayDiscoveryModel()
|
||||
discovery.statusText = "Searching…"
|
||||
discovery.masters = []
|
||||
discovery.gateways = []
|
||||
|
||||
let view = MasterDiscoveryInlineList(discovery: discovery, currentTarget: nil, onSelect: { _ in })
|
||||
let view = GatewayDiscoveryInlineList(discovery: discovery, currentTarget: nil, onSelect: { _ in })
|
||||
_ = view.body
|
||||
}
|
||||
|
||||
@Test func inlineListBuildsBodyWithMasterAndSelection() {
|
||||
let discovery = MasterDiscoveryModel()
|
||||
let discovery = GatewayDiscoveryModel()
|
||||
discovery.statusText = "Found 1"
|
||||
discovery.masters = [
|
||||
.init(
|
||||
discovery.gateways = [
|
||||
GatewayDiscoveryModel.DiscoveredGateway(
|
||||
displayName: "Office Mac",
|
||||
lanHost: "office.local",
|
||||
tailnetDns: "office.tailnet-123.ts.net",
|
||||
sshPort: 2222,
|
||||
debugID: "office"),
|
||||
stableID: "office",
|
||||
debugID: "office",
|
||||
isLocal: false),
|
||||
]
|
||||
|
||||
let currentTarget = "\(NSUserName())@office.tailnet-123.ts.net:2222"
|
||||
let view = MasterDiscoveryInlineList(discovery: discovery, currentTarget: currentTarget, onSelect: { _ in })
|
||||
let view = GatewayDiscoveryInlineList(discovery: discovery, currentTarget: currentTarget, onSelect: { _ in })
|
||||
_ = view.body
|
||||
}
|
||||
|
||||
@Test func menuBuildsBodyWithMasters() {
|
||||
let discovery = MasterDiscoveryModel()
|
||||
let discovery = GatewayDiscoveryModel()
|
||||
discovery.statusText = "Found 2"
|
||||
discovery.masters = [
|
||||
.init(displayName: "A", lanHost: "a.local", tailnetDns: nil, sshPort: 22, debugID: "a"),
|
||||
.init(displayName: "B", lanHost: nil, tailnetDns: "b.ts.net", sshPort: 22, debugID: "b"),
|
||||
discovery.gateways = [
|
||||
GatewayDiscoveryModel.DiscoveredGateway(
|
||||
displayName: "A",
|
||||
lanHost: "a.local",
|
||||
tailnetDns: nil,
|
||||
sshPort: 22,
|
||||
stableID: "a",
|
||||
debugID: "a",
|
||||
isLocal: false),
|
||||
GatewayDiscoveryModel.DiscoveredGateway(
|
||||
displayName: "B",
|
||||
lanHost: nil,
|
||||
tailnetDns: "b.ts.net",
|
||||
sshPort: 22,
|
||||
stableID: "b",
|
||||
debugID: "b",
|
||||
isLocal: false),
|
||||
]
|
||||
|
||||
let view = MasterDiscoveryMenu(discovery: discovery, onSelect: { _ in })
|
||||
let view = GatewayDiscoveryMenu(discovery: discovery, onSelect: { _ in })
|
||||
_ = view.body
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ struct OnboardingViewSmokeTests {
|
||||
let view = OnboardingView(
|
||||
state: state,
|
||||
permissionMonitor: PermissionMonitor.shared,
|
||||
discoveryModel: MasterDiscoveryModel())
|
||||
discoveryModel: GatewayDiscoveryModel())
|
||||
_ = view.body
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ run_step "bundle canvas a2ui" bash -lc "cd '${ROOT_DIR}' && pnpm canvas:a2ui:bun
|
||||
run_step "clean build cache" bash -lc "cd '${ROOT_DIR}/apps/macos' && rm -rf .build .build-swift .swiftpm 2>/dev/null || true"
|
||||
run_step "swift build" bash -lc "cd '${ROOT_DIR}/apps/macos' && swift build -q --product Clawdis"
|
||||
|
||||
# 3) Package app (skip TS + gateway staging; rely on global/custom install for gateway JS).
|
||||
run_step "package app" bash -lc "cd '${ROOT_DIR}' && SKIP_TSC=1 SKIP_GATEWAY_PACKAGE=1 '${ROOT_DIR}/scripts/package-mac-app.sh'"
|
||||
# 3) Package app (default to bundling the embedded gateway + CLI).
|
||||
run_step "package app" bash -lc "cd '${ROOT_DIR}' && SKIP_TSC=${SKIP_TSC:-1} SKIP_GATEWAY_PACKAGE=${SKIP_GATEWAY_PACKAGE:-0} '${ROOT_DIR}/scripts/package-mac-app.sh'"
|
||||
|
||||
choose_app_bundle() {
|
||||
if [[ -n "${APP_BUNDLE}" && -d "${APP_BUNDLE}" ]]; then
|
||||
|
||||
Reference in New Issue
Block a user