Skip to content

Split the proxy core into a shared library, add a Linux CLI - #9

Merged
stuartshields merged 3 commits into
stuartshields:mainfrom
svandragt:linux-cli-support
Sep 8, 2026
Merged

Split the proxy core into a shared library, add a Linux CLI#9
stuartshields merged 3 commits into
stuartshields:mainfrom
svandragt:linux-cli-support

Conversation

@svandragt

@svandragt svandragt commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Closes #8.

Problem

ProxyLight only ran as a macOS menu bar app. Everything lived in one target, so it couldn't build on Linux at all, even though most of the proxy has nothing macOS-specific in it.

Solution

Split the package into a portable core library, the existing macOS app, and a new command-line tool that runs on Linux and macOS.1 The app behaves exactly as before; it now depends on the core instead of owning everything.2

The CLI covers starting the proxy and managing mappings, and nothing else.3 It prints the PAC URL and the CA certificate path; the user points their browser at the URL and imports the certificate by hand. No system-proxy automation, no trust-store install, no login-at-boot, no self-update, as agreed in #8.

Along the way this fixes a real bug: the CLI ignored Ctrl-C and kill, so it couldn't be run under a process manager.4

Tests are split the same way, and the portable suite passes on Linux.5 README and CLAUDE.md describe the new layout and the Linux build.6

Not in this PR

No CI job for Linux yet. Builds were checked by hand in Docker.

Test plan

  • swift build and swift test on macOS: app unchanged, all tests pass
  • docker run --rm -v "$PWD":/src -w /src swift:6.1-noble swift build --product proxylight-cli -c release --static-swift-stdlib produces a binary that runs on the host
  • proxylight-cli mapping add … && proxylight-cli start serves http://127.0.0.1:9876/proxy.pac
  • kill -TERM <pid> stops it

Footnotes

  1. Package.swift now has three targets. ProxyLightCore: mapping engine, PAC generation, CA and leaf certificate generation, the SwiftNIO proxy pipeline, and a new ProxyOrchestrator extracted from AppState. ProxyLight: the macOS app. proxylight-cli: the executable, built on swift-argument-parser.

  2. macOS-only wrappers (SystemProxyManager, CATrustManager, SelfUpdater, LoginItemManager, ProxyRestoreStore) moved to Sources/ProxyLight/MacOS/. MappingStore gained an #if os(macOS) branch so the config directory is ~/Library/Application Support/ProxyLight on macOS and $XDG_CONFIG_HOME/proxylight (default ~/.config/proxylight) on Linux.

  3. Subcommands: start (default, --port), mapping add|list|remove (add --fallback-on-not-found for serve-local-first mode), import, export, ca-path.

  4. The SIGINT/SIGTERM DispatchSource was scheduled on DispatchQueue.main, which never drains because the CLI blocks the main thread on a semaphore instead of running a run loop. It now uses a dedicated queue.

  5. ProxyLightCoreTests (portable) and ProxyLightTests (macOS-only). The core suite is 86/86 against the swift:6.1 Docker image.

  6. The dependencies (swift-nio 2.101, swift-nio-ssl 2.37, swift-certificates 1.19) require Swift tools 6.1, so swift:6.0 images fail. --static-swift-stdlib is needed for the binary to run outside the build container.

Splits Package.swift into ProxyLightCore (portable), the existing
macOS app target, and a new proxylight-cli target, extracting a
plain-Swift ProxyOrchestrator out of AppState so both frontends drive
the same start/stop/mapping logic. The CLI is minimal scope: proxy +
PAC + CA cert generation only, no system-proxy automation, no CA
trust-store install, no login-at-boot, no self-update on Linux.

Closes stuartshields#8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sg1MPNVjkvn53s8bgYdJD9
@svandragt
svandragt marked this pull request as draft July 10, 2026 09:39
@svandragt

svandragt commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author
  • I need to Test
  • change pr to ready

@stuartshields

Copy link
Copy Markdown
Owner

@svandragt Would this also work with Windows Powershell? I don't have a Windows laptop to try it out.

@svandragt

svandragt commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

I would presume not. Windows and Linux are very different. There are free Windows VMs available on Microsoft's site but I'd open a separate issue for that for now, it's not in scope.

Powershell the terminal app can run Windows cli executables fine though. Powershell the scripting language has its own syntax so I don't think that's what you mean.

If you mean WSL then it might work. Also worth testing in a separate issue

@stuartshields

Copy link
Copy Markdown
Owner

@svandragt Yeah WSL is what I meant. I agree that we can test this in a different issue.

svandragt and others added 2 commits September 7, 2026 14:13
DispatchSource.makeSignalSource reads a signalfd on Linux, which never
sees a signal set to SIG_IGN, so Ctrl-C and `kill` left the proxy
running until SIGKILL. Replace it with a self-pipe C handler that works
on both platforms, and flush stdout after the startup banner so it
reaches a log file or journald promptly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014b9yy159qjGz9hj1o1wWx4
Also replace the stale DispatchSource signal-handling note in CLAUDE.md
with the self-pipe rationale.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014b9yy159qjGz9hj1o1wWx4
@svandragt
svandragt marked this pull request as ready for review September 7, 2026 13:51
@svandragt

Copy link
Copy Markdown
Contributor Author

@stuartshields ready. Tested successfully on a DDEV WP project!

@stuartshields
stuartshields merged commit 281e56b into stuartshields:main Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a Linux CLI build alongside the macOS app

2 participants