Skip to content

Latest commit

 

History

History
601 lines (450 loc) · 16.2 KB

File metadata and controls

601 lines (450 loc) · 16.2 KB

Adding Tools to Jarvy

This guide explains how to add new tools to Jarvy using the declarative ToolSpec pattern.

Quick Start

For most tools, adding support is just ~15 lines of code:

  1. Create a new directory: src/tools/{toolname}/
  2. Create two files: mod.rs and {toolname}.rs
  3. Add the module to src/tools/mod.rs
  4. Done! The tool auto-registers at compile time.

Step-by-Step Guide

1. Create the Tool Directory

mkdir src/tools/mytool

2. Create mod.rs

Create src/tools/mytool/mod.rs:

#![allow(clippy::module_inception)]
pub mod mytool;

3. Create the Tool File

Create src/tools/mytool/mytool.rs:

//! mytool - brief description of the tool
//!
//! This tool uses the ToolSpec pattern for declarative installation.

use crate::define_tool;

define_tool!(MYTOOL, {
    command: "mytool",
    macos: { brew: "mytool" },
    linux: { uniform: "mytool" },
    windows: { winget: "Publisher.MyTool" },
});

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ensure_mytool_no_panic() {
        let res = ensure("");
        assert!(res.is_ok() || res.is_err());
    }
}

4. Register the Module

Add to src/tools/mod.rs:

pub mod mytool;

That's it! The define_tool! macro automatically registers the tool via the inventory crate.

The define_tool! Macro

The macro handles all boilerplate, generating:

  • A ToolSpec static with platform-specific install info
  • An ensure() function that checks/installs the tool
  • An add_handler() function for the registry
  • Auto-registration via inventory::submit!

Macro Syntax

define_tool!(NAME, {
    command: "command_to_check",   // Required: command to test existence
    macos: { ... },                // Optional: macOS install options
    linux: { ... },                // Optional: Linux install options
    windows: { ... },              // Optional: Windows install options
    custom_install: Some(fn),      // Optional: custom install function
    default_hook: { ... },         // Optional: post-install hook
    depends_on: &[...],            // Optional: strict dependencies (ALL required)
    depends_on_one_of: &[...],     // Optional: flexible dependencies (ONE OF required)
});

Platform-Specific Options

macOS Options

Homebrew formula (CLI tools):

macos: { brew: "git" },

Homebrew cask (GUI apps):

macos: { cask: "docker" },

Linux Options

Uniform package name (same name on all distros):

linux: { uniform: "git" },

Different names per package manager:

linux: { apt: "docker.io", dnf: "docker", pacman: "docker", apk: "docker" },

Linuxbrew (for tools without native packages):

linux: { brew: "hashicorp/tap/terraform" },

Windows Options

winget (preferred):

windows: { winget: "Git.Git" },

Chocolatey:

windows: { choco: "git" },

Both (winget preferred, choco fallback):

windows: { winget: "Git.Git", choco: "git" },

Package Name Reference

Finding Homebrew Packages

# Search formulas
brew search <name>

# Get package info
brew info <name>

# Search casks (GUI apps)
brew search --cask <name>

Finding Linux Package Names

Distro Command
Debian/Ubuntu apt search <name>
Fedora/RHEL dnf search <name>
Arch pacman -Ss <name>
Alpine apk search <name>

Finding winget Package IDs

# Search packages
winget search <name>

# The ID is typically "Publisher.PackageName"
# Example: Git.Git, Docker.DockerDesktop, Microsoft.VisualStudioCode

Examples

Simple Tool (Uniform Package Name)

Tools like jq have the same name across all package managers:

//! jq - command-line JSON processor

use crate::define_tool;

define_tool!(JQ, {
    command: "jq",
    macos: { brew: "jq" },
    linux: { uniform: "jq" },
    windows: { winget: "jqlang.jq" },
});

Tool with Different Package Names

Tools like Docker have different names on different distros:

//! docker - containerization platform

use crate::define_tool;

define_tool!(DOCKER, {
    command: "docker",
    macos: { cask: "docker" },
    linux: { apt: "docker.io", dnf: "docker", pacman: "docker", apk: "docker" },
    windows: { winget: "Docker.DockerDesktop" },
});

Tool with Homebrew Tap

Some tools require adding a Homebrew tap first. Include the full tap path:

//! terraform - Infrastructure as Code

use crate::define_tool;

define_tool!(TERRAFORM, {
    command: "terraform",
    macos: { brew: "hashicorp/tap/terraform" },
    linux: { brew: "hashicorp/tap/terraform" },
    windows: { winget: "Hashicorp.Terraform" },
});

macOS-Only Tool

For tools that only work on macOS:

//! iterm2 - terminal emulator for macOS

use crate::define_tool;

define_tool!(ITERM2, {
    command: "iTerm",
    macos: { cask: "iterm2" },
});

Tool Dependencies

Jarvy supports two types of tool dependencies to ensure proper installation order and prerequisite checking.

Strict Dependencies (depends_on)

Use depends_on when a tool absolutely requires ALL listed dependencies to function. The tool cannot work without them.

//! lazydocker - Docker TUI that requires Docker daemon

use crate::define_tool;

define_tool!(LAZYDOCKER, {
    command: "lazydocker",
    macos: { brew: "lazydocker" },
    linux: { brew: "lazydocker" },
    windows: { choco: "lazydocker" },
    // Docker TUI directly calls Docker APIs - Docker is required
    depends_on: &["docker"],
});

When to use depends_on:

  • The tool directly calls another tool's APIs (lazydocker → docker)
  • The tool is a runtime that runs inside another tool (kind → docker for Kubernetes-in-Docker)
  • The tool won't start or function without the dependency

Behavior:

  • ALL listed dependencies must be available (installed or in config)
  • If any dependency is missing from config, a warning is shown
  • Dependencies are installed before the dependent tool (topological sort)

Flexible Dependencies (depends_on_one_of)

Use depends_on_one_of when a tool needs at least one of several alternative dependencies. The user has flexibility in which one to use.

//! kubectl - Kubernetes CLI that needs a cluster to talk to

use crate::define_tool;

define_tool!(KUBECTL, {
    command: "kubectl",
    macos: { brew: "kubectl" },
    linux: { uniform: "kubectl" },
    windows: { winget: "Kubernetes.kubectl" },
    // kubectl needs ANY K8s cluster provider - user chooses which one
    depends_on_one_of: &["minikube", "kind", "k3d", "docker", "podman"],
});

When to use depends_on_one_of:

  • Multiple tools can satisfy the same requirement (container runtimes: docker OR podman)
  • The tool works with various backends (kubectl works with any K8s cluster)
  • Users should have flexibility in their setup

Behavior:

  1. If one option is already installed → dependency is satisfied
  2. If none installed but one is in config → that one is installed first
  3. If none installed or in config → advisory warning (tool still installs)

Dependency Examples

Tool Dependency Type Dependencies Reason
lazydocker strict ["docker"] Uses Docker API directly
kind strict ["docker"] Runs K8s clusters inside Docker
kubectl flexible ["minikube", "kind", "docker", ...] Works with any K8s cluster
minikube flexible ["docker", "podman"] Needs a container runtime
helm flexible ["kubectl"] Uses kubeconfig, kubectl may be bundled
k9s flexible ["kubectl"] K8s TUI, reads kubeconfig
dive flexible ["docker", "podman"] Image explorer, works with either

Combined Dependencies

A tool can have both strict and flexible dependencies:

define_tool!(EXAMPLE, {
    command: "example",
    macos: { brew: "example" },
    // Must have git installed
    depends_on: &["git"],
    // And needs one of these container runtimes
    depends_on_one_of: &["docker", "podman"],
});

Dependency Ordering

Jarvy automatically orders tool installation using topological sort:

  1. Tools without dependencies are installed first
  2. Tools with dependencies are installed after their dependencies
  3. For flexible deps, the first matching option in config creates the edge

Example: If config has [kubectl, minikube, docker]:

  • Order: docker → minikube → kubectl
  • (kubectl has flexible dep on minikube; minikube has flexible dep on docker)

Dependency Check Functions

The spec module provides functions for working with dependencies:

use jarvy::tools::spec::{
    get_tool_dependencies,           // Get strict deps
    get_tool_flexible_dependencies,  // Get flexible deps
    check_tool_dependencies,         // Check if deps satisfied
    order_tools_by_dependencies,     // Topological sort
};

// Check dependency status
let result = check_tool_dependencies("kubectl", &config_tools, &installed_tools);
match result {
    DependencyCheckResult::Satisfied => { /* good to go */ }
    DependencyCheckResult::MissingRequired(deps) => { /* strict deps missing */ }
    DependencyCheckResult::WillInstallFlexible(dep) => { /* will install this one first */ }
    DependencyCheckResult::MissingFlexible { options, .. } => { /* advisory warning */ }
}

Complex Tools (Custom Installation)

Some tools require custom installation logic that can't be expressed declaratively:

  • Shell script installers (nvm, rustup)
  • Tools that modify shell configuration
  • Tools with complex version management

When to Use Custom Installation

Use custom_install when:

  • The tool installs via a shell script (curl | bash pattern)
  • The tool requires post-install configuration
  • The tool uses a version manager (nvm, pyenv)
  • The standard package manager flow doesn't work

Writing a Custom Installer

For complex tools, don't use define_tool!. Instead, implement the handler manually:

//! nvm - Node Version Manager

use crate::tools::common::{InstallError, has, run};

pub fn ensure(_min_hint: &str) -> Result<(), InstallError> {
    // Check if already installed
    #[cfg(any(target_os = "macos", target_os = "linux"))]
    {
        let ok = std::process::Command::new("bash")
            .args(["-lc", "command -v nvm >/dev/null"])
            .status()
            .map(|s| s.success())
            .unwrap_or(false);
        if ok {
            return Ok(());
        }
        return install_posix();
    }

    #[cfg(target_os = "windows")]
    {
        if has("nvm") {
            return Ok(());
        }
        return install_windows();
    }
}

#[cfg(any(target_os = "macos", target_os = "linux"))]
fn install_posix() -> Result<(), InstallError> {
    run("bash", &["-lc",
        "curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash"
    ])?;
    Ok(())
}

#[cfg(target_os = "windows")]
fn install_windows() -> Result<(), InstallError> {
    if !has("winget") {
        return Err(InstallError::Prereq(
            "winget not found. Install Windows Package Manager, then re-run.",
        ));
    }
    run("winget", &["install", "-e", "--id", "CoreyButler.NVMforWindows"])?;
    Ok(())
}

/// Registry adapter
pub fn add_handler(min_hint: &str) -> Result<(), InstallError> {
    ensure(min_hint)
}

Important: Tools with custom installers must be manually registered in src/tools/mod.rs:

pub fn register_all() {
    // Auto-registered tools via inventory (define_tool! macro)
    for entry in spec::iter_tools() {
        let _ = register_tool(entry.spec.name, entry.handler);
    }

    // Manual registration for custom installers
    let _ = register_tool("nvm", crate::tools::nvm::nvm::add_handler);
}

How Auto-Registration Works

Tools defined with define_tool! are automatically registered without any manual code:

  1. The define_tool! macro generates an inventory::submit! call
  2. The inventory crate collects all submissions at compile time via linker magic
  3. register_all() iterates over collected entries and registers each tool
  4. No manual registration code needed!
// Inside define_tool! macro expansion:
::inventory::submit! {
    $crate::tools::spec::ToolEntry {
        spec: &MYTOOL,
        handler: add_handler,
    }
}

Testing Your Tool

Run Tests

# Run all tests
cargo test --verbose -- --show-output

# Run tests for a specific tool
cargo test mytool -- --show-output

Verify Compilation

cargo check --verbose
cargo clippy --all-features -- -D warnings

Test Installation (Dry Run)

Create a test jarvy.toml:

[tools]
mytool = "*"

Run with test mode:

JARVY_TEST_MODE=1 cargo run -- setup

Troubleshooting

Tool Not Found in Registry

  1. Check that you added the module to src/tools/mod.rs
  2. Verify the define_tool! macro syntax is correct
  3. Run cargo clean && cargo build to rebuild

Package Name Wrong

  1. Test the package name manually: brew install <name> or apt install <name>
  2. Use the search commands in the Package Name Reference section
  3. Some packages have different names on different distro versions

Custom Tool Not Registering

Tools with custom installers don't auto-register. Add manual registration:

// In src/tools/mod.rs register_all()
let _ = register_tool("mytool", crate::tools::mytool::mytool::add_handler);

Compilation Errors

Common issues:

  • Missing use crate::define_tool; import
  • Incorrect brace placement in macro
  • Module not declared in parent mod.rs

Using the Scaffolding Command

For convenience, use the scaffolding command to generate boilerplate:

cargo run -p cargo-jarvy -- new-tool mytool

This creates the directory structure and files with sensible defaults.

How User Requests Reach Maintainers

When a user (or AI agent) runs jarvy setup with a tool name that isn't in the registry — or invokes jarvy tools --request <name> explicitly — Jarvy emits a canonical tool.unsupported event. As a maintainer, this is your primary inbound signal for "add tool X next."

Two delivery channels:

  1. Telemetry (preferred) — when the user has opted in via jarvy telemetry enable (or implicit consent on the --request path), the event ships via OTLP to the project's collector. Query the jarvy.tool.unsupported counter / event stream for ranked requests. No GitHub account required for the user, no triage friction for you.
  2. GitHub fallback — when telemetry is off, Jarvy prints a pre-filled issue URL pointing at tool_request.yml. The form arrives with the tool name, platform, version, and define_tool! scaffold already populated.

Event payload shape (uniform across setup and --request call sites; see CLAUDE.md Event Taxonomy for the canonical contract):

Field Notes
tool Unknown tool name
version Optional version string from the config
source config | mcp | cli | request
platform darwin | linux | windows
suggestions CSV of fuzzy-matched candidates
channel telemetry | manual
fallback_issue_url Present only when channel = manual
scaffold_cmd cargo run -p cargo-jarvy -- new-tool <name>
exit_code Always 8 (TOOL_UNSUPPORTED)
opt_in_bypassed true when source = request

Triage workflow:

  1. Rank by jarvy.tool.unsupported counter — high-frequency requests ship first.
  2. Pull suggestions to spot typos that aren't real new tools (e.g. dokcer is just docker).
  3. Run the suggested scaffold_cmd and follow the Quick Start above. Most additions are ~15 lines plus a small test.
  4. Tag the PR with add-tool/<name> and mention the originating issue (or telemetry record ID) so the requesting user gets a notification when it ships.

Common asks should ship within days — that's the user-facing promise the tool.unsupported loop is designed to deliver on. See for-ai-agents.md › When a tool isn't supported for the user / AI-agent side of the contract.