diff --git a/src-tauri/src/commands/terminal.rs b/src-tauri/src/commands/terminal.rs index 61fe2ce..5239595 100644 --- a/src-tauri/src/commands/terminal.rs +++ b/src-tauri/src/commands/terminal.rs @@ -3,51 +3,36 @@ use std::collections::HashMap; use tauri::Manager; use tauri::State; -use crate::db::AppDatabase; use crate::git_credential; use crate::terminal::error::TerminalError; use crate::terminal::manager::{SpawnOptions, TerminalManager}; use crate::terminal::types::TerminalInfo; -/// Temp files created for a terminal credential session. -struct TerminalCredFiles { - cred_file: std::path::PathBuf, - helper_script: std::path::PathBuf, -} - -/// Build extra env vars and temp credential files for the terminal session. +/// Build extra env vars for the terminal session. /// -/// Uses `credential.helper` with a custom credential helper script that speaks -/// git's structured credential protocol (host/protocol on stdin, username/password -/// on stdout). This is added via `GIT_CONFIG_COUNT` which APPENDS to the user's -/// existing credential helpers (e.g. macOS Keychain) for multi-valued keys. -/// Our helper is tried first; if it has no match, git falls through to existing helpers. -async fn prepare_credential_env( - db: &AppDatabase, +/// Uses `credential.helper` with a script that calls the app binary with +/// `--credential-helper`. The binary opens the DB, looks up the matching +/// account, and outputs credentials. No credentials are written to disk. +fn prepare_credential_env( app_data_dir: &std::path::Path, - terminal_id: &str, -) -> (Option>, Option) { - let accounts = match git_credential::load_github_accounts(&db.conn).await { - Some(s) if !s.accounts.is_empty() => s.accounts, - _ => return (None, None), +) -> Option> { + // Get the path to the current running binary + let app_binary = match std::env::current_exe() { + Ok(p) => p, + Err(e) => { + eprintln!("[TERM] failed to get current exe path: {}", e); + return None; + } }; - let cred_file = app_data_dir.join(format!("git-creds-{}.tmp", terminal_id)); - if let Err(e) = git_credential::write_credential_store_file(&accounts, &cred_file) { - eprintln!("[TERM] failed to write credential store file: {}", e); - return (None, None); - } - let helper_script = match git_credential::create_credential_helper_script( app_data_dir, - &cred_file, - terminal_id, + &app_binary, ) { Ok(p) => p, Err(e) => { eprintln!("[TERM] failed to create credential helper script: {}", e); - let _ = std::fs::remove_file(&cred_file); - return (None, None); + return None; } }; @@ -62,19 +47,14 @@ async fn prepare_credential_env( "GIT_CONFIG_KEY_0".to_string(), "credential.helper".to_string(), ); - // credential.helper values without '!' prefix get "git-credential-" prepended. - // The '!' prefix tells git to run it as a raw shell command. + // The '!' prefix tells git to run as a raw shell command (not git-credential-). // Paths with spaces (e.g. "Application Support") must be quoted. env.insert( "GIT_CONFIG_VALUE_0".to_string(), format!("!\"{}\"", helper_path_str), ); - let files = TerminalCredFiles { - cred_file, - helper_script, - }; - (Some(env), Some(files)) + Some(env) } #[tauri::command] @@ -82,11 +62,9 @@ pub async fn terminal_spawn( working_dir: String, initial_command: Option, manager: State<'_, TerminalManager>, - db: State<'_, AppDatabase>, app_handle: tauri::AppHandle, window: tauri::WebviewWindow, ) -> Result { - // Generate terminal ID early so we can use it for the credential file name let terminal_id = uuid::Uuid::new_v4().to_string(); let app_data_dir = app_handle @@ -94,12 +72,7 @@ pub async fn terminal_spawn( .app_data_dir() .map_err(|e| TerminalError::SpawnFailed(e.to_string()))?; - let (extra_env, cred_files) = - prepare_credential_env(&db, &app_data_dir, &terminal_id).await; - - let temp_files = cred_files - .map(|f| vec![f.cred_file, f.helper_script]) - .unwrap_or_default(); + let extra_env = prepare_credential_env(&app_data_dir); manager.spawn_with_id( SpawnOptions { @@ -108,7 +81,7 @@ pub async fn terminal_spawn( owner_window_label: window.label().to_string(), initial_command, extra_env, - temp_files, + temp_files: vec![], }, app_handle, ) diff --git a/src-tauri/src/git_credential.rs b/src-tauri/src/git_credential.rs index a3c9776..643473f 100644 --- a/src-tauri/src/git_credential.rs +++ b/src-tauri/src/git_credential.rs @@ -7,103 +7,32 @@ use crate::models::system::{GitHubAccount, GitHubAccountsSettings}; const GITHUB_ACCOUNTS_KEY: &str = "github_accounts"; -/// Write a git credential-store file containing all stored accounts. +/// Create a credential helper that calls the app binary directly with +/// `--credential-helper` flag. The app binary opens the DB, looks up +/// the matching account, and outputs credentials to stdout. /// -/// The credential-store format is one URL per line: -/// `https://username:token@hostname` -/// -/// Returns the path to the written file. -pub fn write_credential_store_file( - accounts: &[GitHubAccount], - file_path: &Path, -) -> std::io::Result<()> { - use std::io::Write; - - let mut content = String::new(); - for account in accounts { - let host = extract_host(&account.server_url).unwrap_or_default(); - if host.is_empty() { - continue; - } - // URL-encode username and token to handle special characters - let username = urlencoding::encode(&account.username); - let token = urlencoding::encode(&account.token); - content.push_str(&format!("https://{}:{}@{}\n", username, token, host)); - } - - let mut file = std::fs::File::create(file_path)?; - file.write_all(content.as_bytes())?; - - // Restrict permissions on Unix (owner read/write only) - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(file_path, std::fs::Permissions::from_mode(0o600))?; - } - - Ok(()) -} - -/// Create a credential helper script that reads from a credential-store file -/// using git's structured credential protocol. -/// -/// Git's credential helper protocol passes structured key=value pairs on stdin: -/// protocol=https -/// host=github.com -/// -/// And expects the response on stdout: -/// username=xxx -/// password=xxx -/// -/// This is far more reliable than GIT_ASKPASS (which requires parsing English prompts). -/// The script is per-terminal (named with terminal_id) to avoid race conditions. +/// This is the simplest and most reliable approach: +/// - No HTTP server, no temp credential files +/// - Always reads latest accounts from DB +/// - No special-character / encoding issues (Rust handles strings natively) +/// - Single shared script across all terminals pub fn create_credential_helper_script( app_data_dir: &Path, - cred_store_path: &Path, - terminal_id: &str, + app_binary_path: &Path, ) -> std::io::Result { - let cred_store_str = cred_store_path.to_string_lossy(); + let binary_str = app_binary_path.to_string_lossy(); #[cfg(unix)] { - let script_path = app_data_dir.join(format!("git-credential-codeg-{}.sh", &terminal_id[..8])); + let script_path = app_data_dir.join("git-credential-codeg.sh"); let content = format!( r#"#!/bin/sh -# Codeg credential helper: reads from credential store file. +# Codeg credential helper — calls the app binary to look up credentials. # Only responds to "get" action; ignores "store" and "erase". [ "$1" != "get" ] && exit 0 - -CRED_FILE="{cred_file}" -[ ! -f "$CRED_FILE" ] && exit 0 - -# Read protocol and host from stdin -HOST="" -PROTO="" -while IFS='=' read -r key value; do - [ -z "$key" ] && break - case "$key" in - host) HOST="$value" ;; - protocol) PROTO="$value" ;; - esac -done - -[ -z "$HOST" ] && exit 0 - -# Find matching line: https://user:pass@host -LINE=$(grep -i "@$HOST" "$CRED_FILE" | head -1) -[ -z "$LINE" ] && exit 0 - -# Parse username and password from https://user:pass@host -USERPASS=$(echo "$LINE" | sed 's|https*://||' | sed 's|@.*||') -USER=$(echo "$USERPASS" | cut -d: -f1) -PASS=$(echo "$USERPASS" | cut -d: -f2-) - -[ -z "$USER" ] && exit 0 - -echo "username=$USER" -echo "password=$PASS" +exec "{binary}" --credential-helper < /dev/stdin "#, - cred_file = cred_store_str + binary = binary_str ); std::fs::write(&script_path, content)?; use std::os::unix::fs::PermissionsExt; @@ -113,52 +42,112 @@ echo "password=$PASS" #[cfg(windows)] { - let script_path = app_data_dir.join(format!("git-credential-codeg-{}.bat", &terminal_id[..8])); + let script_path = app_data_dir.join("git-credential-codeg.bat"); let content = format!( r#"@echo off -setlocal enabledelayedexpansion if not "%~1"=="get" exit /b 0 - -set "CRED_FILE={cred_file}" -if not exist "!CRED_FILE!" exit /b 0 - -set "HOST=" -set "PROTO=" -:readloop -set /p "LINE=" || goto :match -for /f "tokens=1,* delims==" %%a in ("!LINE!") do ( - if "%%a"=="host" set "HOST=%%b" - if "%%a"=="protocol" set "PROTO=%%b" -) -if defined LINE goto :readloop - -:match -if not defined HOST exit /b 0 - -for /f "usebackq delims=" %%L in ("!CRED_FILE!") do ( - echo %%L | findstr /i "!HOST!" >nul - if !errorlevel! equ 0 ( - set "FOUND=%%L" - goto :parse - ) -) -exit /b 0 - -:parse -set "FOUND=!FOUND:https://=!" -for /f "tokens=1 delims=@" %%a in ("!FOUND!") do set "USERPASS=%%a" -for /f "tokens=1,2 delims=:" %%a in ("!USERPASS!") do ( - echo username=%%a - echo password=%%b -) +"{binary}" --credential-helper "#, - cred_file = cred_store_str + binary = binary_str ); std::fs::write(&script_path, content)?; Ok(script_path) } } +/// Run the credential helper mode (called from main when `--credential-helper` is detected). +/// +/// Reads git's credential protocol from stdin (host=xxx, protocol=xxx), +/// opens the DB, finds the matching account, and outputs username/password +/// to stdout. Exits immediately — does NOT start the Tauri GUI. +pub fn run_credential_helper() { + use std::io::BufRead; + + // Read host from stdin (git credential protocol) + let mut host = String::new(); + let stdin = std::io::stdin(); + for line in stdin.lock().lines() { + let line = match line { + Ok(l) => l, + Err(_) => break, + }; + if line.is_empty() { + break; + } + if let Some(value) = line.strip_prefix("host=") { + host = value.to_string(); + } + } + + if host.is_empty() { + return; + } + + // Resolve app data dir the same way Tauri does: ~/Library/Application Support/ + let app_data_dir = match resolve_app_data_dir() { + Some(d) => d, + None => return, + }; + + let db_path = app_data_dir.join("codeg.db"); + if !db_path.exists() { + return; + } + + // Open DB with a lightweight synchronous connection (no async runtime needed) + let db_url = format!( + "sqlite:{}?mode=ro", + urlencoding::encode(&db_path.to_string_lossy()) + ); + + // Use a minimal tokio runtime just for the DB query + let rt = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(rt) => rt, + Err(_) => return, + }; + + rt.block_on(async { + let opts = sea_orm::ConnectOptions::new(db_url); + let conn = match sea_orm::Database::connect(opts).await { + Ok(c) => c, + Err(_) => return, + }; + + let settings = match load_github_accounts(&conn).await { + Some(s) => s, + None => return, + }; + + let remote_url = format!("https://{}", host); + if let Some(account) = find_matching_account(&settings.accounts, &remote_url) { + println!("username={}", account.username); + println!("password={}", account.token); + } + }); +} + +/// Resolve the app data directory (same path Tauri uses). +fn resolve_app_data_dir() -> Option { + // On macOS: ~/Library/Application Support/app.codeg + // On Linux: ~/.local/share/app.codeg + // On Windows: %APPDATA%/app.codeg + #[cfg(target_os = "macos")] + { + dirs::data_dir().map(|d| d.join("app.codeg")) + } + #[cfg(target_os = "linux")] + { + dirs::data_dir().map(|d| d.join("app.codeg")) + } + #[cfg(target_os = "windows")] + { + dirs::data_dir().map(|d| d.join("app.codeg")) + } +} + /// Ensure the GIT_ASKPASS helper script exists in the app data directory. /// Returns the path to the script. pub fn ensure_askpass_script(app_data_dir: &Path) -> std::io::Result { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9e6195b..8116ff7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,7 +2,7 @@ mod acp; mod app_error; mod commands; mod db; -mod git_credential; +pub mod git_credential; mod models; mod network; mod parsers; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 56825f2..d046031 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,12 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { + // When called as a git credential helper, handle it immediately and exit. + // This avoids starting the full Tauri GUI runtime. + if std::env::args().any(|a| a == "--credential-helper") { + codeg_lib::git_credential::run_credential_helper(); + return; + } + codeg_lib::run() }