From 3a6ab81549857d86d8942e67220fcac68700b56f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 13 Dec 2025 11:10:22 +0000 Subject: [PATCH 1/3] fix(ui): increase onboarding horizontal padding --- apps/macos/Sources/Clawdis/Onboarding.swift | 3 ++- docs/mac/remote.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/macos/Sources/Clawdis/Onboarding.swift b/apps/macos/Sources/Clawdis/Onboarding.swift index 03d9570cf..3bc6368a4 100644 --- a/apps/macos/Sources/Clawdis/Onboarding.swift +++ b/apps/macos/Sources/Clawdis/Onboarding.swift @@ -487,7 +487,7 @@ struct OnboardingView: View { .keyboardShortcut(.return) .buttonStyle(.borderedProminent) } - .padding(.horizontal, 20) + .padding(.horizontal, 28) .padding(.bottom, 12) .frame(height: 60) } @@ -497,6 +497,7 @@ struct OnboardingView: View { content() Spacer() } + .padding(.horizontal, 28) .frame(width: self.pageWidth, alignment: .top) } diff --git a/docs/mac/remote.md b/docs/mac/remote.md index 203ea7981..a85fde7c4 100644 --- a/docs/mac/remote.md +++ b/docs/mac/remote.md @@ -22,6 +22,7 @@ This flow lets the macOS app act as a full remote control for a Clawdis gateway 1) Open *Settings → General*. 2) Under **Clawdis runs**, pick **Remote over SSH** and set: - **SSH target**: `user@host` (optional `:port`). + - If the gateway is on the same LAN and advertises Bonjour, pick it from the discovered list to auto-fill this field. - **Identity file** (advanced): path to your key. - **Project root** (advanced): remote checkout path used for commands. 3) Hit **Test remote**. Success indicates the remote `clawdis status --json` runs correctly. Failures usually mean PATH/CLI issues; exit 127 means the CLI isn’t found remotely. From 5b608718bb2581adf8d9a98788b6087a8f51c701 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 13 Dec 2025 11:10:30 +0000 Subject: [PATCH 2/3] test(clawdiskit): cover BonjourEscapes decoding --- .gitignore | 1 + apps/shared/ClawdisKit/Package.swift | 3 ++ .../ClawdisKitTests/BonjourEscapesTests.swift | 28 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 apps/shared/ClawdisKit/Tests/ClawdisKitTests/BonjourEscapesTests.swift diff --git a/.gitignore b/.gitignore index e6cef82e9..abdb707d9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ coverage .pnpm-store **/.DS_Store apps/macos/.build/ +apps/shared/ClawdisKit/.build/ bin/clawdis-mac apps/macos/.build-local/ apps/macos/.swiftpm/ diff --git a/apps/shared/ClawdisKit/Package.swift b/apps/shared/ClawdisKit/Package.swift index 9f207dc63..090c5b944 100644 --- a/apps/shared/ClawdisKit/Package.swift +++ b/apps/shared/ClawdisKit/Package.swift @@ -18,4 +18,7 @@ let package = Package( swiftSettings: [ .enableUpcomingFeature("StrictConcurrency"), ]), + .testTarget( + name: "ClawdisKitTests", + dependencies: ["ClawdisKit"]), ]) diff --git a/apps/shared/ClawdisKit/Tests/ClawdisKitTests/BonjourEscapesTests.swift b/apps/shared/ClawdisKit/Tests/ClawdisKitTests/BonjourEscapesTests.swift new file mode 100644 index 000000000..f71a6dcd4 --- /dev/null +++ b/apps/shared/ClawdisKit/Tests/ClawdisKitTests/BonjourEscapesTests.swift @@ -0,0 +1,28 @@ +import ClawdisKit +import XCTest + +final class BonjourEscapesTests: XCTestCase { + func testDecodePassThrough() { + XCTAssertEqual(BonjourEscapes.decode("hello"), "hello") + XCTAssertEqual(BonjourEscapes.decode(""), "") + } + + func testDecodeSpaces() { + XCTAssertEqual(BonjourEscapes.decode("Clawdis\\032Gateway"), "Clawdis Gateway") + } + + func testDecodeMultipleEscapes() { + XCTAssertEqual( + BonjourEscapes.decode("A\\038B\\047C\\032D"), + "A&B/C D") + } + + func testDecodeIgnoresInvalidEscapeSequences() { + XCTAssertEqual(BonjourEscapes.decode("Hello\\03World"), "Hello\\03World") + XCTAssertEqual(BonjourEscapes.decode("Hello\\XYZWorld"), "Hello\\XYZWorld") + } + + func testDecodeUsesDecimalUnicodeScalarValue() { + XCTAssertEqual(BonjourEscapes.decode("Hello\\065World"), "HelloAWorld") + } +} From 7e7e348a14fd9efe79ef0108be7866e19a3c5e4f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 13 Dec 2025 11:14:05 +0000 Subject: [PATCH 3/3] fix(bonjour): normalize hostnames for beacons --- src/infra/bonjour.test.ts | 31 +++++++++++++++++++++++++++++++ src/infra/bonjour.ts | 10 +++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/infra/bonjour.test.ts b/src/infra/bonjour.test.ts index 8ae6ae0d9..178fda57a 100644 --- a/src/infra/bonjour.test.ts +++ b/src/infra/bonjour.test.ts @@ -18,6 +18,11 @@ vi.mock("@homebridge/ciao", () => { const { startGatewayBonjourAdvertiser } = await import("./bonjour.js"); describe("gateway bonjour advertiser", () => { + type ServiceCall = { + name?: unknown; + txt?: unknown; + }; + const prevEnv = { ...process.env }; afterEach(() => { @@ -84,4 +89,30 @@ describe("gateway bonjour advertiser", () => { expect(destroy).toHaveBeenCalledTimes(2); expect(shutdown).toHaveBeenCalledTimes(1); }); + + it("normalizes hostnames with domains for service names", async () => { + // Allow advertiser to run in unit tests. + delete process.env.VITEST; + process.env.NODE_ENV = "development"; + + vi.spyOn(os, "hostname").mockReturnValue("Mac.localdomain"); + + const destroy = vi.fn().mockResolvedValue(undefined); + const advertise = vi.fn().mockResolvedValue(undefined); + createService.mockReturnValue({ advertise, destroy }); + + const started = await startGatewayBonjourAdvertiser({ + gatewayPort: 18789, + sshPort: 2222, + bridgePort: 18790, + }); + + const [masterCall] = createService.mock.calls as Array<[ServiceCall]>; + expect(masterCall?.[0]?.name).toBe("Mac (Clawdis)"); + expect((masterCall?.[0]?.txt as Record)?.lanHost).toBe( + "Mac.local", + ); + + await started.stop(); + }); }); diff --git a/src/infra/bonjour.ts b/src/infra/bonjour.ts index 332aabea0..f8141ec62 100644 --- a/src/infra/bonjour.ts +++ b/src/infra/bonjour.ts @@ -39,7 +39,15 @@ export async function startGatewayBonjourAdvertiser( const { getResponder, Protocol } = await import("@homebridge/ciao"); const responder = getResponder(); - const hostname = os.hostname().replace(/\.local$/i, ""); + // mDNS service instance names are single DNS labels; dots in hostnames (like + // `Mac.localdomain`) can confuse some resolvers/browsers and break discovery. + // Keep only the first label and normalize away a trailing `.local`. + const hostname = + os + .hostname() + .replace(/\.local$/i, "") + .split(".")[0] + .trim() || "clawdis"; const instanceName = typeof opts.instanceName === "string" && opts.instanceName.trim() ? opts.instanceName.trim()