lux-hammer 0.3.9 → 0.3.12
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 +12 -2
- data/README.md +43 -7
- data/lib/hammer/command.rb +29 -0
- data/lib/lux-hammer.rb +46 -23
- 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: 75362aef2908fdd2a3f4e32ab8c6ce7a4c4ae52fce795f5bca5437325a92da49
|
|
4
|
+
data.tar.gz: 0d318814ed2cdd0da35ca2b66e7637c62d5d2efece0cb24f60da1c5e02f4ad1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 64a8ce40bf7fc4986b6b0796c72fb9a8e9a13b2acb70c4aaa76cd68d5f68158bc9fbde7d5a0a5f3e586978b97f849f712b5addef336754b896e901f71c9feec2
|
|
7
|
+
data.tar.gz: 6f12a27b72098ff513ecc216ee8f2c6deb08e3c3a29cf4f1a7e9c6dfb7750a9b00ad13fe514f66fdda7b7a837b62e2526d1af04ca1af85deed1435e2d34b30a3
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.12
|
data/AGENTS.md
CHANGED
|
@@ -98,10 +98,20 @@ At Hammerfile (block-DSL) top-level scope only:
|
|
|
98
98
|
|
|
99
99
|
At class or `Hammerfile` scope:
|
|
100
100
|
|
|
101
|
-
* `task :name do ... end`
|
|
101
|
+
* `task :name do ... end` - redefining a task is last-write-wins. A
|
|
102
|
+
warning fires (and the entry is tagged `(redefined)` in help) **only**
|
|
103
|
+
when the task it clobbers was also defined inside the main app (cwd).
|
|
104
|
+
Overriding a task that came from outside - a framework default, plugin,
|
|
105
|
+
or gem (an absolute path outside cwd) - is treated as an intentional
|
|
106
|
+
override and stays silent. Locality is read off the location string:
|
|
107
|
+
`relativize_path` rewrites in-app absolutes to a `.`-relative form, so
|
|
108
|
+
anything still starting with `/` is external (see `app_local_location?`).
|
|
102
109
|
* `namespace :name do ... end` - `:self` is reserved for the `hammer`
|
|
103
110
|
binary's built-in namespace (see `lib/hammer/builtins.rb`). Defining
|
|
104
|
-
it from user code raises.
|
|
111
|
+
it from user code raises. Reopening a namespace **merges** (Rake-style):
|
|
112
|
+
the same `namespace :db` can be split across files / `load`ed fragments
|
|
113
|
+
and the blocks accumulate onto one subclass - no warning. Only a
|
|
114
|
+
duplicate *task* name warns (handled by `task`, last write wins).
|
|
105
115
|
* `dotenv false` - opt out of auto `.env` / `.env.local` loading.
|
|
106
116
|
Only meaningful from the `hammer` binary (`Hammer.cli`); the load
|
|
107
117
|
happens after Hammerfile evaluation, before dispatch. Shell-set vars
|
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/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
|
|
@@ -163,11 +164,16 @@ class Hammer
|
|
|
163
164
|
end
|
|
164
165
|
cmd.handler = handler
|
|
165
166
|
|
|
166
|
-
|
|
167
|
+
# Only warn when overriding a task that was also defined inside the
|
|
168
|
+
# main app. Overriding one that came from outside - a framework
|
|
169
|
+
# default, plugin, or gem - is an intentional override, so stay quiet
|
|
170
|
+
# and don't tag it `(redefined)` in help.
|
|
171
|
+
if (prev = commands[cmd.name]) && app_local_location?(prev.location)
|
|
167
172
|
cmd.prev_location = prev.location
|
|
168
173
|
warn_redefinition('task', cmd.name, prev.location, cmd.location)
|
|
169
174
|
end
|
|
170
175
|
|
|
176
|
+
cmd.finalize!
|
|
171
177
|
commands[cmd.name] = cmd
|
|
172
178
|
|
|
173
179
|
# `task` ignores pending class-level state, but clear it so a
|
|
@@ -188,28 +194,29 @@ class Hammer
|
|
|
188
194
|
# namespace :users do ... end
|
|
189
195
|
# end
|
|
190
196
|
#
|
|
197
|
+
# Reopening a namespace merges: the same `namespace :db do ... end` can
|
|
198
|
+
# be split across files (Rake-style) and the blocks accumulate onto one
|
|
199
|
+
# subclass. Only a duplicate *task* name inside warns - that's handled
|
|
200
|
+
# by `task`. The namespace subclass is created lazily on first mention.
|
|
191
201
|
def namespace(name, &block)
|
|
192
|
-
sub =
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
sub.instance_variable_set(:@prev_location, prev.instance_variable_get(:@location))
|
|
209
|
-
warn_redefinition('namespace', name.to_s, prev.instance_variable_get(:@location), sub.instance_variable_get(:@location))
|
|
210
|
-
end
|
|
202
|
+
sub = (@namespaces[name.to_s] ||= begin
|
|
203
|
+
ns = Class.new(Hammer)
|
|
204
|
+
# Track the top-level CLI class so cross-invocation
|
|
205
|
+
# (`hammer 'ns:cmd'`) from inside a namespaced command dispatches
|
|
206
|
+
# against the full tree, not just the current namespace.
|
|
207
|
+
ns.instance_variable_set(:@root, root)
|
|
208
|
+
# Parent link, so `before` hooks defined further up the namespace
|
|
209
|
+
# tree can be collected and run outer -> inner before a command.
|
|
210
|
+
ns.instance_variable_set(:@parent, self)
|
|
211
|
+
# Share the parent's resolved program_name so help banners show
|
|
212
|
+
# "myapp ns:cmd" with the same prefix everywhere - and so the value
|
|
213
|
+
# captured pre-chdir (see `Hammer.cli`) survives into nested classes.
|
|
214
|
+
ns.instance_variable_set(:@program_name, program_name)
|
|
215
|
+
ns.instance_variable_set(:@location, source_location_of(block))
|
|
216
|
+
ns
|
|
217
|
+
end)
|
|
211
218
|
|
|
212
|
-
|
|
219
|
+
Hammer.with_target(sub) { sub.class_eval(&block) } if block
|
|
213
220
|
end
|
|
214
221
|
|
|
215
222
|
# Register a hook to run before every command in this class (root or
|
|
@@ -250,7 +257,23 @@ class Hammer
|
|
|
250
257
|
# built-in C-defined procs, eval'd blocks).
|
|
251
258
|
def source_location_of(block)
|
|
252
259
|
loc = block&.source_location
|
|
253
|
-
loc ? "#{loc[0]}:#{loc[1]}" : '(unknown)'
|
|
260
|
+
loc ? "#{relativize_path(loc[0])}:#{loc[1]}" : '(unknown)'
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Trim the cwd prefix off an absolute path so redefinition warnings
|
|
264
|
+
# read as `./lib/tasks/foo.rb` instead of a long absolute path. Paths
|
|
265
|
+
# outside cwd (framework / gem files) are left absolute.
|
|
266
|
+
def relativize_path(path)
|
|
267
|
+
prefix = "#{Dir.pwd}/"
|
|
268
|
+
path.start_with?(prefix) ? ".#{path[Dir.pwd.length..]}" : path
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# True when a captured location lives inside the main app. relativize_path
|
|
272
|
+
# rewrites in-app absolute paths to a `.`-relative form, so anything still
|
|
273
|
+
# starting with "/" is an absolute path outside cwd - a framework, plugin,
|
|
274
|
+
# or gem file. Relative locations are already cwd-anchored, hence local.
|
|
275
|
+
def app_local_location?(loc)
|
|
276
|
+
!loc.to_s.start_with?('/')
|
|
254
277
|
end
|
|
255
278
|
|
|
256
279
|
# Emit a yellow [hammer] warning on stderr when a task/namespace is
|
|
@@ -733,7 +756,7 @@ class Hammer
|
|
|
733
756
|
|
|
734
757
|
def print_footer
|
|
735
758
|
Shell.say ''
|
|
736
|
-
Shell.say "powered by hammer - #{HOMEPAGE}", :gray
|
|
759
|
+
Shell.say "powered by hammer (v#{VERSION}) - #{HOMEPAGE}", :gray
|
|
737
760
|
end
|
|
738
761
|
|
|
739
762
|
# Hammerfile cheat-sheet shown under `hammer --help`. Same content
|
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.12
|
|
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-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|