diff --git a/assets/licenses.yaml b/assets/licenses.yaml index bc313864..2310343a 100644 --- a/assets/licenses.yaml +++ b/assets/licenses.yaml @@ -109,6 +109,18 @@ dependencies: the core PTY wrapper for libc bindings (socketpair, recvmsg with SCM_RIGHTS, ioctl). Dart-team maintained; zero transitive deps. + - name: dugite-native (Git) + kind: native-binary + version: "2.53.0" + homepage: https://github.com/desktop/dugite-native + license: GPL-2.0 (Git), MIT (dugite-native build scripts) + purpose: >- + Self-contained, relocatable Git distribution for macOS/Linux/Windows. + Bundled so clide can run git inside the macOS app sandbox without + depending on Homebrew or Xcode Command Line Tools. Same approach + used by GitHub Desktop and Tower. Binary not committed — downloaded + at build time via `make dugite-fetch`. + - name: tree-sitter kind: native-shared-lib version: "0.26.8" diff --git a/decisions/tooling.md b/decisions/tooling.md index 97b60f03..ce4b7db7 100644 --- a/decisions/tooling.md +++ b/decisions/tooling.md @@ -42,4 +42,13 @@ Toolchain, supply chain, CI, ignore strategy. - **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml). - **Raised by:** 2026-04-23 format engine evaluation. +### D-59: Bundled git via dugite-native +- **Date:** 2026-04-25 +- **Decision:** Ship a self-contained Git binary from [dugite-native](https://github.com/desktop/dugite-native) (the same distribution GitHub Desktop bundles). Downloaded at build time via `make dugite-fetch`, stored under `native/dugite/`, gitignored. The `Toolchain` class resolves to the bundled binary first, falling back to system git on PATH. +- **Rationale:** The macOS app sandbox blocks execution of Homebrew-installed git (symlinks resolve to Cellar paths that SBPL cannot match without freezing rendering). `/usr/bin/git` is an xcrun shim that refuses to run inside a sandbox. Bundling dugite-native makes clide self-contained — no dependency on Homebrew, Xcode CLT, or system git. The approach is proven: GitHub Desktop, Tower, and other git GUI apps all bundle their own git for the same reason. +- **Alternatives rejected:** (R) libgit2 via FFI — missing porcelain commands (pull/push/rebase), no hooks, would require rewriting GitClient. (R) Build git from source — dugite-native already does this with better infra. (R) SBPL exceptions for Homebrew — `(subpath "/opt/homebrew")` for process-exec freezes Flutter rendering on macOS 26. +- **Cost:** ~57 MB download (~199 MB unpacked, stripped at build time). Must track dugite-native releases for security updates. GPL-2.0 (git binary) applies to the bundled artefact, not to clide's MIT code. +- **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml). +- **Raised by:** 2026-04-25 macOS sandbox investigation. + --- diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index b3c17614..db4272f8 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -10,4 +10,59 @@ class AppDelegate: FlutterAppDelegate { override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } + + override func applicationDidFinishLaunching(_ notification: Notification) { + guard let window = NSApp.mainWindow ?? NSApp.windows.first, + let flutterVC = window.contentViewController as? FlutterViewController else { + return + } + + let channel = FlutterMethodChannel( + name: "clide/window", + binaryMessenger: flutterVC.engine.binaryMessenger) + + channel.setMethodCallHandler { (call, result) in + guard let window = NSApp.mainWindow ?? NSApp.windows.first else { + result(FlutterMethodNotImplemented) + return + } + switch call.method { + case "pickDirectory": + let panel = NSOpenPanel() + panel.canChooseFiles = false + panel.canChooseDirectories = true + panel.allowsMultipleSelection = false + panel.canCreateDirectories = false + panel.prompt = "Open" + panel.message = "Select a project folder" + panel.beginSheetModal(for: window) { response in + if response == .OK, let url = panel.url { + result(url.path) + } else { + result(nil) + } + } + case "startDrag": + if let event = NSApp.currentEvent { + window.performDrag(with: event) + } + result(nil) + case "minimize": + window.miniaturize(nil) + result(nil) + case "maximize": + window.zoom(nil) + result(nil) + case "close": + window.close() + result(nil) + case "isMaximized": + result(window.isZoomed) + case "startResize": + result(nil) + default: + result(FlutterMethodNotImplemented) + } + } + } }