diff options
| author | soryu <soryu@soryu.co> | 2026-02-19 23:40:07 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-19 23:40:07 +0000 |
| commit | ed84d7ec5ece272a2cb8dabd36cbd6074df0887e (patch) | |
| tree | ad8bd85b44c577e656274bc846b0051837907038 | |
| parent | 28b191cc0b0e69b864191673df9c141730c93e4f (diff) | |
| download | soryu-ed84d7ec5ece272a2cb8dabd36cbd6074df0887e.tar.gz soryu-ed84d7ec5ece272a2cb8dabd36cbd6074df0887e.zip | |
feat: add git/gh auth checks, git fetch on worktree, fix contracts overflow (#72)
* feat: soryu-co/soryu - makima: Fix contracts page overflow - constrain layout to viewport height
* feat: soryu-co/soryu - makima: Add git fetch to create_worktree and improve completion prompt merge conflict handling
* WIP: heartbeat checkpoint
| -rw-r--r-- | makima/src/bin/makima.rs | 35 | ||||
| -rw-r--r-- | makima/src/daemon/setup.rs | 220 | ||||
| -rw-r--r-- | makima/src/daemon/worktree/manager.rs | 7 | ||||
| -rw-r--r-- | makima/src/orchestration/directive.rs | 15 |
4 files changed, 173 insertions, 104 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index 406f6e1..070e28e 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -85,36 +85,8 @@ async fn run_daemon( // Check for missing critical dependencies if !dep_result.claude.installed { let os = setup::OperatingSystem::detect(); - - // If npm is available, offer to install Claude Code - if dep_result.npm_available() { - eprintln!(); - if setup::read_yes_no("Would you like to install Claude Code now?") { - match setup::install_claude_with_npm().await { - Ok(()) => { - eprintln!(); - // Re-check to verify installation - let recheck = setup::check_dependencies().await; - if !recheck.claude.installed { - eprintln!("\x1b[31mClaude Code installation could not be verified.\x1b[0m"); - eprintln!("Please try installing manually and restart the daemon."); - std::process::exit(1); - } - } - Err(e) => { - eprintln!("\x1b[31mFailed to install Claude Code: {}\x1b[0m", e); - setup::print_claude_install_instructions(os, true); - std::process::exit(1); - } - } - } else { - setup::print_claude_install_instructions(os, true); - std::process::exit(1); - } - } else { - setup::print_claude_install_instructions(os, false); - std::process::exit(1); - } + setup::print_claude_install_instructions(os); + std::process::exit(1); } if !dep_result.git.installed { @@ -122,6 +94,9 @@ async fn run_daemon( setup::print_git_install_instructions(os); std::process::exit(1); } + + // Print git authentication warnings (non-fatal) + setup::print_git_auth_warnings(&dep_result); } // Install Claude Code skills for makima commands diff --git a/makima/src/daemon/setup.rs b/makima/src/daemon/setup.rs index 0706972..3daf779 100644 --- a/makima/src/daemon/setup.rs +++ b/makima/src/daemon/setup.rs @@ -1,8 +1,8 @@ //! Setup and dependency checking for the makima daemon. //! //! This module provides functionality to check for required dependencies -//! (Claude Code, git, npm) when the daemon starts, and optionally install -//! missing dependencies. +//! (Claude Code, git) and optional tools (gh) when the daemon starts, +//! and verify git authentication capabilities. use std::process::Stdio; use tokio::process::Command; @@ -36,12 +36,29 @@ impl DependencyInfo { } } +/// Result of checking git authentication against remote origins +#[derive(Debug, Clone)] +pub struct GitAuthCheckResult { + /// Whether basic git credential helper is configured + pub credential_helper_configured: bool, + /// Results of testing authentication against specific origins + pub origin_results: Vec<GitOriginAuthResult>, +} + +#[derive(Debug, Clone)] +pub struct GitOriginAuthResult { + pub origin_url: String, + pub can_authenticate: bool, + pub error: Option<String>, +} + /// Result of checking all dependencies #[derive(Debug)] pub struct DependencyCheckResult { pub claude: DependencyInfo, pub git: DependencyInfo, - pub npm: DependencyInfo, + pub git_auth: GitAuthCheckResult, + pub gh: DependencyInfo, } impl DependencyCheckResult { @@ -51,9 +68,9 @@ impl DependencyCheckResult { (!self.git.critical || self.git.installed) } - /// Check if npm is available (for installing claude) - pub fn npm_available(&self) -> bool { - self.npm.installed + /// Check if gh (GitHub CLI) is available + pub fn gh_available(&self) -> bool { + self.gh.installed } } @@ -118,10 +135,10 @@ fn extract_version(output: &str) -> Option<String> { /// Check all dependencies and return their status pub async fn check_dependencies() -> DependencyCheckResult { // Run all checks in parallel for speed - let (claude_version, git_version, npm_version) = tokio::join!( + let (claude_version, git_version, gh_version) = tokio::join!( get_command_version("claude", &["--version"]), get_command_version("git", &["--version"]), - get_command_version("npm", &["--version"]), + get_command_version("gh", &["--version"]), ); let mut claude = DependencyInfo::new("Claude Code", true); @@ -132,11 +149,21 @@ pub async fn check_dependencies() -> DependencyCheckResult { git.version = git_version; git.installed = git.version.is_some(); - let mut npm = DependencyInfo::new("npm", false); - npm.version = npm_version; - npm.installed = npm.version.is_some(); + let mut gh = DependencyInfo::new("gh (GitHub CLI)", false); + gh.version = gh_version; + gh.installed = gh.version.is_some(); - DependencyCheckResult { claude, git, npm } + // Check git authentication (only if git is installed) + let git_auth = if git.installed { + check_git_auth().await + } else { + GitAuthCheckResult { + credential_helper_configured: false, + origin_results: vec![], + } + }; + + DependencyCheckResult { claude, git, git_auth, gh } } /// Display a nice summary of dependency status @@ -167,7 +194,28 @@ pub fn print_dependency_summary(result: &DependencyCheckResult) { eprintln!("{}", format_dep(&result.claude)); eprintln!("{}", format_dep(&result.git)); - eprintln!("{}", format_dep(&result.npm)); + + // Print git auth status + if result.git.installed { + if result.git_auth.credential_helper_configured { + eprintln!(" git credentials: {}configured{}", colors::GREEN, colors::RESET); + } else { + eprintln!(" git credentials: {}no credential helper configured{} (git may prompt for passwords)", colors::YELLOW, colors::RESET); + } + } + + // Print origin auth results if any + for origin in &result.git_auth.origin_results { + if origin.can_authenticate { + eprintln!(" git origin {}: {}ok{}", origin.origin_url, colors::GREEN, colors::RESET); + } else { + let err_msg = origin.error.as_deref().unwrap_or("authentication failed"); + let err_msg = if err_msg.len() > 80 { &err_msg[..80] } else { err_msg }; + eprintln!(" git origin {}: {}failed{} ({})", origin.origin_url, colors::RED, colors::RESET, err_msg.trim()); + } + } + + eprintln!("{}", format_dep(&result.gh)); } /// Detect the current operating system @@ -196,40 +244,19 @@ impl OperatingSystem { } /// Print instructions for installing Claude Code -pub fn print_claude_install_instructions(os: OperatingSystem, npm_available: bool) { +pub fn print_claude_install_instructions(os: OperatingSystem) { eprintln!(); eprintln!("{}{}Claude Code is required to run the makima daemon.{}", colors::BOLD, colors::RED, colors::RESET); eprintln!(); - - if npm_available { - eprintln!("Install with npm:"); - eprintln!(" {}npm install -g @anthropic-ai/claude-code{}", colors::BOLD, colors::RESET); - } else { - eprintln!("To install Claude Code, you first need npm (Node.js package manager)."); - eprintln!(); - match os { - OperatingSystem::MacOS => { - eprintln!("Install Node.js on macOS:"); - eprintln!(" {}brew install node{}", colors::BOLD, colors::RESET); - eprintln!(" Or download from: https://nodejs.org/"); - } - OperatingSystem::Linux => { - eprintln!("Install Node.js on Linux:"); - eprintln!(" {}curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -{}", colors::BOLD, colors::RESET); - eprintln!(" {}sudo apt-get install -y nodejs{}", colors::BOLD, colors::RESET); - eprintln!(" Or use your distribution's package manager"); - } - OperatingSystem::Windows => { - eprintln!("Install Node.js on Windows:"); - eprintln!(" Download from: https://nodejs.org/"); - } - OperatingSystem::Unknown => { - eprintln!("Install Node.js from: https://nodejs.org/"); - } + eprintln!("Install Claude Code:"); + match os { + OperatingSystem::MacOS => { + eprintln!(" {}brew install claude-code{}", colors::BOLD, colors::RESET); + eprintln!(" Or visit: https://docs.anthropic.com/en/docs/claude-code"); + } + _ => { + eprintln!(" Visit: https://docs.anthropic.com/en/docs/claude-code"); } - eprintln!(); - eprintln!("Then install Claude Code:"); - eprintln!(" {}npm install -g @anthropic-ai/claude-code{}", colors::BOLD, colors::RESET); } eprintln!(); eprintln!("For more information, visit: https://docs.anthropic.com/en/docs/claude-code"); @@ -263,44 +290,93 @@ pub fn print_git_install_instructions(os: OperatingSystem) { } } -/// Try to install Claude Code using npm -pub async fn install_claude_with_npm() -> Result<(), String> { - eprintln!("Installing Claude Code via npm..."); - - let result = Command::new("npm") - .args(["install", "-g", "@anthropic-ai/claude-code"]) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .status() +/// Check git authentication capabilities +async fn check_git_auth() -> GitAuthCheckResult { + // Check if a credential helper is configured + let cred_helper = Command::new("git") + .args(["config", "credential.helper"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() .await; + let credential_helper_configured = match cred_helper { + Ok(output) => { + output.status.success() && !String::from_utf8_lossy(&output.stdout).trim().is_empty() + } + _ => false, + }; + + GitAuthCheckResult { + credential_helper_configured, + origin_results: vec![], + } +} + +/// Test git authentication against a specific remote URL. +/// Uses `git ls-remote --exit-code <url>` with a timeout to verify access. +pub async fn check_git_origin_auth(url: &str) -> GitOriginAuthResult { + let result = tokio::time::timeout( + std::time::Duration::from_secs(15), + Command::new("git") + .args(["ls-remote", "--exit-code", url, "HEAD"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + ).await; + match result { - Ok(status) if status.success() => { - eprintln!("{}Claude Code installed successfully!{}", colors::GREEN, colors::RESET); - Ok(()) + Ok(Ok(output)) if output.status.success() => { + GitOriginAuthResult { + origin_url: url.to_string(), + can_authenticate: true, + error: None, + } } - Ok(status) => { - Err(format!("npm install failed with exit code: {:?}", status.code())) + Ok(Ok(output)) => { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + GitOriginAuthResult { + origin_url: url.to_string(), + can_authenticate: false, + error: Some(stderr), + } } - Err(e) => { - Err(format!("Failed to run npm: {}", e)) + Ok(Err(e)) => { + GitOriginAuthResult { + origin_url: url.to_string(), + can_authenticate: false, + error: Some(format!("Failed to run git: {}", e)), + } + } + Err(_) => { + GitOriginAuthResult { + origin_url: url.to_string(), + can_authenticate: false, + error: Some("Timed out after 15 seconds".to_string()), + } } } } -/// Read a single character from stdin (for y/n prompts) -pub fn read_yes_no(prompt: &str) -> bool { - use std::io::{self, Write}; - - eprint!("{} [y/N]: ", prompt); - io::stderr().flush().ok(); - - let mut input = String::new(); - if io::stdin().read_line(&mut input).is_ok() { - let input = input.trim().to_lowercase(); - input == "y" || input == "yes" - } else { - false +/// Print warnings about git authentication issues (non-fatal) +pub fn print_git_auth_warnings(result: &DependencyCheckResult) { + if !result.git_auth.credential_helper_configured && result.git.installed { + eprintln!(); + eprintln!(" {}WARNING:{} No git credential helper configured.", colors::YELLOW, colors::RESET); + eprintln!(" Git operations may prompt for passwords interactively."); + eprintln!(" Configure a credential helper: git config --global credential.helper store"); + } + for origin in &result.git_auth.origin_results { + if !origin.can_authenticate { + eprintln!(); + eprintln!(" {}WARNING:{} Cannot authenticate with git origin: {}", colors::YELLOW, colors::RESET, origin.origin_url); + if let Some(ref err) = origin.error { + let err_trimmed = err.trim(); + if !err_trimmed.is_empty() { + eprintln!(" Error: {}", if err_trimmed.len() > 120 { &err_trimmed[..120] } else { err_trimmed }); + } + } + } } } diff --git a/makima/src/daemon/worktree/manager.rs b/makima/src/daemon/worktree/manager.rs index 30618ea..489c488 100644 --- a/makima/src/daemon/worktree/manager.rs +++ b/makima/src/daemon/worktree/manager.rs @@ -484,6 +484,13 @@ impl WorktreeManager { "Creating worktree with new branch" ); + // Fetch latest from remote to ensure origin refs are fresh + let _ = Command::new("git") + .args(["fetch", "origin", "--prune"]) + .current_dir(source_repo) + .output() + .await; + // Prefer origin/{base_branch} to get latest remote state. // If neither origin/{base_branch} nor {base_branch} exist (e.g. PR branch // was deleted after merge), fall back to the repo's default branch. diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index af6b18c..df44ee4 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -1367,6 +1367,11 @@ git checkout -b "$NEW_BRANCH" origin/{base_branch} git push -u origin "$NEW_BRANCH" ``` +For each step branch merge above, if a merge fails with conflicts: +1. First try: `git merge --abort` then retry with `git merge <the-failing-branch> -X theirs --no-edit` +2. If that also fails, manually resolve the conflicts, `git add .`, and `git commit --no-edit` +3. Continue with remaining merges + 3. Generate a descriptive PR title and create a new PR: Based on the steps completed above, generate a descriptive PR title that summarizes the actual changes (not just the directive name "{title}"). The title should: @@ -1408,7 +1413,10 @@ git merge origin/{base_branch} --no-edit git push origin {directive_branch} ``` -Already-merged branches will be a no-op. If there are merge conflicts, resolve them sensibly. +Already-merged branches will be a no-op. If a merge fails with conflicts: +1. First try: `git merge --abort` then retry with `git merge <the-failing-branch> -X theirs --no-edit` +2. If that also fails, manually resolve the conflicts, `git add .`, and `git commit --no-edit` +3. Continue with remaining merges "#, title = directive.title, goal = directive.goal, @@ -1464,7 +1472,10 @@ makima directive update --pr-url "https://github.com/..." ``` Replace the URL with the actual PR URL from the `gh pr create` output. This step is CRITICAL — the PR will not be tracked by the directive system without it. -If there are merge conflicts, resolve them sensibly before pushing. +For each step branch merge, if a merge fails with conflicts: +1. First try: `git merge --abort` then retry with `git merge <the-failing-branch> -X theirs --no-edit` +2. If that also fails, manually resolve the conflicts, `git add .`, and `git commit --no-edit` +3. Continue with remaining merges "#, title = directive.title, goal = directive.goal, |
