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.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/AGENTS.md +16 -6
- data/README.md +83 -13
- data/lib/hammer/builder.rb +6 -1
- data/lib/hammer/input.rb +121 -0
- data/lib/hammer/option.rb +23 -9
- data/lib/hammer/parser.rb +12 -14
- data/lib/lux-hammer.rb +98 -16
- data/recipes/deploy.rb +15 -7
- data/recipes/lib/llm/usage.rb +353 -16
- data/recipes/lib/llm/wrap.rb +882 -0
- data/recipes/llm.rb +97 -13
- metadata +4 -11
- data/recipes/lib/deploy/boot.rb +0 -52
- data/recipes/lib/deploy/commands.rb +0 -555
- data/recipes/lib/deploy/config.rb +0 -62
- data/recipes/lib/deploy/context.rb +0 -149
- data/recipes/lib/deploy/doctor.rb +0 -238
- data/recipes/lib/deploy/hammer.rb +0 -168
- data/recipes/lib/deploy/manifest.rb +0 -169
- data/recipes/lib/deploy/ssh.rb +0 -129
- data/recipes/lib/deploy/template.rb +0 -39
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,
|
|
555
|
-
# MyCli.hammer :build, cache: false -> skipped (
|
|
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
|
-
|
|
690
|
+
options = effective_options(cmd)
|
|
691
|
+
positional, opts = Parser.new(options).parse(argv)
|
|
631
692
|
opts[:args] = positional
|
|
632
|
-
|
|
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`
|
|
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
|
-
|
|
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 <<
|
|
723
|
+
parts << o.switch if val
|
|
659
724
|
else
|
|
660
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
992
|
+
unless local.empty?
|
|
916
993
|
Shell.say ''
|
|
917
994
|
Shell.say 'Options:', :yellow
|
|
918
|
-
|
|
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
|
-
#
|
|
20
|
-
#
|
|
21
|
-
|
|
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
|
-
#
|
|
31
|
-
#
|
|
32
|
-
LuxDeploy::Hammer.register(self
|
|
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)
|