lux-hammer 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/AGENTS.md +68 -11
- data/README.md +98 -18
- data/lib/hammer/builtins.rb +153 -55
- data/lib/hammer/recipe.rb +2 -2
- data/lib/lux-hammer.rb +116 -93
- data/recipes/llm.rb +434 -0
- metadata +2 -1
data/lib/lux-hammer.rb
CHANGED
|
@@ -188,12 +188,7 @@ class Hammer
|
|
|
188
188
|
# namespace :users do ... end
|
|
189
189
|
# end
|
|
190
190
|
#
|
|
191
|
-
# `:self` is reserved for the `hammer` binary's built-ins (see
|
|
192
|
-
# Hammer::Builtins). User code that tries to open it raises.
|
|
193
191
|
def namespace(name, &block)
|
|
194
|
-
if name.to_s == 'self' && !Thread.current[:hammer_builtins_loading]
|
|
195
|
-
raise Error, "namespace 'self' is reserved for hammer's built-in commands"
|
|
196
|
-
end
|
|
197
192
|
sub = Class.new(Hammer)
|
|
198
193
|
# Track the top-level CLI class so cross-invocation
|
|
199
194
|
# (`hammer 'ns:cmd'`) from inside a namespaced command dispatches
|
|
@@ -369,13 +364,18 @@ class Hammer
|
|
|
369
364
|
argv = argv.dup
|
|
370
365
|
name = argv.shift
|
|
371
366
|
|
|
372
|
-
|
|
373
|
-
|
|
367
|
+
# Bare invocation OR leading flag (other than -h/--help) -> :default
|
|
368
|
+
# task. Re-prepend the flag so :default's option parser sees it.
|
|
369
|
+
# If no :default is defined, fall back to top-level help.
|
|
370
|
+
if name.nil? || (name.start_with?('-') && name != '-h' && name != '--help')
|
|
371
|
+
argv.unshift(name) if name
|
|
372
|
+
return dispatch_to_builtin('default', argv) { print_help }
|
|
374
373
|
end
|
|
375
374
|
|
|
375
|
+
# Explicit help requests -> :help task. Remaining positionals (e.g.
|
|
376
|
+
# `help build`, `help db:`) reach :help via opts[:args].
|
|
376
377
|
if name == 'help' || name == '-h' || name == '--help'
|
|
377
|
-
|
|
378
|
-
return print_help(target, extended: true)
|
|
378
|
+
return dispatch_to_builtin('help', argv) { print_help(argv.first, extended: true) }
|
|
379
379
|
end
|
|
380
380
|
|
|
381
381
|
# Trailing colon ("db:") -> namespace listing. Bare ":" lists root.
|
|
@@ -404,6 +404,16 @@ class Hammer
|
|
|
404
404
|
exit 1
|
|
405
405
|
end
|
|
406
406
|
|
|
407
|
+
# Run a built-in routing task (:default or :help) if defined,
|
|
408
|
+
# otherwise yield to the fallback block. The implicit run banner
|
|
409
|
+
# is suppressed (the user typed `hammer --version`, not `hammer
|
|
410
|
+
# default --version` - no point echoing the rewritten form).
|
|
411
|
+
def dispatch_to_builtin(name, argv)
|
|
412
|
+
cmd = commands[name]
|
|
413
|
+
return yield unless cmd
|
|
414
|
+
run_command(cmd, argv, full: name, quiet: true)
|
|
415
|
+
end
|
|
416
|
+
|
|
407
417
|
public
|
|
408
418
|
|
|
409
419
|
# Find a command by canonical name or alt within this class. Falls
|
|
@@ -526,14 +536,14 @@ class Hammer
|
|
|
526
536
|
end
|
|
527
537
|
end
|
|
528
538
|
|
|
529
|
-
def run_command(cmd, argv, full: nil)
|
|
539
|
+
def run_command(cmd, argv, full: nil, quiet: false)
|
|
530
540
|
# -h / --help is reserved on every command. Anywhere before a `--`
|
|
531
541
|
# stop-marker, it short-circuits to per-command help.
|
|
532
542
|
return print_command_help(cmd, full) if help_requested?(argv)
|
|
533
543
|
|
|
534
544
|
positional, opts = Parser.new(cmd.options).parse(argv)
|
|
535
545
|
opts[:args] = positional
|
|
536
|
-
print_run_banner(cmd, full || cmd.name, positional, opts)
|
|
546
|
+
print_run_banner(cmd, full || cmd.name, positional, opts) unless quiet || ENV['HAMMER_QUIET']
|
|
537
547
|
instance = new
|
|
538
548
|
run_before_hooks(instance, opts)
|
|
539
549
|
run_needs(cmd)
|
|
@@ -652,7 +662,7 @@ class Hammer
|
|
|
652
662
|
entries.each do |name, file|
|
|
653
663
|
desc = Hammer::Recipe.desc(file)
|
|
654
664
|
installed = Hammer::Recipe.installed_path(name)
|
|
655
|
-
suffix = installed ? "(installed: #{installed})" : "[install: #{program_name}
|
|
665
|
+
suffix = installed ? "(installed: #{installed})" : "[install: #{program_name} recipes --install #{name}]"
|
|
656
666
|
Shell.say " #{name.ljust(width)} # #{desc}"
|
|
657
667
|
Shell.say " #{' ' * width} #{suffix}", :gray
|
|
658
668
|
end
|
|
@@ -661,8 +671,8 @@ class Hammer
|
|
|
661
671
|
# `extended:` is accepted for parity with `print_help` but intentionally
|
|
662
672
|
# not used here - the global-flags / Hammerfile-example / footer block
|
|
663
673
|
# is root-help-only. A namespace listing is just the commands under
|
|
664
|
-
# that prefix; tool-meta noise (
|
|
665
|
-
#
|
|
674
|
+
# that prefix; tool-meta noise (Recipes: section) is reserved for
|
|
675
|
+
# `hammer --help` at the top level.
|
|
666
676
|
def print_namespace_help(prefix, ns, full: false, extended: false)
|
|
667
677
|
Shell.say "Usage: #{program_name} #{prefix}:COMMAND [ARGS]", :cyan
|
|
668
678
|
rows = []
|
|
@@ -707,14 +717,16 @@ class Hammer
|
|
|
707
717
|
print_footer unless hammer_bin
|
|
708
718
|
end
|
|
709
719
|
|
|
710
|
-
#
|
|
711
|
-
#
|
|
712
|
-
#
|
|
720
|
+
# Listed under `Default task options:` in `--help` so users see what
|
|
721
|
+
# flags fire on bare-flag invocation (`hammer --version` etc).
|
|
722
|
+
# Re-rendered from the live `:default` task so user-defined
|
|
723
|
+
# overrides surface their own flags here automatically.
|
|
713
724
|
def print_global_flags
|
|
714
|
-
|
|
725
|
+
default = root.commands['default']
|
|
726
|
+
return unless default && !default.options.empty?
|
|
715
727
|
Shell.say ''
|
|
716
|
-
Shell.say '
|
|
717
|
-
Shell.say
|
|
728
|
+
Shell.say 'Default task options:', :yellow
|
|
729
|
+
default.options.each { |o| Shell.say " #{o.usage}" }
|
|
718
730
|
end
|
|
719
731
|
|
|
720
732
|
def print_footer
|
|
@@ -722,35 +734,13 @@ class Hammer
|
|
|
722
734
|
Shell.say "powered by hammer - #{HOMEPAGE}", :gray
|
|
723
735
|
end
|
|
724
736
|
|
|
725
|
-
#
|
|
726
|
-
#
|
|
727
|
-
#
|
|
737
|
+
# Hammerfile cheat-sheet shown under `hammer --help`. Same content
|
|
738
|
+
# as `hammer --init` writes - single source of truth via
|
|
739
|
+
# `Hammer::STARTER_HAMMERFILE`. For exhaustive docs see `hammer agents`.
|
|
728
740
|
def print_hammerfile_example
|
|
729
741
|
Shell.say ''
|
|
730
742
|
Shell.say 'Hammerfile example:', :yellow
|
|
731
|
-
Shell.say
|
|
732
|
-
desc 'My project tools - build, deploy, test'
|
|
733
|
-
|
|
734
|
-
task :hello do
|
|
735
|
-
desc 'Greet someone'
|
|
736
|
-
example 'hello world --loud'
|
|
737
|
-
opt :loud, type: :boolean, alias: :l
|
|
738
|
-
proc do |opts|
|
|
739
|
-
msg = "hello \#{opts[:args].first || 'world'}"
|
|
740
|
-
say(opts[:loud] ? msg.upcase : msg, :cyan)
|
|
741
|
-
end
|
|
742
|
-
end
|
|
743
|
-
|
|
744
|
-
namespace :db do
|
|
745
|
-
before { hammer :env }
|
|
746
|
-
|
|
747
|
-
task :migrate do
|
|
748
|
-
desc 'Run migrations'
|
|
749
|
-
needs 'db:check'
|
|
750
|
-
proc { sh 'bin/rails db:migrate' }
|
|
751
|
-
end
|
|
752
|
-
end
|
|
753
|
-
RUBY
|
|
743
|
+
Shell.say Hammer::STARTER_HAMMERFILE
|
|
754
744
|
end
|
|
755
745
|
|
|
756
746
|
def print_command_list(klass, prefix = nil)
|
|
@@ -896,7 +886,7 @@ class Hammer
|
|
|
896
886
|
Shell.print_error "unknown recipe: #{name}"
|
|
897
887
|
Shell.say 'available recipes:', :yellow
|
|
898
888
|
Recipe.all.keys.sort.each { |n| Shell.say " #{n}" }
|
|
899
|
-
Shell.say 'try `hammer
|
|
889
|
+
Shell.say 'try `hammer recipes` to list with descriptions', :gray
|
|
900
890
|
exit 1
|
|
901
891
|
end
|
|
902
892
|
|
|
@@ -919,12 +909,45 @@ class Hammer
|
|
|
919
909
|
end
|
|
920
910
|
end
|
|
921
911
|
|
|
922
|
-
#
|
|
912
|
+
# Canonical starter Hammerfile. Written verbatim by `hammer --init`
|
|
913
|
+
# and shown inline by `print_hammerfile_example` (the `Hammerfile
|
|
914
|
+
# example:` block in `hammer --help`) - one source of truth for both.
|
|
915
|
+
# Annotated as a mini tutorial of the common DSL surface in one task.
|
|
916
|
+
# Single-quoted heredoc so file contents pass through unescaped.
|
|
917
|
+
STARTER_HAMMERFILE ||= <<~'RUBY'
|
|
918
|
+
desc 'My project tools'
|
|
919
|
+
|
|
920
|
+
# `namespace :name do ... end` groups tasks under a colon prefix.
|
|
921
|
+
# Address as `hammer demo:greet`.
|
|
922
|
+
namespace :demo do
|
|
923
|
+
# run before every task - global, or on a namespace
|
|
924
|
+
before { say.gray "[demo] booting in #{Dir.pwd}" }
|
|
925
|
+
|
|
926
|
+
task :greet do
|
|
927
|
+
desc 'Demo task - shows the common DSL features in one place'
|
|
928
|
+
example 'demo:greet world --from=alice --loud --times=3'
|
|
929
|
+
alt :hi # `hammer demo:hi` also dispatches here
|
|
930
|
+
|
|
931
|
+
opt :from, default: 'anon', desc: 'who is greeting'
|
|
932
|
+
opt :loud, type: :boolean, alias: :l, desc: 'shout the greeting'
|
|
933
|
+
opt :times, type: :integer, default: 1, desc: 'repeat N times'
|
|
934
|
+
|
|
935
|
+
proc do |opts|
|
|
936
|
+
who = opts[:args].first || 'world'
|
|
937
|
+
msg = "hello #{who} from #{opts[:from]}"
|
|
938
|
+
msg = msg.upcase if opts[:loud]
|
|
939
|
+
opts[:times].times { say msg, :cyan }
|
|
940
|
+
end
|
|
941
|
+
end
|
|
942
|
+
end
|
|
943
|
+
RUBY
|
|
944
|
+
|
|
945
|
+
# Default install dir used by install.sh and `hammer update`.
|
|
923
946
|
SELF_UPDATE_DIR ||= File.expand_path('~/.local/share/lux-hammer')
|
|
924
947
|
SELF_UPDATE_REPO ||= 'https://github.com/dux/hammer.git'
|
|
925
948
|
SELF_INSTALL_URL ||= 'https://raw.githubusercontent.com/dux/hammer/main/install.sh'
|
|
926
949
|
|
|
927
|
-
# `hammer
|
|
950
|
+
# `hammer update`: pull main in the install-script checkout and
|
|
928
951
|
# reinstall the gem. Assumes the install.sh layout - if the dir is
|
|
929
952
|
# missing, point the user at the curl-pipe installer.
|
|
930
953
|
def self.self_update
|
|
@@ -959,31 +982,25 @@ class Hammer
|
|
|
959
982
|
# finds a Hammerfile, evaluates it as the block DSL, then dispatches
|
|
960
983
|
# ARGV against the resulting CLI.
|
|
961
984
|
#
|
|
962
|
-
#
|
|
963
|
-
#
|
|
964
|
-
#
|
|
985
|
+
# `--system` forces the no-Hammerfile branch even from inside a
|
|
986
|
+
# project - the escape hatch for reaching `recipes`/`init` when a
|
|
987
|
+
# user-defined task tree would otherwise own the root.
|
|
965
988
|
def self.cli(argv = ARGV)
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
if (i = argv.index('--update'))
|
|
969
|
-
argv = argv.dup
|
|
970
|
-
argv.delete_at(i)
|
|
971
|
-
argv.unshift('self:update')
|
|
972
|
-
end
|
|
973
|
-
|
|
974
|
-
wants_builtins = builtins_triggered?(argv)
|
|
989
|
+
argv = argv.dup
|
|
990
|
+
force_system = !!argv.delete('--system')
|
|
975
991
|
|
|
976
|
-
path = find_hammerfile(Dir.pwd)
|
|
992
|
+
path = force_system ? nil : find_hammerfile(Dir.pwd)
|
|
977
993
|
unless path
|
|
978
|
-
# No Hammerfile
|
|
979
|
-
#
|
|
980
|
-
#
|
|
981
|
-
if
|
|
994
|
+
# No Hammerfile (or --system) - all built-ins are reachable. Bare
|
|
995
|
+
# `hammer`, `hammer recipes`, `hammer update`, `hammer agents`,
|
|
996
|
+
# `hammer version`, `hammer init` all work.
|
|
997
|
+
if force_system || dispatches_to_builtin?(argv) || looks_like_builtin?(argv)
|
|
982
998
|
klass = Class.new(Hammer)
|
|
983
999
|
klass.instance_variable_set(:@hammer_binary, true)
|
|
984
1000
|
klass.program_name
|
|
985
1001
|
require_relative 'hammer/builtins'
|
|
986
|
-
Hammer::Builtins.
|
|
1002
|
+
Hammer::Builtins.register_core(klass)
|
|
1003
|
+
Hammer::Builtins.register_no_project(klass)
|
|
987
1004
|
klass.start(argv)
|
|
988
1005
|
return
|
|
989
1006
|
end
|
|
@@ -1004,24 +1021,17 @@ class Hammer
|
|
|
1004
1021
|
|
|
1005
1022
|
Shell.say "create one - example:"
|
|
1006
1023
|
puts
|
|
1007
|
-
Shell.say
|
|
1008
|
-
desc 'My project tools'
|
|
1009
|
-
|
|
1010
|
-
task :hello do
|
|
1011
|
-
desc 'say hello'
|
|
1012
|
-
proc do |opts|
|
|
1013
|
-
say.green "hello \#{opts[:args].first || 'world'}"
|
|
1014
|
-
end
|
|
1015
|
-
end
|
|
1016
|
-
RUBY
|
|
1024
|
+
Shell.say STARTER_HAMMERFILE
|
|
1017
1025
|
Shell.say ''
|
|
1018
|
-
|
|
1026
|
+
bin = File.basename($PROGRAM_NAME)
|
|
1027
|
+
Shell.say "tip: run `#{bin} init` to drop the example above into ./Hammerfile", :gray
|
|
1028
|
+
Shell.say "tip: run `#{bin} agents` for AI-friendly Hammerfile authoring docs", :gray
|
|
1019
1029
|
exit 1
|
|
1020
1030
|
end
|
|
1021
1031
|
|
|
1022
1032
|
klass = Class.new(Hammer)
|
|
1023
1033
|
# Mark this class as the `hammer` binary's root so help output can
|
|
1024
|
-
# surface binary-only sections (`Recipes
|
|
1034
|
+
# surface binary-only sections (`Recipes:` listing).
|
|
1025
1035
|
klass.instance_variable_set(:@hammer_binary, true)
|
|
1026
1036
|
# Resolve before chdir so paths like `bin/foo` stay relative to the
|
|
1027
1037
|
# cwd the user actually invoked from. `program_name` memoizes.
|
|
@@ -1036,24 +1046,37 @@ class Hammer
|
|
|
1036
1046
|
# are NOT visible during Hammerfile evaluation, only inside handlers.
|
|
1037
1047
|
Hammer::Dotenv.load(Dir.pwd) if klass.dotenv_enabled?
|
|
1038
1048
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1049
|
+
# Core built-ins register AFTER Hammerfile eval so user-defined
|
|
1050
|
+
# tasks win (the `unless commands.key?(...)` guards in register_core
|
|
1051
|
+
# skip the built-in when overridden - no redefinition warning).
|
|
1052
|
+
# `register_no_project` (:recipes, :init) is intentionally NOT
|
|
1053
|
+
# called here - those would clash too easily with user tasks. Use
|
|
1054
|
+
# `hammer --system recipes` to reach them from inside a project.
|
|
1055
|
+
require_relative 'hammer/builtins'
|
|
1056
|
+
Hammer::Builtins.register_core(klass)
|
|
1043
1057
|
|
|
1044
1058
|
klass.start(argv)
|
|
1045
1059
|
end
|
|
1046
1060
|
|
|
1047
|
-
# True if argv
|
|
1048
|
-
#
|
|
1049
|
-
#
|
|
1050
|
-
#
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
argv.
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1061
|
+
# True if argv goes through a built-in dispatch path (`:default` or
|
|
1062
|
+
# `:help`) - meaning bare `hammer`, leading-flag invocations like
|
|
1063
|
+
# `hammer -h`, or explicit help requests. These don't need a project
|
|
1064
|
+
# Hammerfile to run.
|
|
1065
|
+
def self.dispatches_to_builtin?(argv)
|
|
1066
|
+
return true if argv.empty?
|
|
1067
|
+
first = argv.first
|
|
1068
|
+
first == 'help' || first == '-h' || first == '--help' || first.start_with?('-')
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1071
|
+
# True if argv names a built-in task (`recipes`, `update`, `agents`,
|
|
1072
|
+
# `version`, `init`). Used in the no-Hammerfile branch to wake up the
|
|
1073
|
+
# built-ins for invocations like `hammer recipes` that aren't a flag
|
|
1074
|
+
# or help request.
|
|
1075
|
+
BUILTIN_TASKS ||= %w[recipes update agents version init].freeze
|
|
1076
|
+
def self.looks_like_builtin?(argv)
|
|
1077
|
+
first = argv.first
|
|
1078
|
+
return false unless first
|
|
1079
|
+
BUILTIN_TASKS.include?(first) || BUILTIN_TASKS.any? { |t| first.start_with?("#{t}:") }
|
|
1057
1080
|
end
|
|
1058
1081
|
|
|
1059
1082
|
# Walk up the directory tree looking for a Hammerfile.
|