lux-hammer 0.3.12 → 0.3.14
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 +49 -40
- data/README.md +61 -44
- data/gui/Hammer.app/Contents/Info.plist +16 -0
- data/gui/Hammer.app/Contents/MacOS/HammerGUI +0 -0
- data/lib/hammer/builtins.rb +64 -33
- data/lib/hammer/command.rb +20 -0
- data/lib/hammer/dotenv.rb +3 -2
- data/lib/hammer/loader.rb +13 -7
- data/lib/hammer/option.rb +21 -0
- data/lib/hammer/parser.rb +40 -10
- data/lib/hammer/recipe.rb +2 -2
- data/lib/hammer/shell.rb +5 -2
- data/lib/lux-hammer.rb +149 -64
- data/recipes/deploy.rb +32 -0
- data/recipes/git-helper.rb +2 -2
- data/recipes/lib/deploy/boot.rb +52 -0
- data/recipes/lib/deploy/commands.rb +555 -0
- data/recipes/lib/deploy/config.rb +62 -0
- data/recipes/lib/deploy/context.rb +149 -0
- data/recipes/lib/deploy/doctor.rb +238 -0
- data/recipes/lib/deploy/hammer.rb +168 -0
- data/recipes/lib/deploy/manifest.rb +169 -0
- data/recipes/lib/deploy/ssh.rb +129 -0
- data/recipes/lib/deploy/template.rb +39 -0
- metadata +14 -2
data/lib/hammer/parser.rb
CHANGED
|
@@ -8,9 +8,9 @@ class Hammer
|
|
|
8
8
|
@options = options
|
|
9
9
|
@by_switch = {}
|
|
10
10
|
options.each do |opt|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
opt.aliases.each { |a|
|
|
11
|
+
register_switch(opt.switch, opt)
|
|
12
|
+
register_switch(opt.negation, opt) if opt.boolean?
|
|
13
|
+
opt.aliases.each { |a| register_switch(a, opt) }
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -31,7 +31,11 @@ class Hammer
|
|
|
31
31
|
if token.start_with?('--') && token.include?('=')
|
|
32
32
|
key, val = token.split('=', 2)
|
|
33
33
|
opt = lookup!(key)
|
|
34
|
-
|
|
34
|
+
if opt.boolean? && key.start_with?('--no-')
|
|
35
|
+
values[opt.name] = !opt.cast(val) # `--no-x=false` -> true
|
|
36
|
+
else
|
|
37
|
+
values[opt.name] = opt.cast(val)
|
|
38
|
+
end
|
|
35
39
|
i += 1
|
|
36
40
|
next
|
|
37
41
|
end
|
|
@@ -50,7 +54,18 @@ class Hammer
|
|
|
50
54
|
next
|
|
51
55
|
end
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
# Glued short flag with value: `-pVALUE` (non-boolean short opts only).
|
|
58
|
+
if token.start_with?('-') && !token.start_with?('--') && token.length > 2
|
|
59
|
+
if (opt = @by_switch[token[0, 2]]) && !opt.boolean?
|
|
60
|
+
values[opt.name] = opt.cast(token[2..])
|
|
61
|
+
i += 1
|
|
62
|
+
next
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Dash-led and not a negative number -> a genuinely unknown flag.
|
|
67
|
+
# Bare `-` and negative numbers (`-5`) fall through to positionals.
|
|
68
|
+
if token.start_with?('-') && token.length > 1 && token !~ /\A-\d/
|
|
54
69
|
raise Error, "unknown option: #{token}"
|
|
55
70
|
end
|
|
56
71
|
|
|
@@ -59,15 +74,21 @@ class Hammer
|
|
|
59
74
|
end
|
|
60
75
|
|
|
61
76
|
# Fill un-set non-boolean opts from positional args in declaration
|
|
62
|
-
# order. Booleans always need an explicit flag.
|
|
77
|
+
# order. Booleans always need an explicit flag. Scalars take one
|
|
78
|
+
# positional each; an :array opt slurps whatever's left, so it's
|
|
79
|
+
# filled last and never starves a later-declared scalar opt.
|
|
80
|
+
array_opt = nil
|
|
63
81
|
@options.each do |opt|
|
|
64
|
-
break if positional.empty?
|
|
65
82
|
next if opt.boolean? || values.key?(opt.name)
|
|
66
83
|
if opt.type == :array
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
values[opt.name] = opt.cast(positional.shift)
|
|
84
|
+
array_opt = opt
|
|
85
|
+
next
|
|
70
86
|
end
|
|
87
|
+
break if positional.empty?
|
|
88
|
+
values[opt.name] = opt.cast(positional.shift)
|
|
89
|
+
end
|
|
90
|
+
if array_opt && !positional.empty?
|
|
91
|
+
values[array_opt.name] = array_opt.cast(positional.shift(positional.size))
|
|
71
92
|
end
|
|
72
93
|
|
|
73
94
|
@options.each do |opt|
|
|
@@ -80,6 +101,15 @@ class Hammer
|
|
|
80
101
|
|
|
81
102
|
private
|
|
82
103
|
|
|
104
|
+
# Map a flag string to its option, refusing silent shadowing when two
|
|
105
|
+
# options would claim the same switch/negation/alias.
|
|
106
|
+
def register_switch(flag, opt)
|
|
107
|
+
if (prev = @by_switch[flag]) && prev != opt
|
|
108
|
+
raise Error, "flag #{flag} claimed by both :#{prev.name} and :#{opt.name}"
|
|
109
|
+
end
|
|
110
|
+
@by_switch[flag] = opt
|
|
111
|
+
end
|
|
112
|
+
|
|
83
113
|
def lookup!(key)
|
|
84
114
|
@by_switch[key] or raise Error, "unknown option: #{key}"
|
|
85
115
|
end
|
data/lib/hammer/recipe.rb
CHANGED
|
@@ -49,14 +49,14 @@ class Hammer
|
|
|
49
49
|
''
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
# Ruby wrapper text printed by `hammer recipes --install`. User
|
|
52
|
+
# Ruby wrapper text printed by `hammer h:recipes --install`. User
|
|
53
53
|
# redirects it to a file in PATH and chmods +x. The leading comment
|
|
54
54
|
# documents the canonical install command. Name is passed as a
|
|
55
55
|
# string literal so hyphenated names (`git-helper`) work too.
|
|
56
56
|
def stub(name)
|
|
57
57
|
<<~RUBY
|
|
58
58
|
#!/usr/bin/env ruby
|
|
59
|
-
# install: hammer recipes --install #{name} > ~/bin/#{name} && chmod +x $_
|
|
59
|
+
# install: hammer h:recipes --install #{name} > ~/bin/#{name} && chmod +x $_
|
|
60
60
|
require 'lux-hammer'
|
|
61
61
|
Hammer.recipe('#{name}', ARGV)
|
|
62
62
|
RUBY
|
data/lib/hammer/shell.rb
CHANGED
|
@@ -13,8 +13,11 @@ class Hammer
|
|
|
13
13
|
module_function
|
|
14
14
|
|
|
15
15
|
def color?
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# Only an explicit color!(value) override is sticky; otherwise the
|
|
17
|
+
# tty decision is recomputed so a redirected $stdout (tests, capture
|
|
18
|
+
# blocks) is honored instead of frozen at first read.
|
|
19
|
+
return @color if defined?(@color) && !@color.nil?
|
|
20
|
+
$stdout.tty? && ENV['NO_COLOR'].nil?
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def color!(value)
|
data/lib/lux-hammer.rb
CHANGED
|
@@ -94,8 +94,8 @@ class Hammer
|
|
|
94
94
|
# If the method takes no args, call it without opts. Otherwise pass
|
|
95
95
|
# opts. So both `def build` and `def build(opts)` work.
|
|
96
96
|
m = method_name
|
|
97
|
-
|
|
98
|
-
cmd.handler =
|
|
97
|
+
takes_arg = instance_method(method_name).parameters.any? { |type, _| %i[req opt rest].include?(type) }
|
|
98
|
+
cmd.handler = takes_arg ? proc { |opts| send(m, opts) } : proc { send(m) }
|
|
99
99
|
cmd.finalize!
|
|
100
100
|
commands[cmd.name] = cmd
|
|
101
101
|
|
|
@@ -413,6 +413,13 @@ class Hammer
|
|
|
413
413
|
return print_help
|
|
414
414
|
end
|
|
415
415
|
|
|
416
|
+
# An exact namespace beats a fuzzy command match, so `hammer h` lists
|
|
417
|
+
# the `h:` namespace instead of prefix-matching some `h...` command.
|
|
418
|
+
if !commands.key?(name) && namespaces.key?(name)
|
|
419
|
+
ns, canonical = resolve_namespace(name)
|
|
420
|
+
return print_namespace_help(canonical, ns)
|
|
421
|
+
end
|
|
422
|
+
|
|
416
423
|
cmd, owner, canonical = resolve(name)
|
|
417
424
|
return owner.run_command(cmd, argv, full: canonical) if cmd
|
|
418
425
|
|
|
@@ -518,6 +525,7 @@ class Hammer
|
|
|
518
525
|
# Tries prefix match first, then substring; raises AmbiguousMatch
|
|
519
526
|
# when either pass hits more than one item.
|
|
520
527
|
def fuzzy_pick(name, items, kind, &keys_for)
|
|
528
|
+
return nil if name.empty?
|
|
521
529
|
[:start_with?, :include?].each do |op|
|
|
522
530
|
matches = items.select { |item| keys_for.call(item).any? { |k| k.send(op, name) } }
|
|
523
531
|
next if matches.empty?
|
|
@@ -548,24 +556,66 @@ class Hammer
|
|
|
548
556
|
opts.each do |k, v|
|
|
549
557
|
next if v == false
|
|
550
558
|
flag = "--#{k.to_s.tr('_', '-')}"
|
|
551
|
-
|
|
559
|
+
if v == true
|
|
560
|
+
argv << flag
|
|
561
|
+
else
|
|
562
|
+
argv << "#{flag}=#{v.is_a?(Array) ? v.join(',') : v}"
|
|
563
|
+
end
|
|
552
564
|
end
|
|
553
565
|
start(argv)
|
|
554
566
|
end
|
|
555
567
|
|
|
556
568
|
# Yield [full_colon_path, Command] for every command in this class
|
|
557
|
-
# and all nested namespaces.
|
|
558
|
-
|
|
569
|
+
# and all nested namespaces. `include_builtins: false` prunes
|
|
570
|
+
# namespaces flagged `@builtin_namespace` (the reserved `h:` tree) -
|
|
571
|
+
# used so the compact listing hides built-ins outside `--help`. Only
|
|
572
|
+
# affects descent from a parent; iterating a flagged namespace
|
|
573
|
+
# directly (e.g. `hammer h:`) still lists its own commands.
|
|
574
|
+
def each_command(prefix = nil, include_builtins: true, &block)
|
|
559
575
|
commands.each_value do |c|
|
|
560
576
|
full = prefix ? "#{prefix}:#{c.name}" : c.name
|
|
561
577
|
yield full, c
|
|
562
578
|
end
|
|
563
579
|
namespaces.each do |ns_name, sub|
|
|
580
|
+
next if !include_builtins && sub.instance_variable_get(:@builtin_namespace)
|
|
564
581
|
sub_prefix = prefix ? "#{prefix}:#{ns_name}" : ns_name
|
|
565
|
-
sub.each_command(sub_prefix, &block)
|
|
582
|
+
sub.each_command(sub_prefix, include_builtins: include_builtins, &block)
|
|
566
583
|
end
|
|
567
584
|
end
|
|
568
585
|
|
|
586
|
+
# Machine-readable spec for `h:json` -> the macOS GUI (and, later,
|
|
587
|
+
# for lux itself to render the default listing). One hash:
|
|
588
|
+
# commands => { group => { full_path => task_meta } }
|
|
589
|
+
# Grouping/sort mirror the bare-`hammer` listing exactly: group by
|
|
590
|
+
# the first namespace segment (a bare task sharing a namespace's name
|
|
591
|
+
# joins that group via section_for), root tasks under "__root",
|
|
592
|
+
# "__root" first, remaining groups in first-encounter order, tasks
|
|
593
|
+
# within a group by [depth, name]. Hidden (no-`desc`) tasks are
|
|
594
|
+
# skipped and the reserved `h:` tree is pruned unless include_builtins.
|
|
595
|
+
def export_spec(include_builtins: false)
|
|
596
|
+
groups = {} # group => { full_path => meta }, in first-encounter order
|
|
597
|
+
|
|
598
|
+
each_command(include_builtins: include_builtins) do |path, c|
|
|
599
|
+
next if c.desc.empty?
|
|
600
|
+
section = section_for(path, nil, self)
|
|
601
|
+
key = section == :root ? '__root' : section.to_s
|
|
602
|
+
(groups[key] ||= {})[path] = c.to_h(path)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
sort_tasks = ->(h) { h.sort_by { |p, _| [p.count(':'), p] }.to_h }
|
|
606
|
+
ordered = {}
|
|
607
|
+
ordered['__root'] = sort_tasks.call(groups.delete('__root')) if groups.key?('__root')
|
|
608
|
+
groups.each { |k, v| ordered[k] = sort_tasks.call(v) }
|
|
609
|
+
|
|
610
|
+
{
|
|
611
|
+
schema: 1,
|
|
612
|
+
hammer_version: VERSION,
|
|
613
|
+
program_name: program_name,
|
|
614
|
+
app_desc: app_desc,
|
|
615
|
+
commands: ordered
|
|
616
|
+
}
|
|
617
|
+
end
|
|
618
|
+
|
|
569
619
|
def run_command(cmd, argv, full: nil, quiet: false)
|
|
570
620
|
# -h / --help is reserved on every command. Anywhere before a `--`
|
|
571
621
|
# stop-marker, it short-circuits to per-command help.
|
|
@@ -601,17 +651,22 @@ class Hammer
|
|
|
601
651
|
if o.boolean?
|
|
602
652
|
parts << (val ? "--#{o.name}" : "--no-#{o.name}")
|
|
603
653
|
else
|
|
604
|
-
parts << "--#{o.name}=#{val}"
|
|
654
|
+
parts << "--#{o.name}=#{val.is_a?(Array) ? val.join(',') : val}"
|
|
605
655
|
end
|
|
606
656
|
end
|
|
607
657
|
parts.concat(positional)
|
|
608
|
-
|
|
658
|
+
# Diagnostic, not program output - to stderr so stdout stays clean
|
|
659
|
+
# for machine-readable tasks (`h:json`, `h:version`) and pipes.
|
|
660
|
+
warn Shell.paint("> #{parts.join(' ')}", :gray)
|
|
609
661
|
end
|
|
610
662
|
|
|
611
663
|
# Fire `before` hooks from root down through the namespace chain.
|
|
612
664
|
# Each class's hooks fire at most once per top-level `start`, so
|
|
613
665
|
# prereqs dispatched via `needs` won't re-trigger them.
|
|
614
666
|
def run_before_hooks(instance, opts)
|
|
667
|
+
# Built-in `h:` meta-commands parent to the project root but must not
|
|
668
|
+
# trigger the project's own `before` hooks (dotenv, env checks, ...).
|
|
669
|
+
return if instance_variable_get(:@builtin_namespace)
|
|
615
670
|
ran = Thread.current[:hammer_before_ran] ||= {}
|
|
616
671
|
ancestor_chain.each do |klass|
|
|
617
672
|
next if ran[klass.object_id]
|
|
@@ -661,15 +716,29 @@ class Hammer
|
|
|
661
716
|
|
|
662
717
|
print_top_banner
|
|
663
718
|
Shell.say "Usage: #{program_name} COMMAND [ARGS]", :cyan
|
|
719
|
+
# Compact (bare-invocation) view only - the extended `--help` view
|
|
720
|
+
# already IS the full usage, so don't nag about it there.
|
|
721
|
+
unless extended
|
|
722
|
+
Shell.say "add `--help` to show usage help", :gray
|
|
723
|
+
# No project Hammerfile + no custom tasks loaded: point the user at
|
|
724
|
+
# `h:init`. The flag is set by `Hammer.cli` when the lookup misses.
|
|
725
|
+
if instance_variable_get(:@no_hammerfile)
|
|
726
|
+
Shell.say "no Hammerfile found in #{Dir.pwd} - run `#{program_name} h:init` to create one", :gray
|
|
727
|
+
end
|
|
728
|
+
end
|
|
664
729
|
if @app_desc && !@app_desc.empty?
|
|
665
730
|
Shell.say ''
|
|
666
731
|
@app_desc.each_line { |l| Shell.say " #{l.chomp}" }
|
|
667
732
|
end
|
|
733
|
+
# Built-in `h:` commands only surface in the extended view
|
|
734
|
+
# (`--help` / `-h` / `help`); the bare-invocation listing stays
|
|
735
|
+
# focused on the project's own tasks. They remain dispatchable
|
|
736
|
+
# regardless - this only governs what the listing shows.
|
|
668
737
|
if expanded
|
|
669
|
-
each_command { |path, c| print_full_block(path, c) unless c.desc.empty? }
|
|
738
|
+
each_command(include_builtins: extended) { |path, c| print_full_block(path, c) unless c.desc.empty? }
|
|
670
739
|
else
|
|
671
740
|
Shell.say ''
|
|
672
|
-
print_command_list(
|
|
741
|
+
print_command_list(include_builtins: extended)
|
|
673
742
|
end
|
|
674
743
|
print_recipes_section if extended && root.instance_variable_get(:@hammer_binary)
|
|
675
744
|
print_extras if extended
|
|
@@ -688,7 +757,7 @@ class Hammer
|
|
|
688
757
|
entries.each do |name, file|
|
|
689
758
|
desc = Hammer::Recipe.desc(file)
|
|
690
759
|
installed = Hammer::Recipe.installed_path(name)
|
|
691
|
-
suffix = installed ? "(installed: #{installed})" : "[install: #{program_name} recipes --install #{name}]"
|
|
760
|
+
suffix = installed ? "(installed: #{installed})" : "[install: #{program_name} h:recipes --install #{name}]"
|
|
692
761
|
Shell.say " #{name.ljust(width)} # #{desc}"
|
|
693
762
|
Shell.say " #{' ' * width} #{suffix}", :gray
|
|
694
763
|
end
|
|
@@ -702,8 +771,8 @@ class Hammer
|
|
|
702
771
|
Shell.say "Usage: #{program_name} #{prefix}:COMMAND [ARGS]", :cyan
|
|
703
772
|
rows = []
|
|
704
773
|
sibling = find_namespace_sibling(prefix)
|
|
705
|
-
rows << [prefix, sibling] if sibling && !sibling.desc.empty?
|
|
706
|
-
ns.each_command(prefix) { |path, c| rows << [path, c] unless c.desc.empty? }
|
|
774
|
+
rows << [prefix, sibling.to_h(prefix)] if sibling && !sibling.desc.empty?
|
|
775
|
+
ns.each_command(prefix) { |path, c| rows << [path, c.to_h(path)] unless c.desc.empty? }
|
|
707
776
|
unless rows.empty?
|
|
708
777
|
Shell.say ''
|
|
709
778
|
Shell.say 'Commands:', :yellow
|
|
@@ -761,45 +830,37 @@ class Hammer
|
|
|
761
830
|
|
|
762
831
|
# Hammerfile cheat-sheet shown under `hammer --help`. Same content
|
|
763
832
|
# as `hammer --init` writes - single source of truth via
|
|
764
|
-
# `Hammer::STARTER_HAMMERFILE`. For exhaustive docs see `hammer agents`.
|
|
833
|
+
# `Hammer::STARTER_HAMMERFILE`. For exhaustive docs see `hammer h:agents`.
|
|
765
834
|
def print_hammerfile_example
|
|
766
835
|
Shell.say ''
|
|
767
836
|
Shell.say 'Hammerfile example:', :yellow
|
|
768
837
|
Shell.say Hammer::STARTER_HAMMERFILE
|
|
769
838
|
end
|
|
770
839
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
Shell.say 'Commands:', :yellow
|
|
787
|
-
emit_rows(rooted.sort_by { |full, _| [full.count(':'), full] }, width)
|
|
788
|
-
first = false
|
|
789
|
-
end
|
|
790
|
-
|
|
791
|
-
groups.each do |section, items|
|
|
792
|
-
Shell.say unless first
|
|
793
|
-
first = false
|
|
794
|
-
Shell.say "#{section}:", :yellow
|
|
795
|
-
emit_rows(items.sort_by { |full, _| [full.count(':'), full] }, width)
|
|
840
|
+
# Pure rendering off `export_spec` - the same grouped structure
|
|
841
|
+
# `h:json` emits, so the listing and the JSON can never drift.
|
|
842
|
+
# `export_spec` already does the work: drops hidden (no-`desc`)
|
|
843
|
+
# tasks, prunes the `h:` tree unless include_builtins, groups by
|
|
844
|
+
# first namespace segment ("__root" for bare tasks), orders "__root"
|
|
845
|
+
# first, and sorts each group by [depth, name].
|
|
846
|
+
def print_command_list(include_builtins: true)
|
|
847
|
+
groups = export_spec(include_builtins: include_builtins)[:commands]
|
|
848
|
+
return if groups.empty?
|
|
849
|
+
|
|
850
|
+
width = groups.values.flat_map(&:keys).map(&:length).max
|
|
851
|
+
groups.each_with_index do |(section, tasks), i|
|
|
852
|
+
Shell.say unless i.zero?
|
|
853
|
+
Shell.say(section == '__root' ? 'Commands:' : "#{section}:", :yellow)
|
|
854
|
+
emit_rows(tasks.to_a, width)
|
|
796
855
|
end
|
|
797
856
|
end
|
|
798
857
|
|
|
858
|
+
# `rows` is an array of [full_path, task_meta] - the per-task hashes
|
|
859
|
+
# from `Command#to_h` (also used to render namespace listings).
|
|
799
860
|
def emit_rows(rows, width)
|
|
800
|
-
rows.each do |full,
|
|
801
|
-
brief =
|
|
802
|
-
brief = "#{brief} #{Shell.paint('(redefined)', :yellow)}" if
|
|
861
|
+
rows.each do |full, t|
|
|
862
|
+
brief = t[:alts].empty? ? t[:brief] : "#{t[:brief]} (alt: #{t[:alts].join(', ')})"
|
|
863
|
+
brief = "#{brief} #{Shell.paint('(redefined)', :yellow)}" if t[:redefined]
|
|
803
864
|
Shell.say " #{program_name} #{full.ljust(width)} # #{brief}"
|
|
804
865
|
end
|
|
805
866
|
end
|
|
@@ -911,7 +972,7 @@ class Hammer
|
|
|
911
972
|
Shell.print_error "unknown recipe: #{name}"
|
|
912
973
|
Shell.say 'available recipes:', :yellow
|
|
913
974
|
Recipe.all.keys.sort.each { |n| Shell.say " #{n}" }
|
|
914
|
-
Shell.say 'try `hammer recipes` to list with descriptions', :gray
|
|
975
|
+
Shell.say 'try `hammer h:recipes` to list with descriptions', :gray
|
|
915
976
|
exit 1
|
|
916
977
|
end
|
|
917
978
|
|
|
@@ -967,12 +1028,12 @@ class Hammer
|
|
|
967
1028
|
end
|
|
968
1029
|
RUBY
|
|
969
1030
|
|
|
970
|
-
# Default install dir used by install.sh and `hammer update`.
|
|
1031
|
+
# Default install dir used by install.sh and `hammer h:update`.
|
|
971
1032
|
SELF_UPDATE_DIR ||= File.expand_path('~/.local/share/lux-hammer')
|
|
972
1033
|
SELF_UPDATE_REPO ||= 'https://github.com/dux/hammer.git'
|
|
973
1034
|
SELF_INSTALL_URL ||= 'https://raw.githubusercontent.com/dux/hammer/main/install.sh'
|
|
974
1035
|
|
|
975
|
-
# `hammer update`: pull main in the install-script checkout and
|
|
1036
|
+
# `hammer h:update`: pull main in the install-script checkout and
|
|
976
1037
|
# reinstall the gem. Assumes the install.sh layout - if the dir is
|
|
977
1038
|
# missing, point the user at the curl-pipe installer.
|
|
978
1039
|
def self.self_update
|
|
@@ -1013,6 +1074,7 @@ class Hammer
|
|
|
1013
1074
|
def self.cli(argv = ARGV)
|
|
1014
1075
|
argv = argv.dup
|
|
1015
1076
|
force_system = !!argv.delete('--system')
|
|
1077
|
+
launch_gui = !!argv.delete('--gui')
|
|
1016
1078
|
|
|
1017
1079
|
# Shebang invocation: `hammer /path/to/script ...args` (kernel passes
|
|
1018
1080
|
# the script path as argv[0] for `#!/usr/bin/env hammer` files).
|
|
@@ -1026,17 +1088,25 @@ class Hammer
|
|
|
1026
1088
|
end
|
|
1027
1089
|
|
|
1028
1090
|
path = force_system ? nil : find_hammerfile(Dir.pwd)
|
|
1091
|
+
|
|
1092
|
+
# `hammer --gui` opens the native macOS runner pointed at this project
|
|
1093
|
+
# (the Hammerfile's dir, or cwd when none was found). The CLI just
|
|
1094
|
+
# launches the bundled app and returns.
|
|
1095
|
+
return launch_gui!(path ? File.dirname(path) : Dir.pwd) if launch_gui
|
|
1096
|
+
|
|
1029
1097
|
unless path
|
|
1030
1098
|
# No Hammerfile (or --system) - all built-ins are reachable. Bare
|
|
1031
|
-
# `hammer`, `hammer recipes`, `hammer update`, `hammer agents`,
|
|
1032
|
-
# `hammer version`, `hammer init` all work.
|
|
1099
|
+
# `hammer`, `hammer h:recipes`, `hammer h:update`, `hammer h:agents`,
|
|
1100
|
+
# `hammer h:version`, `hammer h:init` all work.
|
|
1033
1101
|
if force_system || dispatches_to_builtin?(argv) || looks_like_builtin?(argv)
|
|
1034
1102
|
klass = Class.new(Hammer)
|
|
1035
1103
|
klass.instance_variable_set(:@hammer_binary, true)
|
|
1104
|
+
# No project Hammerfile was found - only built-ins are loaded. The
|
|
1105
|
+
# bare-invocation help uses this to note that no Hammerfile exists.
|
|
1106
|
+
klass.instance_variable_set(:@no_hammerfile, true)
|
|
1036
1107
|
klass.program_name
|
|
1037
1108
|
require_relative 'hammer/builtins'
|
|
1038
|
-
Hammer::Builtins.
|
|
1039
|
-
Hammer::Builtins.register_no_project(klass)
|
|
1109
|
+
Hammer::Builtins.register(klass)
|
|
1040
1110
|
klass.start(argv)
|
|
1041
1111
|
return
|
|
1042
1112
|
end
|
|
@@ -1060,8 +1130,8 @@ class Hammer
|
|
|
1060
1130
|
Shell.say STARTER_HAMMERFILE
|
|
1061
1131
|
Shell.say ''
|
|
1062
1132
|
bin = File.basename($PROGRAM_NAME)
|
|
1063
|
-
Shell.say "tip: run `#{bin} init` to drop the example above into ./Hammerfile", :gray
|
|
1064
|
-
Shell.say "tip: run `#{bin} agents` for AI-friendly Hammerfile authoring docs", :gray
|
|
1133
|
+
Shell.say "tip: run `#{bin} h:init` to drop the example above into ./Hammerfile", :gray
|
|
1134
|
+
Shell.say "tip: run `#{bin} h:agents` for AI-friendly Hammerfile authoring docs", :gray
|
|
1065
1135
|
exit 1
|
|
1066
1136
|
end
|
|
1067
1137
|
|
|
@@ -1082,14 +1152,13 @@ class Hammer
|
|
|
1082
1152
|
# are NOT visible during Hammerfile evaluation, only inside handlers.
|
|
1083
1153
|
Hammer::Dotenv.load(Dir.pwd) if klass.dotenv_enabled?
|
|
1084
1154
|
|
|
1085
|
-
#
|
|
1086
|
-
#
|
|
1087
|
-
#
|
|
1088
|
-
# `
|
|
1089
|
-
#
|
|
1090
|
-
# `hammer --system recipes` to reach them from inside a project.
|
|
1155
|
+
# Built-ins register AFTER Hammerfile eval so user-defined tasks win
|
|
1156
|
+
# (the `unless commands.key?(...)` guards skip a built-in when the
|
|
1157
|
+
# Hammerfile already owns the name - no redefinition warning). All
|
|
1158
|
+
# built-ins live under `h:`, so they can't collide with project root
|
|
1159
|
+
# tasks and the full set registers in every context.
|
|
1091
1160
|
require_relative 'hammer/builtins'
|
|
1092
|
-
Hammer::Builtins.
|
|
1161
|
+
Hammer::Builtins.register(klass)
|
|
1093
1162
|
|
|
1094
1163
|
klass.start(argv)
|
|
1095
1164
|
end
|
|
@@ -1109,7 +1178,7 @@ class Hammer
|
|
|
1109
1178
|
end
|
|
1110
1179
|
|
|
1111
1180
|
# Evaluate a shebang script as a self-contained CLI. Mirrors `recipe`
|
|
1112
|
-
# semantics: no chdir, no `@hammer_binary` flag, no `
|
|
1181
|
+
# semantics: no chdir, no `@hammer_binary` flag, no `Builtins.register`
|
|
1113
1182
|
# built-ins (so the script's `--help` shows only what it defines).
|
|
1114
1183
|
# `program_name` is the script's basename so help reads "myscript foo"
|
|
1115
1184
|
# rather than "hammer foo" - works even when invoked via a symlink in
|
|
@@ -1131,15 +1200,14 @@ class Hammer
|
|
|
1131
1200
|
first == 'help' || first == '-h' || first == '--help' || first.start_with?('-')
|
|
1132
1201
|
end
|
|
1133
1202
|
|
|
1134
|
-
# True if argv
|
|
1135
|
-
# `
|
|
1136
|
-
# built-ins for invocations like `hammer recipes` that aren't a flag
|
|
1203
|
+
# True if argv targets the reserved `h:` built-in namespace (`h`, `h:`,
|
|
1204
|
+
# `h:update`, ...). Used in the no-Hammerfile branch to wake up the
|
|
1205
|
+
# built-ins for invocations like `hammer h:recipes` that aren't a flag
|
|
1137
1206
|
# or help request.
|
|
1138
|
-
BUILTIN_TASKS ||= %w[recipes update agents version init].freeze
|
|
1139
1207
|
def self.looks_like_builtin?(argv)
|
|
1140
1208
|
first = argv.first
|
|
1141
1209
|
return false unless first
|
|
1142
|
-
|
|
1210
|
+
first == 'h' || first.start_with?('h:')
|
|
1143
1211
|
end
|
|
1144
1212
|
|
|
1145
1213
|
# Walk up the directory tree looking for a Hammerfile.
|
|
@@ -1154,4 +1222,21 @@ class Hammer
|
|
|
1154
1222
|
end
|
|
1155
1223
|
end
|
|
1156
1224
|
|
|
1225
|
+
# Spawn the vendored macOS GUI (gui/Hammer.app), pointed at the project
|
|
1226
|
+
# dir and this hammer binary. Launched directly (not via `open`) so it
|
|
1227
|
+
# inherits the caller's environment - the GUI shells back out to this
|
|
1228
|
+
# same `hammer` for `h:json` and task runs, and that needs the same PATH.
|
|
1229
|
+
def self.launch_gui!(project_dir)
|
|
1230
|
+
bin = File.expand_path('../gui/Hammer.app/Contents/MacOS/HammerGUI', __dir__)
|
|
1231
|
+
unless File.executable?(bin)
|
|
1232
|
+
Shell.print_error "GUI app not found at #{bin}"
|
|
1233
|
+
Shell.say 'build it: ./gui/HammerGUI/build_app.sh', :yellow
|
|
1234
|
+
exit 1
|
|
1235
|
+
end
|
|
1236
|
+
hammer_bin = (File.realpath($PROGRAM_NAME) rescue File.expand_path($PROGRAM_NAME))
|
|
1237
|
+
pid = Process.spawn(bin, '--project', File.expand_path(project_dir), '--hammer', hammer_bin)
|
|
1238
|
+
Process.detach(pid)
|
|
1239
|
+
Shell.say "launched Hammer GUI for #{project_dir} (pid #{pid})", :green
|
|
1240
|
+
end
|
|
1241
|
+
|
|
1157
1242
|
end
|
data/recipes/deploy.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env hammer
|
|
2
|
+
# desc: SSH/rsync deploy - Caddy + systemd + atomic releases
|
|
3
|
+
|
|
4
|
+
desc <<~TXT
|
|
5
|
+
Stupid-simple SSH/rsync deploy (Caddy + systemd + atomic releases).
|
|
6
|
+
|
|
7
|
+
Quickstart:
|
|
8
|
+
deploy app:init # copy templates into ./config/deploy/
|
|
9
|
+
deploy doctor # check & prep the host
|
|
10
|
+
deploy up # deploy current branch
|
|
11
|
+
|
|
12
|
+
Server:
|
|
13
|
+
deploy server:log # tail the systemd journal
|
|
14
|
+
deploy server:ssh # shell into the current release
|
|
15
|
+
deploy server:restart # restart the web service
|
|
16
|
+
deploy log --log errors # dump a remote log
|
|
17
|
+
TXT
|
|
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'
|
|
22
|
+
|
|
23
|
+
# Auto-load the app's deploy bootstrap, if present, before tasks fire.
|
|
24
|
+
# A consumer can inject Ruby (e.g. a pre-deploy hook) without writing a
|
|
25
|
+
# custom Hammerfile.
|
|
26
|
+
init = File.join(Dir.pwd, 'config', 'deploy', 'init.rb')
|
|
27
|
+
load init if File.file?(init)
|
|
28
|
+
|
|
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'))
|
data/recipes/git-helper.rb
CHANGED
|
@@ -142,8 +142,8 @@ helpers do
|
|
|
142
142
|
if message.empty?
|
|
143
143
|
run 'git reset --mixed'
|
|
144
144
|
exit
|
|
145
|
-
elsif message.length <
|
|
146
|
-
say 'Please add better commit message, min length
|
|
145
|
+
elsif message.length < 1
|
|
146
|
+
say 'Please add better commit message, min length 1 chars', :red
|
|
147
147
|
next
|
|
148
148
|
else
|
|
149
149
|
bump_version
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'pathname'
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'set'
|
|
5
|
+
require 'shellwords'
|
|
6
|
+
require 'yaml'
|
|
7
|
+
|
|
8
|
+
module LuxDeploy
|
|
9
|
+
# Recipe layout: this file and its siblings live under
|
|
10
|
+
# recipes/lib/deploy/, so ROOT points here and `templates/` sits
|
|
11
|
+
# right next to us (no gem root anymore).
|
|
12
|
+
ROOT ||= Pathname.new(__dir__)
|
|
13
|
+
VERSION ||= '0.2.0'
|
|
14
|
+
|
|
15
|
+
# Branches that select `.env` instead of `.env.staging`.
|
|
16
|
+
MAIN_BRANCHES ||= %w[master main]
|
|
17
|
+
|
|
18
|
+
# Server-side conventions. Not config-tunable because doctor and the
|
|
19
|
+
# deploy flow both hardcode these paths in the host setup. A different
|
|
20
|
+
# caddy/systemd layout means a different recipe.
|
|
21
|
+
PORT_RANGE ||= (3010..3990).step(10).to_a
|
|
22
|
+
CADDY_SITES ||= '/etc/caddy/sites'
|
|
23
|
+
SYSTEMD_DIR ||= '/etc/systemd/system'
|
|
24
|
+
|
|
25
|
+
class Error < StandardError
|
|
26
|
+
def to_s
|
|
27
|
+
"ERROR: #{super}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Host-supplied defaults that sit under the user's .yaml. Set once by a
|
|
32
|
+
# wrapping plugin/Hammerfile (e.g. lux-fw seeds 'lux-web' / 'lux-apps'),
|
|
33
|
+
# consumed by Config.new. Empty by default so the recipe stays "generic".
|
|
34
|
+
@defaults = {}
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
attr_reader :defaults
|
|
38
|
+
|
|
39
|
+
def set_defaults(hash)
|
|
40
|
+
@defaults = (hash || {}).each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
require_relative 'config'
|
|
46
|
+
require_relative 'ssh'
|
|
47
|
+
require_relative 'template'
|
|
48
|
+
require_relative 'doctor'
|
|
49
|
+
require_relative 'context'
|
|
50
|
+
require_relative 'manifest'
|
|
51
|
+
require_relative 'commands'
|
|
52
|
+
require_relative 'hammer'
|