lux-hammer 0.3.13 → 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 +3 -0
- data/README.md +14 -0
- data/gui/Hammer.app/Contents/Info.plist +16 -0
- data/gui/Hammer.app/Contents/MacOS/HammerGUI +0 -0
- data/lib/hammer/builtins.rb +24 -0
- data/lib/hammer/command.rb +20 -0
- data/lib/hammer/option.rb +19 -0
- data/lib/lux-hammer.rb +83 -32
- data/recipes/git-helper.rb +2 -2
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ea304583caa18c45db8f340aecf5e08803f23eebe4f9152eefec7ac77c64b18
|
|
4
|
+
data.tar.gz: 72eafeef47b8ae8c2ff41e226756199f085865899532c3d7669dd5025c4b4019
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7cfa688c6d9d21acc72a6c8d614acefe2942746fb8b8b55f4f72fb08d9f1d37a6eccbd3326551348ea572889e3404790c4a59f64577d394f257e1ab826959c45
|
|
7
|
+
data.tar.gz: 535fb35b15ae31e9e861ad0ff32d9e216382b533cb877b12560185cd722724c0d01a0dde350facebf209178166d88c4109328e7882ba6a14ae0273eb111d2215
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.14
|
data/AGENTS.md
CHANGED
|
@@ -279,6 +279,9 @@ Task contracts:
|
|
|
279
279
|
forward flags through). Bare invocation lists.
|
|
280
280
|
* `h:init` - writes `Hammer::STARTER_HAMMERFILE` to `./Hammerfile`;
|
|
281
281
|
refuses if one exists.
|
|
282
|
+
* `h:json` - `puts JSON` of `root.export_spec` (tasks grouped exactly
|
|
283
|
+
like the bare listing via `section_for`, root group keyed `__root`).
|
|
284
|
+
`--all` keeps the `h:` tree, `--compact` minifies.
|
|
282
285
|
|
|
283
286
|
The `:default` task and the `help` / `-h` / `--help` requests are
|
|
284
287
|
invoked via `run_command(cmd, argv, full: name, quiet: true)` - the
|
data/README.md
CHANGED
|
@@ -562,6 +562,10 @@ Under the `h:` namespace:
|
|
|
562
562
|
* `h:version` - print the lux-hammer version.
|
|
563
563
|
* `h:recipes` - list / install / show / edit recipes.
|
|
564
564
|
* `h:init` - write a starter Hammerfile in cwd (refuses if one exists).
|
|
565
|
+
* `h:json` - dump the CLI definition as JSON (tasks grouped like the
|
|
566
|
+
bare listing, with desc/options/examples/aliases/needs); `--all`
|
|
567
|
+
includes the `h:` tasks, `--compact` minifies. Output is plain stdout
|
|
568
|
+
(the run banner goes to stderr), so `hammer h:json | jq` just works.
|
|
565
569
|
|
|
566
570
|
Each is guarded by `unless commands.key?` within the namespace, so you
|
|
567
571
|
can override one by reopening `namespace :h` in your Hammerfile.
|
|
@@ -574,6 +578,16 @@ hammer --system h:recipes # list recipes, ignoring any local Hammer
|
|
|
574
578
|
hammer --system h:recipes --install srt ~/bin/srt
|
|
575
579
|
```
|
|
576
580
|
|
|
581
|
+
`--gui` opens a native macOS window for the current project: a sidebar of
|
|
582
|
+
your tasks (grouped like the listing), a form per task built from its
|
|
583
|
+
options, and a Run button that streams the task's output. It reads the
|
|
584
|
+
project via `h:json` and runs each task as a `hammer <path> ...`
|
|
585
|
+
subprocess. macOS / arm64 only.
|
|
586
|
+
|
|
587
|
+
```sh
|
|
588
|
+
hammer --gui # open the runner for the nearest Hammerfile
|
|
589
|
+
```
|
|
590
|
+
|
|
577
591
|
Customize bare `hammer` by replacing `:default`:
|
|
578
592
|
|
|
579
593
|
```ruby
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleName</key><string>Hammer</string>
|
|
6
|
+
<key>CFBundleDisplayName</key><string>Hammer</string>
|
|
7
|
+
<key>CFBundleIdentifier</key><string>com.lux-hammer.gui</string>
|
|
8
|
+
<key>CFBundleVersion</key><string>1</string>
|
|
9
|
+
<key>CFBundleShortVersionString</key><string>1.0</string>
|
|
10
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
11
|
+
<key>CFBundleExecutable</key><string>HammerGUI</string>
|
|
12
|
+
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
|
13
|
+
<key>NSHighResolutionCapable</key><true/>
|
|
14
|
+
<key>NSPrincipalClass</key><string>NSApplication</string>
|
|
15
|
+
</dict>
|
|
16
|
+
</plist>
|
|
Binary file
|
data/lib/hammer/builtins.rb
CHANGED
|
@@ -31,6 +31,7 @@ class Hammer
|
|
|
31
31
|
register_version(h) unless h.commands.key?('version')
|
|
32
32
|
register_recipes(h) unless h.commands.key?('recipes')
|
|
33
33
|
register_init(h) unless h.commands.key?('init')
|
|
34
|
+
register_json(h) unless h.commands.key?('json')
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def register_help(klass)
|
|
@@ -112,6 +113,29 @@ class Hammer
|
|
|
112
113
|
end
|
|
113
114
|
end
|
|
114
115
|
|
|
116
|
+
def register_json(klass)
|
|
117
|
+
klass.class_eval do
|
|
118
|
+
task :json do
|
|
119
|
+
desc <<~TXT
|
|
120
|
+
Dump the CLI definition as JSON: tasks grouped exactly like
|
|
121
|
+
the bare-`hammer` listing, each with desc, options, examples,
|
|
122
|
+
aliases, needs. Consumed by the macOS GUI and any tooling
|
|
123
|
+
that wants the full Hammerfile spec.
|
|
124
|
+
TXT
|
|
125
|
+
example 'h:json'
|
|
126
|
+
example 'h:json --all # include the reserved h: tasks'
|
|
127
|
+
example 'h:json --compact # minified, single line'
|
|
128
|
+
opt :all, type: :boolean, desc: 'include reserved built-in h: tasks'
|
|
129
|
+
opt :compact, type: :boolean, desc: 'minified JSON (default: pretty)'
|
|
130
|
+
proc do |opts|
|
|
131
|
+
require 'json'
|
|
132
|
+
spec = self.class.root.export_spec(include_builtins: opts[:all])
|
|
133
|
+
puts opts[:compact] ? JSON.generate(spec) : JSON.pretty_generate(spec)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
115
139
|
# `:recipes` rolls all recipe-management actions into one task. Bare
|
|
116
140
|
# invocation lists; opts pick the action and positional args carry
|
|
117
141
|
# the recipe name (and optional target path for --install). Run via
|
data/lib/hammer/command.rb
CHANGED
|
@@ -63,6 +63,26 @@ class Hammer
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Structured form for JSON export (`h:json`). `path` is the full
|
|
67
|
+
# colon path supplied by the tree walk - a Command doesn't know its
|
|
68
|
+
# own namespace prefix. `hidden` follows the help rule: a task with
|
|
69
|
+
# no `desc` still dispatches but is hidden from listings.
|
|
70
|
+
def to_h(path = name)
|
|
71
|
+
{
|
|
72
|
+
name: name,
|
|
73
|
+
path: path,
|
|
74
|
+
desc: desc,
|
|
75
|
+
brief: brief,
|
|
76
|
+
hidden: desc.empty?,
|
|
77
|
+
redefined: !prev_location.nil?,
|
|
78
|
+
location: location,
|
|
79
|
+
alts: alts,
|
|
80
|
+
needs: needs,
|
|
81
|
+
examples: examples,
|
|
82
|
+
options: options.map(&:to_h)
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
66
86
|
private
|
|
67
87
|
|
|
68
88
|
def short_flag?(switch)
|
data/lib/hammer/option.rb
CHANGED
|
@@ -78,5 +78,24 @@ class Hammer
|
|
|
78
78
|
return '' if default.nil?
|
|
79
79
|
"(default: #{default.inspect})"
|
|
80
80
|
end
|
|
81
|
+
|
|
82
|
+
# Structured form for JSON export (`h:json`). The GUI maps `type`
|
|
83
|
+
# to a form widget; `default`/`required`/`desc` decorate it. Mirrors
|
|
84
|
+
# the data behind `usage`. `negation` is only meaningful for booleans.
|
|
85
|
+
def to_h
|
|
86
|
+
h = {
|
|
87
|
+
name: name.to_s,
|
|
88
|
+
type: type.to_s,
|
|
89
|
+
default: default,
|
|
90
|
+
required: required,
|
|
91
|
+
desc: desc,
|
|
92
|
+
placeholder: placeholder,
|
|
93
|
+
switch: switch,
|
|
94
|
+
aliases: aliases,
|
|
95
|
+
usage: usage.strip
|
|
96
|
+
}
|
|
97
|
+
h[:negation] = negation if boolean?
|
|
98
|
+
h
|
|
99
|
+
end
|
|
81
100
|
end
|
|
82
101
|
end
|
data/lib/lux-hammer.rb
CHANGED
|
@@ -583,6 +583,39 @@ class Hammer
|
|
|
583
583
|
end
|
|
584
584
|
end
|
|
585
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
|
+
|
|
586
619
|
def run_command(cmd, argv, full: nil, quiet: false)
|
|
587
620
|
# -h / --help is reserved on every command. Anywhere before a `--`
|
|
588
621
|
# stop-marker, it short-circuits to per-command help.
|
|
@@ -622,7 +655,9 @@ class Hammer
|
|
|
622
655
|
end
|
|
623
656
|
end
|
|
624
657
|
parts.concat(positional)
|
|
625
|
-
|
|
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)
|
|
626
661
|
end
|
|
627
662
|
|
|
628
663
|
# Fire `before` hooks from root down through the namespace chain.
|
|
@@ -703,7 +738,7 @@ class Hammer
|
|
|
703
738
|
each_command(include_builtins: extended) { |path, c| print_full_block(path, c) unless c.desc.empty? }
|
|
704
739
|
else
|
|
705
740
|
Shell.say ''
|
|
706
|
-
print_command_list(
|
|
741
|
+
print_command_list(include_builtins: extended)
|
|
707
742
|
end
|
|
708
743
|
print_recipes_section if extended && root.instance_variable_get(:@hammer_binary)
|
|
709
744
|
print_extras if extended
|
|
@@ -736,8 +771,8 @@ class Hammer
|
|
|
736
771
|
Shell.say "Usage: #{program_name} #{prefix}:COMMAND [ARGS]", :cyan
|
|
737
772
|
rows = []
|
|
738
773
|
sibling = find_namespace_sibling(prefix)
|
|
739
|
-
rows << [prefix, sibling] if sibling && !sibling.desc.empty?
|
|
740
|
-
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? }
|
|
741
776
|
unless rows.empty?
|
|
742
777
|
Shell.say ''
|
|
743
778
|
Shell.say 'Commands:', :yellow
|
|
@@ -802,38 +837,30 @@ class Hammer
|
|
|
802
837
|
Shell.say Hammer::STARTER_HAMMERFILE
|
|
803
838
|
end
|
|
804
839
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
Shell.say 'Commands:', :yellow
|
|
821
|
-
emit_rows(rooted.sort_by { |full, _| [full.count(':'), full] }, width)
|
|
822
|
-
first = false
|
|
823
|
-
end
|
|
824
|
-
|
|
825
|
-
groups.each do |section, items|
|
|
826
|
-
Shell.say unless first
|
|
827
|
-
first = false
|
|
828
|
-
Shell.say "#{section}:", :yellow
|
|
829
|
-
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)
|
|
830
855
|
end
|
|
831
856
|
end
|
|
832
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).
|
|
833
860
|
def emit_rows(rows, width)
|
|
834
|
-
rows.each do |full,
|
|
835
|
-
brief =
|
|
836
|
-
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]
|
|
837
864
|
Shell.say " #{program_name} #{full.ljust(width)} # #{brief}"
|
|
838
865
|
end
|
|
839
866
|
end
|
|
@@ -1047,6 +1074,7 @@ class Hammer
|
|
|
1047
1074
|
def self.cli(argv = ARGV)
|
|
1048
1075
|
argv = argv.dup
|
|
1049
1076
|
force_system = !!argv.delete('--system')
|
|
1077
|
+
launch_gui = !!argv.delete('--gui')
|
|
1050
1078
|
|
|
1051
1079
|
# Shebang invocation: `hammer /path/to/script ...args` (kernel passes
|
|
1052
1080
|
# the script path as argv[0] for `#!/usr/bin/env hammer` files).
|
|
@@ -1060,6 +1088,12 @@ class Hammer
|
|
|
1060
1088
|
end
|
|
1061
1089
|
|
|
1062
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
|
+
|
|
1063
1097
|
unless path
|
|
1064
1098
|
# No Hammerfile (or --system) - all built-ins are reachable. Bare
|
|
1065
1099
|
# `hammer`, `hammer h:recipes`, `hammer h:update`, `hammer h:agents`,
|
|
@@ -1188,4 +1222,21 @@ class Hammer
|
|
|
1188
1222
|
end
|
|
1189
1223
|
end
|
|
1190
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
|
+
|
|
1191
1242
|
end
|
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
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lux-hammer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dino Reic
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|
|
@@ -36,6 +36,8 @@ files:
|
|
|
36
36
|
- "./AGENTS.md"
|
|
37
37
|
- "./README.md"
|
|
38
38
|
- "./bin/hammer"
|
|
39
|
+
- "./gui/Hammer.app/Contents/Info.plist"
|
|
40
|
+
- "./gui/Hammer.app/Contents/MacOS/HammerGUI"
|
|
39
41
|
- "./lib/hammer/builder.rb"
|
|
40
42
|
- "./lib/hammer/builtins.rb"
|
|
41
43
|
- "./lib/hammer/command.rb"
|