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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 173d405adff150c31284b295fbcc13b56d73a70d6752509f4954f5a89d5f4df7
4
- data.tar.gz: 1439b6d6e44bec16a5571ffaec390d98ea945360862fb4514f55fedf89a575df
3
+ metadata.gz: d5fc940f903d901587d17cd94c6f34eed5f92de64ded400a882228e34037cad1
4
+ data.tar.gz: 480bd5d42e755ea6c92d956e48eb146777718e6be46a74042b9c8f0192479c27
5
5
  SHA512:
6
- metadata.gz: 537c36703b561535f96fc6dd5454a2f6e01df6cac7059b8115b9c6ed176875edab529fbe0d530695d4bbc233fadb67964f2485baa8497de324e4a76b33de353b
7
- data.tar.gz: 3abb187bb1fe58b03571fdbad1aac1e5a22a2023aeda4b8289ac0508813a85916bbc50a6b07b1988d9d888d3aff50b09658d77b5427704c1061a2c475c9bf151
6
+ metadata.gz: 33bf95c45d0eba2bf3d37aa55f7dcb6dcf317c25b3badeaba965f00f582c27cb92285db07f360e290c3c8d41f82f575bbb6171583bd5cce106b065843f7ad08d
7
+ data.tar.gz: 2dec31a7d86c8a3f525fa2beac4134576a634141001550b9226eed0ed723f6768a56bf404be4fbdf239a954eccb094be060695838a7c25cd820aae72af7db186
data/.version CHANGED
@@ -1 +1 @@
1
- 0.2.6
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 { Dotenv.load } # runs before every command
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
 
@@ -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).instance_eval(File.read(abs_path), abs_path)
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 = 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
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 ||= Class.new(StandardError)
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? || name == 'help' || name == '-h' || name == '--help'
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:") -> expanded namespace listing with full
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(bare, ns, full: true) if ns
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(nil, full: true)
353
+ return print_help
314
354
  end
315
355
 
316
- cmd, owner = resolve(name)
317
- return owner.run_command(cmd, argv, full: name) if cmd
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(name, ns) if ns
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
- commands[name.to_s] || commands.values.find { |c| c.matches?(name) }
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]. Returns [nil, nil]
335
- # if any segment is missing or the final segment isn't a command.
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
- klass = klass.namespaces[ns] or return [nil, nil]
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
- cmd ? [cmd, klass] : [nil, nil]
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" -> namespace class, or nil if any segment missing.
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
- parts.each { |ns| klass = klass.namespaces[ns] or return nil }
351
- klass
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:` - expanded namespace listing.
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(bare, ns, full: true) if ns
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, target) if cmd
457
- ns = resolve_namespace(target)
458
- return print_namespace_help(target, ns, full: full) if ns
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).instance_eval(&block)
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).instance_eval(File.read(hf), hf)
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).instance_eval(File.read(path), path)
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.6
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.10
66
+ rubygems_version: 4.0.11
66
67
  specification_version: 4
67
68
  summary: Thor-inspired tiny CLI builder
68
69
  test_files: []