summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-19 23:40:07 +0000
committerGitHub <noreply@github.com>2026-02-19 23:40:07 +0000
commited84d7ec5ece272a2cb8dabd36cbd6074df0887e (patch)
treead8bd85b44c577e656274bc846b0051837907038
parent28b191cc0b0e69b864191673df9c141730c93e4f (diff)
downloadsoryu-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.rs35
-rw-r--r--makima/src/daemon/setup.rs220
-rw-r--r--makima/src/daemon/worktree/manager.rs7
-rw-r--r--makima/src/orchestration/directive.rs15
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,