Class: Hammer
- Inherits:
-
Object
- Object
- Hammer
- Includes:
- Shell
- Defined in:
- lib/lux-hammer.rb,
lib/hammer/shell.rb,
lib/hammer/dotenv.rb,
lib/hammer/loader.rb,
lib/hammer/option.rb,
lib/hammer/parser.rb,
lib/hammer/recipe.rb,
lib/hammer/builder.rb,
lib/hammer/command.rb,
lib/hammer/builtins.rb,
lib/hammer/command_builder.rb
Overview
Thor-inspired tiny CLI builder.
Class DSL:
class MyCli < Hammer
task :build do
desc 'Build the project'
example 'build -v --env=prod'
opt :verbose, type: :boolean, alias: :v
opt :env, type: :string, default: 'dev'
proc do |opts|
say.green "building #{opts[:env]} args=#{opts[:args].inspect}"
end
end
end
MyCli.start(ARGV)
Block DSL is identical, just inside ‘Hammer.run`:
Hammer.run(ARGV) do
task :hello do
desc 'Greet someone'
opt :loud, type: :boolean, alias: :l
proc do |opts|
msg = "hello #{opts[:args].first || 'world'}"
msg = msg.upcase if opts[:loud]
say msg, :cyan
end
end
end
Defined Under Namespace
Modules: Builtins, DSL, Dotenv, Recipe, Shell Classes: Builder, Command, CommandBuilder, Loader, Option, Parser
Class Method Summary collapse
- .alt(*names) ⇒ Object
-
.ancestor_chain ⇒ Object
Root -> …
-
.app_desc(text = nil) ⇒ Object
Top-level description for the whole CLI.
-
.app_local_location?(loc) ⇒ Boolean
True when a captured location lives inside the main app.
-
.before(&block) ⇒ Object
Register a hook to run before every command in this class (root or namespace).
- .before_hooks ⇒ Object
-
.cli(argv = ARGV) ⇒ Object
Entry point for the ‘hammer` binary.
- .commands ⇒ Object
-
.default_program_name ⇒ Object
Program name shown in help/usage: the invocation path relative to cwd if the script lives inside it (e.g. ‘bin/foo` when invoked from the project root), otherwise the basename (e.g. `lux` for a globally installed bin in PATH).
-
.desc(text) ⇒ Object
—– class-level DSL for ‘def`-style commands ——————— Set pending metadata that the next `def` will consume.
-
.dispatches_to_builtin?(argv) ⇒ Boolean
True if argv goes through a built-in dispatch path (‘:default` or `:help`) - meaning bare `hammer`, leading-flag invocations like `hammer -h`, or explicit help requests.
-
.dotenv(flag = true) ⇒ Object
Toggle auto-loading of ‘.env` / `.env.local` for the `hammer` binary.
- .dotenv_enabled? ⇒ Boolean
-
.each_command(prefix = nil, &block) ⇒ Object
Yield [full_colon_path, Command] for every command in this class and all nested namespaces.
- .emit_rows(rows, width) ⇒ Object
- .example(text) ⇒ Object
-
.find_command(name) ⇒ Object
Find a command by canonical name or alt within this class.
-
.find_hammerfile(start) ⇒ Object
Walk up the directory tree looking for a Hammerfile.
-
.find_namespace(name) ⇒ Object
Find a namespace by name within this class.
-
.find_namespace_sibling(canonical) ⇒ Object
Returns the command at the parent that shares its name with this namespace.
-
.fuzzy_pick(name, items, kind, &keys_for) ⇒ Object
Shared fuzzy matcher used by find_command and find_namespace.
-
.hammer(name, *args, **opts) ⇒ Object
Programmatic dispatch by name.
- .inherited(sub) ⇒ Object
-
.load(*paths, **kwargs) ⇒ Object
Load Hammerfile fragments and register their commands on this class.
-
.loader ⇒ Object
Per-target Loader instance.
- .looks_like_builtin?(argv) ⇒ Boolean
- .method_added(method_name) ⇒ Object
-
.namespace(name, &block) ⇒ Object
Open a namespace (group of commands).
- .namespaces ⇒ Object
- .needs(*names) ⇒ Object
- .opt(name, **o) ⇒ Object
- .parent ⇒ Object
-
.print_ai_help ⇒ Object
Dump the gem’s AGENTS.md to stdout - AI-optimized guide for writing Hammerfiles.
- .print_command_help(cmd, full = nil) ⇒ Object
- .print_command_list(klass, prefix = nil) ⇒ Object
-
.print_extras ⇒ Object
Extras shown only in the extended (‘help` / `-h` / `–help`) view: global flags, GitHub footer, and a Hammerfile example for the `hammer` binary.
- .print_footer ⇒ Object
-
.print_full_block(path, cmd) ⇒ Object
One “task block” for the expanded listing: blank line separator then the standard per-command help (usage + desc + options + examples).
-
.print_global_flags ⇒ Object
Listed under ‘Default task options:` in `–help` so users see what flags fire on bare-flag invocation (`hammer –version` etc).
-
.print_hammerfile_example ⇒ Object
Hammerfile cheat-sheet shown under ‘hammer –help`.
-
.print_help(target = nil, expanded: false, extended: false) ⇒ Object
‘extended: true` is the verbose `help` / `-h` / `–help` form - appends global flags, the GitHub footer, and (for the hammer binary) a Hammerfile example.
-
.print_namespace_help(prefix, ns, expanded: false, extended: false) ⇒ Object
‘extended:` is accepted for parity with `print_help` but intentionally not used here - the global-flags / Hammerfile-example / footer block is root-help-only.
-
.print_recipes_section ⇒ Object
Lists recipes (gem + user-dir) under their own section in ‘hammer –help`.
-
.print_run_banner(cmd, full, positional, opts) ⇒ Object
Print a gray “> prog cmd –opt=val ARG” banner before a command runs.
-
.print_top_banner ⇒ Object
Gray “lux-hammer X.Y.Z - <homepage>” line shown above top-level help in both bare-invocation and ‘–help` modes, so the link is always one glance away.
-
.program_name ⇒ Object
Resolved lazily on first read and memoized, so callers that need the cwd-relative form (see ‘default_program_name`) can warm the cache before chdir-ing elsewhere.
-
.recipe(name, argv = ARGV) ⇒ Object
Entry point for recipe stubs in PATH.
-
.relativize_path(path) ⇒ Object
Trim the cwd prefix off an absolute path so redefinition warnings read as ‘./lib/tasks/foo.rb` instead of a long absolute path.
-
.resolve(path) ⇒ Object
Walk “ns1:ns2:cmd” -> [command, owning_class, canonical_path].
-
.resolve_namespace(path) ⇒ Object
Walk “ns1:ns2” -> [namespace_class, canonical_path].
-
.root ⇒ Object
Topmost class in this CLI tree.
-
.run(argv = ARGV, &block) ⇒ Object
Define and run a CLI inline.
-
.run_before_hooks(instance, opts) ⇒ Object
Fire ‘before` hooks from root down through the namespace chain.
- .run_command(cmd, argv, full: nil, quiet: false) ⇒ Object
-
.run_needs(cmd) ⇒ Object
Dispatch a command’s declared ‘needs` through the root class, with per-invocation dedupe.
- .run_or_exit(*cmd, **opts) ⇒ Object
-
.run_shebang(path, argv) ⇒ Object
Evaluate a shebang script as a self-contained CLI.
-
.section_for(full, prefix, klass = nil) ⇒ Object
‘db’ for ‘db:migrate’ or ‘db:users:list’ viewed from root; ‘users’ for ‘db:users:list’ viewed from ‘db’; :root if the command sits at the view’s top level.
-
.self_update ⇒ Object
‘hammer update`: pull main in the install-script checkout and reinstall the gem.
-
.shebang_script(arg) ⇒ Object
Returns the script path if ‘arg` looks like a shebang script that delegates to hammer (first line starts with `#!` and mentions `hammer`).
-
.source_location_of(block) ⇒ Object
“file:line” of the block that defined a task/namespace.
-
.start(argv = ARGV) ⇒ Object
Entry point.
-
.task(name, &block) ⇒ Object
Define a command.
-
.usage_signature(cmd) ⇒ Object
“ URL [ENV] [OPTIONS]” - shows the positional-fill names for declared non-boolean opts (required bare, optional bracketed), plus a generic [OPTIONS] tail if any flags exist.
-
.warn_redefinition(kind, name, prev_loc, new_loc) ⇒ Object
Emit a yellow [hammer] warning on stderr when a task/namespace is redefined.
-
.with_target(klass) ⇒ Object
Push ‘klass` as the current Hammer target for the duration of the block.
Instance Method Summary collapse
-
#hammer(name, *args, **opts) ⇒ Object
Inside a command’s ‘proc do |opts| …
Methods included from Shell
ask, choose, choose_numbered, color!, color?, error, paint, print_error, say, sh, yes?
Class Method Details
.alt(*names) ⇒ Object
81 |
# File 'lib/lux-hammer.rb', line 81 def alt(*names) ; @pending_alts.concat(names) end |
.ancestor_chain ⇒ Object
Root -> … -> self. Used to gather ‘before` hooks for a command.
287 288 289 290 291 292 293 294 295 |
# File 'lib/lux-hammer.rb', line 287 def ancestor_chain chain = [] klass = self while klass chain.unshift klass klass = klass.parent end chain end |
.app_desc(text = nil) ⇒ Object
Top-level description for the whole CLI. Set from a Hammerfile (block DSL) via ‘desc ’text’‘ at top level - see `Hammer::Builder#desc`. Rendered under the Usage line in `–help` output.
119 120 121 122 |
# File 'lib/lux-hammer.rb', line 119 def app_desc(text = nil) return @app_desc if text.nil? @app_desc = text.to_s.rstrip end |
.app_local_location?(loc) ⇒ Boolean
True when a captured location lives inside the main app. relativize_path rewrites in-app absolute paths to a ‘.`-relative form, so anything still starting with “/” is an absolute path outside cwd - a framework, plugin, or gem file. Relative locations are already cwd-anchored, hence local.
275 276 277 |
# File 'lib/lux-hammer.rb', line 275 def app_local_location?(loc) !loc.to_s.start_with?('/') end |
.before(&block) ⇒ Object
Register a hook to run before every command in this class (root or namespace). Hooks receive the command’s ‘opts` hash. All hooks run outer -> inner, once per top-level `start` (prereqs don’t re-trigger).
before { |opts| Dotenv.load }
namespace :db do
before { hammer :env }
task :migrate do ... end
end
231 232 233 |
# File 'lib/lux-hammer.rb', line 231 def before(&block) before_hooks << block end |
.before_hooks ⇒ Object
235 236 237 |
# File 'lib/lux-hammer.rb', line 235 def before_hooks @before_hooks end |
.cli(argv = ARGV) ⇒ Object
Entry point for the ‘hammer` binary. Walks up from CWD until it finds a Hammerfile, evaluates it as the block DSL, then dispatches ARGV against the resulting CLI.
‘–system` forces the no-Hammerfile branch even from inside a project - the escape hatch for reaching `recipes`/`init` when a user-defined task tree would otherwise own the root.
1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 |
# File 'lib/lux-hammer.rb', line 1013 def self.cli(argv = ARGV) argv = argv.dup force_system = !!argv.delete('--system') # Shebang invocation: `hammer /path/to/script ...args` (kernel passes # the script path as argv[0] for `#!/usr/bin/env hammer` files). # Treat the script as a self-contained CLI: no Hammerfile lookup, no # chdir (commands run in the caller's cwd), no `hammer`-binary # built-ins/banners. Detection requires a `#!`+`hammer` first line so # task names that happen to be paths don't get hijacked. if (script = shebang_script(argv.first)) argv.shift return run_shebang(script, argv) end path = force_system ? nil : find_hammerfile(Dir.pwd) unless path # No Hammerfile (or --system) - all built-ins are reachable. Bare # `hammer`, `hammer recipes`, `hammer update`, `hammer agents`, # `hammer version`, `hammer init` all work. if force_system || dispatches_to_builtin?(argv) || looks_like_builtin?(argv) klass = Class.new(Hammer) klass.instance_variable_set(:@hammer_binary, true) klass.program_name require_relative 'hammer/builtins' Hammer::Builtins.register_core(klass) Hammer::Builtins.register_no_project(klass) klass.start(argv) return end Shell.print_error "no Hammerfile found in #{Dir.pwd} or any parent directory" # Heuristic: *.rb files referencing `Hammer.` are likely inline CLIs # the user could promote into a Hammerfile. excludes = %w[.git node_modules tmp vendor coverage dist build] .map { |d| "--exclude-dir=#{d}" }.join(' ') candidates = `grep -rl --include='*.rb' #{excludes} 'Hammer\\.' . 2>/dev/null` .lines.map(&:strip).reject(&:empty?) unless candidates.empty? Shell.say "possible CLI implementation(s) - files referencing `Hammer.`:", :yellow candidates.first(10).each { |f| Shell.say " #{f.sub(%r{\A\./}, '')}" } Shell.say '' end Shell.say "create one - example:" puts Shell.say STARTER_HAMMERFILE Shell.say '' bin = File.basename($PROGRAM_NAME) Shell.say "tip: run `#{bin} init` to drop the example above into ./Hammerfile", :gray Shell.say "tip: run `#{bin} agents` for AI-friendly Hammerfile authoring docs", :gray exit 1 end klass = Class.new(Hammer) # Mark this class as the `hammer` binary's root so help output can # surface binary-only sections (`Recipes:` listing). klass.instance_variable_set(:@hammer_binary, true) # Resolve before chdir so paths like `bin/foo` stay relative to the # cwd the user actually invoked from. `program_name` memoizes. klass.program_name # chdir into the Hammerfile's directory for the entire run so commands # operate on the project root (Rake-style). Dir.chdir(File.dirname(path)) Builder.new(klass).evaluate(File.read(path), path) # Auto-load `.env` / `.env.local` after eval so a top-level # `dotenv false` in the Hammerfile can suppress it. Trade-off: vars # are NOT visible during Hammerfile evaluation, only inside handlers. Hammer::Dotenv.load(Dir.pwd) if klass.dotenv_enabled? # Core built-ins register AFTER Hammerfile eval so user-defined # tasks win (the `unless commands.key?(...)` guards in register_core # skip the built-in when overridden - no redefinition warning). # `register_no_project` (:recipes, :init) is intentionally NOT # called here - those would clash too easily with user tasks. Use # `hammer --system recipes` to reach them from inside a project. require_relative 'hammer/builtins' Hammer::Builtins.register_core(klass) klass.start(argv) end |
.commands ⇒ Object
304 305 306 |
# File 'lib/lux-hammer.rb', line 304 def commands @commands ||= {} end |
.default_program_name ⇒ Object
Program name shown in help/usage: the invocation path relative to cwd if the script lives inside it (e.g. ‘bin/foo` when invoked from the project root), otherwise the basename (e.g. `lux` for a globally installed bin in PATH).
128 129 130 131 132 133 134 135 136 137 |
# File 'lib/lux-hammer.rb', line 128 def default_program_name prog = $PROGRAM_NAME return File.basename(prog) unless prog.include?('/') # Resolve symlinks on both sides so e.g. macOS `/tmp` -> `/private/tmp` # doesn't cause a false miss when comparing prefixes. abs = File.realpath(prog) rescue File.(prog) cwd = File.realpath(Dir.pwd) rescue Dir.pwd return abs[(cwd.length + 1)..] if abs.start_with?("#{cwd}/") File.basename(prog) end |
.desc(text) ⇒ Object
78 |
# File 'lib/lux-hammer.rb', line 78 def desc(text) ; @pending_desc = text.to_s.rstrip end |
.dispatches_to_builtin?(argv) ⇒ Boolean
True if argv goes through a built-in dispatch path (‘:default` or `:help`) - meaning bare `hammer`, leading-flag invocations like `hammer -h`, or explicit help requests. These don’t need a project Hammerfile to run.
1128 1129 1130 1131 1132 |
# File 'lib/lux-hammer.rb', line 1128 def self.dispatches_to_builtin?(argv) return true if argv.empty? first = argv.first first == 'help' || first == '-h' || first == '--help' || first.start_with?('-') end |
.dotenv(flag = true) ⇒ Object
Toggle auto-loading of ‘.env` / `.env.local` for the `hammer` binary. Default is ON. Call `dotenv false` at the top of a Hammerfile to suppress. No-op for standalone `MyCli.start` - auto-load only fires from `Hammer.cli`.
243 244 245 |
# File 'lib/lux-hammer.rb', line 243 def dotenv(flag = true) @dotenv_enabled = flag end |
.dotenv_enabled? ⇒ Boolean
247 248 249 |
# File 'lib/lux-hammer.rb', line 247 def dotenv_enabled? @dotenv_enabled != false end |
.each_command(prefix = nil, &block) ⇒ Object
Yield [full_colon_path, Command] for every command in this class and all nested namespaces.
558 559 560 561 562 563 564 565 566 567 |
# File 'lib/lux-hammer.rb', line 558 def each_command(prefix = nil, &block) commands.each_value do |c| full = prefix ? "#{prefix}:#{c.name}" : c.name yield full, c end namespaces.each do |ns_name, sub| sub_prefix = prefix ? "#{prefix}:#{ns_name}" : ns_name sub.each_command(sub_prefix, &block) end end |
.emit_rows(rows, width) ⇒ Object
799 800 801 802 803 804 805 |
# File 'lib/lux-hammer.rb', line 799 def emit_rows(rows, width) rows.each do |full, c| brief = c.alts.empty? ? c.brief : "#{c.brief} (alt: #{c.alts.join(', ')})" brief = "#{brief} #{Shell.paint('(redefined)', :yellow)}" if c.prev_location Shell.say " #{program_name} #{full.ljust(width)} # #{brief}" end end |
.example(text) ⇒ Object
79 |
# File 'lib/lux-hammer.rb', line 79 def example(text) ; @pending_examples << text end |
.find_command(name) ⇒ Object
Find a command by canonical name or alt within this class. Falls back to fuzzy match (prefix first, then substring) when no exact hit. Raises AmbiguousMatch if the fuzzy pass matches more than one.
452 453 454 455 456 457 |
# File 'lib/lux-hammer.rb', line 452 def find_command(name) name = name.to_s exact = commands[name] || commands.values.find { |c| c.matches?(name) } return exact if exact fuzzy_pick(name, commands.values, 'command') { |c| [c.name, *c.alts] } end |
.find_hammerfile(start) ⇒ Object
Walk up the directory tree looking for a Hammerfile.
1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 |
# File 'lib/lux-hammer.rb', line 1146 def self.find_hammerfile(start) dir = File.(start) loop do candidate = File.join(dir, 'Hammerfile') return candidate if File.file?(candidate) parent = File.dirname(dir) return nil if parent == dir dir = parent end end |
.find_namespace(name) ⇒ Object
Find a namespace by name within this class. Same fuzzy fallback as find_command.
461 462 463 464 465 466 |
# File 'lib/lux-hammer.rb', line 461 def find_namespace(name) name = name.to_s return namespaces[name] if namespaces.key?(name) pair = fuzzy_pick(name, namespaces.to_a, 'namespace') { |p| [p.first] } pair&.last end |
.find_namespace_sibling(canonical) ⇒ Object
Returns the command at the parent that shares its name with this namespace. E.g. for path “gem:version” returns the ‘version` command in the `gem` namespace (if defined), so `gem:version:` listings can include `gem:version` itself at the top.
472 473 474 475 476 477 478 479 480 |
# File 'lib/lux-hammer.rb', line 472 def find_namespace_sibling(canonical) parts = canonical.to_s.split(':') return nil if parts.empty? parent = self parts[0..-2].each do |seg| parent = parent.namespaces[seg] or return nil end parent.commands[parts.last] end |
.fuzzy_pick(name, items, kind, &keys_for) ⇒ Object
Shared fuzzy matcher used by find_command and find_namespace. The block returns the strings to match against for each item (canonical name plus alts for commands, just the key for namespaces). Tries prefix match first, then substring; raises AmbiguousMatch when either pass hits more than one item.
520 521 522 523 524 525 526 527 528 529 530 531 |
# File 'lib/lux-hammer.rb', line 520 def fuzzy_pick(name, items, kind, &keys_for) [:start_with?, :include?].each do |op| matches = items.select { |item| keys_for.call(item).any? { |k| k.send(op, name) } } next if matches.empty? if matches.size > 1 labels = matches.map { |m| keys_for.call(m).first }.sort raise AmbiguousMatch, "multiple #{kind}s match '#{name}': #{labels.join(', ')}" end return matches.first end nil end |
.hammer(name, *args, **opts) ⇒ Object
Programmatic dispatch by name. Useful for scripting and tests.
MyCli.hammer :build -> start(["build"])
MyCli.hammer 'db:users:list' -> start(["db:users:list"])
MyCli.hammer :eval, 'puts 42' -> start(["eval", "puts 42"])
MyCli.hammer :build, env: 'prod' -> start(["build", "--env=prod"])
MyCli.hammer :build, verbose: true -> start(["build", "--verbose"])
MyCli.hammer :build, no_cache: true -> start(["build", "--no-cache"])
MyCli.hammer :build, cache: false -> skipped (no-op)
Symbols are single-segment names; pass a string with colons for namespaced paths. Trailing positionals become positional ARGV. Underscores in option keys become dashes in flags.
546 547 548 549 550 551 552 553 554 |
# File 'lib/lux-hammer.rb', line 546 def hammer(name, *args, **opts) argv = [name.to_s, *args.map(&:to_s)] opts.each do |k, v| next if v == false flag = "--#{k.to_s.tr('_', '-')}" argv << (v == true ? flag : "#{flag}=#{v}") end start(argv) end |
.inherited(sub) ⇒ Object
52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/lux-hammer.rb', line 52 def inherited(sub) super sub.instance_variable_set(:@commands, {}) sub.instance_variable_set(:@namespaces, {}) sub.instance_variable_set(:@before_hooks, []) sub.instance_variable_set(:@parent, nil) sub.instance_variable_set(:@program_name, nil) sub.instance_variable_set(:@app_desc, nil) sub.instance_variable_set(:@pending_desc, nil) sub.instance_variable_set(:@pending_examples, []) sub.instance_variable_set(:@pending_options, []) sub.instance_variable_set(:@pending_alts, []) sub.instance_variable_set(:@pending_needs, []) end |
.load(*paths, **kwargs) ⇒ Object
Load Hammerfile fragments and register their commands on this class. Rake-style: split a CLI across multiple files.
load # auto-discover *_hammer.rb under caller dir
load auto: true # same
load 'tasks/db_hammer.rb' # one file
load 'tasks/*_hammer.rb' # glob
Paths resolve relative to the file calling ‘load`. See `Hammer::Loader` for the full implementation.
340 341 342 343 344 345 346 347 |
# File 'lib/lux-hammer.rb', line 340 def load(*paths, **kwargs) if self == Hammer raise Error, 'use `load` from inside a Hammerfile / Hammer.run block / Hammer subclass body, ' \ 'or call SubClass.load - Hammer.load itself has no target' end anchor = Loader.caller_anchor(caller_locations(1, 1).first) loader.load(anchor, paths, kwargs) end |
.loader ⇒ Object
Per-target Loader instance. Owns the dedup cache, so re-entrant ‘load` from inside a fragment is safe and idempotent.
314 315 316 |
# File 'lib/lux-hammer.rb', line 314 def loader @loader ||= Loader.new(self) end |
.looks_like_builtin?(argv) ⇒ Boolean
1139 1140 1141 1142 1143 |
# File 'lib/lux-hammer.rb', line 1139 def self.looks_like_builtin?(argv) first = argv.first return false unless first BUILTIN_TASKS.include?(first) || BUILTIN_TASKS.any? { |t| first.start_with?("#{t}:") } end |
.method_added(method_name) ⇒ Object
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/lux-hammer.rb', line 84 def method_added(method_name) super return unless @pending_desc cmd = Command.new(name: method_name.to_s, desc: @pending_desc) @pending_examples.each { |e| cmd.add_example(e) } @pending_options.each { |o| cmd.add_option(o) } @pending_alts.each { |n| cmd.add_alt(n) } @pending_needs.each { |n| cmd.add_need(n) } # If the method takes no args, call it without opts. Otherwise pass # opts. So both `def build` and `def build(opts)` work. m = method_name arity = instance_method(method_name).arity cmd.handler = arity.zero? ? proc { send(m) } : proc { |opts| send(m, opts) } cmd.finalize! commands[cmd.name] = cmd @pending_desc = nil @pending_examples = [] @pending_options = [] @pending_alts = [] @pending_needs = [] end |
.namespace(name, &block) ⇒ Object
Open a namespace (group of commands). Everything inside the block (task, nested namespace, …) belongs to that namespace, evaluated against an anonymous Hammer subclass.
namespace :db do
task :migrate do ... end
namespace :users do ... end
end
Reopening a namespace merges: the same ‘namespace :db do … end` can be split across files (Rake-style) and the blocks accumulate onto one subclass. Only a duplicate task name inside warns - that’s handled by ‘task`. The namespace subclass is created lazily on first mention.
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/lux-hammer.rb', line 201 def namespace(name, &block) sub = (@namespaces[name.to_s] ||= begin ns = Class.new(Hammer) # Track the top-level CLI class so cross-invocation # (`hammer 'ns:cmd'`) from inside a namespaced command dispatches # against the full tree, not just the current namespace. ns.instance_variable_set(:@root, root) # Parent link, so `before` hooks defined further up the namespace # tree can be collected and run outer -> inner before a command. ns.instance_variable_set(:@parent, self) # Share the parent's resolved program_name so help banners show # "myapp ns:cmd" with the same prefix everywhere - and so the value # captured pre-chdir (see `Hammer.cli`) survives into nested classes. ns.instance_variable_set(:@program_name, program_name) ns.instance_variable_set(:@location, source_location_of(block)) ns end) Hammer.with_target(sub) { sub.class_eval(&block) } if block end |
.namespaces ⇒ Object
308 309 310 |
# File 'lib/lux-hammer.rb', line 308 def namespaces @namespaces ||= {} end |
.needs(*names) ⇒ Object
82 |
# File 'lib/lux-hammer.rb', line 82 def needs(*names) ; @pending_needs.concat(names) end |
.opt(name, **o) ⇒ Object
80 |
# File 'lib/lux-hammer.rb', line 80 def opt(name, **o) ; @pending_options << Option.new(name, **o) end |
.parent ⇒ Object
251 252 253 |
# File 'lib/lux-hammer.rb', line 251 def parent @parent end |
.print_ai_help ⇒ Object
Dump the gem’s AGENTS.md to stdout - AI-optimized guide for writing Hammerfiles. Bundled with the gem and resolved relative to this file so it works from any install location.
927 928 929 930 931 932 933 934 935 |
# File 'lib/lux-hammer.rb', line 927 def self.print_ai_help path = File.('../AGENTS.md', __dir__) if File.file?(path) puts File.read(path) else Shell.print_error "AGENTS.md not found at #{path}" exit 1 end end |
.print_command_help(cmd, full = nil) ⇒ Object
839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 |
# File 'lib/lux-hammer.rb', line 839 def print_command_help(cmd, full = nil) full ||= cmd.name Shell.say "Usage: #{program_name} #{full}#{usage_signature(cmd)}", :cyan cmd.desc.each_line do |line| stripped = line.chomp Shell.say(stripped.empty? ? '' : " #{stripped}") end unless cmd.desc.empty? Shell.say " alias: #{cmd.alts.join(', ')}" unless cmd.alts.empty? unless cmd..empty? Shell.say '' Shell.say 'Options:', :yellow cmd..each { |o| Shell.say " #{o.usage}" } end unless cmd.examples.empty? Shell.say '' Shell.say 'Examples:', :yellow cmd.examples.each { |e| Shell.say " #{program_name} #{e}" } end end |
.print_command_list(klass, prefix = nil) ⇒ Object
771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 |
# File 'lib/lux-hammer.rb', line 771 def print_command_list(klass, prefix = nil) rows = [] # Commands without a `desc` are hidden from listings but still # dispatchable + `hammer`-callable - useful for private helpers # invoked from `before` hooks or other commands (e.g. `:env`, `:app`). klass.each_command(prefix) { |full, c| rows << [full, c] unless c.desc.empty? } return if rows.empty? # group by "section" = everything between the view prefix and the # leaf name. Bare leaves go in :root. groups = rows.group_by { |full, _| section_for(full, prefix, klass) } width = rows.map { |full, _| full.length }.max first = true if (rooted = groups.delete(:root)) Shell.say 'Commands:', :yellow emit_rows(rooted.sort_by { |full, _| [full.count(':'), full] }, width) first = false end groups.each do |section, items| Shell.say unless first first = false Shell.say "#{section}:", :yellow emit_rows(items.sort_by { |full, _| [full.count(':'), full] }, width) end end |
.print_extras ⇒ Object
Extras shown only in the extended (‘help` / `-h` / `–help`) view: global flags, GitHub footer, and a Hammerfile example for the `hammer` binary. The footer is skipped for the hammer binary because `print_top_banner` already surfaces the same link.
738 739 740 741 742 743 |
# File 'lib/lux-hammer.rb', line 738 def print_extras hammer_bin = root.instance_variable_get(:@hammer_binary) print_global_flags print_hammerfile_example if hammer_bin unless hammer_bin end |
.print_footer ⇒ Object
757 758 759 760 |
# File 'lib/lux-hammer.rb', line 757 def Shell.say '' Shell.say "powered by hammer (v#{VERSION}) - #{HOMEPAGE}", :gray end |
.print_full_block(path, cmd) ⇒ Object
One “task block” for the expanded listing: blank line separator then the standard per-command help (usage + desc + options + examples).
717 718 719 720 |
# File 'lib/lux-hammer.rb', line 717 def print_full_block(path, cmd) Shell.say '' print_command_help(cmd, path) end |
.print_global_flags ⇒ Object
Listed under ‘Default task options:` in `–help` so users see what flags fire on bare-flag invocation (`hammer –version` etc). Re-rendered from the live `:default` task so user-defined overrides surface their own flags here automatically.
749 750 751 752 753 754 755 |
# File 'lib/lux-hammer.rb', line 749 def print_global_flags default = root.commands['default'] return unless default && !default..empty? Shell.say '' Shell.say 'Default task options:', :yellow default..each { |o| Shell.say " #{o.usage}" } end |
.print_hammerfile_example ⇒ Object
Hammerfile cheat-sheet shown under ‘hammer –help`. Same content as `hammer –init` writes - single source of truth via `Hammer::STARTER_HAMMERFILE`. For exhaustive docs see `hammer agents`.
765 766 767 768 769 |
# File 'lib/lux-hammer.rb', line 765 def print_hammerfile_example Shell.say '' Shell.say 'Hammerfile example:', :yellow Shell.say Hammer::STARTER_HAMMERFILE end |
.print_help(target = nil, expanded: false, extended: false) ⇒ Object
‘extended: true` is the verbose `help` / `-h` / `–help` form - appends global flags, the GitHub footer, and (for the hammer binary) a Hammerfile example. Bare invocation passes `extended: false` so the no-args output stays a clean command listing.
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 |
# File 'lib/lux-hammer.rb', line 644 def print_help(target = nil, expanded: false, extended: false) if target # `help ns:` is equivalent to `ns:` - namespace listing. if target.end_with?(':') && target != ':' = target.chomp(':') ns, canonical = resolve_namespace() return print_namespace_help(canonical, ns) if ns Shell.print_error("unknown: #{target}") return end cmd, _, canonical = resolve(target) return print_command_help(cmd, canonical) if cmd ns, canonical = resolve_namespace(target) return print_namespace_help(canonical, ns, expanded: ) if ns Shell.print_error("unknown: #{target}") return end Shell.say "Usage: #{program_name} COMMAND [ARGS]", :cyan if @app_desc && !@app_desc.empty? Shell.say '' @app_desc.each_line { |l| Shell.say " #{l.chomp}" } end if each_command { |path, c| print_full_block(path, c) unless c.desc.empty? } else Shell.say '' print_command_list(self) end print_recipes_section if extended && root.instance_variable_get(:@hammer_binary) print_extras if extended end |
.print_namespace_help(prefix, ns, expanded: false, extended: false) ⇒ Object
‘extended:` is accepted for parity with `print_help` but intentionally not used here - the global-flags / Hammerfile-example / footer block is root-help-only. `expanded:` is also accepted for parity; a namespace listing is always the compact command list.
701 702 703 704 705 706 707 708 709 710 711 712 713 |
# File 'lib/lux-hammer.rb', line 701 def print_namespace_help(prefix, ns, expanded: false, extended: false) Shell.say "Usage: #{program_name} #{prefix}:COMMAND [ARGS]", :cyan rows = [] sibling = find_namespace_sibling(prefix) rows << [prefix, sibling] if sibling && !sibling.desc.empty? ns.each_command(prefix) { |path, c| rows << [path, c] unless c.desc.empty? } unless rows.empty? Shell.say '' Shell.say 'Commands:', :yellow width = rows.map { |path, _| path.length }.max emit_rows(rows.sort_by { |path, _| [path.count(':'), path] }, width) end end |
.print_recipes_section ⇒ Object
Lists recipes (gem + user-dir) under their own section in ‘hammer –help`. Each row shows the recipe’s ‘# desc:` line and either an install hint or the path of the existing stub on PATH. Only rendered when this CLI is the `hammer` binary’s root.
682 683 684 685 686 687 688 689 690 691 692 693 694 695 |
# File 'lib/lux-hammer.rb', line 682 def print_recipes_section entries = Hammer::Recipe.all return if entries.empty? Shell.say '' Shell.say 'Recipes:', :yellow width = entries.keys.map(&:length).max entries.each do |name, file| desc = Hammer::Recipe.desc(file) installed = Hammer::Recipe.installed_path(name) suffix = installed ? "(installed: #{installed})" : "[install: #{program_name} recipes --install #{name}]" Shell.say " #{name.ljust(width)} # #{desc}" Shell.say " #{' ' * width} #{suffix}", :gray end end |
.print_run_banner(cmd, full, positional, opts) ⇒ Object
Print a gray “> prog cmd –opt=val ARG” banner before a command runs. Helps see what was actually picked when fuzzy matching resolved a partial name. Only opts that differ from their default are shown; booleans render as ‘–flag` / `–no-flag`.
596 597 598 599 600 601 602 603 604 605 606 607 608 609 |
# File 'lib/lux-hammer.rb', line 596 def (cmd, full, positional, opts) parts = ["#{program_name} #{full}"] cmd..each do |o| val = opts[o.name] next if val.nil? || val == o.default if o.boolean? parts << (val ? "--#{o.name}" : "--no-#{o.name}") else parts << "--#{o.name}=#{val}" end end parts.concat(positional) Shell.say "> #{parts.join(' ')}", :gray end |
.print_top_banner ⇒ Object
Gray “lux-hammer X.Y.Z - <homepage>” line shown above top-level help in both bare-invocation and ‘–help` modes, so the link is always one glance away. User CLIs skip it (the lux-hammer name/link is irrelevant outside the `hammer` binary).
728 729 730 731 732 |
# File 'lib/lux-hammer.rb', line 728 def return unless root.instance_variable_get(:@hammer_binary) Shell.say "lux-hammer #{VERSION} - #{HOMEPAGE}", :gray Shell.say '' end |
.program_name ⇒ Object
Resolved lazily on first read and memoized, so callers that need the cwd-relative form (see ‘default_program_name`) can warm the cache before chdir-ing elsewhere.
112 113 114 |
# File 'lib/lux-hammer.rb', line 112 def program_name @program_name ||= default_program_name end |
.recipe(name, argv = ARGV) ⇒ Object
Entry point for recipe stubs in PATH. A recipe is a standalone Hammerfile-style script bundled with the gem (or in ~/.config/hammer/recipes/) that is exposed as its own bin via a tiny Ruby wrapper containing:
require 'lux-hammer'
Hammer.recipe(:srt, ARGV)
The recipe runs as a self-contained CLI: program_name is the recipe name, only its own tasks show in –help, no global hammer commands appear. Runs in the caller’s cwd (no chdir, no Hammerfile lookup).
908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 |
# File 'lib/lux-hammer.rb', line 908 def self.recipe(name, argv = ARGV) path = Recipe.path(name) unless path Shell.print_error "unknown recipe: #{name}" Shell.say 'available recipes:', :yellow Recipe.all.keys.sort.each { |n| Shell.say " #{n}" } Shell.say 'try `hammer recipes` to list with descriptions', :gray exit 1 end klass = Class.new(Hammer) klass.instance_variable_set(:@program_name, name.to_s) Builder.new(klass).evaluate(File.read(path), path) klass.start(argv) end |
.relativize_path(path) ⇒ Object
Trim the cwd prefix off an absolute path so redefinition warnings read as ‘./lib/tasks/foo.rb` instead of a long absolute path. Paths outside cwd (framework / gem files) are left absolute.
266 267 268 269 |
# File 'lib/lux-hammer.rb', line 266 def relativize_path(path) prefix = "#{Dir.pwd}/" path.start_with?(prefix) ? ".#{path[Dir.pwd.length..]}" : path end |
.resolve(path) ⇒ Object
Walk “ns1:ns2:cmd” -> [command, owning_class, canonical_path]. Returns [nil, nil, nil] if any segment is missing or the final segment isn’t a command. canonical_path uses the canonical name of every segment (so a fuzzy ‘b` resolves to `build` in the path).
486 487 488 489 490 491 492 493 494 495 496 497 498 499 |
# File 'lib/lux-hammer.rb', line 486 def resolve(path) parts = path.to_s.split(':') klass = self canonical = [] parts[0..-2].each do |ns| sub = klass.find_namespace(ns) or return [nil, nil, nil] canonical << klass.namespaces.key(sub) klass = sub end cmd = klass.find_command(parts.last) return [nil, nil, nil] unless cmd canonical << cmd.name [cmd, klass, canonical.join(':')] end |
.resolve_namespace(path) ⇒ Object
Walk “ns1:ns2” -> [namespace_class, canonical_path]. Returns [nil, nil] if any segment is missing.
503 504 505 506 507 508 509 510 511 512 513 |
# File 'lib/lux-hammer.rb', line 503 def resolve_namespace(path) parts = path.to_s.split(':') klass = self canonical = [] parts.each do |ns| sub = klass.find_namespace(ns) or return [nil, nil] canonical << klass.namespaces.key(sub) klass = sub end [klass, canonical.join(':')] end |
.root ⇒ Object
Topmost class in this CLI tree. For user-defined ‘class MyCli < Hammer` or `Class.new(Hammer)` it’s self; for namespace subclasses it’s whichever class opened the namespace.
300 301 302 |
# File 'lib/lux-hammer.rb', line 300 def root @root || self end |
.run(argv = ARGV, &block) ⇒ Object
Define and run a CLI inline. Inside the block use ‘task :name do … end`, `namespace`, and `load`.
Without a block: load ./Hammerfile if it exists, otherwise auto-discover *_hammer.rb under Dir.pwd, then dispatch ARGV.
882 883 884 885 886 887 888 889 890 891 892 893 894 895 |
# File 'lib/lux-hammer.rb', line 882 def self.run(argv = ARGV, &block) klass = Class.new(Hammer) if block Builder.new(klass).evaluate(&block) else hf = File.join(Dir.pwd, 'Hammerfile') if File.file?(hf) Builder.new(klass).evaluate(File.read(hf), hf) else klass.loader.load(Dir.pwd, [], auto: true) end end klass.start(argv) end |
.run_before_hooks(instance, opts) ⇒ Object
Fire ‘before` hooks from root down through the namespace chain. Each class’s hooks fire at most once per top-level ‘start`, so prereqs dispatched via `needs` won’t re-trigger them.
614 615 616 617 618 619 620 621 |
# File 'lib/lux-hammer.rb', line 614 def run_before_hooks(instance, opts) ran = Thread.current[:hammer_before_ran] ||= {} ancestor_chain.each do |klass| next if ran[klass.object_id] ran[klass.object_id] = true klass.before_hooks.each { |hook| instance.instance_exec(opts, &hook) } end end |
.run_command(cmd, argv, full: nil, quiet: false) ⇒ Object
569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 |
# File 'lib/lux-hammer.rb', line 569 def run_command(cmd, argv, full: nil, quiet: false) # -h / --help is reserved on every command. Anywhere before a `--` # stop-marker, it short-circuits to per-command help. return print_command_help(cmd, full) if help_requested?(argv) positional, opts = Parser.new(cmd.).parse(argv) opts[:args] = positional (cmd, full || cmd.name, positional, opts) unless quiet || ENV['HAMMER_QUIET'] instance = new run_before_hooks(instance, opts) run_needs(cmd) instance.instance_exec(opts, &cmd.handler) rescue Parser::Error => e Shell.print_error(e.) print_command_help(cmd, full) exit 1 rescue Hammer::Error => e # Raised by `error 'msg'` inside a handler - controlled exit, no # backtrace, no per-command help spam. Shell.print_error(e.) exit 1 end |
.run_needs(cmd) ⇒ Object
Dispatch a command’s declared ‘needs` through the root class, with per-invocation dedupe. Prereqs run with default options (no argv).
625 626 627 628 629 630 631 632 633 634 635 636 |
# File 'lib/lux-hammer.rb', line 625 def run_needs(cmd) return if cmd.needs.empty? ran = Thread.current[:hammer_needs_ran] ||= {} cmd.needs.each do |path| key = path.to_s next if ran[key] ran[key] = true target, = root.resolve(key) raise Error, "needs: unknown command '#{key}' in #{cmd.name}" unless target root.start([key]) end end |
.run_or_exit(*cmd, **opts) ⇒ Object
1000 1001 1002 1003 1004 |
# File 'lib/lux-hammer.rb', line 1000 def self.run_or_exit(*cmd, **opts) return if system(*cmd, **opts) Shell.print_error "command failed: #{cmd.join(' ')}" exit 1 end |
.run_shebang(path, argv) ⇒ Object
Evaluate a shebang script as a self-contained CLI. Mirrors ‘recipe` semantics: no chdir, no `@hammer_binary` flag, no `register_core` built-ins (so the script’s ‘–help` shows only what it defines). `program_name` is the script’s basename so help reads “myscript foo” rather than “hammer foo” - works even when invoked via a symlink in PATH, since argv is the path the user typed.
1117 1118 1119 1120 1121 1122 |
# File 'lib/lux-hammer.rb', line 1117 def self.run_shebang(path, argv) klass = Class.new(Hammer) klass.instance_variable_set(:@program_name, File.basename(path)) Builder.new(klass).evaluate(File.read(path), path) klass.start(argv) end |
.section_for(full, prefix, klass = nil) ⇒ Object
‘db’ for ‘db:migrate’ or ‘db:users:list’ viewed from root; ‘users’ for ‘db:users:list’ viewed from ‘db’; :root if the command sits at the view’s top level. Only the first segment under the view groups, so deeper paths fold into their top-level section.
Exception: a bare command that shares its name with a sibling namespace (e.g. ‘mount` alongside a `mount:` namespace) groups under that namespace’s section, not :root.
815 816 817 818 819 820 821 822 823 |
# File 'lib/lux-hammer.rb', line 815 def section_for(full, prefix, klass = nil) segs = full.split(':') segs = segs[prefix.split(':').size..] || [] if prefix && !prefix.empty? if segs.size == 1 && klass && klass.namespaces.key?(segs.first) return segs.first end parent = segs[0..-2] parent.empty? ? :root : parent.first end |
.self_update ⇒ Object
‘hammer update`: pull main in the install-script checkout and reinstall the gem. Assumes the install.sh layout - if the dir is missing, point the user at the curl-pipe installer.
978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 |
# File 'lib/lux-hammer.rb', line 978 def self.self_update dir = ENV['LUX_HAMMER_DIR'] || SELF_UPDATE_DIR unless File.directory?(File.join(dir, '.git')) Shell.print_error "no lux-hammer git checkout at #{dir}" Shell.say 'reinstall with:', :yellow Shell.say " curl -fsSL #{SELF_INSTALL_URL} | bash" exit 1 end Shell.say "* updating lux-hammer at #{dir}", :cyan Dir.chdir(dir) do run_or_exit('git', 'fetch', '--quiet', 'origin', 'main') run_or_exit('git', 'reset', '--quiet', '--hard', 'origin/main') version = File.read('.version').strip gem_file = "lux-hammer-#{version}.gem" run_or_exit('gem', 'build', 'lux-hammer.gemspec', out: File::NULL) run_or_exit('gem', 'install', '--quiet', gem_file) File.unlink(gem_file) if File.exist?(gem_file) Shell.say "* lux-hammer #{version} installed", :green end end |
.shebang_script(arg) ⇒ Object
Returns the script path if ‘arg` looks like a shebang script that delegates to hammer (first line starts with `#!` and mentions `hammer`). Returns nil otherwise. Used by `cli` to detect `#!/usr/bin/env hammer` invocations where the kernel passes the script path as argv.
1102 1103 1104 1105 1106 1107 1108 1109 |
# File 'lib/lux-hammer.rb', line 1102 def self.shebang_script(arg) return nil unless arg return nil if arg.start_with?('-') return nil unless File.file?(arg) && File.readable?(arg) head = File.open(arg, &:gets).to_s return nil unless head.start_with?('#!') && head.include?('hammer') arg end |
.source_location_of(block) ⇒ Object
“file:line” of the block that defined a task/namespace. Falls back to “(unknown)” for blocks without a usable source_location (rare - built-in C-defined procs, eval’d blocks).
258 259 260 261 |
# File 'lib/lux-hammer.rb', line 258 def source_location_of(block) loc = block&.source_location loc ? "#{relativize_path(loc[0])}:#{loc[1]}" : '(unknown)' end |
.start(argv = ARGV) ⇒ Object
Entry point. Parses ARGV, finds the right command, runs it. Command names are Rake-style colon paths: “build”, “db:migrate”, “db:users:list”.
Rake-style chained dispatch: ‘hammer build + deploy + notify`. A bare `+` argv token separates commands; `++` escapes to a literal `+` positional. Quoted shell args (`–foo=“a + b”`) arrive as a single token and are not split.
357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/lux-hammer.rb', line 357 def start(argv = ARGV) # Track prereqs fired during this top-level invocation so a `needs` # chain runs each prereq at most once. Nested `start` calls (e.g. # `needs` -> `hammer` -> `start`, or a `+` chain) share the set; # the outermost call owns its lifetime. outer = Thread.current[:hammer_needs_ran].nil? Thread.current[:hammer_needs_ran] ||= {} Thread.current[:hammer_before_ran] ||= {} split_chain(argv).each { |seg| dispatch(seg) } ensure Thread.current[:hammer_needs_ran] = nil if outer Thread.current[:hammer_before_ran] = nil if outer end |
.task(name, &block) ⇒ Object
Define a command. Block runs in a CommandBuilder context and must return a Proc as its last expression. That proc is the handler and receives a single ‘opts` hash with symbol keys; positional ARGV lives at `opts`.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
# File 'lib/lux-hammer.rb', line 143 def task(name, &block) cmd = Command.new(name: name.to_s) cmd.location = source_location_of(block) handler = CommandBuilder.new(cmd).instance_eval(&block) unless handler.is_a?(Proc) raise Error, <<~MSG task(:#{name}) block must end with a `proc do |opts| ... end`. The proc's return value is what becomes the command handler. Example: task :#{name} do desc 'what it does' example '#{name} foo --env=prod' opt :env, default: 'dev' proc do |opts| # your code here - opts[:env], opts[:args], ... end end MSG end cmd.handler = handler # Only warn when overriding a task that was also defined inside the # main app. Overriding one that came from outside - a framework # default, plugin, or gem - is an intentional override, so stay quiet # and don't tag it `(redefined)` in help. if (prev = commands[cmd.name]) && app_local_location?(prev.location) cmd.prev_location = prev.location warn_redefinition('task', cmd.name, prev.location, cmd.location) end cmd.finalize! commands[cmd.name] = cmd # `task` ignores pending class-level state, but clear it so a # later `def` doesn't accidentally consume stale metadata. @pending_desc = nil @pending_examples = [] @pending_options = [] @pending_alts = [] @pending_needs = [] end |
.usage_signature(cmd) ⇒ Object
“ URL [ENV] [OPTIONS]” - shows the positional-fill names for declared non-boolean opts (required bare, optional bracketed), plus a generic [OPTIONS] tail if any flags exist.
828 829 830 831 832 833 834 835 836 837 |
# File 'lib/lux-hammer.rb', line 828 def usage_signature(cmd) pos = cmd..reject(&:boolean?).map { |o| name = o.name.to_s.upcase o.required ? name : "[#{name}]" } out = pos.join(' ') out = "#{out} ".lstrip unless out.empty? out += '[OPTIONS]' unless cmd..empty? out.empty? ? '' : " #{out}" end |
.warn_redefinition(kind, name, prev_loc, new_loc) ⇒ Object
Emit a yellow [hammer] warning on stderr when a task/namespace is redefined. Last write wins (commands = cmd), but the prior location is captured so listings can tag the entry as ‘(redefined)`.
282 283 284 |
# File 'lib/lux-hammer.rb', line 282 def warn_redefinition(kind, name, prev_loc, new_loc) warn Shell.paint("[hammer] redefined #{kind} :#{name} - was #{prev_loc || '(unknown)'}, now #{new_loc || '(unknown)'}", :yellow) end |
.with_target(klass) ⇒ Object
Push ‘klass` as the current Hammer target for the duration of the block. Top-level DSL methods (`task`, `namespace`, `before` - see `Hammer::DSL`) read this thread-local, so files `require`d from inside a Hammerfile register against the right target.
322 323 324 325 326 327 328 |
# File 'lib/lux-hammer.rb', line 322 def with_target(klass) prev = Thread.current[:hammer_target] Thread.current[:hammer_target] = klass yield ensure Thread.current[:hammer_target] = prev end |
Instance Method Details
#hammer(name, *args, **opts) ⇒ Object
Inside a command’s ‘proc do |opts| … end`, call sibling commands:
task :deploy do
proc do |opts|
hammer :build
hammer 'db:migrate', pretend: true
end
end
Dispatches from the root class so colon paths resolve against the full tree even when called from inside a namespaced command.
871 872 873 |
# File 'lib/lux-hammer.rb', line 871 def hammer(name, *args, **opts) self.class.root.hammer(name, *args, **opts) end |