lux-hammer 0.3.17 → 0.3.21

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.
data/lib/lux-hammer.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative 'hammer/shell'
2
+ require_relative 'hammer/input'
2
3
  require_relative 'hammer/option'
3
4
  require_relative 'hammer/parser'
4
5
  require_relative 'hammer/command'
@@ -49,12 +50,23 @@ class Hammer
49
50
  # Gem version, read once from the bundled .version file.
50
51
  VERSION ||= File.read(File.expand_path('../.version', __dir__)).strip
51
52
 
53
+ # Convenience aliases so recipes can write `Hammer.prepare_json!(opts)`
54
+ # without the Input module path. See Hammer::Input.
55
+ def self.prepare_json!(opts, key: :json)
56
+ Input.prepare_json!(opts, key: key)
57
+ end
58
+
59
+ def self.attach_stdin!(opts)
60
+ Input.attach_stdin!(opts)
61
+ end
62
+
52
63
  class << self
53
64
  def inherited(sub)
54
65
  super
55
66
  sub.instance_variable_set(:@commands, {})
56
67
  sub.instance_variable_set(:@namespaces, {})
57
68
  sub.instance_variable_set(:@before_hooks, [])
69
+ sub.instance_variable_set(:@global_options, [])
58
70
  sub.instance_variable_set(:@parent, nil)
59
71
  sub.instance_variable_set(:@program_name, nil)
60
72
  sub.instance_variable_set(:@app_desc, nil)
@@ -230,6 +242,7 @@ class Hammer
230
242
  # outer -> inner, once per top-level `start` (prereqs don't re-trigger).
231
243
  #
232
244
  # before { |opts| Dotenv.load }
245
+ # before { |opts| Hammer::Input.prepare_json!(opts) }
233
246
  # namespace :db do
234
247
  # before { hammer :env }
235
248
  # task :migrate do ... end
@@ -242,6 +255,52 @@ class Hammer
242
255
  @before_hooks
243
256
  end
244
257
 
258
+ # Options available on every command under this class (and children).
259
+ # Declared once at root (or a namespace); merged after per-task opts so
260
+ # positionals still fill task params first. type: :json never takes
261
+ # positionals regardless.
262
+ #
263
+ # global_opt :json, type: :json, desc: 'JSON body (inline, @file, pipe)'
264
+ # global_opt :as_json, type: :boolean, alias: :j, default: false
265
+ def global_opt(name, **o)
266
+ (@global_options ||= []) << Option.new(name, **o)
267
+ end
268
+
269
+ def global_options
270
+ @global_options || []
271
+ end
272
+
273
+ # Root -> self chain of global_opt declarations (outer first).
274
+ def collected_global_options
275
+ ancestor_chain.flat_map { |k| k.global_options }
276
+ end
277
+
278
+ # Per-task options + globals. Globals last so bare positionals bind to
279
+ # the task's own opts first. Short aliases finalized on the combined set.
280
+ def effective_options(cmd)
281
+ extras = collected_global_options
282
+ return cmd.options if extras.empty?
283
+
284
+ merged = cmd.options + extras
285
+ finalize_option_list!(merged)
286
+ merged
287
+ end
288
+
289
+ def finalize_option_list!(options)
290
+ claimed = ['-h']
291
+ options.each do |o|
292
+ o.aliases.each { |a| claimed << a if a.length == 2 && a.start_with?('-') && a[1] != '-' }
293
+ end
294
+ options.each do |o|
295
+ next if o.aliases.any? { |a| a.length == 2 && a.start_with?('-') && a[1] != '-' }
296
+ short = "-#{o.name.to_s[0]}"
297
+ next if claimed.include?(short)
298
+ o.aliases << short
299
+ claimed << short
300
+ end
301
+ options
302
+ end
303
+
245
304
  # Toggle auto-loading of `.env` / `.env.local` for the `hammer`
246
305
  # binary. Default is ON. Call `dotenv false` at the top of a
247
306
  # Hammerfile to suppress. No-op for standalone `MyCli.start` -
@@ -551,16 +610,17 @@ class Hammer
551
610
  # MyCli.hammer :eval, 'puts 42' -> start(["eval", "puts 42"])
552
611
  # MyCli.hammer :build, env: 'prod' -> start(["build", "--env=prod"])
553
612
  # MyCli.hammer :build, verbose: true -> start(["build", "--verbose"])
554
- # MyCli.hammer :build, no_cache: true -> start(["build", "--no-cache"])
555
- # MyCli.hammer :build, cache: false -> skipped (no-op)
613
+ # MyCli.hammer :build, no_reset: true -> start(["build", "--no-reset"])
614
+ # MyCli.hammer :build, cache: false -> skipped (false flags are omitted)
556
615
  #
557
616
  # Symbols are single-segment names; pass a string with colons for
558
617
  # namespaced paths. Trailing positionals become positional ARGV.
559
- # Underscores in option keys become dashes in flags.
618
+ # Underscores in option keys become dashes in flags. Boolean presence
619
+ # is the only truth: there is no auto `--no-X` negation.
560
620
  def hammer(name, *args, **opts)
561
621
  argv = [name.to_s, *args.map(&:to_s)]
562
622
  opts.each do |k, v|
563
- next if v == false
623
+ next if v == false || v.nil?
564
624
  flag = "--#{k.to_s.tr('_', '-')}"
565
625
  if v == true
566
626
  argv << flag
@@ -627,9 +687,13 @@ class Hammer
627
687
  # stop-marker, it short-circuits to per-command help.
628
688
  return print_command_help(cmd, full) if help_requested?(argv)
629
689
 
630
- positional, opts = Parser.new(cmd.options).parse(argv)
690
+ options = effective_options(cmd)
691
+ positional, opts = Parser.new(options).parse(argv)
631
692
  opts[:args] = positional
632
- print_run_banner(cmd, full || cmd.name, positional, opts) unless quiet || ENV['HAMMER_QUIET']
693
+ # Always attach piped stdin (nil when TTY / empty). Recipes that want
694
+ # JSON body handling call Hammer::Input.prepare_json! in a before hook.
695
+ Hammer::Input.attach_stdin!(opts)
696
+ print_run_banner(cmd, full || cmd.name, positional, opts, options: options) unless quiet || ENV['HAMMER_QUIET']
633
697
  instance = new
634
698
  run_before_hooks(instance, opts)
635
699
  run_needs(cmd)
@@ -648,16 +712,25 @@ class Hammer
648
712
  # Print a gray "> prog cmd --opt=val ARG" banner before a command
649
713
  # runs. Helps see what was actually picked when fuzzy matching
650
714
  # resolved a partial name. Only opts that differ from their default
651
- # are shown; booleans render as `--flag` / `--no-flag`.
652
- def print_run_banner(cmd, full, positional, opts)
715
+ # are shown; booleans render as `--flag` when true.
716
+ def print_run_banner(cmd, full, positional, opts, options: nil)
717
+ options ||= effective_options(cmd)
653
718
  parts = ["#{program_name} #{full}"]
654
- cmd.options.each do |o|
719
+ options.each do |o|
655
720
  val = opts[o.name]
656
721
  next if val.nil? || val == o.default
657
722
  if o.boolean?
658
- parts << (val ? "--#{o.name}" : "--no-#{o.name}")
723
+ parts << o.switch if val
659
724
  else
660
- parts << "--#{o.name}=#{val.is_a?(Array) ? val.join(',') : val}"
725
+ rendered =
726
+ if o.type == :json || val.is_a?(Hash)
727
+ val.respond_to?(:to_json) ? val.to_json : val.inspect
728
+ elsif val.is_a?(Array)
729
+ val.join(',')
730
+ else
731
+ val
732
+ end
733
+ parts << "#{o.switch}=#{rendered}"
661
734
  end
662
735
  end
663
736
  parts.concat(positional)
@@ -891,20 +964,24 @@ class Hammer
891
964
 
892
965
  # " URL [ENV] [OPTIONS]" - shows the positional-fill names for
893
966
  # declared non-boolean opts (required bare, optional bracketed), plus
894
- # a generic [OPTIONS] tail if any flags exist.
967
+ # a generic [OPTIONS] tail if any flags exist. type: :json and other
968
+ # skip_positional_fill? opts are flag-only (not listed as positionals).
895
969
  def usage_signature(cmd)
896
- pos = cmd.options.reject(&:boolean?).map { |o|
970
+ options = effective_options(cmd)
971
+ pos = options.reject { |o| o.boolean? || o.skip_positional_fill? }.map { |o|
897
972
  name = o.name.to_s.upcase
898
973
  o.required ? name : "[#{name}]"
899
974
  }
900
975
  out = pos.join(' ')
901
976
  out = "#{out} ".lstrip unless out.empty?
902
- out += '[OPTIONS]' unless cmd.options.empty?
977
+ out += '[OPTIONS]' unless options.empty?
903
978
  out.empty? ? '' : " #{out}"
904
979
  end
905
980
 
906
981
  def print_command_help(cmd, full = nil)
907
982
  full ||= cmd.name
983
+ local = cmd.options
984
+ global = collected_global_options
908
985
  Shell.say "Usage: #{program_name} #{full}#{usage_signature(cmd)}", :cyan
909
986
  cmd.desc.each_line do |line|
910
987
  stripped = line.chomp
@@ -912,10 +989,15 @@ class Hammer
912
989
  end unless cmd.desc.empty?
913
990
  Shell.say " alias: #{cmd.alts.join(', ')}" unless cmd.alts.empty?
914
991
  Shell.say " cron: #{cmd.cron}" if cmd.cron
915
- unless cmd.options.empty?
992
+ unless local.empty?
916
993
  Shell.say ''
917
994
  Shell.say 'Options:', :yellow
918
- cmd.options.each { |o| Shell.say " #{o.usage}" }
995
+ local.each { |o| Shell.say " #{o.usage}" }
996
+ end
997
+ unless global.empty?
998
+ Shell.say ''
999
+ Shell.say 'Global options:', :yellow
1000
+ global.each { |o| Shell.say " #{o.usage}" }
919
1001
  end
920
1002
  unless cmd.examples.empty?
921
1003
  Shell.say ''
data/recipes/deploy.rb CHANGED
@@ -16,9 +16,17 @@ desc <<~TXT
16
16
  deploy log --log errors # dump a remote log
17
17
  TXT
18
18
 
19
- # Loads the bundled lib (config/ssh/template/doctor/context/manifest/
20
- # commands/hammer) - same require chain the gem's lib/lux_deploy.rb had.
21
- require_relative 'lib/deploy/boot'
19
+ # The engine lives in the lux-deploy gem. This recipe used to carry a vendored
20
+ # copy of it, which drifted a full major version behind and would have deployed
21
+ # 0.2 semantics onto a 0.3 host. Requiring the gem keeps one engine.
22
+ #
23
+ # Not a gemspec dependency - lux-hammer stays zero-dependency, and only this
24
+ # one recipe needs it. The require is what enforces it, at invocation time.
25
+ begin
26
+ require 'lux_deploy'
27
+ rescue LoadError
28
+ abort 'lux-deploy is not installed. Run: gem install lux-deploy'
29
+ end
22
30
 
23
31
  # Auto-load the app's deploy bootstrap, if present, before tasks fire.
24
32
  # A consumer can inject Ruby (e.g. a pre-deploy hook) without writing a
@@ -26,7 +34,7 @@ require_relative 'lib/deploy/boot'
26
34
  init = File.join(Dir.pwd, 'config', 'deploy', 'init.rb')
27
35
  load init if File.file?(init)
28
36
 
29
- # `self` here is the recipe's Builder context - same surface a Hammerfile
30
- # gets. Pass templates_dir explicitly since there's no gem ROOT to fall
31
- # back to; it resolves to recipes/lib/deploy/templates.
32
- LuxDeploy::Hammer.register(self, templates_dir: File.join(__dir__, 'lib/deploy/templates'))
37
+ # `self` here is the recipe's Builder context - same surface a Hammerfile gets.
38
+ # No templates_dir: the gem falls back to its own LuxDeploy::ROOT/templates,
39
+ # which is what the standalone `lux-deploy` binary uses too.
40
+ LuxDeploy::Hammer.register(self)