3 Commits

Author SHA1 Message Date
110597a21e feat: add non-interactive CLI support for project commands
All checks were successful
Release / release (push) Successful in 1m40s
Add --repos flag to project create/add for passing repos without
interactive picker. Add --no-template flag to skip template selection.
Add project list subcommand with --repos and --json output. Prevent
empty multi-select confirmation in interactive mode. Update skill
reference and README with new flags and examples.
2026-03-24 12:10:43 +01:00
2f0303aada feat: add skill subcommand for LLM-readable reference and fix shell integration docs
All checks were successful
Release / release (push) Successful in 1m40s
- 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
2026-03-24 11:35:25 +01:00
451fbe1640 feat: switch homebrew distribution from cask to formula
All checks were successful
Release / release (push) Successful in 1m39s
Replace homebrew_casks with brews in goreleaser config to use
a standard Formula instead of a Cask, avoiding macOS quarantine.
2026-03-24 10:33:49 +01:00
7 changed files with 512 additions and 42 deletions

View File

@@ -46,15 +46,19 @@ release:
name_template: "v{{ .Version }}" name_template: "v{{ .Version }}"
mode: keep-existing mode: keep-existing
homebrew_casks: brews:
- name: gitnow - name: gitnow
binaries:
- gitnow
repository: repository:
owner: kjuulh owner: kjuulh
name: homebrew-tap name: homebrew-tap
token: "{{ .Env.RELEASE_TOKEN }}" token: "{{ .Env.RELEASE_TOKEN }}"
url: directory: Formula
template: "https://git.kjuulh.io/kjuulh/gitnow/releases/download/{{ .Tag }}/{{ .ArtifactName }}" url_template: "https://git.kjuulh.io/kjuulh/gitnow/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
homepage: "https://gitnow-client.prod.kjuulh.app" homepage: "https://gitnow-client.prod.kjuulh.app"
description: "Git Now is a utility for easily navigating git projects from common upstream providers." 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)"

View File

@@ -30,7 +30,7 @@ cargo binstall gitnow
gitnow gitnow
# Or install gitnow scripts (in your .bashrc, .zshrc) this will use native shell commands to move you around # 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 git-now # Long
gn # Short alias gn # Short alias
``` ```
@@ -137,17 +137,35 @@ gitnow project create my-feature
# Create from a template # Create from a template
gitnow project create my-feature -t default gitnow project create my-feature -t default
# Create non-interactively with specific repos (fuzzy-matched)
gitnow project create my-feature --repos repo-a --repos repo-b --no-template --no-shell
# Open an existing project (interactive selection) # Open an existing project (interactive selection)
gitnow project gitnow project
# Open by name # Open by name
gitnow project my-feature gitnow project my-feature
# Add more repos to a project # List all projects
gitnow project list
# List with repo details
gitnow project list --repos
# List as JSON (for scripting)
gitnow project list --repos --json
# Add more repos to a project (interactive)
gitnow project add my-feature gitnow project add my-feature
# Add repos non-interactively
gitnow project add my-feature --repos repo-c --repos repo-d
# Delete a project # Delete a project
gitnow project delete my-feature gitnow project delete my-feature
# Delete without confirmation
gitnow project delete my-feature --force
``` ```
Project directories live at `~/.gitnow/projects/` by default. Templates live at `~/.gitnow/templates/`. Both are configurable: Project directories live at `~/.gitnow/projects/` by default. Templates live at `~/.gitnow/templates/`. Both are configurable:
@@ -165,7 +183,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: 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 ```bash
eval $(gitnow init zsh) eval "$(gitnow init zsh)"
git-now # or gn git-now # or gn
``` ```

View File

@@ -1,6 +1,7 @@
pub mod project; pub mod project;
pub mod root; pub mod root;
pub mod shell; pub mod shell;
pub mod skill;
pub mod update; pub mod update;
pub mod worktree; pub mod worktree;
pub mod clone { pub mod clone {

View File

@@ -7,12 +7,15 @@ use crate::{
cache::load_repositories, cache::load_repositories,
chooser::Chooser, chooser::Chooser,
custom_command::CustomCommandApp, custom_command::CustomCommandApp,
fuzzy_matcher::FuzzyMatcherApp,
interactive::{InteractiveApp, Searchable}, interactive::{InteractiveApp, Searchable},
project_metadata::{ProjectMetadata, RepoEntry}, project_metadata::{ProjectMetadata, RepoEntry},
shell::ShellApp, shell::ShellApp,
template_command, template_command,
}; };
use super::root::RepositoryMatcher;
#[derive(clap::Parser)] #[derive(clap::Parser)]
pub struct ProjectCommand { pub struct ProjectCommand {
#[command(subcommand)] #[command(subcommand)]
@@ -35,6 +38,8 @@ enum ProjectSubcommand {
Add(ProjectAddCommand), Add(ProjectAddCommand),
/// Delete an existing project /// Delete an existing project
Delete(ProjectDeleteCommand), Delete(ProjectDeleteCommand),
/// List all projects and their repositories
List(ProjectListCommand),
} }
#[derive(clap::Parser)] #[derive(clap::Parser)]
@@ -47,6 +52,15 @@ pub struct ProjectCreateCommand {
#[arg(long = "template", short = 't')] #[arg(long = "template", short = 't')]
template: Option<String>, template: Option<String>,
/// Skip template selection entirely (even if templates exist)
#[arg(long = "no-template", default_value = "false")]
no_template: bool,
/// Repositories to include (fuzzy-matched against the cache). Can be
/// specified multiple times: --repos foo --repos bar
#[arg(long = "repos", short = 'r')]
repos: Vec<String>,
/// Skip cache when fetching repositories /// Skip cache when fetching repositories
#[arg(long = "no-cache", default_value = "false")] #[arg(long = "no-cache", default_value = "false")]
no_cache: bool, no_cache: bool,
@@ -62,11 +76,27 @@ pub struct ProjectAddCommand {
#[arg()] #[arg()]
name: Option<String>, name: Option<String>,
/// Repositories to add (fuzzy-matched against the cache). Can be
/// specified multiple times: --repos foo --repos bar
#[arg(long = "repos", short = 'r')]
repos: Vec<String>,
/// Skip cache when fetching repositories /// Skip cache when fetching repositories
#[arg(long = "no-cache", default_value = "false")] #[arg(long = "no-cache", default_value = "false")]
no_cache: bool, no_cache: bool,
} }
#[derive(clap::Parser)]
pub struct ProjectListCommand {
/// Show repository details for each project
#[arg(long = "repos", default_value = "false")]
repos: bool,
/// Output as JSON
#[arg(long = "json", default_value = "false")]
json: bool,
}
#[derive(clap::Parser)] #[derive(clap::Parser)]
pub struct ProjectDeleteCommand { pub struct ProjectDeleteCommand {
/// Project name to delete /// Project name to delete
@@ -284,6 +314,7 @@ impl ProjectCommand {
Some(ProjectSubcommand::Create(mut create)) => create.execute(app, chooser).await, Some(ProjectSubcommand::Create(mut create)) => create.execute(app, chooser).await,
Some(ProjectSubcommand::Add(mut add)) => add.execute(app).await, Some(ProjectSubcommand::Add(mut add)) => add.execute(app).await,
Some(ProjectSubcommand::Delete(mut delete)) => delete.execute(app).await, Some(ProjectSubcommand::Delete(mut delete)) => delete.execute(app).await,
Some(ProjectSubcommand::List(list)) => list.execute(app).await,
None => self.open_existing(app, chooser).await, None => self.open_existing(app, chooser).await,
} }
} }
@@ -368,10 +399,25 @@ impl ProjectCreateCommand {
let repositories = load_repositories(app, !self.no_cache).await?; let repositories = load_repositories(app, !self.no_cache).await?;
eprintln!("Select repositories (Tab to toggle, Enter to confirm):"); let selected_repos = if !self.repos.is_empty() {
let selected_repos = app let matcher = app.fuzzy_matcher();
.interactive() let mut matched = Vec::new();
.interactive_multi_search(&repositories)?; for needle in &self.repos {
let results = matcher.match_repositories(needle, &repositories);
let repo = results
.first()
.ok_or_else(|| anyhow::anyhow!("no repository matching '{}' found", needle))?
.to_owned();
if !matched.iter().any(|r: &crate::git_provider::Repository| r.ssh_url == repo.ssh_url) {
matched.push(repo);
}
}
matched
} else {
eprintln!("Select repositories (Tab to toggle, Enter to confirm):");
app.interactive()
.interactive_multi_search(&repositories)?
};
if selected_repos.is_empty() { if selected_repos.is_empty() {
anyhow::bail!("no repositories selected"); anyhow::bail!("no repositories selected");
@@ -383,29 +429,33 @@ impl ProjectCreateCommand {
// Apply template if requested // Apply template if requested
let templates_dir = get_templates_dir(app); let templates_dir = get_templates_dir(app);
let template = match self.template.take() { let template = if self.no_template {
Some(name) => { None
let templates = list_subdirectories(&templates_dir)?; } else {
Some( match self.template.take() {
templates Some(name) => {
.into_iter() let templates = list_subdirectories(&templates_dir)?;
.find(|t| t.name == name) Some(
.ok_or_else(|| { templates
anyhow::anyhow!( .into_iter()
"template '{}' not found in {}", .find(|t| t.name == name)
name, .ok_or_else(|| {
templates_dir.display() anyhow::anyhow!(
) "template '{}' not found in {}",
})?, name,
) templates_dir.display()
} )
None => { })?,
let templates = list_subdirectories(&templates_dir)?; )
if !templates.is_empty() { }
eprintln!("Select a project template (Esc to skip):"); None => {
app.interactive().interactive_search_items(&templates)? let templates = list_subdirectories(&templates_dir)?;
} else { if !templates.is_empty() {
None eprintln!("Select a project template (Esc to skip):");
app.interactive().interactive_search_items(&templates)?
} else {
None
}
} }
} }
}; };
@@ -455,10 +505,25 @@ impl ProjectAddCommand {
let repositories = load_repositories(app, !self.no_cache).await?; let repositories = load_repositories(app, !self.no_cache).await?;
eprintln!("Select repositories to add (Tab to toggle, Enter to confirm):"); let selected_repos = if !self.repos.is_empty() {
let selected_repos = app let matcher = app.fuzzy_matcher();
.interactive() let mut matched = Vec::new();
.interactive_multi_search(&repositories)?; for needle in &self.repos {
let results = matcher.match_repositories(needle, &repositories);
let repo = results
.first()
.ok_or_else(|| anyhow::anyhow!("no repository matching '{}' found", needle))?
.to_owned();
if !matched.iter().any(|r: &crate::git_provider::Repository| r.ssh_url == repo.ssh_url) {
matched.push(repo);
}
}
matched
} else {
eprintln!("Select repositories to add (Tab to toggle, Enter to confirm):");
app.interactive()
.interactive_multi_search(&repositories)?
};
if selected_repos.is_empty() { if selected_repos.is_empty() {
anyhow::bail!("no repositories selected"); anyhow::bail!("no repositories selected");
@@ -482,6 +547,86 @@ impl ProjectAddCommand {
} }
} }
impl ProjectListCommand {
async fn execute(&self, app: &'static App) -> anyhow::Result<()> {
let projects_dir = get_projects_dir(app);
let projects = list_subdirectories(&projects_dir)?;
if projects.is_empty() {
if self.json {
println!("[]");
} else {
eprintln!(
"no projects found in {}. Use 'gitnow project create' to create one.",
projects_dir.display()
);
}
return Ok(());
}
if self.json {
let mut entries = Vec::new();
for project in &projects {
let mut entry = serde_json::Map::new();
entry.insert(
"name".into(),
serde_json::Value::String(project.name.clone()),
);
entry.insert(
"path".into(),
serde_json::Value::String(project.path.display().to_string()),
);
if let Some(meta) = &project.metadata {
entry.insert(
"created_at".into(),
serde_json::Value::String(meta.created_at.to_rfc3339()),
);
if let Some(template) = &meta.template {
entry.insert(
"template".into(),
serde_json::Value::String(template.clone()),
);
}
if self.repos {
let repos: Vec<serde_json::Value> = meta
.repositories
.iter()
.map(|r| {
serde_json::json!({
"provider": r.provider,
"owner": r.owner,
"repo_name": r.repo_name,
"ssh_url": r.ssh_url,
})
})
.collect();
entry.insert("repositories".into(), serde_json::Value::Array(repos));
}
}
entries.push(serde_json::Value::Object(entry));
}
println!("{}", serde_json::to_string_pretty(&entries)?);
} else {
for project in &projects {
if let Some(meta) = &project.metadata {
println!("{} ({})", project.name, meta.created_ago());
} else {
println!("{}", project.name);
}
if self.repos {
if let Some(meta) = &project.metadata {
for repo in &meta.repositories {
println!(" {}/{}/{}", repo.provider, repo.owner, repo.repo_name);
}
}
}
}
}
Ok(())
}
}
impl ProjectDeleteCommand { impl ProjectDeleteCommand {
async fn execute(&mut self, app: &'static App) -> anyhow::Result<()> { async fn execute(&mut self, app: &'static App) -> anyhow::Result<()> {
let projects_dir = get_projects_dir(app); let projects_dir = get_projects_dir(app);

View File

@@ -0,0 +1,287 @@
/// 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 <REGEX> # 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 project list [OPTIONS] # list all projects and their repos
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 <REGEX>`
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 <REGEX>` | 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 `<project>/<sanitized-branch>/`
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, select repositories to clone into it,
and optionally apply a template.
| Flag | Description |
|---------------------|------------------------------------------------------|
| `[NAME]` | Project name (prompted if omitted) |
| `-r, --repos` | Repositories to include (fuzzy-matched). Repeatable: `--repos foo --repos bar`. Skips interactive picker when provided. |
| `-t, --template` | Template name to bootstrap from |
| `--no-template` | Skip template selection entirely |
| `--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.
**Non-interactive usage:**
```
gitnow project create my-feature --repos repo-a --repos repo-b --no-template --no-shell
```
#### `gitnow project add [NAME] [OPTIONS]`
Add more repositories to an existing project.
| Flag | Description |
|--------------|------------------------------------------|
| `[NAME]` | Project name (interactive if omitted) |
| `-r, --repos`| Repositories to add (fuzzy-matched). Repeatable. Skips interactive picker when provided. |
| `--no-cache` | Skip local cache when listing repos |
**Non-interactive usage:**
```
gitnow project add my-feature --repos repo-c --repos repo-d
```
#### `gitnow project list [OPTIONS]`
List all projects and optionally show their repositories.
| Flag | Description |
|--------------|------------------------------------------|
| `--repos` | Show repository details for each project |
| `--json` | Output as JSON |
#### `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 <partial-name>` — jump to a repo by fuzzy name
- `gitnow worktree <repo> -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).
"#;

View File

@@ -317,6 +317,10 @@ pub mod multi_select {
return Ok(Vec::new()); return Ok(Vec::new());
} }
KeyCode::Enter => { KeyCode::Enter => {
if self.selected_labels.is_empty() {
// Don't allow confirming with no selections
continue;
}
terminal.resize(ratatui::layout::Rect::ZERO)?; terminal.resize(ratatui::layout::Rect::ZERO)?;
let selected: Vec<T> = self let selected: Vec<T> = self
.items .items

View File

@@ -3,8 +3,8 @@ use std::path::PathBuf;
use anyhow::Context; use anyhow::Context;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use commands::{ use commands::{
clone::CloneCommand, project::ProjectCommand, root::RootCommand, shell::Shell, update::Update, clone::CloneCommand, project::ProjectCommand, root::RootCommand, shell::Shell,
worktree::WorktreeCommand, skill::SkillCommand, update::Update, worktree::WorktreeCommand,
}; };
use config::Config; use config::Config;
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
@@ -29,7 +29,13 @@ mod template_command;
mod worktree; mod worktree;
#[derive(Parser)] #[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 { struct Command {
#[command(subcommand)] #[command(subcommand)]
command: Option<Commands>, command: Option<Commands>,
@@ -71,6 +77,8 @@ enum Commands {
Worktree(WorktreeCommand), Worktree(WorktreeCommand),
/// Manage scratch-pad projects with multiple repositories /// Manage scratch-pad projects with multiple repositories
Project(ProjectCommand), Project(ProjectCommand),
/// Print an LLM-readable reference of all gitnow capabilities
Skill(SkillCommand),
} }
const DEFAULT_CONFIG_PATH: &str = ".config/gitnow/gitnow.toml"; const DEFAULT_CONFIG_PATH: &str = ".config/gitnow/gitnow.toml";
@@ -131,6 +139,9 @@ async fn main() -> anyhow::Result<()> {
Commands::Project(mut project) => { Commands::Project(mut project) => {
project.execute(app, &chooser).await?; project.execute(app, &chooser).await?;
} }
Commands::Skill(skill) => {
skill.execute().await?;
}
}, },
None => { None => {
RootCommand::new(app) RootCommand::new(app)