diff --git a/INSTALL.md b/INSTALL.md index 2fdc94b..51e7935 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -37,6 +37,14 @@ Signed in to the Autter dashboard? [**Settings → CLI setup**](https://app.autt The installer downloads Autter into `~/.autter/bin`, adds it to your user `PATH`, configures supported coding agents and editors, and starts the background service. On macOS, Linux, and WSL it then starts onboarding when the shell is interactive. Automated or non-interactive installs can finish onboarding later. +### System requirements + +- **git** 2.22 or newer (required) +- **Linux**: glibc 2.35 or newer (Ubuntu 22.04+, Debian 12+, Fedora 36+). Ubuntu 20.04 and older WSL2 distros are not supported natively — use a newer WSL distro or run inside an `ubuntu:22.04` Docker container +- **macOS**: 11 (Big Sur) or newer +- **Windows**: 10 or newer +- **npm path**: Node.js 18+ + On macOS, Linux, and WSL, make `autter` available in the terminal you already have open (the installer prints this command at the end too): ```bash @@ -47,9 +55,12 @@ New terminals pick it up automatically. Restart your IDE (not just its terminal ```bash autter --version -autter debug +autter doctor # v1.6.10+ — focused setup validation (exits 1 on failure) +autter debug # full support dump (always exits 0) ``` +`autter doctor` runs end-to-end checks (git proxy, hooks, checkpoint round-trip). On v1.6.9 and earlier, use `autter debug` instead. + You do not need to configure each repository separately. Continue using Git, your IDE, and your coding agents as usual. > Using Nix? See the [Nix installation guide](README-nix.md) for NixOS, nix-darwin, and Home Manager options. diff --git a/README.md b/README.md index cc53dc4..59359a7 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ autter onboard The npm package is a thin bootstrapper: it downloads the same release binary into `~/.autter/bin` and verifies its checksum, so hooks and self-updates work identically to the script installs. +**System requirements:** git 2.22+, Linux glibc 2.35+ (Ubuntu 22.04+), macOS 11+, Windows 10+, Node.js 18+ for the npm path. See [INSTALL.md](INSTALL.md) for details including Docker-based setup on older Linux distros. + > **Git Bash is not WSL.** The bash installer needs a real Linux environment, so on Windows it only runs inside [WSL](https://learn.microsoft.com/windows/wsl/about) — in Git Bash it exits with instructions. Use the Windows command instead (it works from Git Bash too), and install the CLI where your coding agents actually run: agents launched from Windows need the native install, agents inside WSL need the WSL install. Signed in to Autter? [**Settings → CLI setup**](https://app.autter.dev/cli/install) in the dashboard generates a single-use command that runs this same installer **and signs the machine in automatically** via a short-lived signed token — no separate login step. diff --git a/install.ps1 b/install.ps1 index 4e72fa4..f81caf4 100644 --- a/install.ps1 +++ b/install.ps1 @@ -335,6 +335,16 @@ if (-not $arch) { } $os = 'windows' +# git is required — autter wraps git and cannot function without it. +try { + $null = & git --version 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-ErrorAndExit 'git is required but not found. Install Git for Windows (https://git-scm.com/download/win) and re-run the installer.' + } +} catch { + Write-ErrorAndExit 'git is required but not found. Install Git for Windows (https://git-scm.com/download/win) and re-run the installer.' +} + # Determine binary name and download URLs $binaryName = "autter-$os-$arch" @@ -604,6 +614,20 @@ if (Test-Path -LiteralPath $finalExe) { Move-Item -Force -Path $tmpFile -Destination $finalExe try { Unblock-File -Path $finalExe -ErrorAction SilentlyContinue } catch { } +# Verify the binary runs before reporting success. +try { + $installedVersion = & $finalExe --version 2>&1 | Out-String + $installedVersion = $installedVersion.Trim() + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($installedVersion)) { + Remove-Item -Force -ErrorAction SilentlyContinue $finalExe + Write-ErrorAndExit "The autter binary could not run on this system:`n$installedVersion" + } + Write-Host "Installed autter $installedVersion" +} catch { + Remove-Item -Force -ErrorAction SilentlyContinue $finalExe + Write-ErrorAndExit "The autter binary could not run on this system: $($_.Exception.Message)" +} + # Refresh git.exe for existing wrapper users (it's a copy, not a symlink on Windows) $gitShim = Join-Path $installDir 'git.exe' if (Test-Path -LiteralPath $gitShim) { diff --git a/install.sh b/install.sh index 9396e98..1df4ffd 100755 --- a/install.sh +++ b/install.sh @@ -277,6 +277,74 @@ case $ARCH in ;; esac +# Minimum glibc for Linux release binaries (built on Ubuntu 22.04). +MIN_GLIBC_MAJOR=2 +MIN_GLIBC_MINOR=35 + +# Require git before downloading — autter wraps git and cannot function without it. +check_git() { + if ! command -v git >/dev/null 2>&1; then + error "git is required but not found. Install git 2.22 or newer, then re-run the installer." + fi +} + +# Linux release binaries need glibc 2.35+ (Ubuntu 22.04). Ubuntu 20.04 / older WSL2 +# distros fail at runtime with GLIBC_2.32+ symbol errors — catch that up front. +check_linux_glibc() { + if [ "$OS" != "linux" ] || [ -n "${AUTTER_LOCAL_BINARY:-}" ]; then + return 0 + fi + + if ! command -v ldd >/dev/null 2>&1; then + return 0 + fi + + local glibc_version + glibc_version=$(ldd --version 2>&1 | head -n1 | grep -oE '[0-9]+\.[0-9]+' | head -n1) + if [ -z "$glibc_version" ]; then + return 0 + fi + + local major minor + major=${glibc_version%%.*} + minor=${glibc_version#*.} + + if [ "$major" -lt "$MIN_GLIBC_MAJOR" ] \ + || { [ "$major" -eq "$MIN_GLIBC_MAJOR" ] && [ "$minor" -lt "$MIN_GLIBC_MINOR" ]; }; then + error "Unsupported glibc version ($glibc_version). autter requires glibc ${MIN_GLIBC_MAJOR}.${MIN_GLIBC_MINOR} or newer (Ubuntu 22.04+, Debian 12+, Fedora 36+). + +On Ubuntu 20.04 or older WSL2 distros, use a newer base image or run inside Docker: + docker run -it --rm -v \"\$PWD\":/work -w /work ubuntu:22.04 bash + # then re-run this installer inside the container" + fi +} + +# Fail the install when the downloaded binary cannot execute (glibc mismatch, etc.). +verify_binary_runs() { + local bin="$1" + local output + if output=$("$bin" --version 2>&1); then + printf '%s' "$output" + return 0 + fi + + rm -f "$bin" 2>/dev/null || true + if [ "$OS" = "linux" ] && printf '%s' "$output" | grep -q 'GLIBC_'; then + error "The autter binary could not run on this system (incompatible glibc). + +$output + +autter requires glibc ${MIN_GLIBC_MAJOR}.${MIN_GLIBC_MINOR} or newer (Ubuntu 22.04+). On Ubuntu 20.04 / older WSL2, switch to a newer distro or use Docker: + docker run -it --rm -v \"\$PWD\":/work -w /work ubuntu:22.04 bash" + fi + error "The autter binary could not run on this system: + +$output" +} + +check_git +check_linux_glibc + # Map OS to binary name case $OS in "darwin") @@ -459,11 +527,10 @@ else warn "Failed to create ~/.local/bin/autter symlink. This is non-fatal." fi +# Verify the binary runs before reporting success (catches glibc mismatches, etc.). +INSTALLED_VERSION=$(verify_binary_runs "${INSTALL_DIR}/autter") success "Successfully installed autter into ${INSTALL_DIR}" success "You can now run 'autter' from your terminal" - -# Print installed version -INSTALLED_VERSION=$(${INSTALL_DIR}/autter --version 2>&1 || echo "unknown") echo "Installed autter ${INSTALLED_VERSION}" # Login user with install token if provided diff --git a/npm/install.js b/npm/install.js index 1016124..e5f1d59 100644 --- a/npm/install.js +++ b/npm/install.js @@ -101,6 +101,57 @@ async function verifyChecksum(buf, asset, tag) { } } +function checkGit() { + try { + execFileSync('git', ['--version'], { encoding: 'utf8', timeout: 10_000, stdio: 'pipe' }); + } catch { + throw new Error( + 'git is required but not found. Install git 2.22 or newer, then re-run: npm install -g @autter/cli' + ); + } +} + +function checkLinuxGlibc() { + if (process.platform !== 'linux') return; + try { + const out = execFileSync('ldd', ['--version'], { encoding: 'utf8', timeout: 10_000 }); + const match = out.match(/(\d+)\.(\d+)/); + if (!match) return; + const major = Number(match[1]); + const minor = Number(match[2]); + if (major < 2 || (major === 2 && minor < 35)) { + throw new Error( + `Unsupported glibc version (${major}.${minor}). autter requires glibc 2.35+ (Ubuntu 22.04+, Debian 12+, Fedora 36+). ` + + 'On Ubuntu 20.04 / older WSL2, use a newer distro or run inside ubuntu:22.04 Docker.' + ); + } + } catch (err) { + if (err.message?.includes('Unsupported glibc')) throw err; + // ldd missing — rely on post-download binary verify + } +} + +function verifyBinaryRuns(bin) { + try { + const out = execFileSync(bin, ['--version'], { encoding: 'utf8', timeout: 10_000 }); + return out.trim().split(/\s+/)[0] || null; + } catch (err) { + const detail = err.stderr?.toString() || err.stdout?.toString() || err.message || String(err); + try { + fs.rmSync(bin, { force: true }); + } catch { + // best effort + } + if (process.platform === 'linux' && detail.includes('GLIBC')) { + throw new Error( + `The autter binary could not run on this system (incompatible glibc).\n${detail}\n\n` + + 'autter requires glibc 2.35+ (Ubuntu 22.04+). On Ubuntu 20.04 / older WSL2, use a newer distro or Docker.' + ); + } + throw new Error(`The autter binary could not run on this system: ${detail}`); + } +} + // `autter --version` prints the bare version ("1.6.8", or "1.6.8 (debug)"). function installedVersion(bin) { try { @@ -188,6 +239,8 @@ async function ensureBinary() { } } + verifyBinaryRuns(dest); + await reportInstallPing(tag); return { bin: dest, downloaded: true }; } @@ -200,6 +253,14 @@ async function main() { return; } + try { + checkGit(); + checkLinuxGlibc(); + } catch (err) { + console.warn(`autter: ${err.message}`); + return; + } + let result; try { result = await ensureBinary(); diff --git a/src/commands/install_hooks.rs b/src/commands/install_hooks.rs index a3a0746..4815035 100644 --- a/src/commands/install_hooks.rs +++ b/src/commands/install_hooks.rs @@ -486,8 +486,8 @@ async fn async_run_install( let installers = get_all_installers(); let mut installed_tools: HashSet = HashSet::new(); - // Track agents whose hooks were updated (name, process_names) for restart warnings - let mut updated_agents: Vec<(String, Vec)> = Vec::new(); + // Track agents whose hooks were checked (updated or already up to date) for restart warnings + let mut agents_for_restart: Vec<(String, Vec)> = Vec::new(); let mut not_detected: Vec<&str> = Vec::new(); for installer in &installers { @@ -541,7 +541,7 @@ async fn async_run_install( .map(|s| s.to_string()) .collect(); if !pnames.is_empty() { - updated_agents.push((name.to_string(), pnames)); + agents_for_restart.push((name.to_string(), pnames)); } } } @@ -552,6 +552,21 @@ async fn async_run_install( statuses.insert(id.to_string(), InstallStatus::AlreadyInstalled); detailed_results .push((id.to_string(), InstallResult::already_installed())); + + // Hooks may be up to date on disk but the agent still needs a + // restart to load them — track for the restart warning below. + if !options.dry_run { + let pnames: Vec = installer + .process_names() + .iter() + .map(|s| s.to_string()) + .collect(); + if !pnames.is_empty() + && !agents_for_restart.iter().any(|(n, _)| n == name) + { + agents_for_restart.push((name.to_string(), pnames)); + } + } } Err(e) => { let error_msg = e.to_string(); @@ -614,7 +629,7 @@ async fn async_run_install( // Track restart detection for extras-only agents (e.g. JetBrains, VS Code) if extras_changed && !options.dry_run - && !updated_agents.iter().any(|(n, _)| n == name) + && !agents_for_restart.iter().any(|(n, _)| n == name) { let pnames: Vec = installer .process_names() @@ -622,7 +637,7 @@ async fn async_run_install( .map(|s| s.to_string()) .collect(); if !pnames.is_empty() { - updated_agents.push((name.to_string(), pnames)); + agents_for_restart.push((name.to_string(), pnames)); } } } @@ -683,11 +698,12 @@ async fn async_run_install( println!("{}", paint("1", " autter install-hooks --dry-run=false")); } - // Check for running agents that had hooks updated and warn about restart - if !options.dry_run && !updated_agents.is_empty() { + // Warn when agents that use hooks are running — they must be restarted for + // attribution to take effect, including when hooks were already up to date. + if !options.dry_run && !agents_for_restart.is_empty() { let mut any_running = false; - for (agent_name, pnames) in &updated_agents { + for (agent_name, pnames) in &agents_for_restart { let refs: Vec<&str> = pnames.iter().map(|s| s.as_str()).collect(); let pids = find_running_pids(&refs); if !pids.is_empty() { @@ -726,7 +742,16 @@ async fn async_run_install( "This is expected — once you commit and start a fresh session, attribution will work correctly." ); println!( - "If the issue persists, please open an issue at https://github.com/autter-dev/autter-cli/issues" + "If the issue persists, run 'autter doctor' (or 'autter debug' on older versions)." + ); + } else if !agents_for_restart.is_empty() { + println!(); + println!( + "{}", + paint( + "33", + "If any coding agent was open during hook setup, restart it now for AI attribution to take effect." + ) ); } } @@ -772,7 +797,19 @@ fn warn_if_git_version_too_old() { let text = String::from_utf8_lossy(&o.stdout).into_owned(); parse_git_version(&text) } - Err(_) => None, + Err(_) => { + eprintln!(); + eprintln!( + "{}", + paint_err( + "1;31", + "WARNING: git not found — autter requires git to function." + ) + ); + eprintln!("Install git 2.22+ and re-run: autter install-hooks"); + eprintln!(); + return; + } }; if let Some(v) = version { diff --git a/src/mdm/agents/gemini.rs b/src/mdm/agents/gemini.rs index 9d8b8b9..c354789 100644 --- a/src/mdm/agents/gemini.rs +++ b/src/mdm/agents/gemini.rs @@ -21,28 +21,77 @@ impl GeminiInstaller { /// Returns `(hooks_installed, hooks_up_to_date)` from a parsed settings value. /// `hooks_installed` = autter checkpoint command exists in ANY matcher block. - /// `hooks_up_to_date` = autter checkpoint command exists in the `"*"` catch-all block. + /// `hooks_up_to_date` = `tools.enableHooks` is true and autter checkpoints exist in + /// both the `BeforeTool` and `AfterTool` `"*"` catch-all blocks (AI attribution needs + /// the post-edit AfterTool hook). fn hook_status(settings: &Value) -> (bool, bool) { - let before_tool_blocks = settings - .get("hooks") - .and_then(|h| h.get("BeforeTool")) - .and_then(|v| v.as_array()); + let enable_hooks = settings + .get("tools") + .and_then(|t| t.get("enableHooks")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let mut hooks_installed = false; + for hook_type in &["BeforeTool", "AfterTool"] { + if Self::any_block_has_autter(settings, hook_type) { + hooks_installed = true; + break; + } + } + + let hooks_up_to_date = enable_hooks + && Self::catch_all_has_autter(settings, "BeforeTool") + && Self::catch_all_has_autter(settings, "AfterTool"); + + (hooks_installed, hooks_up_to_date) + } - let Some(blocks) = before_tool_blocks else { - return (false, false); + fn any_block_has_autter(settings: &Value, hook_type: &str) -> bool { + let Some(blocks) = settings + .get("hooks") + .and_then(|h| h.get(hook_type)) + .and_then(|v| v.as_array()) + else { + return false; }; - let mut hooks_installed = false; - let mut hooks_up_to_date = false; + blocks.iter().any(|block| { + block + .get("hooks") + .and_then(|h| h.as_array()) + .map(|hooks| { + hooks.iter().any(|hook| { + hook.get("command") + .and_then(|c| c.as_str()) + .map(is_autter_checkpoint_command) + .unwrap_or(false) + }) + }) + .unwrap_or(false) + }) + } + + fn catch_all_has_autter(settings: &Value, hook_type: &str) -> bool { + let Some(blocks) = settings + .get("hooks") + .and_then(|h| h.get(hook_type)) + .and_then(|v| v.as_array()) + else { + return false; + }; - for block in blocks { + blocks.iter().any(|block| { let is_catch_all = block .get("matcher") .and_then(|m| m.as_str()) .map(|m| m == GEMINI_CATCH_ALL_MATCHER) .unwrap_or(false); - let has_autter = block + if !is_catch_all { + return false; + } + + block .get("hooks") .and_then(|h| h.as_array()) .map(|hooks| { @@ -53,17 +102,8 @@ impl GeminiInstaller { .unwrap_or(false) }) }) - .unwrap_or(false); - - if has_autter { - hooks_installed = true; - if is_catch_all { - hooks_up_to_date = true; - } - } - } - - (hooks_installed, hooks_up_to_date) + .unwrap_or(false) + }) } fn install_hooks_at( @@ -967,13 +1007,48 @@ mod tests { #[test] fn c2_autter_in_catch_all_returns_up_to_date() { - let cmd = expected_before_cmd(); - let settings = json!({"hooks": {"BeforeTool": [{"matcher": "*", "hooks": [{"type":"command","command": cmd}]}]}}); + let before_cmd = expected_before_cmd(); + let after_cmd = expected_after_cmd(); + let settings = json!({ + "tools": {"enableHooks": true}, + "hooks": { + "BeforeTool": [{"matcher": "*", "hooks": [{"type":"command","command": before_cmd}]}], + "AfterTool": [{"matcher": "*", "hooks": [{"type":"command","command": after_cmd}]}] + } + }); let (installed, up_to_date) = GeminiInstaller::hook_status(&settings); assert!(installed); assert!(up_to_date); } + #[test] + fn c2b_before_tool_only_not_up_to_date() { + let cmd = expected_before_cmd(); + let settings = json!({ + "tools": {"enableHooks": true}, + "hooks": {"BeforeTool": [{"matcher": "*", "hooks": [{"type":"command","command": cmd}]}]} + }); + let (installed, up_to_date) = GeminiInstaller::hook_status(&settings); + assert!(installed); + assert!(!up_to_date); + } + + #[test] + fn c2c_enable_hooks_disabled_not_up_to_date() { + let before_cmd = expected_before_cmd(); + let after_cmd = expected_after_cmd(); + let settings = json!({ + "tools": {"enableHooks": false}, + "hooks": { + "BeforeTool": [{"matcher": "*", "hooks": [{"type":"command","command": before_cmd}]}], + "AfterTool": [{"matcher": "*", "hooks": [{"type":"command","command": after_cmd}]}] + } + }); + let (installed, up_to_date) = GeminiInstaller::hook_status(&settings); + assert!(installed); + assert!(!up_to_date); + } + #[test] fn c3_autter_only_in_old_matcher_not_up_to_date() { let cmd = expected_before_cmd();