eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import AppKit
2import SwiftUI
3
4// 🤖 Settings → Advanced → "Command-line tool": install the bundled eleven CLI
5// (Contents/Helpers/eleven, embedded by the "Embed eleven CLI" build phase —
6// see ios/project.yml and docs/eleven-cli.md) onto the user's PATH.
7//
8// The app is sandboxed, so it can't just write /usr/local/bin: the Install
9// button runs an NSOpenPanel (the powerbox grants write access to whatever
10// folder the user picks) and drops a symlink to the bundled binary there. When
11// even that fails — stock /usr/local/bin is root-owned — we fall back to
12// showing the one Terminal command that does it, with a Copy button. A symlink
13// (not a copy) means Sparkle updates update the CLI too.
14// The Advanced tab (Settings window, LChatMacApp): power-user machinery that
15// would crowd General — today just the CLI install (Paul, 2026-07-15).
16struct MacAdvancedSettingsView: View {
17 var body: some View {
18 Form {
19 CLIToolSection()
20 // CLIToolSection renders nothing when the helper isn't embedded
21 // (a build without Go) — a wholly blank tab would read as broken,
22 // so say the quiet part.
23 if !FileManager.default.fileExists(
24 atPath: Bundle.main.bundleURL.appendingPathComponent("Contents/Helpers/eleven").path) {
25 Section {
26 Text("The command-line tool isn't included in this build.")
27 .font(.footnote).foregroundStyle(.secondary)
28 }
29 }
30 PerfSettingsSection()
31 }
32 .formStyle(.grouped)
33 // No frame — the Settings window's single fixed size lives on the
34 // TabView (LChatMacApp), same as every other pane.
35 }
36}
37
38struct CLIToolSection: View {
39 @AppStorage("cliInstalledPath") private var installedPath = ""
40 @State private var note: String?
41 @State private var fallbackCommand: String?
42
43 private var bundled: URL {
44 Bundle.main.bundleURL.appendingPathComponent("Contents/Helpers/eleven")
45 }
46
47 var body: some View {
48 // Older builds (or a build without Go on the machine) carry no helper;
49 // don't offer to install what isn't there.
50 if FileManager.default.fileExists(atPath: bundled.path) {
51 Section("Command-line tool") {
52 Text("Chat from your terminal: `eleven chats`, `eleven chat`, and `eleven send`. It signs in with the same passkey (`eleven signin`).")
53 .font(.callout)
54 .foregroundStyle(.secondary)
55 HStack {
56 if installedPath.isEmpty {
57 Text("Not installed").foregroundStyle(.secondary)
58 } else {
59 Label(installedPath, systemImage: installStillValid ? "checkmark.circle" : "exclamationmark.triangle")
60 .foregroundStyle(.secondary)
61 .font(.callout)
62 }
63 Spacer()
64 Button(installedPath.isEmpty ? "Install…" : "Reinstall…") { install() }
65 }
66 if let note {
67 Text(note).font(.footnote).foregroundStyle(.secondary)
68 }
69 if let fallbackCommand {
70 // That folder wasn't writable even via the powerbox (stock
71 // /usr/local/bin is root-owned) — hand over the one-liner.
72 VStack(alignment: .leading, spacing: 6) {
73 Text("That folder needs an administrator — run this in Terminal instead:")
74 .font(.footnote)
75 HStack {
76 Text(fallbackCommand)
77 .font(.system(.footnote, design: .monospaced))
78 .textSelection(.enabled)
79 .lineLimit(2)
80 Button("Copy") {
81 NSPasteboard.general.clearContents()
82 NSPasteboard.general.setString(fallbackCommand, forType: .string)
83 }
84 }
85 }
86 }
87 }
88 }
89 }
90
91 // A best-effort staleness check: the symlink should still resolve to a
92 // binary (the app may have moved). Sandbox may deny the stat — treat
93 // unreadable as fine rather than alarm on a false negative.
94 private var installStillValid: Bool {
95 guard !installedPath.isEmpty else { return false }
96 let dest = (try? FileManager.default.destinationOfSymbolicLink(atPath: installedPath)) ?? bundled.path
97 return FileManager.default.fileExists(atPath: dest)
98 }
99
100 private func install() {
101 note = nil
102 fallbackCommand = nil
103 let panel = NSOpenPanel()
104 panel.canChooseDirectories = true
105 panel.canChooseFiles = false
106 panel.canCreateDirectories = true
107 panel.allowsMultipleSelection = false
108 panel.message = "Choose where to install the eleven command. /usr/local/bin keeps it on your PATH."
109 panel.prompt = "Install Here"
110 panel.directoryURL = URL(fileURLWithPath: "/usr/local/bin", isDirectory: true)
111 guard panel.runModal() == .OK, let dir = panel.url else { return }
112
113 let target = dir.appendingPathComponent("eleven")
114 let accessing = dir.startAccessingSecurityScopedResource()
115 defer { if accessing { dir.stopAccessingSecurityScopedResource() } }
116 do {
117 try? FileManager.default.removeItem(at: target)
118 try FileManager.default.createSymbolicLink(at: target, withDestinationURL: bundled)
119 installedPath = target.path
120 note = onPath(dir.path)
121 ? "Installed — open a new terminal and run `eleven signin`."
122 : "Installed. \(dir.path) isn’t usually on PATH — add it, or reinstall to /usr/local/bin."
123 } catch {
124 fallbackCommand = "sudo mkdir -p /usr/local/bin && sudo ln -sf \"\(bundled.path)\" /usr/local/bin/eleven"
125 }
126 }
127
128 // The folders shells have on PATH without any profile edits.
129 private func onPath(_ dir: String) -> Bool {
130 ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin", "/bin"].contains(dir)
131 }
132}
133