Hiiro
A lightweight, extensible CLI framework for Ruby. Build your own multi-command tools similar to git or docker.
Features
- Subcommand dispatch - Route commands to executables or Ruby blocks
-
Abbreviation matching - Type
h ex helinstead ofh example hello - Plugin system - Extend functionality with reusable modules
- Per-command storage - Each command gets its own pin/config namespace
-
TUI helpers - Build keyboard-driven list screens with
Hiiro::Tui::ListScreen
See docs/ for detailed documentation on all subcommands.
Installation
Via RubyGems
Requires Ruby 3.2 or newer.
gem install hiiro
# Install plugins and subcommands
h setupThis installs:
- Plugins to
~/.config/hiiro/plugins/ - Subcommands (
h-pane,h-todo, etc.) to~/bin/
Ensure ~/bin is in your $PATH.
Dependencies
# For h alert (macOS)
brew install terminal-notifier
# Terminal workspace, tab, and pane management
# Install Herdr 0.8.2 or newer.
# For fuzzy-finder
brew install sk # (or fzf)
# For GitHub PR management
brew install ghQuick Start
# List available subcommands
h
# Simple test
h ping
# => pongSave clipboard content
h save saves whatever is on the macOS clipboard to ~/saved/ and prints the
saved path. Images (via pngpaste) are saved as PNG; otherwise the text from
pbpaste is saved as .txt. Arguments are saved as text instead of the
clipboard: h save hello world saves hello world.
Files are named <timestamp>-<slug>.txt or <timestamp>-image.png, where the
slug is the first few words of the text. Collisions get a -2, -3 suffix.
Saved files can be listed, printed, copied back to the clipboard, opened,
edited, or removed with h save ls|show|copy|open|edit|rm. See
docs/h-save.md.
Task CLI
t and tt are gem executables in exe/, installed alongside h. They are thin
Hiiro.run launchers over Hiiro::TaskCli in lib/hiiro/task_cli.rb, which uses
the add_cmd DSL and existing task records. bin/t and bin/tt are symlinks to
the exe/ files for running from a checkout with ruby -Ilib bin/t. t new NAME
creates a task and its notes directory; t tree new NAME adds a Git worktree.
Use t COMMAND [TASK] [ARGS...]. Bare t, t ls, or t list lists every task,
including done and archived tasks, with the count of open todos after each name.
Commands that act on a task take it from the first positional argument when
that word names a task (exact or unique case-sensitive prefix); otherwise the
current task is used and the word stays in the payload, exactly like the old
h task commands. -t TASK forces a task, -f picks one with a fuzzy finder,
. is the current task, and - selects orphan todos. On a terminal, an ambiguous
prefix or a missing current task opens the fuzzy finder; non-interactive runs fail.
The current task is the calling Herdr workspace, then the current directory
inside a task home, code directory, worktree, or registered directory, then the
task saved with t use TASK. t switch TASK [APP] focuses or creates the task
workspace and also saves it; APP (exact name or unique prefix from the apps
registry) opens the workspace in that app's directory under the task worktree.
t cd TASK [APP] sends the same cd to the calling Herdr pane. t current
only prints.
t new investigation
t next investigation "Inspect the failing request"
t todo add investigation Compare the retry settings
tt add Inspect --help output # current task
t doc new investigation findings
t show investigation
t todo rm 42 # an ID printed by show or todo list
t use investigation
t switch investigation
t switch investigation frontend # open in an app directory (unique prefix)
t switch --show
t next . "Write the handoff"
t start investigation frontend # tree new + app start directoryA task has one next_action and can have multiple independent todos. t show
and t todo print todos with their IDs, statuses, and text in ID order.
t todo show ID prints one todo's text and t todo rm ID deletes it.
tt ... is t todo .... Use t todo - and tt add - TEXT for orphan todos.
Every argument after the task in todo add is literal text, including flags and
--, except a leading -h or --help. Empty text fails. Only t new NAME
creates tasks.
Todos share the existing todos table with h todo. t writes only to the
database and does not rewrite todo.yml. Adding or removing a todo does not
change the task's next action, status, or saved selection. Task completion and
archival preserve todos. h task is a symlink to t, so both share one grammar.
t omp [TASK], t codex or cdx, and t claude or cld start fresh
native CLI sessions in new focused Herdr tabs. Claude always runs claude.
t path [TASK] [APP], t cd [TASK] [APP], and t switch [TASK] [APP]
resolve APP by exact name or unique prefix and use its configured relative
directory beneath the task's code directory. t start NAME [APP] is the
direct form of t tree new NAME [APP].
Tasks can share an existing directory, including a worktree. t does not change
Git state or promise task-isolated persisted AI sessions in shared directories.
See the task command reference for all commands and the workflow introduction for practical examples.
Subcommands
Base Commands
| Command | Description |
|---|---|
h version |
Display the Hiiro version |
h ping |
Simple test command (returns "pong") |
h setup |
Install plugins and subcommands to system paths |
h edit |
Open the h script in your editor |
h alert |
macOS desktop notifications via terminal-notifier |
h herdr |
Herdr task popups plus Vim-style pane focus, movement, resize, swap, and zoom controls (h herdr install) |
h task |
Same program as t: task records, todos, worktrees (t NAME tree new), and Herdr workspaces |
External Subcommands
| Command | Description |
|---|---|
h alias |
Append safely quoted zsh aliases |
h app |
Manage app directories within tasks/projects |
h bin |
Create, list, and edit Hiiro executables |
h branch |
Git branch management with fuzzy selection and copy |
h claude |
Claude CLI wrapper with Herdr split support |
h commit |
Select commits using fuzzy finder |
h config |
Open config files (vim, git, Herdr, zsh, starship, claude) |
h env |
Append safely quoted environment variables |
h link |
Manage saved links with URL, description, and shorthand |
h pane |
Herdr pane management |
h plugin |
Manage hiiro plugins (list, edit, search) |
h pr |
GitHub PR management via gh CLI |
h project |
Project navigation with Herdr workspace management |
h queue |
Claude prompt queue with Herdr-based task execution |
h service |
Manage background dev services with env variations and groups |
h file |
Track and open frequently-used files per app |
h run |
Run dev tools (lint/test/format) against changed files |
h session |
Compatibility commands for Herdr workspaces |
h sha |
Extract short SHA from git log |
h todo |
Todo list management with tags and task association |
h window |
Compatibility commands for Herdr tabs |
h wtree |
Git worktree management |
h claude agents|commands|skills -a prints the absolute file path for each matching .claude tool, including SKILL.md for skills.
Shell helpers
h env add exam ple
h alias add ll 'ls -al'
h alias add --global PAGER '| less'
h bin add scratch
h bin add josh list showh env add NAME VALUE appends a literal export NAME="VALUE" to
~/.zshenv.d/vars.zsh when that file exists, otherwise to ~/.zshenv.
h alias add NAME COMMAND... similarly prefers ~/.zshrc.d/aliases.zsh,
falling back to ~/.zshrc. Existing contents are preserved. A single quoted
command string preserves shell syntax; separate command arguments retain their
argument boundaries. Use --global or -g for a global zsh alias.
These commands use native Hiiro option parsing and help. Use -- before
flag-like data, for example h env add LABEL -- --literal or
h alias add inspect -- tool --help. Start a new shell or source the relevant
configuration file to apply additions.
h bin add NAME [COMMAND ...] creates an executable ~/bin/h-NAME using
Hiiro.run. Without commands its block is empty; each command adds an empty
add_cmd block with a Ruby symbol. Existing files and symlinks are never
overwritten. See the bin command reference.
Abbreviations
Any subcommand can be abbreviated as long as the prefix uniquely matches:
h ses ls # matches h session ls
h win # matches h windowIf multiple commands match, the first match wins and a warning is logged (when logging is enabled).
Plugins
Plugins are Ruby modules loaded from ~/.config/hiiro/plugins/:
| Plugin | Description |
|---|---|
| Pins | Per-command YAML key-value storage |
| Project | Project directory navigation with Herdr workspace management |
| Tasks |
Hiiro::TaskManager worktree creation and current-task environment used by t, h service, h run, and h file
|
| Notify | Herdr desktop notifications |
Adding Subcommands
Method 1: External Executables
Create an executable named h-<subcommand> anywhere in your $PATH:
# ~/bin/h-greet
#!/bin/bash
echo "Hello, $1!"h greet World
# => Hello, World!For nested subcommands, use hiiro in your script:
#!/usr/bin/env ruby
# ~/bin/h-example
require 'hiiro'
Hiiro.run(*ARGV) do
add_subcmd(:hello) { puts "Hi!" }
add_subcmd(:bye) { puts "Goodbye!" }
endh example hello # => Hi!
h example bye # => Goodbye!Building TUIs
Use Hiiro::Tui::ListScreen for simple full-screen list interfaces. Subclass it, override header_lines, format_row, and handle_key, then run it from a normal Hiiro.run subcommand.
Method 2: Inline Subcommands
Modify exe/h directly to add subcommands to the base h command:
Hiiro.run(*ARGV, plugins: [Tasks], cwd: Dir.pwd) do
add_subcmd(:hello) do |*args|
puts "Hello, #{args.first || 'World'}!"
end
endGlobal values (like cwd) are accessible via get_value:
add_subcmd(:pwd) do |*args|
puts get_value(:cwd)
endCommands with declared options
Use add_cmd to expose a command's options in help. Undeclared names in opts:
become boolean flags with a default of false. A flag does not consume the next
positional argument.
Hiiro.run do
add_option :task, short: :t, desc: 'Task name'
add_cmd :test, opts: %i[a b c d task] do
puts 'a was set' if opts.a
puts opts.task if opts.task
puts opts.args
end
endHere, test -a payload -t investigation sets opts.a to true, leaves
payload in opts.args, and sets opts.task to investigation.
test -h and test --help display the selected options without running the
command block.
Automatic flags use their first letter as a short alias only when it is unique
among the command's automatic flags and does not conflict with a selected
explicit option or -h. Every automatic flag has a long form. Explicit flags
and value options keep their definitions.
For a command that delegates to run_child, use add_cmd(..., passthrough: true).
It forwards control without parsing the parent's arguments or intercepting the
child's --help. The child declares and parses its own options.
Generated help uses the original command block's source location and declared args:, not the
internal wrapper's signature.
run_child is the convenience form of make_child(...).run. make_child
returns an unrun child when the caller needs to configure it before dispatch.
Module-level build_hiiro methods are builders that return such a child; callers
run the result. They are not required for an inline child command group.
Child commands inherit the parent's resolvers, so a task can be bound once and
shared with nested groups without consuming its name again.
For a CLI that should dispatch only registered blocks, pass
external_commands: false to Hiiro.run and its run_child or make_child calls. This
prevents unrelated same-prefix executables on PATH from taking precedence.
Pass builtin_commands: false as well to skip the automatic pry and edit
commands, for CLIs whose first word is data rather than a command name.
Raise Hiiro::Error (or a subclass) for expected user-facing failures such as
bad arguments or missing records. Hiiro#run prints ERROR: message to stderr
and exits 1 without a backtrace; any other exception still prints its backtrace.
open_default(target) opens a path or URL with the OS handler (open or xdg-open).
Writing Plugins
Plugins are Ruby modules that extend Hiiro instances:
# ~/.config/hiiro/plugins/myplugin.rb
module MyPlugin
def self.load(hiiro)
attach_methods(hiiro)
add_subcommands(hiiro)
end
def self.add_subcommands(hiiro)
hiiro.add_subcmd(:mycmd) do |*args|
# command logic
end
end
def self.attach_methods(hiiro)
hiiro.instance_eval do
def my_helper
# helper method available to other plugins
end
end
end
endLoad plugins in your command:
Hiiro.run(*ARGV, plugins: [MyPlugin]) do
# ...
endConfiguration
All configuration lives in ~/.config/hiiro/:
~/.config/hiiro/
plugins/ # Plugin files (auto-loaded)
pins/ # Pin storage (per command)
services/ # Service runtime state
bg-history.txt # Background command history
env_templates/ # Base .env templates for services
tasks/ # Task metadata
projects.yml # Project aliases
apps.yml # App directory mappings
services.yml # Service and service group definitions
tools.yml # Runner tool definitions
app_files.yml # Per-app tracked file lists
todo.yml # Todo items
Runtime data lives separately:
~/.local/share/hiiro/
queue/ # Prompt queue (wip, pending, running, done, failed)
notify_log.yml # Herdr notification log
Testing
Run the test suite:
bundle exec rake testLicense
MIT
COMPLEX SERVICES
❯ in this setup...can you configure a couple complex services that use variations, etc. i need examples of how to configure it. and then can you show me how to run it for different variations
⏺ Here's an example ~/.config/hiiro/services.yml:
# Individual services
api-rails:
base_dir: ~/work/api
host: localhost
port: 3000
init:
- bundle install
start:
- bin/rails s -p 3000
cleanup: []
env_file: .env.development
base_env: api-rails.env
env_vars:
DATABASE_URL:
variations:
local: postgres://localhost:5432/api_dev
docker: postgres://db:5432/api_dev
REDIS_URL:
variations:
local: redis://localhost:6379
docker: redis://redis:6379
graphql:
base_dir: ~/work/graphql
host: localhost
port: 4000
init:
- npm install
start:
- npm run dev
env_file: .env.local
base_env: graphql.env
env_vars:
API_URL:
variations:
local: http://localhost:3000
staging: https://api.staging.example.com
AUTH_PROVIDER:
variations:
local: http://localhost:8080/auth
staging: https://auth.staging.example.com
frontend:
base_dir: ~/work/frontend
host: localhost
port: 8080
start:
- npm run dev
env_file: .env.local
base_env: frontend.env
env_vars:
GRAPHQL_URL:
variations:
local: http://localhost:4000/graphql
staging: https://graphql.staging.example.com/graphql
# Service groups
full-stack:
services:
- name: api-rails
use:
DATABASE_URL: local
REDIS_URL: local
- name: graphql
use:
API_URL: local
AUTH_PROVIDER: local
- name: frontend
use:
GRAPHQL_URL: local
staging-fe:
services:
- name: graphql
use:
API_URL: staging
AUTH_PROVIDER: staging
- name: frontend
use:
GRAPHQL_URL: local
Usage:
# Start a single service (defaults to "local" variations)
h service start api-rails
# Start with specific variations
h service start api-rails --use DATABASE_URL=docker --use REDIS_URL=docker
# Start the full local stack (one Herdr tab per service)
h service start full-stack
# Start frontend pointing at staging backends
h service start staging-fe
# No args — fuzzy select from all services + groups
h service start
The key ideas:
- local is the default variation — if you don't pass --use, each env var gets its local value
- Groups let you pre-bake variation combos — full-stack vs staging-fe are just different use: presets for the same services
- Base env templates live in ~/.config/hiiro/env_templates/ (e.g., api-rails.env) — they get copied to base_dir/env_file first, then variations are injected on top
Development
Testing locally:
ruby -I lib bin/h-ps search ruby