Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions apps/mac/Lobu/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ final class AppState: ObservableObject {
}
startAutoPollIfSignedIn()
}
} else if credentials == nil, shouldAutoConnectAtLaunch() {
// First launch with no stored creds AND the URL points at the
// embedded runner we manage → auto-connect. No need to surface
// a "Start" button: spawn the runner, adopt no-auth credentials,
// signed in within ~1 s. Failures (CLI missing, port conflict)
// leave `credentials` nil and the connection card surfaces.
Task { @MainActor in await connect() }
} else {
startAutoPollIfSignedIn()
}
Expand Down Expand Up @@ -465,6 +472,16 @@ final class AppState: ObservableObject {
setStatus("")
}

/// True iff init() should auto-trigger connect() at launch instead of
/// showing the connection card. We only do this when the URL targets the
/// embedded runner we manage — auto-spawning a process for a URL the
/// user typed pointing at someone else's server would be surprising.
private func shouldAutoConnectAtLaunch() -> Bool {
let raw = customServerDraft.trimmingCharacters(in: .whitespacesAndNewlines)
guard let url = URL(string: raw) else { return false }
return AppState.matchesManagedRunner(url)
}

// Contract with `ensureBootstrapPat` in packages/server/src/start-local.ts.
// Changing either side without the other makes the menu bar's display
// disagree with reality.
Expand Down Expand Up @@ -523,10 +540,16 @@ final class AppState: ObservableObject {
setStatus("Lobu is running on this Mac (~/lobu).")
} catch LocalLobuRunner.RunnerError.cliNotFound {
localLobuStatus = .cliMissing
setStatus(LocalLobuRunner.RunnerError.cliNotFound.errorDescription ?? "Lobu CLI not installed.")
// The connection card surfaces `cliMissing` inline with the
// install hint — don't echo it into the header status too.
setStatus("")
} catch {
localLobuStatus = .failed(message: error.localizedDescription)
setStatus(error.localizedDescription)
// Same — connectionCard's `connectStatusLine` renders the failure
// in orange right under the Start button. Echoing it into
// `state.status` would also push it into the header, producing
// the duplicate the user saw.
setStatus("")
}
}

Expand Down
35 changes: 35 additions & 0 deletions apps/mac/Lobu/LocalLobuRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ final class LocalLobuRunner {
// bind. start-local.ts defaults HOST to 0.0.0.0, so we must pin it
// explicitly here — otherwise the runner just crashes on boot.
env["HOST"] = "127.0.0.1"
// The embedded gateway refuses to boot without an ENCRYPTION_KEY (used
// to encrypt at-rest connection secrets). For a personal-use install
// the only sensible default is an ephemeral key generated on first
// boot and persisted under LOBU_DATA_DIR — opt in here so the user
// doesn't have to manage a secret manually.
env["LOBU_ALLOW_EPHEMERAL_ENCRYPTION_KEY"] = "1"
// The embedded server requires Node 22.x–24.x (isolated-vm constraint).
// If the user's PATH leads with a newer Node (Homebrew often does
// `node` = latest), inject the keg-only Node 22 location first so
// `lobu` picks the right interpreter without forcing the user to
// manage PATH manually.
env["PATH"] = Self.preferredPath(currentPath: env["PATH"] ?? "")
proc.environment = env
proc.standardInput = FileHandle.nullDevice // no TTY — any prompt gets EOF and fails fast

Expand Down Expand Up @@ -151,6 +163,29 @@ final class LocalLobuRunner {
return true
}

/// Return a PATH that puts a known Node 22 install first if one exists,
/// so the spawned `lobu run` picks an interpreter the embedded server
/// supports (22.x–24.x; isolated-vm doesn't build on Node 25+).
///
/// Looks in the obvious places: Homebrew keg-only `node@22`, plus the
/// version-manager shim paths the user might already have configured.
/// Order matters — Homebrew's keg-only formula is the most common case
/// on macOS, so it goes first.
private static func preferredPath(currentPath: String) -> String {
let candidates = [
"/opt/homebrew/opt/node@22/bin",
"/usr/local/opt/node@22/bin",
"/opt/homebrew/opt/node@24/bin",
"/usr/local/opt/node@24/bin",
"\(NSHomeDirectory())/.local/share/mise/installs/node/22/bin",
"\(NSHomeDirectory())/.local/share/fnm/aliases/default/bin",
]
let fm = FileManager.default
let prefixes = candidates.filter { fm.isExecutableFile(atPath: "\($0)/node") }
if prefixes.isEmpty { return currentPath }
return (prefixes + [currentPath]).joined(separator: ":")
}

private static func locateLobuCLI() -> String? {
if let resourceURL = Bundle.main.resourceURL {
let bundled = resourceURL.appendingPathComponent("lobu-cli/bin/lobu").path
Expand Down
Loading