Project

hiiro

0.0
The project is in a healthy, maintained state
Build multi-command CLI tools with subcommand dispatch, abbreviation matching, and a plugin system. Similar to git or docker command structure.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 5.0
~> 13.0

Runtime

 Project Readme

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 hel instead of h 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 setup

This 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 gh

Quick Start

# List available subcommands
h

# Simple test
h ping
# => pong

Save 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 directory

A 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 show

h 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 window

If 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!" }
end
h 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
end

Global values (like cwd) are accessible via get_value:

add_subcmd(:pwd) do |*args|
  puts get_value(:cwd)
end

Commands 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
end

Here, 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
end

Load plugins in your command:

Hiiro.run(*ARGV, plugins: [MyPlugin]) do
  # ...
end

Configuration

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 test

License

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