lux-hammer 0.3.8 → 0.3.10
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/README.md +43 -7
- data/lib/hammer/command.rb +29 -0
- data/lib/hammer/parser.rb +5 -1
- data/lib/hammer/shell.rb +4 -4
- data/lib/lux-hammer.rb +55 -13
- data/recipes/git-helper.rb +2 -0
- data/recipes/llm.rb +3 -0
- data/recipes/srt.rb +2 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 81af43fc44619384d2b62f58e7d3178a1b6edb22cbb1ef4e836b73e2ee469913
|
|
4
|
+
data.tar.gz: 47d012b5e54d1e8df3441de01eeaee3e3267f71c4ac8545b80f413d419bd3fc6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d1dd7d495b96a6f7023601427dadcd492e593934c6407eb48dcb7c53fea26e33ea44ec40ef3a69856199fa75dd9d6278a8d32b14cbd4733b692dbbfe662f97ee
|
|
7
|
+
data.tar.gz: 2a9b223844fd645a79403c653a2bf25be28ba5654eef8562738b2f3768b0a72bddfd8b5ceb3dfe68ac7c583ecaaefc18fcee4d64f3f47964a93ff8f12b0fb586
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.10
|
data/README.md
CHANGED
|
@@ -946,6 +946,41 @@ Hammer.run ARGV do
|
|
|
946
946
|
end
|
|
947
947
|
```
|
|
948
948
|
|
|
949
|
+
### `#!/usr/bin/env hammer` (single-file scripts)
|
|
950
|
+
|
|
951
|
+
For a one-off CLI that lives as a single executable file, point the
|
|
952
|
+
shebang straight at `hammer` - no `require`, no boilerplate:
|
|
953
|
+
|
|
954
|
+
```ruby
|
|
955
|
+
#!/usr/bin/env hammer
|
|
956
|
+
# desc: tiny greeter
|
|
957
|
+
|
|
958
|
+
task :hello do
|
|
959
|
+
desc 'say hi'
|
|
960
|
+
opt :loud, type: :boolean, alias: :l
|
|
961
|
+
proc do |opts|
|
|
962
|
+
msg = "hello #{opts[:args].first || 'world'}"
|
|
963
|
+
say.cyan(opts[:loud] ? msg.upcase : msg)
|
|
964
|
+
end
|
|
965
|
+
end
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
```sh
|
|
969
|
+
$ chmod +x greet
|
|
970
|
+
$ ./greet hello dino -l
|
|
971
|
+
HELLO DINO
|
|
972
|
+
$ ./greet --help
|
|
973
|
+
Usage: greet COMMAND [ARGS]
|
|
974
|
+
...
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
The file body is plain Hammerfile DSL (`task`, `namespace`, `before`,
|
|
978
|
+
`load`). `hammer` detects the shebang, evaluates the script as a
|
|
979
|
+
self-contained CLI, and uses the script's basename as the program name
|
|
980
|
+
in help - so `./greet --help` reads "Usage: greet ..." rather than
|
|
981
|
+
"Usage: hammer ...". The current working directory is left alone, so
|
|
982
|
+
relative paths inside the script resolve where the user ran it.
|
|
983
|
+
|
|
949
984
|
## Complete example (every feature)
|
|
950
985
|
|
|
951
986
|
```ruby
|
|
@@ -1192,21 +1227,19 @@ few small things that have been bugging me about both for years.
|
|
|
1192
1227
|
|
|
1193
1228
|
| | Thor | hammer |
|
|
1194
1229
|
|-|-|-|
|
|
1195
|
-
| Lines of code | ~6,000 | ~400 |
|
|
1196
1230
|
| Runtime deps | a few | zero |
|
|
1197
|
-
| Root constants | `Thor`, `Thor::Group`, `Thor::Shell`, `Thor::Actions`, ... | just `Hammer` |
|
|
1198
1231
|
| Command DSL | `desc 'usage', 'help'` + `method_option` + `def name(arg)` | `task :name do ... proc do \|opts\| end end` (or classic `desc` + `def`) |
|
|
1199
1232
|
| Opts container | `Thor::CoreExt::HashWithIndifferentAccess` | plain `Hash` with symbol keys |
|
|
1200
1233
|
| Positional args | method positional params + `method_option`, two parallel systems | declared-order opts fill from positional, single system |
|
|
1201
1234
|
| Sub-namespaces | `register SubClass, 'name', '...'` (inheritance ceremony) | `namespace :name do ... end` (no classes needed) |
|
|
1235
|
+
| Command aliases | none (workarounds via `map`) | `alt :s, :srv` |
|
|
1236
|
+
| Pre-hooks | none built-in | `before { ... }` per scope, `needs :env` per command |
|
|
1237
|
+
| Chained dispatch | no | `hammer build + deploy + notify` |
|
|
1202
1238
|
| Cross-invoke | `invoke 'name', [args], opts` | `hammer :name, **opts` (looks like a method call) |
|
|
1203
1239
|
| Inline CLI | class only | class DSL **or** `Hammer.run do ... end` block DSL **or** a `Hammerfile` |
|
|
1204
1240
|
|
|
1205
1241
|
**What hammer does better and why:**
|
|
1206
1242
|
|
|
1207
|
-
* **One root constant.** Thor exposes `Thor`, `Thor::Group`, `Thor::Shell`,
|
|
1208
|
-
`Thor::Actions` at the top level - Bundler had to vendor its own copy at
|
|
1209
|
-
`Bundler::Thor` to avoid clashes. Hammer is just `Hammer`.
|
|
1210
1243
|
* **The opts hash is just a Hash.** Symbol keys, always. No magic accessor
|
|
1211
1244
|
object to remember, no string-vs-symbol confusion, no method_missing.
|
|
1212
1245
|
* **Positional args fill opts in declaration order.** Thor either forces
|
|
@@ -1225,15 +1258,18 @@ few small things that have been bugging me about both for years.
|
|
|
1225
1258
|
| | Rake | hammer |
|
|
1226
1259
|
|-|-|-|
|
|
1227
1260
|
| Primary use case | build/task automation with file deps | general CLIs |
|
|
1228
|
-
| Task file | `Rakefile` | `Hammerfile` |
|
|
1261
|
+
| Task file | `Rakefile` | `Hammerfile` (walks up parent dirs) |
|
|
1229
1262
|
| Namespacing | colon paths (`db:migrate`) | colon paths (`db:migrate`) - parity |
|
|
1230
1263
|
| Per-task options | `task[a,b,c]` positional only | typed `opt`s with flags, aliases, defaults, required |
|
|
1231
1264
|
| Help | `rake -T` (plain list) | bare `hammer` lists everything grouped by namespace; `hammer X -h` for per-command help with examples and defaults |
|
|
1232
1265
|
| Cross-invoke | `Rake::Task['db:migrate'].invoke` | `hammer 'db:migrate'` |
|
|
1233
|
-
| Prerequisites | `task :build => [:clean, :compile]` (declarative DAG) |
|
|
1266
|
+
| Prerequisites | `task :build => [:clean, :compile]` (declarative DAG) | `needs :clean, :compile` (declarative, deduped across `+` chains) |
|
|
1267
|
+
| Pre-hooks | none built-in | `before { ... }` scoped to root or namespace |
|
|
1268
|
+
| Chained dispatch | no (multi-task argv runs sequentially, no per-task flags) | `hammer build prod -v + db:migrate --pretend + deploy` |
|
|
1234
1269
|
| File tasks | yes (mtime-based) | no |
|
|
1235
1270
|
| Aliases | none (workarounds via re-defined tasks) | `alt :short_name` |
|
|
1236
1271
|
| Split across files | `import 'other.rake'` | `load auto: true` (or explicit paths/globs) |
|
|
1272
|
+
| Shareable scripts | no | recipes (one-file CLIs installable as their own binary) |
|
|
1237
1273
|
|
|
1238
1274
|
**What hammer does better and why:**
|
|
1239
1275
|
|
data/lib/hammer/command.rb
CHANGED
|
@@ -39,5 +39,34 @@ class Hammer
|
|
|
39
39
|
name = name.to_s
|
|
40
40
|
name == @name || @alts.include?(name)
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
# Auto-assign a single-letter short alias (first letter of the opt
|
|
44
|
+
# name) to any opt that does not already declare one. Explicit
|
|
45
|
+
# aliases and `-h` are reserved first, so they always win. On
|
|
46
|
+
# collision the opt simply gets no short form - long flag still
|
|
47
|
+
# works. Idempotent.
|
|
48
|
+
def finalize!
|
|
49
|
+
return if @finalized
|
|
50
|
+
@finalized = true
|
|
51
|
+
|
|
52
|
+
claimed = ['-h']
|
|
53
|
+
@options.each do |o|
|
|
54
|
+
o.aliases.each { |a| claimed << a if short_flag?(a) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@options.each do |o|
|
|
58
|
+
next if o.aliases.any? { |a| short_flag?(a) }
|
|
59
|
+
short = "-#{o.name.to_s[0]}"
|
|
60
|
+
next if claimed.include?(short)
|
|
61
|
+
o.aliases << short
|
|
62
|
+
claimed << short
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def short_flag?(switch)
|
|
69
|
+
switch.length == 2 && switch.start_with?('-') && switch[1] != '-'
|
|
70
|
+
end
|
|
42
71
|
end
|
|
43
72
|
end
|
data/lib/hammer/parser.rb
CHANGED
|
@@ -63,7 +63,11 @@ class Hammer
|
|
|
63
63
|
@options.each do |opt|
|
|
64
64
|
break if positional.empty?
|
|
65
65
|
next if opt.boolean? || values.key?(opt.name)
|
|
66
|
-
|
|
66
|
+
if opt.type == :array
|
|
67
|
+
values[opt.name] = opt.cast(positional.shift(positional.size))
|
|
68
|
+
else
|
|
69
|
+
values[opt.name] = opt.cast(positional.shift)
|
|
70
|
+
end
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
@options.each do |opt|
|
data/lib/hammer/shell.rb
CHANGED
|
@@ -31,8 +31,8 @@ class Hammer
|
|
|
31
31
|
|
|
32
32
|
# `say` with no args returns a proxy so you can write `say.cyan 'hi'`.
|
|
33
33
|
# `say('')` still prints a blank line; `say('x', :cyan)` is unchanged.
|
|
34
|
-
def say(text =
|
|
35
|
-
return SayProxy.new if text
|
|
34
|
+
def say(text = :_say_no_arg, color = nil)
|
|
35
|
+
return SayProxy.new if text == :_say_no_arg
|
|
36
36
|
puts paint(text, color)
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -116,9 +116,9 @@ class Hammer
|
|
|
116
116
|
$stdout.print "\e[#{items.size}A\r\e[J"
|
|
117
117
|
$stdout.print "#{paint("> #{items[selected]}", :green)}\r\n"
|
|
118
118
|
return selected
|
|
119
|
-
when "\x03"
|
|
119
|
+
when "\x03" # Ctrl-C
|
|
120
120
|
$stdout.print "\e[#{items.size}A\r\e[J"
|
|
121
|
-
|
|
121
|
+
raise Interrupt
|
|
122
122
|
when "\e"
|
|
123
123
|
# ESC may stand alone or start an arrow sequence \e[A / \e[B.
|
|
124
124
|
if IO.select([io], nil, nil, 0.01) && io.getch == '['
|
data/lib/lux-hammer.rb
CHANGED
|
@@ -96,6 +96,7 @@ class Hammer
|
|
|
96
96
|
m = method_name
|
|
97
97
|
arity = instance_method(method_name).arity
|
|
98
98
|
cmd.handler = arity.zero? ? proc { send(m) } : proc { |opts| send(m, opts) }
|
|
99
|
+
cmd.finalize!
|
|
99
100
|
commands[cmd.name] = cmd
|
|
100
101
|
|
|
101
102
|
@pending_desc = nil
|
|
@@ -168,6 +169,7 @@ class Hammer
|
|
|
168
169
|
warn_redefinition('task', cmd.name, prev.location, cmd.location)
|
|
169
170
|
end
|
|
170
171
|
|
|
172
|
+
cmd.finalize!
|
|
171
173
|
commands[cmd.name] = cmd
|
|
172
174
|
|
|
173
175
|
# `task` ignores pending class-level state, but clear it so a
|
|
@@ -414,6 +416,13 @@ class Hammer
|
|
|
414
416
|
run_command(cmd, argv, full: name, quiet: true)
|
|
415
417
|
end
|
|
416
418
|
|
|
419
|
+
# True when -h or --help appears in argv before a `--` stop-marker.
|
|
420
|
+
def help_requested?(argv)
|
|
421
|
+
stop = argv.index('--')
|
|
422
|
+
scan = stop ? argv[0...stop] : argv
|
|
423
|
+
scan.include?('-h') || scan.include?('--help')
|
|
424
|
+
end
|
|
425
|
+
|
|
417
426
|
public
|
|
418
427
|
|
|
419
428
|
# Find a command by canonical name or alt within this class. Falls
|
|
@@ -605,30 +614,26 @@ class Hammer
|
|
|
605
614
|
end
|
|
606
615
|
end
|
|
607
616
|
|
|
608
|
-
|
|
609
|
-
stop = argv.index('--')
|
|
610
|
-
scan = stop ? argv[0...stop] : argv
|
|
611
|
-
scan.include?('-h') || scan.include?('--help')
|
|
612
|
-
end
|
|
617
|
+
public
|
|
613
618
|
|
|
614
619
|
# `extended: true` is the verbose `help` / `-h` / `--help` form -
|
|
615
620
|
# appends global flags, the GitHub footer, and (for the hammer binary)
|
|
616
621
|
# a Hammerfile example. Bare invocation passes `extended: false` so
|
|
617
622
|
# the no-args output stays a clean command listing.
|
|
618
|
-
def print_help(target = nil,
|
|
623
|
+
def print_help(target = nil, expanded: false, extended: false)
|
|
619
624
|
if target
|
|
620
625
|
# `help ns:` is equivalent to `ns:` - namespace listing.
|
|
621
626
|
if target.end_with?(':') && target != ':'
|
|
622
627
|
bare = target.chomp(':')
|
|
623
628
|
ns, canonical = resolve_namespace(bare)
|
|
624
|
-
return print_namespace_help(canonical, ns
|
|
629
|
+
return print_namespace_help(canonical, ns) if ns
|
|
625
630
|
Shell.print_error("unknown: #{target}")
|
|
626
631
|
return
|
|
627
632
|
end
|
|
628
633
|
cmd, _, canonical = resolve(target)
|
|
629
634
|
return print_command_help(cmd, canonical) if cmd
|
|
630
635
|
ns, canonical = resolve_namespace(target)
|
|
631
|
-
return print_namespace_help(canonical, ns,
|
|
636
|
+
return print_namespace_help(canonical, ns, expanded: expanded) if ns
|
|
632
637
|
Shell.print_error("unknown: #{target}")
|
|
633
638
|
return
|
|
634
639
|
end
|
|
@@ -639,7 +644,7 @@ class Hammer
|
|
|
639
644
|
Shell.say ''
|
|
640
645
|
@app_desc.each_line { |l| Shell.say " #{l.chomp}" }
|
|
641
646
|
end
|
|
642
|
-
if
|
|
647
|
+
if expanded
|
|
643
648
|
each_command { |path, c| print_full_block(path, c) unless c.desc.empty? }
|
|
644
649
|
else
|
|
645
650
|
Shell.say ''
|
|
@@ -670,10 +675,9 @@ class Hammer
|
|
|
670
675
|
|
|
671
676
|
# `extended:` is accepted for parity with `print_help` but intentionally
|
|
672
677
|
# not used here - the global-flags / Hammerfile-example / footer block
|
|
673
|
-
# is root-help-only.
|
|
674
|
-
#
|
|
675
|
-
|
|
676
|
-
def print_namespace_help(prefix, ns, full: false, extended: false)
|
|
678
|
+
# is root-help-only. `expanded:` is also accepted for parity; a namespace
|
|
679
|
+
# listing is always the compact command list.
|
|
680
|
+
def print_namespace_help(prefix, ns, expanded: false, extended: false)
|
|
677
681
|
Shell.say "Usage: #{program_name} #{prefix}:COMMAND [ARGS]", :cyan
|
|
678
682
|
rows = []
|
|
679
683
|
sibling = find_namespace_sibling(prefix)
|
|
@@ -989,6 +993,17 @@ class Hammer
|
|
|
989
993
|
argv = argv.dup
|
|
990
994
|
force_system = !!argv.delete('--system')
|
|
991
995
|
|
|
996
|
+
# Shebang invocation: `hammer /path/to/script ...args` (kernel passes
|
|
997
|
+
# the script path as argv[0] for `#!/usr/bin/env hammer` files).
|
|
998
|
+
# Treat the script as a self-contained CLI: no Hammerfile lookup, no
|
|
999
|
+
# chdir (commands run in the caller's cwd), no `hammer`-binary
|
|
1000
|
+
# built-ins/banners. Detection requires a `#!`+`hammer` first line so
|
|
1001
|
+
# task names that happen to be paths don't get hijacked.
|
|
1002
|
+
if (script = shebang_script(argv.first))
|
|
1003
|
+
argv.shift
|
|
1004
|
+
return run_shebang(script, argv)
|
|
1005
|
+
end
|
|
1006
|
+
|
|
992
1007
|
path = force_system ? nil : find_hammerfile(Dir.pwd)
|
|
993
1008
|
unless path
|
|
994
1009
|
# No Hammerfile (or --system) - all built-ins are reachable. Bare
|
|
@@ -1058,6 +1073,33 @@ class Hammer
|
|
|
1058
1073
|
klass.start(argv)
|
|
1059
1074
|
end
|
|
1060
1075
|
|
|
1076
|
+
# Returns the script path if `arg` looks like a shebang script that
|
|
1077
|
+
# delegates to hammer (first line starts with `#!` and mentions
|
|
1078
|
+
# `hammer`). Returns nil otherwise. Used by `cli` to detect
|
|
1079
|
+
# `#!/usr/bin/env hammer` invocations where the kernel passes the
|
|
1080
|
+
# script path as argv[0].
|
|
1081
|
+
def self.shebang_script(arg)
|
|
1082
|
+
return nil unless arg
|
|
1083
|
+
return nil if arg.start_with?('-')
|
|
1084
|
+
return nil unless File.file?(arg) && File.readable?(arg)
|
|
1085
|
+
head = File.open(arg, &:gets).to_s
|
|
1086
|
+
return nil unless head.start_with?('#!') && head.include?('hammer')
|
|
1087
|
+
arg
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
# Evaluate a shebang script as a self-contained CLI. Mirrors `recipe`
|
|
1091
|
+
# semantics: no chdir, no `@hammer_binary` flag, no `register_core`
|
|
1092
|
+
# built-ins (so the script's `--help` shows only what it defines).
|
|
1093
|
+
# `program_name` is the script's basename so help reads "myscript foo"
|
|
1094
|
+
# rather than "hammer foo" - works even when invoked via a symlink in
|
|
1095
|
+
# PATH, since argv[0] is the path the user typed.
|
|
1096
|
+
def self.run_shebang(path, argv)
|
|
1097
|
+
klass = Class.new(Hammer)
|
|
1098
|
+
klass.instance_variable_set(:@program_name, File.basename(path))
|
|
1099
|
+
Builder.new(klass).evaluate(File.read(path), path)
|
|
1100
|
+
klass.start(argv)
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1061
1103
|
# True if argv goes through a built-in dispatch path (`:default` or
|
|
1062
1104
|
# `:help`) - meaning bare `hammer`, leading-flag invocations like
|
|
1063
1105
|
# `hammer -h`, or explicit help requests. These don't need a project
|
data/recipes/git-helper.rb
CHANGED
data/recipes/llm.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env hammer
|
|
1
2
|
# desc: personal LLM utility CLI (memory store, prompt-token expander, ...)
|
|
3
|
+
# executable: chmod +x this file and run directly, or symlink into PATH
|
|
2
4
|
|
|
3
5
|
desc <<~TXT
|
|
4
6
|
llm - personal LLM utility CLI
|
|
@@ -54,6 +56,7 @@ namespace :memory do
|
|
|
54
56
|
end
|
|
55
57
|
[meta, body.to_s.sub(/\A\n+/, '')]
|
|
56
58
|
end
|
|
59
|
+
|
|
57
60
|
task :list do
|
|
58
61
|
desc 'List stored memories with type and one-line description'
|
|
59
62
|
example 'llm memory list'
|
data/recipes/srt.rb
CHANGED
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.10
|
|
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-05-
|
|
11
|
+
date: 2026-05-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|