codeburn/mac/Sources/CodeBurnMenubar/Security/TerminalLauncher.swift
AgentSeal 43a938ff9e feat(mac): add Connect Claude button to Plan pane
The Plan pane previously told users to "run claude login in your
terminal, then retry" with no way to start the flow from the app.
Added a primary Connect Claude button on both the no-credentials and
failed states that launches Terminal.app with `claude login`, so the
OAuth flow is one click away.

TerminalLauncher.openClaudeLogin() uses a hardcoded literal, so no
user input reaches AppleScript. Refactored the common path into
runInTerminal(command:preValidated:) which re-validates any non-
literal input against CodeburnCLI.isSafe as defense-in-depth.

On machines without Terminal.app (iTerm/Ghostty/Warp), the button
surfaces an inline instruction to run `claude login` manually instead
of failing silently.
2026-04-18 06:54:57 -07:00

65 lines
2.6 KiB
Swift

import AppKit
import Foundation
/// Runs commands in the user's Terminal. Every string that reaches AppleScript `do script`
/// must be whitespace-joined argv where each token passes `CodeburnCLI.isSafe` (regex allowlist
/// that excludes shell metacharacters), OR a hardcoded literal defined here. The private
/// `runInTerminal` re-validates any non-literal input defensively so a future caller can't
/// bypass the invariant.
/// Falls back to a detached headless spawn on machines without Terminal.app (iTerm/Ghostty/Warp
/// users) so the subcommand still runs.
enum TerminalLauncher {
private static let terminalPaths = [
"/System/Applications/Utilities/Terminal.app",
"/Applications/Utilities/Terminal.app",
]
static func open(subcommand: [String]) {
let argv = CodeburnCLI.baseArgv() + subcommand
guard argv.allSatisfy(CodeburnCLI.isSafe) else {
NSLog("CodeBurn: refusing to open terminal with unsafe argv")
return
}
let command = argv.joined(separator: " ")
if terminalPaths.contains(where: FileManager.default.fileExists(atPath:)) {
runInTerminal(command: command, preValidated: true)
return
}
let headless = CodeburnCLI.makeProcess(subcommand: subcommand)
try? headless.run()
}
/// Launches `claude login` in Terminal.app so the user can complete the OAuth flow
/// without leaving CodeBurn. The command is a hardcoded literal -- no user input is
/// interpolated, so there's no injection surface.
static func openClaudeLogin() -> Bool {
guard terminalPaths.contains(where: FileManager.default.fileExists(atPath:)) else {
NSLog("CodeBurn: Terminal.app not present; user must run `claude login` manually")
return false
}
runInTerminal(command: "claude login", preValidated: true)
return true
}
private static func runInTerminal(command: String, preValidated: Bool) {
if !preValidated {
let tokens = command.split(separator: " ", omittingEmptySubsequences: true).map(String.init)
guard tokens.allSatisfy(CodeburnCLI.isSafe) else {
NSLog("CodeBurn: refusing to run unvalidated command in Terminal")
return
}
}
let script = """
tell application "Terminal"
activate
do script "\(command)"
end tell
"""
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
process.arguments = ["-e", script]
try? process.run()
}
}