From 2f0303aadad1f181cfc5272d4175bef4924a3783 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Tue, 24 Mar 2026 11:35:05 +0100 Subject: [PATCH] feat: add skill subcommand for LLM-readable reference and fix shell integration docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New gitnow skill command outputs a comprehensive machine-readable reference - Fix eval quoting in README: eval $(…) → eval "$(…)" - Add homebrew caveats with shell integration instructions - Add after_help hint directing LLMs to gitnow skill --- .goreleaser.yaml | 4 + README.md | 4 +- crates/gitnow/src/commands.rs | 1 + crates/gitnow/src/commands/skill.rs | 264 ++++++++++++++++++++++++++++ crates/gitnow/src/main.rs | 17 +- 5 files changed, 285 insertions(+), 5 deletions(-) create mode 100644 crates/gitnow/src/commands/skill.rs diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 4f33040..9b54859 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -58,3 +58,7 @@ brews: description: "Git Now is a utility for easily navigating git projects from common upstream providers." install: | bin.install "gitnow" + caveats: | + To enable shell integration, add this to your .zshrc: + + eval "$(gitnow init zsh)" diff --git a/README.md b/README.md index 4bd36af..2b11c8a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ cargo binstall gitnow gitnow # Or install gitnow scripts (in your .bashrc, .zshrc) this will use native shell commands to move you around -eval $(gitnow init zsh) +eval "$(gitnow init zsh)" git-now # Long gn # Short alias ``` @@ -165,7 +165,7 @@ Commands that navigate to a directory (`gitnow`, `gitnow project`, `gitnow proje The recommended way to use gitnow is with shell integration, which uses a **chooser file** to communicate the selected path back to your shell: ```bash -eval $(gitnow init zsh) +eval "$(gitnow init zsh)" git-now # or gn ``` diff --git a/crates/gitnow/src/commands.rs b/crates/gitnow/src/commands.rs index a7b4082..1a7f777 100644 --- a/crates/gitnow/src/commands.rs +++ b/crates/gitnow/src/commands.rs @@ -1,6 +1,7 @@ pub mod project; pub mod root; pub mod shell; +pub mod skill; pub mod update; pub mod worktree; pub mod clone { diff --git a/crates/gitnow/src/commands/skill.rs b/crates/gitnow/src/commands/skill.rs new file mode 100644 index 0000000..efcadc4 --- /dev/null +++ b/crates/gitnow/src/commands/skill.rs @@ -0,0 +1,264 @@ +/// The `skill` subcommand outputs a comprehensive, LLM-readable description of +/// everything gitnow can do — commands, flags, configuration, and workflows. + +#[derive(clap::Parser)] +pub struct SkillCommand {} + +impl SkillCommand { + pub async fn execute(&self) -> anyhow::Result<()> { + print!("{}", SKILL_TEXT); + Ok(()) + } +} + +const SKILL_TEXT: &str = r#"# gitnow — Navigate git projects at the speed of thought + +gitnow is a CLI tool for discovering, cloning, and navigating git repositories +from multiple providers (GitHub, Gitea). It maintains a local cache of known +repositories and provides fuzzy-search, interactive selection, worktree +management, and scratch-pad project workspaces. + +## Quick reference + +``` +gitnow [OPTIONS] [SEARCH] # search/clone/open a repository +gitnow update # refresh the local repository cache +gitnow clone --search # batch-clone repositories matching a pattern +gitnow worktree [SEARCH] [OPTIONS] # create and enter a git worktree for a branch +gitnow project [SEARCH] [OPTIONS] # open an existing scratch-pad project +gitnow project create [NAME] # create a new multi-repo project +gitnow project add [NAME] # add repositories to a project +gitnow project delete [NAME] # delete a project +gitnow init zsh # print zsh shell integration script +gitnow skill # print this reference (you are here) +``` + +## Commands in detail + +### Default (no subcommand) + +``` +gitnow [OPTIONS] [SEARCH] +``` + +Search for a repository, optionally clone it, and open a shell inside it. + +- If SEARCH is provided, fuzzy-matches against cached repositories. +- If omitted, opens an interactive fuzzy-search picker. +- Clones the repository if it does not exist locally. +- Spawns a sub-shell in the repository directory. + +**Flags:** +| Flag | Description | +|-----------------------|----------------------------------------------------------| +| `--no-cache` | Skip reading from the local cache; fetch fresh data | +| `--no-clone` | Do not clone the repository if it is missing locally | +| `--no-shell` | Print the path instead of spawning a shell | +| `--force-refresh` | Force a fresh clone even if the repo already exists | +| `--force-cache-update`| Update the cache before searching | +| `--chooser-file PATH` | Write selected path to this file (implies --no-shell) | +| `-c, --config PATH` | Path to config file (global flag) | + +**Environment variables:** +- `GITNOW_CONFIG` — path to config file (overrides default) +- `GITNOW_CHOOSER_FILE` — equivalent to `--chooser-file` + +--- + +### `gitnow update` + +Fetch all repositories from configured providers and update the local cache. +Should be run periodically or after adding new providers/organisations. + +No flags. + +--- + +### `gitnow clone --search ` + +Batch-clone all repositories whose relative path matches the given regex. +Clones up to 5 repositories concurrently. Skips repos that already exist locally. + +**Required flags:** +| Flag | Description | +|--------------------|--------------------------------------------| +| `--search ` | Regular expression to match repository paths | + +--- + +### `gitnow worktree [SEARCH] [OPTIONS]` + +Create a git worktree for a specific branch of a repository. This is useful for +working on multiple branches simultaneously without switching. + +**Workflow:** +1. Select a repository (fuzzy search or interactive picker) +2. Bare-clone the repository if not already present +3. List remote branches +4. Select a branch (interactive picker, or `--branch`) +5. Create a worktree directory at `//` +6. Spawn a shell in the worktree (or write path to chooser file) + +**Flags:** +| Flag | Description | +|-------------------|------------------------------------------------| +| `[SEARCH]` | Optional search string to pre-filter repos | +| `-b, --branch` | Branch name (skips interactive branch picker) | +| `--no-cache` | Skip the local cache | +| `--no-shell` | Print path instead of spawning a shell | + +--- + +### `gitnow project [SEARCH] [OPTIONS]` + +Manage scratch-pad projects — directories containing multiple cloned repositories, +optionally bootstrapped from a template. + +When called without a subcommand, opens an existing project (interactive picker +or fuzzy match on SEARCH). + +**Flags:** +| Flag | Description | +|--------------|------------------------------------------| +| `[SEARCH]` | Filter existing projects by name | +| `--no-shell` | Print path instead of spawning a shell | + +#### `gitnow project create [NAME] [OPTIONS]` + +Create a new project directory, interactively select repositories to clone into it, +and optionally apply a template. + +| Flag | Description | +|---------------------|------------------------------------------------------| +| `[NAME]` | Project name (prompted if omitted) | +| `-t, --template` | Template name to bootstrap from | +| `--no-cache` | Skip local cache when listing repos | +| `--no-shell` | Print path instead of spawning a shell | + +Templates live in `~/.gitnow/templates/` (or the configured directory). Each +subdirectory is a template; its contents are copied into the new project. + +#### `gitnow project add [NAME] [OPTIONS]` + +Add more repositories to an existing project. + +| Flag | Description | +|--------------|------------------------------------------| +| `[NAME]` | Project name (interactive if omitted) | +| `--no-cache` | Skip local cache when listing repos | + +#### `gitnow project delete [NAME] [OPTIONS]` + +Delete a project directory. + +| Flag | Description | +|---------------|--------------------------------------| +| `[NAME]` | Project name (interactive if omitted)| +| `-f, --force` | Skip confirmation prompt | + +--- + +### `gitnow init zsh` + +Print a zsh shell integration script to stdout. Typically used as: + +```zsh +eval "$(gitnow init zsh)" +``` + +This provides a shell function that changes directory after gitnow exits, +using the chooser-file mechanism. + +--- + +### `gitnow skill` + +Print this LLM-readable reference document to stdout. + +--- + +## Configuration + +Config file location (in priority order): +1. `--config` / `-c` flag +2. `GITNOW_CONFIG` environment variable +3. `~/.config/gitnow/gitnow.toml` + +### Config file format (TOML) + +```toml +[settings] +# Where repositories are cloned to (default: ~/git) +projects = { directory = "~/git" } + +# Custom clone command (minijinja template) +# Available variables: {{ ssh_url }}, {{ path }} +# Default: "git clone {{ ssh_url }} {{ path }}" +clone_command = "git clone {{ ssh_url }} {{ path }}" + +# Commands to run after cloning a repository +post_clone_command = "echo 'cloned!'" +# or as a list: +# post_clone_command = ["cmd1", "cmd2"] + +# Commands to run when opening an already-cloned repository +post_update_command = "git fetch --prune" + +[settings.cache] +# Where the cache is stored (default: ~/.cache/gitnow) +location = "~/.cache/gitnow" + +# Cache duration (default: 7 days). Set to false to disable. +duration = { days = 7, hours = 0, minutes = 0 } + +[settings.worktree] +# Custom worktree commands (minijinja templates) +clone_command = "git clone --bare {{ ssh_url }} {{ bare_path }}" +add_command = "git -C {{ bare_path }} worktree add {{ worktree_path }} {{ branch }}" +list_branches_command = "git -C {{ bare_path }} branch -r --format=%(refname:short)" + +[settings.project] +# Where scratch-pad projects are stored (default: ~/.gitnow/projects) +directory = "~/.gitnow/projects" +# Where project templates live (default: ~/.gitnow/templates) +templates_directory = "~/.gitnow/templates" + +# --- Providers --- + +[[providers.github]] +access_token = "ghp_..." # or { env = "GITHUB_TOKEN" } +current_user = "your-username" # optional, for user-specific repos +users = ["user1"] # fetch repos for these users +organisations = ["org1", "org2"] # fetch repos for these orgs +url = "https://api.github.com" # optional, for GitHub Enterprise + +[[providers.gitea]] +url = "https://gitea.example.com/api/v1" +access_token = "token" # or { env = "GITEA_TOKEN" } +current_user = "your-username" +users = ["user1"] +organisations = ["org1"] +``` + +Multiple provider entries are supported — gitnow aggregates repositories from all of them. + +## Typical workflows + +### First-time setup +1. Create `~/.config/gitnow/gitnow.toml` with at least one provider +2. Run `gitnow update` to populate the cache +3. Run `gitnow` to interactively search and clone a repo + +### Daily use +- `gitnow ` — jump to a repo by fuzzy name +- `gitnow worktree -b feature-x` — start work on a branch in a worktree +- `gitnow project create my-feature` — set up a multi-repo workspace + +### Shell integration (zsh) +Add to `.zshrc`: +```zsh +eval "$(gitnow init zsh)" +``` +This wraps gitnow so that selecting a repository changes your current shell's +working directory (instead of spawning a sub-shell). +"#; diff --git a/crates/gitnow/src/main.rs b/crates/gitnow/src/main.rs index be7f040..283442b 100644 --- a/crates/gitnow/src/main.rs +++ b/crates/gitnow/src/main.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use anyhow::Context; use clap::{Parser, Subcommand}; use commands::{ - clone::CloneCommand, project::ProjectCommand, root::RootCommand, shell::Shell, update::Update, - worktree::WorktreeCommand, + clone::CloneCommand, project::ProjectCommand, root::RootCommand, shell::Shell, + skill::SkillCommand, update::Update, worktree::WorktreeCommand, }; use config::Config; use tracing::level_filters::LevelFilter; @@ -29,7 +29,13 @@ mod template_command; mod worktree; #[derive(Parser)] -#[command(author, version, about, long_about = Some("Navigate git projects at the speed of thought"))] +#[command( + author, + version, + about, + long_about = Some("Navigate git projects at the speed of thought"), + after_help = "TIP: LLM/AI agents should run `gitnow skill` for a complete machine-readable reference." +)] struct Command { #[command(subcommand)] command: Option, @@ -71,6 +77,8 @@ enum Commands { Worktree(WorktreeCommand), /// Manage scratch-pad projects with multiple repositories Project(ProjectCommand), + /// Print an LLM-readable reference of all gitnow capabilities + Skill(SkillCommand), } const DEFAULT_CONFIG_PATH: &str = ".config/gitnow/gitnow.toml"; @@ -131,6 +139,9 @@ async fn main() -> anyhow::Result<()> { Commands::Project(mut project) => { project.execute(app, &chooser).await?; } + Commands::Skill(skill) => { + skill.execute().await?; + } }, None => { RootCommand::new(app)