Add help to commands list and --help/-h flag support to subcommands (#967)
Some checks failed
Integration Tests / test (3.2.9, 1.5) (push) Failing after 3s
Integration Tests / test (3.2.9, 1.6) (push) Failing after 2s
Integration Tests / test (3.2.9, 1.7) (push) Failing after 2s
Integration Tests / test (3.2.9, 1.8) (push) Failing after 2s
Integration Tests / test (3.2.9, 1.9) (push) Failing after 3s
Integration Tests / test (3.2.9, 2.0) (push) Failing after 2s
Integration Tests / test (3.2.9, 2.1) (push) Failing after 2s
Integration Tests / test (3.2.9, 2.2) (push) Failing after 2s
Integration Tests / test (3.2.9, 2.3) (push) Failing after 2s
Integration Tests / test (3.2.9, 2.4) (push) Failing after 2s
Integration Tests / test (3.2.9, 2.5) (push) Failing after 2s
Integration Tests / test (3.2.9, 2.6) (push) Failing after 2s
Integration Tests / test (3.2.9, 2.7) (push) Failing after 3s
Integration Tests / test (3.2.9, 2.8) (push) Failing after 2s
Integration Tests / test (3.2.9, 2.9) (push) Failing after 2s
Integration Tests / test (3.2.9, 2.9a) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.0) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.0a) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.1) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.1a) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.1b) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.1c) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.2) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.2a) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.3) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.3a) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.4) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.5) (push) Failing after 2s
Integration Tests / test (3.2.9, 3.5a) (push) Failing after 3s
Integration Tests / test (3.3.9, 1.5) (push) Failing after 2s
Integration Tests / test (3.3.9, 1.6) (push) Failing after 3s
Integration Tests / test (3.3.9, 1.7) (push) Failing after 3s
Integration Tests / test (3.3.9, 1.8) (push) Failing after 2s
Integration Tests / test (3.3.9, 1.9) (push) Failing after 2s
Integration Tests / test (3.3.9, 2.0) (push) Failing after 2s
Integration Tests / test (3.3.9, 2.1) (push) Failing after 2s
Integration Tests / test (3.3.9, 2.2) (push) Failing after 2s
Integration Tests / test (3.3.9, 2.3) (push) Failing after 2s
Integration Tests / test (3.3.9, 2.4) (push) Failing after 2s
Integration Tests / test (3.3.9, 2.5) (push) Failing after 2s
Integration Tests / test (3.3.9, 2.6) (push) Failing after 2s
Integration Tests / test (3.3.9, 2.7) (push) Failing after 2s
Integration Tests / test (3.3.9, 2.8) (push) Failing after 3s
Integration Tests / test (3.3.9, 2.9) (push) Failing after 3s
Integration Tests / test (3.3.9, 2.9a) (push) Failing after 2s
Integration Tests / test (3.3.9, 3.0) (push) Failing after 3s
Integration Tests / test (3.3.9, 3.0a) (push) Failing after 2s
Integration Tests / test (3.3.9, 3.1) (push) Failing after 2s
Integration Tests / test (3.3.9, 3.1a) (push) Failing after 3s
Integration Tests / test (3.3.9, 3.1b) (push) Failing after 3s
Integration Tests / test (3.3.9, 3.1c) (push) Failing after 3s
Integration Tests / test (3.3.9, 3.2) (push) Failing after 3s
Integration Tests / test (3.3.9, 3.2a) (push) Failing after 3s
Integration Tests / test (3.3.9, 3.3) (push) Failing after 3s
Integration Tests / test (3.3.9, 3.3a) (push) Failing after 2s
Integration Tests / test (3.3.9, 3.4) (push) Failing after 2s
Integration Tests / test (3.3.9, 3.5) (push) Failing after 2s
Integration Tests / test (3.3.9, 3.5a) (push) Failing after 2s
Integration Tests / test (3.4.5, 1.5) (push) Failing after 3s
Integration Tests / test (3.4.5, 1.6) (push) Failing after 2s
Integration Tests / test (3.4.5, 1.7) (push) Failing after 2s
Integration Tests / test (3.4.5, 1.8) (push) Failing after 2s
Integration Tests / test (3.4.5, 1.9) (push) Failing after 2s
Integration Tests / test (3.4.5, 2.0) (push) Failing after 2s
Integration Tests / test (3.4.5, 2.1) (push) Failing after 2s
Integration Tests / test (3.4.5, 2.2) (push) Failing after 2s
Integration Tests / test (3.4.5, 2.3) (push) Failing after 2s
Integration Tests / test (3.4.5, 2.4) (push) Failing after 3s
Integration Tests / test (3.4.5, 2.5) (push) Failing after 3s
Integration Tests / test (3.4.5, 2.6) (push) Failing after 3s
Integration Tests / test (3.4.5, 2.7) (push) Failing after 3s
Integration Tests / test (3.4.5, 2.8) (push) Failing after 3s
Integration Tests / test (3.4.5, 2.9) (push) Failing after 2s
Integration Tests / test (3.4.5, 2.9a) (push) Failing after 3s
Integration Tests / test (3.4.5, 3.0) (push) Failing after 3s
Integration Tests / test (3.4.5, 3.0a) (push) Failing after 3s
Integration Tests / test (3.4.5, 3.1) (push) Failing after 3s
Integration Tests / test (3.4.5, 3.1a) (push) Failing after 3s
Integration Tests / test (3.4.5, 3.1b) (push) Failing after 3s
Integration Tests / test (3.4.5, 3.1c) (push) Failing after 3s
Integration Tests / test (3.4.5, 3.2) (push) Failing after 2s
Integration Tests / test (3.4.5, 3.2a) (push) Failing after 2s
Integration Tests / test (3.4.5, 3.3) (push) Failing after 2s
Integration Tests / test (3.4.5, 3.3a) (push) Failing after 2s
Integration Tests / test (3.4.5, 3.4) (push) Failing after 2s
Integration Tests / test (3.4.5, 3.5) (push) Failing after 2s
Integration Tests / test (3.4.5, 3.5a) (push) Failing after 2s
Integration Tests / finish (push) Failing after 2s

Problem:
- The 'help' command was not listed in the output of 'mux commands',
  making it less discoverable for users
- Using --help or -h flags with subcommands (e.g., 'mux start --help')
  would fail with 'Project --help doesn't exist' instead of showing
  help information, which is inconsistent with common CLI patterns

Solution:
- Added 'help' entry to the COMMANDS hash so it appears in the
  commands list output
- Added --help/-h method_option to key subcommands (start, stop, new,
  debug, copy, delete, list) that invokes Thor's help system before
  command execution
- Updated tests to verify 'help' appears in commands output and that
  --help/-h flags work correctly for subcommands

This provides parity between 'mux help <subcommand>' and
'mux <subcommand> --help', improving user experience and discoverability.

Fixes: https://github.com/tmuxinator/tmuxinator/issues/191#issuecomment-3539068931
This commit is contained in:
Andrew Kofink
2025-11-16 16:57:16 -05:00
committed by GitHub
parent 1cbffad2b2
commit 71275f96cb
3 changed files with 164 additions and 10 deletions

View File

@@ -1,4 +1,7 @@
## Unreleased
### Features
- Add `help` to the list of commands output by `tmuxinator commands`
- Add `--help`/`-h` flag support to subcommands (start, stop, new, debug, copy, delete, list) for easier access to command-specific help
## Misc
- Update CI ruby versions to latest, remove Ruby 3.1

View File

@@ -23,6 +23,7 @@ module Tmuxinator
delete: "Deletes given project",
doctor: "Look for problems in your configuration",
edit: "Alias of new",
help: "Shows help for a specific command",
implode: "Deletes all tmuxinator projects",
local: "Start a tmux session using ./.tmuxinator.y[a]ml",
list: "Lists all tmuxinator projects",
@@ -83,8 +84,16 @@ module Tmuxinator
method_option :local, type: :boolean,
aliases: ["-l"],
desc: "Create local project file at ./.tmuxinator.yml"
method_option :help, type: :boolean,
aliases: ["-h"],
desc: "Display usage information"
def new(name = nil, session = nil)
if options[:help] || name.nil?
invoke :help, ["new"]
return
end
def new(name, session = nil)
if session
new_project_with_session(name, session)
else
@@ -259,7 +268,15 @@ module Tmuxinator
"the current session"
method_option "no-pre-window", type: :boolean, default: false,
desc: "Skip pre_window commands"
method_option :help, type: :boolean,
aliases: "-h",
desc: "Display usage information"
def start(name = nil, *args)
if options[:help]
invoke :help, ["start"]
return
end
params = start_params(name, *args)
show_version_warning if version_warning?(
@@ -276,8 +293,16 @@ module Tmuxinator
desc: "Path to project config file"
method_option "suppress-tmux-version-warning",
desc: "Don't show a warning for unsupported tmux versions"
method_option :help, type: :boolean,
aliases: "-h",
desc: "Display usage information"
def stop(name = nil)
if options[:help]
invoke :help, ["stop"]
return
end
# project-config takes precedence over a named project in the case that
# both are provided.
if options["project-config"]
@@ -342,7 +367,15 @@ module Tmuxinator
"the current session"
method_option "no-pre-window", type: :boolean, default: false,
desc: "Skip pre_window commands"
method_option :help, type: :boolean,
aliases: "-h",
desc: "Display usage information"
def debug(name = nil, *args)
if options[:help]
invoke :help, ["debug"]
return
end
params = start_params(name, *args)
project = create_project(params)
@@ -353,8 +386,16 @@ module Tmuxinator
desc "copy [EXISTING] [NEW]", COMMANDS[:copy]
map "c" => :copy
map "cp" => :copy
method_option :help, type: :boolean,
aliases: "-h",
desc: "Display usage information"
def copy(existing = nil, new = nil)
if options[:help] || existing.nil? || new.nil?
invoke :help, ["copy"]
return
end
def copy(existing, new)
existing_config_path = Tmuxinator::Config.project(existing)
new_config_path = Tmuxinator::Config.project(new)
@@ -374,18 +415,32 @@ module Tmuxinator
desc "delete [PROJECT1] [PROJECT2] ...", COMMANDS[:delete]
map "d" => :delete
map "rm" => :delete
method_option :help, type: :boolean,
aliases: "-h",
desc: "Display usage information"
def delete(*projects)
projects.each do |project|
if Tmuxinator::Config.exist?(name: project)
config = Tmuxinator::Config.project(project)
if options[:help] || projects.empty?
invoke :help, ["delete"]
return
end
if yes?("Are you sure you want to delete #{project}?(y/n)", :red)
FileUtils.rm(config)
say "Deleted #{project}"
delete_projects(*projects)
end
no_commands do
def delete_projects(*projects)
projects.each do |project|
if Tmuxinator::Config.exist?(name: project)
config = Tmuxinator::Config.project(project)
if yes?("Are you sure you want to delete #{project}?(y/n)", :red)
FileUtils.rm(config)
say "Deleted #{project}"
end
else
say "#{project} does not exist!"
end
else
say "#{project} does not exist!"
end
end
end
@@ -411,8 +466,16 @@ module Tmuxinator
method_option :active, type: :boolean,
aliases: ["-a"],
desc: "Filter output by active project sessions."
method_option :help, type: :boolean,
aliases: ["-h"],
desc: "Display usage information"
def list
if options[:help]
invoke :help, ["list"]
return
end
say "tmuxinator projects:"
configs = Tmuxinator::Config.configs(active: options[:active])
if options[:newline]

View File

@@ -206,6 +206,7 @@ describe Tmuxinator::Cli do
delete
doctor
edit
help
implode
local
list
@@ -297,6 +298,24 @@ describe Tmuxinator::Cli do
end
end
context "with --help flag" do
it "shows help instead of starting project" do
ARGV.replace(["start", "--help"])
expect(Kernel).not_to receive(:exec)
out, _err = capture_io { cli.start }
expect(out).to include("start [PROJECT] [ARGS]")
expect(out).to include("Options:")
end
it "shows help with -h flag" do
ARGV.replace(["start", "-h"])
expect(Kernel).not_to receive(:exec)
out, _err = capture_io { cli.start }
expect(out).to include("start [PROJECT] [ARGS]")
expect(out).to include("Options:")
end
end
context "deprecations" do
before do
allow($stdin).to receive_messages(getc: "y")
@@ -340,6 +359,16 @@ describe Tmuxinator::Cli do
end
end
context "with --help flag" do
it "shows help instead of stopping project" do
ARGV.replace(["stop", "--help"])
expect(Kernel).not_to receive(:exec)
out, _err = capture_io { cli.start }
expect(out).to include("stop [PROJECT]")
expect(out).to include("Options:")
end
end
include_examples :unsupported_version_message, :stop, :foo
end
@@ -526,6 +555,15 @@ describe Tmuxinator::Cli do
allow(File).to receive(:open) { |&block| block.yield file }
end
context "with --help flag" do
it "shows help instead of creating project" do
ARGV.replace(["new", "--help"])
out, _err = capture_io { cli.start }
expect(out).to include("new [PROJECT]")
expect(out).to include("Options:")
end
end
context "without the --local option" do
before do
ARGV.replace(["new", name])
@@ -651,6 +689,31 @@ describe Tmuxinator::Cli do
allow(Tmuxinator::Config).to receive(:exist?) { true }
end
context "with --help flag" do
it "shows help instead of copying project" do
ARGV.replace(["copy", "--help"])
expect(FileUtils).not_to receive(:copy_file)
out, _err = capture_io { cli.start }
expect(out).to include("copy [EXISTING] [NEW]")
expect(out).to include("Options:")
end
it "shows help with -h flag" do
ARGV.replace(["copy", "-h"])
expect(FileUtils).not_to receive(:copy_file)
out, _err = capture_io { cli.start }
expect(out).to include("copy [EXISTING] [NEW]")
expect(out).to include("Options:")
end
it "shows help when only one argument provided" do
ARGV.replace(["copy", "foo"])
expect(FileUtils).not_to receive(:copy_file)
out, _err = capture_io { cli.start }
expect(out).to include("copy [EXISTING] [NEW]")
end
end
context "new project already exists" do
before do
allow(Thor::LineEditor).to receive_messages(readline: "y")
@@ -732,6 +795,31 @@ describe Tmuxinator::Cli do
end
describe "#delete" do
context "with --help flag" do
it "shows help instead of deleting project" do
ARGV.replace(["delete", "--help"])
expect(FileUtils).not_to receive(:rm)
out, _err = capture_io { cli.start }
expect(out).to include("delete [PROJECT1] [PROJECT2]")
expect(out).to include("Options:")
end
it "shows help with -h flag" do
ARGV.replace(["delete", "-h"])
expect(FileUtils).not_to receive(:rm)
out, _err = capture_io { cli.start }
expect(out).to include("delete [PROJECT1] [PROJECT2]")
expect(out).to include("Options:")
end
it "shows help when no arguments provided" do
ARGV.replace(["delete"])
expect(FileUtils).not_to receive(:rm)
out, _err = capture_io { cli.start }
expect(out).to include("delete [PROJECT1] [PROJECT2]")
end
end
context "with a single argument" do
before do
ARGV.replace(["delete", "foo"])