lux-hammer 0.3.16 → 0.3.20

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 ''
@@ -114,7 +114,7 @@ module LuxDeploy
114
114
  host = Context.read_host(opts)
115
115
  config = Config.load
116
116
  ssh = SSH.new(host, service_user: config.service_user, dry_run: false)
117
- Doctor.run(ssh, config, fix: opts.fetch(:fix, true))
117
+ Doctor.run(ssh, config, fix: !opts[:no_fix])
118
118
  end
119
119
 
120
120
  # -------- app:init ----------------------------------------------------
@@ -76,7 +76,7 @@ module LuxDeploy
76
76
  target.task :doctor do
77
77
  desc 'Check & prepare host: service user, dirs, caddy, ruby, bundler'
78
78
  opt :server, desc: 'Override hostname from config/deploy/.yaml'
79
- opt :fix, type: :boolean, default: true, desc: 'Auto-fix safe items (default true; --no-fix to skip)'
79
+ opt :no_fix, type: :boolean, desc: 'Do not auto-fix safe items (default is to fix)'
80
80
 
81
81
  proc { |opts| LuxDeploy::Hammer.safe(opts) { LuxDeploy::Commands.doctor(opts) } }
82
82
  end