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.
This commit is contained in:
AgentSeal 2026-04-18 06:54:57 -07:00
parent 9483d66e65
commit 43a938ff9e
2 changed files with 84 additions and 27 deletions

View file

@ -1,9 +1,11 @@
import AppKit
import Foundation
/// Opens a codeburn subcommand in the user's Terminal. The argv is validated through
/// `CodeburnCLI.isSafe` before it's interpolated into AppleScript so there's no path for a
/// rogue environment variable to smuggle shell metacharacters into the `do script` call.
/// 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 {
@ -21,20 +23,43 @@ enum TerminalLauncher {
let command = argv.joined(separator: " ")
if terminalPaths.contains(where: FileManager.default.fileExists(atPath:)) {
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()
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()
}
}