lux-hammer 0.2.6 → 0.2.7
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 +23 -0
- data/README.md +10 -1
- data/lib/hammer/builder.rb +33 -0
- data/lib/hammer/dotenv.rb +50 -0
- data/lib/hammer/loader.rb +1 -1
- data/lib/hammer/option.rb +12 -9
- data/lib/lux-hammer.rb +143 -31
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d5fc940f903d901587d17cd94c6f34eed5f92de64ded400a882228e34037cad1
|
|
4
|
+
data.tar.gz: 480bd5d42e755ea6c92d956e48eb146777718e6be46a74042b9c8f0192479c27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33bf95c45d0eba2bf3d37aa55f7dcb6dcf317c25b3badeaba965f00f582c27cb92285db07f360e290c3c8d41f82f575bbb6171583bd5cce106b065843f7ad08d
|
|
7
|
+
data.tar.gz: 2dec31a7d86c8a3f525fa2beac4134576a634141001550b9226eed0ed723f6768a56bf404be4fbdf239a954eccb094be060695838a7c25cd820aae72af7db186
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.7
|
data/AGENTS.md
CHANGED
|
@@ -84,6 +84,12 @@ At class or `Hammerfile` scope:
|
|
|
84
84
|
|
|
85
85
|
* `task :name do ... end`
|
|
86
86
|
* `namespace :name do ... end`
|
|
87
|
+
* `dotenv false` - opt out of auto `.env` / `.env.local` loading.
|
|
88
|
+
Only meaningful from the `hammer` binary (`Hammer.cli`); the load
|
|
89
|
+
happens after Hammerfile evaluation, before dispatch. Shell-set vars
|
|
90
|
+
always win (`ENV[k] ||= v`). For full dotenv-gem features
|
|
91
|
+
(interpolation, multiline) keep `dotenv false` and call `Dotenv.load`
|
|
92
|
+
from a `before` hook instead.
|
|
87
93
|
* `load` / `load auto: true` / `load 'path/file.rb'` / `load 'glob/*.rb'` /
|
|
88
94
|
`load 'some/dir'` - pull in Hammerfile fragments from `*_hammer.rb`
|
|
89
95
|
files. Paths resolve relative to the caller's file. A directory
|
|
@@ -140,6 +146,23 @@ explicit ADR-level discussion. Keys:
|
|
|
140
146
|
* The root `Hammer` subclass holds `@commands` and `@namespaces`.
|
|
141
147
|
* `resolve('a:b:c')` walks `@namespaces['a'].namespaces['b']` and
|
|
142
148
|
finds command `c` in the last class.
|
|
149
|
+
* Each segment is fuzzy-matched: exact name/alt first, then prefix
|
|
150
|
+
(`b` -> `build`), then substring (`es` -> `test`). Per-scope: matching
|
|
151
|
+
only looks at this class's commands/namespaces, not globally across
|
|
152
|
+
the tree. Ambiguous fuzzy matches raise `Hammer::AmbiguousMatch`,
|
|
153
|
+
which `dispatch` catches and prints as
|
|
154
|
+
`multiple commands match 'b': bake, build`. Same fallback applies to
|
|
155
|
+
namespaces via `find_namespace`.
|
|
156
|
+
* `resolve` and `resolve_namespace` return a `canonical` colon-path
|
|
157
|
+
alongside the resolved class/command, so help banners and the
|
|
158
|
+
pre-run banner display the canonical name even when the user typed
|
|
159
|
+
a fuzzy prefix.
|
|
160
|
+
* Before a command's handler runs, `run_command` prints a gray
|
|
161
|
+
`> prog cmd --opt=val ARG` banner so it's visible which command was
|
|
162
|
+
actually picked when fuzzy matching kicks in. Only options that
|
|
163
|
+
differ from their default are listed; booleans render as `--flag`
|
|
164
|
+
/ `--no-flag`. Help paths (`-h`, bare namespace) short-circuit
|
|
165
|
+
before the banner.
|
|
143
166
|
* There is **no per-level dispatch**. A namespace is a container, not a
|
|
144
167
|
CLI of its own. Do not reintroduce `subclass.start(remaining_argv)`.
|
|
145
168
|
* `start(argv)` is a two-step pipeline: `split_chain(argv)` (private)
|
data/README.md
CHANGED
|
@@ -469,7 +469,7 @@ runs before every command in that scope (and its nested namespaces).
|
|
|
469
469
|
Hooks fire outer -> inner, then the command's handler:
|
|
470
470
|
|
|
471
471
|
```ruby
|
|
472
|
-
before {
|
|
472
|
+
before { hammer :env } # runs before every command
|
|
473
473
|
|
|
474
474
|
namespace :db do
|
|
475
475
|
before { hammer :env } # runs before every db:* command
|
|
@@ -479,6 +479,15 @@ namespace :db do
|
|
|
479
479
|
end
|
|
480
480
|
```
|
|
481
481
|
|
|
482
|
+
`.env` and `.env.local` next to the `Hammerfile` are loaded
|
|
483
|
+
automatically by the `hammer` binary - no `before` hook needed.
|
|
484
|
+
Shell-set vars are never overwritten, and `.env.local` overrides
|
|
485
|
+
`.env`. Put `dotenv false` at the top of the Hammerfile to opt out:
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
dotenv false # disable auto .env loading
|
|
489
|
+
```
|
|
490
|
+
|
|
482
491
|
`before` is intentionally not available inside `task` - the proc body
|
|
483
492
|
*is* the command body, just put the setup line at the top of the proc.
|
|
484
493
|
|
data/lib/hammer/builder.rb
CHANGED
|
@@ -21,6 +21,11 @@ class Hammer
|
|
|
21
21
|
@klass.before(&block)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# Opt out of auto `.env` loading in `Hammer.cli`. Default is on.
|
|
25
|
+
def dotenv(flag = true)
|
|
26
|
+
@klass.dotenv(flag)
|
|
27
|
+
end
|
|
28
|
+
|
|
24
29
|
# Same surface as `Hammer.load`. Resolved relative to the file that
|
|
25
30
|
# called us, so `load auto: true` inside a Hammerfile picks up
|
|
26
31
|
# *_hammer.rb under the Hammerfile's directory.
|
|
@@ -28,5 +33,33 @@ class Hammer
|
|
|
28
33
|
anchor = Loader.caller_anchor(caller_locations(1, 1).first)
|
|
29
34
|
@klass.loader.load(anchor, paths, kwargs)
|
|
30
35
|
end
|
|
36
|
+
|
|
37
|
+
# instance_eval wrapper that also publishes the current Hammer target
|
|
38
|
+
# via Thread.current[:hammer_target]. That lets `*_hammer.rb` files
|
|
39
|
+
# pulled in through Ruby's own `require` (e.g. `Dir.require_all
|
|
40
|
+
# 'lib/tasks'` inside a Hammerfile) call `task`/`namespace`/`before`
|
|
41
|
+
# at top-level scope and have them register on the right target.
|
|
42
|
+
def evaluate(source = nil, path = nil, &block)
|
|
43
|
+
Hammer.with_target(@klass) do
|
|
44
|
+
block ? instance_eval(&block) : instance_eval(source, path)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Top-level Hammer DSL. Extended onto `main` (below) so any file
|
|
50
|
+
# `require`d from within a Hammerfile - where `self == main` - can
|
|
51
|
+
# still call `task`, `namespace`, and `before`. Delegates to whichever
|
|
52
|
+
# Hammer subclass is currently being evaluated.
|
|
53
|
+
module DSL
|
|
54
|
+
%i[task namespace before dotenv].each do |m|
|
|
55
|
+
define_method(m) do |*args, &block|
|
|
56
|
+
target = Thread.current[:hammer_target]
|
|
57
|
+
raise Hammer::Error, "`#{m}` called outside a Hammer context " \
|
|
58
|
+
'(Hammerfile / Hammer.run block / *_hammer.rb)' unless target
|
|
59
|
+
target.send(m, *args, &block)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
31
62
|
end
|
|
32
63
|
end
|
|
64
|
+
|
|
65
|
+
TOPLEVEL_BINDING.eval('self').extend(Hammer::DSL)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
class Hammer
|
|
2
|
+
# Tiny built-in .env loader. Kept dependency-free on purpose - the
|
|
3
|
+
# gemspec advertises zero runtime deps.
|
|
4
|
+
#
|
|
5
|
+
# `Hammer.cli` calls `Dotenv.load(Dir.pwd)` after evaluating the
|
|
6
|
+
# Hammerfile (unless the Hammerfile said `dotenv false`), so vars
|
|
7
|
+
# from `.env` are available to every command handler.
|
|
8
|
+
#
|
|
9
|
+
# Semantics:
|
|
10
|
+
# * `.env` is loaded, then `.env.local` (latter overrides former).
|
|
11
|
+
# * Missing files are silently skipped - auto-load must not raise.
|
|
12
|
+
# * Existing `ENV[k]` always wins (`ENV[k] ||= v`); shell-set values
|
|
13
|
+
# are never clobbered.
|
|
14
|
+
# * Supported lines: `KEY=value`, `KEY="value"`, `KEY='value'`, optional
|
|
15
|
+
# leading `export `, `#` comments, blank lines.
|
|
16
|
+
# * NOT supported: `${VAR}` interpolation, multiline values, inline
|
|
17
|
+
# comments after a value. Reach for the `dotenv` gem from a `before`
|
|
18
|
+
# hook if you need those.
|
|
19
|
+
module Dotenv
|
|
20
|
+
FILES ||= %w[.env .env.local].freeze
|
|
21
|
+
|
|
22
|
+
def self.load(dir)
|
|
23
|
+
FILES.each do |name|
|
|
24
|
+
path = File.join(dir, name)
|
|
25
|
+
next unless File.file?(path)
|
|
26
|
+
parse(File.read(path)).each { |k, v| ENV[k] ||= v }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.parse(text)
|
|
31
|
+
out = {}
|
|
32
|
+
text.each_line do |line|
|
|
33
|
+
line = line.strip
|
|
34
|
+
next if line.empty? || line.start_with?('#')
|
|
35
|
+
line = line.sub(/\Aexport\s+/, '')
|
|
36
|
+
key, sep, val = line.partition('=')
|
|
37
|
+
next if sep.empty?
|
|
38
|
+
key = key.strip
|
|
39
|
+
next if key.empty?
|
|
40
|
+
val = val.strip
|
|
41
|
+
if (val.start_with?('"') && val.end_with?('"')) ||
|
|
42
|
+
(val.start_with?("'") && val.end_with?("'"))
|
|
43
|
+
val = val[1..-2]
|
|
44
|
+
end
|
|
45
|
+
out[key] = val
|
|
46
|
+
end
|
|
47
|
+
out
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/hammer/loader.rb
CHANGED
|
@@ -72,7 +72,7 @@ class Hammer
|
|
|
72
72
|
def load_one(abs_path)
|
|
73
73
|
return if @loaded[abs_path]
|
|
74
74
|
@loaded[abs_path] = true
|
|
75
|
-
Builder.new(@target).
|
|
75
|
+
Builder.new(@target).evaluate(File.read(abs_path), abs_path)
|
|
76
76
|
rescue Hammer::Error
|
|
77
77
|
raise
|
|
78
78
|
rescue StandardError => e
|
data/lib/hammer/option.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
class Hammer
|
|
2
2
|
# Single option/flag definition.
|
|
3
3
|
class Option
|
|
4
|
-
attr_reader :name, :type, :default, :desc, :aliases, :required
|
|
4
|
+
attr_reader :name, :type, :default, :desc, :aliases, :required, :placeholder
|
|
5
5
|
|
|
6
|
-
ALLOWED_KEYS ||= %i[type default desc alias req].freeze
|
|
6
|
+
ALLOWED_KEYS ||= %i[type default desc alias req placeholder].freeze
|
|
7
7
|
RESERVED_FLAGS ||= %w[-h --help].freeze
|
|
8
8
|
|
|
9
9
|
def initialize(name, **opts)
|
|
@@ -13,12 +13,15 @@ class Hammer
|
|
|
13
13
|
"unknown opt parameter(s) for :#{name}: #{bad.join(', ')}. " \
|
|
14
14
|
"allowed: #{ALLOWED_KEYS.join(', ')}"
|
|
15
15
|
end
|
|
16
|
-
@name
|
|
17
|
-
@type
|
|
18
|
-
@default
|
|
19
|
-
@desc
|
|
20
|
-
@aliases
|
|
21
|
-
@required
|
|
16
|
+
@name = name.to_sym
|
|
17
|
+
@type = opts[:type] || :string
|
|
18
|
+
@default = opts[:default]
|
|
19
|
+
@desc = opts[:desc]
|
|
20
|
+
@aliases = Array(opts[:alias]).map { |a| Option.normalize_alias(a) }
|
|
21
|
+
@required = opts[:req] ? true : false
|
|
22
|
+
# Custom usage placeholder, e.g. `placeholder: 't/f'` -> `--log t/f`.
|
|
23
|
+
# Falls back to uppercased name (`--log LOG`) when nil.
|
|
24
|
+
@placeholder = opts[:placeholder]
|
|
22
25
|
|
|
23
26
|
# Reserve -h / --help so every command supports them uniformly.
|
|
24
27
|
if RESERVED_FLAGS.include?(switch)
|
|
@@ -60,7 +63,7 @@ class Hammer
|
|
|
60
63
|
|
|
61
64
|
def usage
|
|
62
65
|
flag = switch
|
|
63
|
-
flag += " #{name.to_s.upcase}" unless boolean?
|
|
66
|
+
flag += " #{placeholder || name.to_s.upcase}" unless boolean?
|
|
64
67
|
line = aliases.empty? ? flag : "#{aliases.join(', ')}, #{flag}"
|
|
65
68
|
suffix = annotation
|
|
66
69
|
line = "#{line.ljust(28)} #{desc}" if desc
|
data/lib/lux-hammer.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative 'hammer/command'
|
|
|
5
5
|
require_relative 'hammer/loader'
|
|
6
6
|
require_relative 'hammer/builder'
|
|
7
7
|
require_relative 'hammer/command_builder'
|
|
8
|
+
require_relative 'hammer/dotenv'
|
|
8
9
|
|
|
9
10
|
# Thor-inspired tiny CLI builder.
|
|
10
11
|
#
|
|
@@ -40,7 +41,11 @@ require_relative 'hammer/command_builder'
|
|
|
40
41
|
class Hammer
|
|
41
42
|
include Shell
|
|
42
43
|
|
|
43
|
-
Error
|
|
44
|
+
Error ||= Class.new(StandardError)
|
|
45
|
+
AmbiguousMatch ||= Class.new(Error)
|
|
46
|
+
|
|
47
|
+
# Gem version, read once from the bundled .version file.
|
|
48
|
+
VERSION ||= File.read(File.expand_path('../.version', __dir__)).strip
|
|
44
49
|
|
|
45
50
|
class << self
|
|
46
51
|
def inherited(sub)
|
|
@@ -178,7 +183,7 @@ class Hammer
|
|
|
178
183
|
# "myapp ns:cmd" with the same prefix everywhere - and so the value
|
|
179
184
|
# captured pre-chdir (see `Hammer.cli`) survives into nested classes.
|
|
180
185
|
sub.instance_variable_set(:@program_name, program_name)
|
|
181
|
-
sub.class_eval(&block) if block
|
|
186
|
+
Hammer.with_target(sub) { sub.class_eval(&block) } if block
|
|
182
187
|
@namespaces[name.to_s] = sub
|
|
183
188
|
end
|
|
184
189
|
|
|
@@ -199,6 +204,18 @@ class Hammer
|
|
|
199
204
|
@before_hooks
|
|
200
205
|
end
|
|
201
206
|
|
|
207
|
+
# Toggle auto-loading of `.env` / `.env.local` for the `hammer`
|
|
208
|
+
# binary. Default is ON. Call `dotenv false` at the top of a
|
|
209
|
+
# Hammerfile to suppress. No-op for standalone `MyCli.start` -
|
|
210
|
+
# auto-load only fires from `Hammer.cli`.
|
|
211
|
+
def dotenv(flag = true)
|
|
212
|
+
@dotenv_enabled = flag
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def dotenv_enabled?
|
|
216
|
+
@dotenv_enabled != false
|
|
217
|
+
end
|
|
218
|
+
|
|
202
219
|
def parent
|
|
203
220
|
@parent
|
|
204
221
|
end
|
|
@@ -235,6 +252,18 @@ class Hammer
|
|
|
235
252
|
@loader ||= Loader.new(self)
|
|
236
253
|
end
|
|
237
254
|
|
|
255
|
+
# Push `klass` as the current Hammer target for the duration of the
|
|
256
|
+
# block. Top-level DSL methods (`task`, `namespace`, `before` - see
|
|
257
|
+
# `Hammer::DSL`) read this thread-local, so files `require`d from
|
|
258
|
+
# inside a Hammerfile register against the right target.
|
|
259
|
+
def with_target(klass)
|
|
260
|
+
prev = Thread.current[:hammer_target]
|
|
261
|
+
Thread.current[:hammer_target] = klass
|
|
262
|
+
yield
|
|
263
|
+
ensure
|
|
264
|
+
Thread.current[:hammer_target] = prev
|
|
265
|
+
end
|
|
266
|
+
|
|
238
267
|
# Load Hammerfile fragments and register their commands on this
|
|
239
268
|
# class. Rake-style: split a CLI across multiple files.
|
|
240
269
|
#
|
|
@@ -295,60 +324,119 @@ class Hammer
|
|
|
295
324
|
argv = argv.dup
|
|
296
325
|
name = argv.shift
|
|
297
326
|
|
|
298
|
-
if name.nil?
|
|
327
|
+
if name.nil?
|
|
328
|
+
# Bare invocation of the `hammer` binary: print a gem banner
|
|
329
|
+
# above the help so users can see which lux-hammer they have.
|
|
330
|
+
# Skipped for `-h` / `help` (those stay terse) and for user-
|
|
331
|
+
# built CLIs (their own program name, not lux-hammer).
|
|
332
|
+
if root.instance_variable_get(:@hammer_binary)
|
|
333
|
+
Shell.say "lux-hammer #{VERSION}"
|
|
334
|
+
Shell.say ''
|
|
335
|
+
end
|
|
336
|
+
return print_help
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
if name == 'help' || name == '-h' || name == '--help'
|
|
299
340
|
target = argv.shift
|
|
300
341
|
return print_help(target)
|
|
301
342
|
end
|
|
302
343
|
|
|
303
|
-
# Trailing colon ("db:") ->
|
|
304
|
-
# per-command help on every task. Bare ":" expands the root.
|
|
344
|
+
# Trailing colon ("db:") -> namespace listing. Bare ":" lists root.
|
|
305
345
|
if name.end_with?(':') && name != ':'
|
|
306
346
|
bare = name.chomp(':')
|
|
307
|
-
ns = resolve_namespace(bare)
|
|
308
|
-
return print_namespace_help(
|
|
347
|
+
ns, canonical = resolve_namespace(bare)
|
|
348
|
+
return print_namespace_help(canonical, ns) if ns
|
|
309
349
|
Shell.print_error("unknown namespace: #{bare}")
|
|
310
350
|
print_help
|
|
311
351
|
exit 1
|
|
312
352
|
elsif name == ':'
|
|
313
|
-
return print_help
|
|
353
|
+
return print_help
|
|
314
354
|
end
|
|
315
355
|
|
|
316
|
-
cmd, owner = resolve(name)
|
|
317
|
-
return owner.run_command(cmd, argv, full:
|
|
356
|
+
cmd, owner, canonical = resolve(name)
|
|
357
|
+
return owner.run_command(cmd, argv, full: canonical) if cmd
|
|
318
358
|
|
|
319
|
-
ns = resolve_namespace(name)
|
|
320
|
-
return print_namespace_help(
|
|
359
|
+
ns, canonical = resolve_namespace(name)
|
|
360
|
+
return print_namespace_help(canonical, ns) if ns
|
|
321
361
|
|
|
322
362
|
Shell.print_error("unknown command: #{name}")
|
|
323
363
|
print_help
|
|
324
364
|
exit 1
|
|
365
|
+
rescue AmbiguousMatch => e
|
|
366
|
+
Shell.print_error(e.message)
|
|
367
|
+
exit 1
|
|
325
368
|
end
|
|
326
369
|
|
|
327
370
|
public
|
|
328
371
|
|
|
329
|
-
# Find a command by canonical name or alt within this class.
|
|
372
|
+
# Find a command by canonical name or alt within this class. Falls
|
|
373
|
+
# back to fuzzy match (prefix first, then substring) when no exact
|
|
374
|
+
# hit. Raises AmbiguousMatch if the fuzzy pass matches more than one.
|
|
330
375
|
def find_command(name)
|
|
331
|
-
|
|
376
|
+
name = name.to_s
|
|
377
|
+
exact = commands[name] || commands.values.find { |c| c.matches?(name) }
|
|
378
|
+
return exact if exact
|
|
379
|
+
fuzzy_pick(name, commands.values, 'command') { |c| [c.name, *c.alts] }
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Find a namespace by name within this class. Same fuzzy fallback
|
|
383
|
+
# as find_command.
|
|
384
|
+
def find_namespace(name)
|
|
385
|
+
name = name.to_s
|
|
386
|
+
return namespaces[name] if namespaces.key?(name)
|
|
387
|
+
pair = fuzzy_pick(name, namespaces.to_a, 'namespace') { |p| [p.first] }
|
|
388
|
+
pair&.last
|
|
332
389
|
end
|
|
333
390
|
|
|
334
|
-
# Walk "ns1:ns2:cmd" -> [command, owning_class
|
|
335
|
-
# if any segment is missing or the final
|
|
391
|
+
# Walk "ns1:ns2:cmd" -> [command, owning_class, canonical_path].
|
|
392
|
+
# Returns [nil, nil, nil] if any segment is missing or the final
|
|
393
|
+
# segment isn't a command. canonical_path uses the canonical name
|
|
394
|
+
# of every segment (so a fuzzy `b` resolves to `build` in the path).
|
|
336
395
|
def resolve(path)
|
|
337
396
|
parts = path.to_s.split(':')
|
|
338
397
|
klass = self
|
|
398
|
+
canonical = []
|
|
339
399
|
parts[0..-2].each do |ns|
|
|
340
|
-
|
|
400
|
+
sub = klass.find_namespace(ns) or return [nil, nil, nil]
|
|
401
|
+
canonical << klass.namespaces.key(sub)
|
|
402
|
+
klass = sub
|
|
341
403
|
end
|
|
342
404
|
cmd = klass.find_command(parts.last)
|
|
343
|
-
|
|
405
|
+
return [nil, nil, nil] unless cmd
|
|
406
|
+
canonical << cmd.name
|
|
407
|
+
[cmd, klass, canonical.join(':')]
|
|
344
408
|
end
|
|
345
409
|
|
|
346
|
-
# Walk "ns1:ns2" ->
|
|
410
|
+
# Walk "ns1:ns2" -> [namespace_class, canonical_path].
|
|
411
|
+
# Returns [nil, nil] if any segment is missing.
|
|
347
412
|
def resolve_namespace(path)
|
|
348
413
|
parts = path.to_s.split(':')
|
|
349
414
|
klass = self
|
|
350
|
-
|
|
351
|
-
|
|
415
|
+
canonical = []
|
|
416
|
+
parts.each do |ns|
|
|
417
|
+
sub = klass.find_namespace(ns) or return [nil, nil]
|
|
418
|
+
canonical << klass.namespaces.key(sub)
|
|
419
|
+
klass = sub
|
|
420
|
+
end
|
|
421
|
+
[klass, canonical.join(':')]
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Shared fuzzy matcher used by find_command and find_namespace.
|
|
425
|
+
# The block returns the strings to match against for each item
|
|
426
|
+
# (canonical name plus alts for commands, just the key for namespaces).
|
|
427
|
+
# Tries prefix match first, then substring; raises AmbiguousMatch
|
|
428
|
+
# when either pass hits more than one item.
|
|
429
|
+
def fuzzy_pick(name, items, kind, &keys_for)
|
|
430
|
+
[:start_with?, :include?].each do |op|
|
|
431
|
+
matches = items.select { |item| keys_for.call(item).any? { |k| k.send(op, name) } }
|
|
432
|
+
next if matches.empty?
|
|
433
|
+
if matches.size > 1
|
|
434
|
+
labels = matches.map { |m| keys_for.call(m).first }.sort
|
|
435
|
+
raise AmbiguousMatch, "multiple #{kind}s match '#{name}': #{labels.join(', ')}"
|
|
436
|
+
end
|
|
437
|
+
return matches.first
|
|
438
|
+
end
|
|
439
|
+
nil
|
|
352
440
|
end
|
|
353
441
|
|
|
354
442
|
# Programmatic dispatch by name. Useful for scripting and tests.
|
|
@@ -394,6 +482,7 @@ class Hammer
|
|
|
394
482
|
|
|
395
483
|
positional, opts = Parser.new(cmd.options).parse(argv)
|
|
396
484
|
opts[:args] = positional
|
|
485
|
+
print_run_banner(cmd, full || cmd.name, positional, opts)
|
|
397
486
|
instance = new
|
|
398
487
|
run_before_hooks(instance, opts)
|
|
399
488
|
run_needs(cmd)
|
|
@@ -409,6 +498,25 @@ class Hammer
|
|
|
409
498
|
exit 1
|
|
410
499
|
end
|
|
411
500
|
|
|
501
|
+
# Print a gray "> prog cmd --opt=val ARG" banner before a command
|
|
502
|
+
# runs. Helps see what was actually picked when fuzzy matching
|
|
503
|
+
# resolved a partial name. Only opts that differ from their default
|
|
504
|
+
# are shown; booleans render as `--flag` / `--no-flag`.
|
|
505
|
+
def print_run_banner(cmd, full, positional, opts)
|
|
506
|
+
parts = ["#{program_name} #{full}"]
|
|
507
|
+
cmd.options.each do |o|
|
|
508
|
+
val = opts[o.name]
|
|
509
|
+
next if val.nil? || val == o.default
|
|
510
|
+
if o.boolean?
|
|
511
|
+
parts << (val ? "--#{o.name}" : "--no-#{o.name}")
|
|
512
|
+
else
|
|
513
|
+
parts << "--#{o.name}=#{val}"
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
parts.concat(positional)
|
|
517
|
+
Shell.say "> #{parts.join(' ')}", :gray
|
|
518
|
+
end
|
|
519
|
+
|
|
412
520
|
# Fire `before` hooks from root down through the namespace chain.
|
|
413
521
|
# Each class's hooks fire at most once per top-level `start`, so
|
|
414
522
|
# prereqs dispatched via `needs` won't re-trigger them.
|
|
@@ -444,18 +552,18 @@ class Hammer
|
|
|
444
552
|
|
|
445
553
|
def print_help(target = nil, full: false)
|
|
446
554
|
if target
|
|
447
|
-
# `help ns:` is equivalent to `ns:` -
|
|
555
|
+
# `help ns:` is equivalent to `ns:` - namespace listing.
|
|
448
556
|
if target.end_with?(':') && target != ':'
|
|
449
557
|
bare = target.chomp(':')
|
|
450
|
-
ns = resolve_namespace(bare)
|
|
451
|
-
return print_namespace_help(
|
|
558
|
+
ns, canonical = resolve_namespace(bare)
|
|
559
|
+
return print_namespace_help(canonical, ns) if ns
|
|
452
560
|
Shell.print_error("unknown: #{target}")
|
|
453
561
|
return
|
|
454
562
|
end
|
|
455
|
-
cmd, _ = resolve(target)
|
|
456
|
-
return print_command_help(cmd,
|
|
457
|
-
ns = resolve_namespace(target)
|
|
458
|
-
return print_namespace_help(
|
|
563
|
+
cmd, _, canonical = resolve(target)
|
|
564
|
+
return print_command_help(cmd, canonical) if cmd
|
|
565
|
+
ns, canonical = resolve_namespace(target)
|
|
566
|
+
return print_namespace_help(canonical, ns, full: full) if ns
|
|
459
567
|
Shell.print_error("unknown: #{target}")
|
|
460
568
|
return
|
|
461
569
|
end
|
|
@@ -620,11 +728,11 @@ class Hammer
|
|
|
620
728
|
def self.run(argv = ARGV, &block)
|
|
621
729
|
klass = Class.new(Hammer)
|
|
622
730
|
if block
|
|
623
|
-
Builder.new(klass).
|
|
731
|
+
Builder.new(klass).evaluate(&block)
|
|
624
732
|
else
|
|
625
733
|
hf = File.join(Dir.pwd, 'Hammerfile')
|
|
626
734
|
if File.file?(hf)
|
|
627
|
-
Builder.new(klass).
|
|
735
|
+
Builder.new(klass).evaluate(File.read(hf), hf)
|
|
628
736
|
else
|
|
629
737
|
klass.loader.load(Dir.pwd, [], auto: true)
|
|
630
738
|
end
|
|
@@ -699,7 +807,11 @@ class Hammer
|
|
|
699
807
|
# chdir into the Hammerfile's directory for the entire run so commands
|
|
700
808
|
# operate on the project root (Rake-style).
|
|
701
809
|
Dir.chdir(File.dirname(path))
|
|
702
|
-
Builder.new(klass).
|
|
810
|
+
Builder.new(klass).evaluate(File.read(path), path)
|
|
811
|
+
# Auto-load `.env` / `.env.local` after eval so a top-level
|
|
812
|
+
# `dotenv false` in the Hammerfile can suppress it. Trade-off: vars
|
|
813
|
+
# are NOT visible during Hammerfile evaluation, only inside handlers.
|
|
814
|
+
Hammer::Dotenv.load(Dir.pwd) if klass.dotenv_enabled?
|
|
703
815
|
klass.start(argv)
|
|
704
816
|
end
|
|
705
817
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lux-hammer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dino Reic
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- "./lib/hammer/builder.rb"
|
|
39
39
|
- "./lib/hammer/command.rb"
|
|
40
40
|
- "./lib/hammer/command_builder.rb"
|
|
41
|
+
- "./lib/hammer/dotenv.rb"
|
|
41
42
|
- "./lib/hammer/loader.rb"
|
|
42
43
|
- "./lib/hammer/option.rb"
|
|
43
44
|
- "./lib/hammer/parser.rb"
|
|
@@ -62,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
62
63
|
- !ruby/object:Gem::Version
|
|
63
64
|
version: '0'
|
|
64
65
|
requirements: []
|
|
65
|
-
rubygems_version: 4.0.
|
|
66
|
+
rubygems_version: 4.0.11
|
|
66
67
|
specification_version: 4
|
|
67
68
|
summary: Thor-inspired tiny CLI builder
|
|
68
69
|
test_files: []
|