lux-hammer 0.2.1 → 0.2.3

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: 17ad95331316a0cd95554605d58b2a7dce64f9db9d6adc997b34cf5f3b1e2b1a
4
- data.tar.gz: 633ad47b777e68b848f11552e5345d5b468e01a0fa27a273dc6551aac7d18af1
3
+ metadata.gz: ae2c5c9dbcd3b17941440e327e04e84dbffaec7209e4ce15b205cf83525b3724
4
+ data.tar.gz: 516eb4a4cb0f8f02f489eb00d2e4500cfe70f600eaebe192fb1749c81fe37074
5
5
  SHA512:
6
- metadata.gz: '0568fccc6f143b0671166a08c1853a3f682bf2c4c5488791fa7d46e1c4d8b3cc311ae29a016e9cf9a93f2cc31a577a551af72f0f4754cc9e9bd3adb66c5f6731'
7
- data.tar.gz: bf5e04d1e37e50f138c7c14079b889018f85bda195dad8676df4f634a37fe62414a1511a451aacb08f1e0860651426d8a7bca78cf3bad35b1fbf3dc137423438
6
+ metadata.gz: d922e58738cb3afc95c54864fc79d9a6ec556e3634a94f258711652abf220ba7f436e1d0f0cf24f48ba9c6372af22339d8377b491c9de93f708e19a7ca13692d
7
+ data.tar.gz: 6ad385997b87bf70ba58ec3ae6d701b3717d5e9584ef00aae29ebeb5c9b3666a39f6032277e275987f6de94161809767eeb838e288718fda09287327009756b6
data/.version CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.2.3
data/README.md CHANGED
@@ -1,9 +1,52 @@
1
1
  # hammer
2
2
 
3
- A modern CLI builder for Ruby. The good parts of
4
- [Rake](https://github.com/ruby/rake) and
5
- [Thor](https://github.com/rails/thor) without the cruft. Drop a
6
- `Hammerfile`, run `hammer`, ship.
3
+ The bastard Frankenstein child of Rake, Thor, and Joshua. Sewn
4
+ together from three good ideas, with the rest of each parent left on
5
+ the cutting room floor.
6
+
7
+ Drop a `Hammerfile`, run `hammer`, ship.
8
+
9
+ ```ruby
10
+ namespace :db do # Rake-style colon paths
11
+ define :migrate do # Joshua-style define block
12
+ desc 'Run pending migrations'
13
+ opt :pretend, type: :boolean, alias: :p # Thor-style typed opts
14
+ proc do |o|
15
+ say.green "migrating pretend=#{o[:pretend].inspect}"
16
+ end
17
+ end
18
+ end
19
+ ```
20
+
21
+ ```sh
22
+ $ hammer db:migrate --pretend
23
+ migrating pretend=true
24
+ ```
25
+
26
+ ## The bloodline
27
+
28
+ * **From [Rake](https://github.com/ruby/rake)** we took the *namespace
29
+ tree* and *task prereqs* - `db:users:list` colon paths, `needs :env`
30
+ dependencies, the one-file `Hammerfile` at the project root. Left on
31
+ the table: the build-system DNA (file/mtime tasks) and that
32
+ `task[a,b,c]` argument syntax nobody enjoys typing into a shell.
33
+
34
+ * **From [Thor](https://github.com/rails/thor)** we took *real CLI
35
+ ergonomics* - typed options, defaults, aliases, required flags, and
36
+ generated `-h` help. Left on the table: the four-constant top-level
37
+ (`Thor`, `Thor::Group`, `Thor::Shell`, `Thor::Actions`), the
38
+ parallel method-params-vs-`method_option` arg systems, the
39
+ `HashWithIndifferentAccess`, and the file generator half of the gem
40
+ that most CLIs never reach for.
41
+
42
+ * **From [Joshua](https://github.com/dux/joshua)** we took the
43
+ *`define :name do ... end` block DSL* - declarative metadata up
44
+ top, one `proc do |opts| ... end` at the bottom doing the work. No
45
+ `def`-and-`desc` split, no class required, no boilerplate between
46
+ "what this command is" and "what it does".
47
+
48
+ The result: ~400 lines of code, zero runtime dependencies, and a file
49
+ that reads top-to-bottom like a recipe.
7
50
 
8
51
  ## Install
9
52
 
@@ -93,7 +136,7 @@ Rake::Task['db:migrate'].invoke('prod')
93
136
  ```
94
137
  ```ruby
95
138
  # hammer - reads like a normal Ruby method call
96
- hammer_db_migrate(env: 'prod')
139
+ hammer 'db:migrate', env: 'prod'
97
140
  ```
98
141
 
99
142
  ### Thor: `desc` welded to a method, no aliases, two arg systems
@@ -426,7 +469,7 @@ Hooks fire outer -> inner, then the command's handler:
426
469
  before { Dotenv.load } # runs before every command
427
470
 
428
471
  namespace :db do
429
- before { hammer_env } # runs before every db:* command
472
+ before { hammer :env } # runs before every db:* command
430
473
  define :migrate do
431
474
  proc { |opts| ... } # no boilerplate require inside
432
475
  end
@@ -442,7 +485,7 @@ Pairs naturally with hidden commands (next section): keep `:env` /
442
485
  ## Hidden commands (no `desc`)
443
486
 
444
487
  A command declared without a `desc` is **hidden from help listings**
445
- but stays fully dispatchable and `hammer_*`-callable:
488
+ but stays fully dispatchable and `hammer`-callable:
446
489
 
447
490
  ```ruby
448
491
  define :env do
@@ -450,7 +493,7 @@ define :env do
450
493
  end
451
494
 
452
495
  namespace :db do
453
- before { hammer_env } # call it from a hook
496
+ before { hammer :env } # call it from a hook
454
497
  define :migrate do
455
498
  desc 'Run migrations'
456
499
  proc { |_| ... }
@@ -459,7 +502,7 @@ end
459
502
  ```
460
503
 
461
504
  `hammer` and `hammer db` won't list `env`, but `hammer env`,
462
- `hammer_env` from another proc, and `before { hammer_env }` all work.
505
+ `hammer :env` from another proc, and `before { hammer :env }` all work.
463
506
 
464
507
  ## Prereqs (`needs`)
465
508
 
@@ -483,7 +526,7 @@ end
483
526
  ```
484
527
 
485
528
  Prereq names are colon paths resolved against the root class - same
486
- lookup as `hammer_*`. Use `needs 'db:env'` to depend on a namespaced
529
+ lookup as `hammer`. Use `needs 'db:env'` to depend on a namespaced
487
530
  command.
488
531
 
489
532
  Each prereq fires **at most once per top-level invocation**, so if
@@ -491,7 +534,7 @@ Each prereq fires **at most once per top-level invocation**, so if
491
534
  only once. Prereqs run with default options (no argv passed through).
492
535
 
493
536
  `needs` vs `before`:
494
- * `before { hammer_env }` - fires for *every* command in a scope.
537
+ * `before { hammer :env }` - fires for *every* command in a scope.
495
538
  * `needs :env` - declared per command, deduped across the call chain.
496
539
 
497
540
  ## Command aliases (`alt`)
@@ -509,7 +552,7 @@ Then `hammer server`, `hammer s`, and `hammer srv` all dispatch to the
509
552
  same command. Alts work inside namespaces too: `alt :m` on `db:migrate`
510
553
  makes `db:m` resolve.
511
554
 
512
- ## Cross-invocation (`hammer_*`)
555
+ ## Cross-invocation (`hammer`)
513
556
 
514
557
  From inside any command's proc - or from outside via the class - you can
515
558
  invoke other commands without re-shelling out:
@@ -517,28 +560,54 @@ invoke other commands without re-shelling out:
517
560
  ```ruby
518
561
  define :deploy do
519
562
  proc do |opts|
520
- hammer_build(env: 'prod', verbose: true)
521
- hammer_db_migrate
563
+ hammer :build, env: 'prod', verbose: true
564
+ hammer 'db:migrate'
522
565
  say.green 'deployed'
523
566
  end
524
567
  end
525
568
  ```
526
569
 
527
- The mapping mirrors the CLI literally:
570
+ Signature: `hammer(name, *args, **opts)`. `name` is a symbol for a
571
+ single command or a colon-path string for namespaced commands. Trailing
572
+ positionals become positional ARGV; kwargs become CLI flags:
528
573
 
529
- * `hammer_X_Y_Z` → command path `X:Y:Z` (underscores in the method
530
- name become colons)
531
- * positional args → positional ARGV
574
+ * `hammer :foo` → `foo`
575
+ * `hammer 'db:users:list'` → `db:users:list`
576
+ * `hammer :evaluate, 'puts 42'` `evaluate "puts 42"` (positional ARGV)
532
577
  * `verbose: true` → `--verbose`
533
- * `no_cache: true` → `--no-cache` (just the same rule - underscores in
534
- the kwarg key become dashes)
578
+ * `no_cache: true` → `--no-cache` (underscores in the key become dashes)
535
579
  * `dry_run: true` → `--dry-run`
536
580
  * `env: 'prod'` → `--env=prod`
537
581
  * `anything: false` → skipped (no-op; use `no_x: true` to negate)
538
582
 
539
- `MyCli.hammer_db_users_list("a", verbose: true)` also works at the
583
+ `MyCli.hammer 'db:users:list', verbose: true` also works at the
540
584
  class level, useful for tests and scripting.
541
585
 
586
+ ## Chained dispatch (`+`)
587
+
588
+ Run several commands in one shot, Rake-style, using a bare `+` token
589
+ as the separator:
590
+
591
+ ```sh
592
+ hammer build + deploy + notify
593
+ hammer build prod -v + db:migrate --pretend + deploy --url=https://x.com
594
+ ```
595
+
596
+ Each segment is parsed and dispatched independently - its own opts,
597
+ its own positional args. `needs` prereqs dedupe across the whole
598
+ chain, so a shared `:env` step runs once even if every chained command
599
+ declares `needs :env`.
600
+
601
+ A `+` only acts as a separator when it arrives as its own argv token.
602
+ Quoting protects it: `--foo="a + b"` reaches the parser as a single
603
+ token and is left alone. To pass a literal `+` as a positional, double
604
+ it - `++` unescapes to a single `+`:
605
+
606
+ ```sh
607
+ hammer echo ++ x # echo gets positional args ["+", "x"]
608
+ hammer echo + bar # runs echo, then runs bar
609
+ ```
610
+
542
611
  ## Shell helpers
543
612
 
544
613
  These are mixed into every handler (and also live on `Hammer::Shell` for
@@ -673,7 +742,7 @@ end
673
742
  define :deploy do
674
743
  desc 'Deploy to prod'
675
744
  proc do |_|
676
- hammer_db_migrate # cross-file invocation just works
745
+ hammer 'db:migrate' # cross-file invocation just works
677
746
  say.cyan 'deployed'
678
747
  end
679
748
  end
@@ -798,7 +867,7 @@ define :deploy do
798
867
  opt :force, type: :boolean
799
868
 
800
869
  proc do |opts|
801
- hammer_build(env: 'prod')
870
+ hammer :build, env: 'prod'
802
871
  exit 0 unless yes? "deploy to #{opts[:url]}?" unless opts[:force]
803
872
  say.yellow "deploying to #{opts[:url]}"
804
873
  end
@@ -844,18 +913,18 @@ end
844
913
 
845
914
  ```sh
846
915
  $ hammer
847
- Usage: demo COMMAND [ARGS]
916
+ Usage: hammer COMMAND [ARGS]
848
917
 
849
918
  Commands:
850
- demo build # Build the project
851
- demo deploy (alt: ship) # Deploy to URL
919
+ hammer build # Build the project
920
+ hammer deploy (alt: ship) # Deploy to URL
852
921
 
853
922
  db:
854
- demo db:migrate (alt: m) # Run pending migrations
923
+ hammer db:migrate (alt: m) # Run pending migrations
855
924
 
856
925
  db:users:
857
- demo db:users:list # List users
858
- demo db:users:create # Create a user
926
+ hammer db:users:list # List users
927
+ hammer db:users:create # Create a user
859
928
 
860
929
  $ hammer build prod -v
861
930
  building prod
@@ -872,17 +941,17 @@ $ hammer db:users:create --email=dino@example.com --admin
872
941
  create dino@example.com admin=true
873
942
 
874
943
  $ hammer db # bare namespace shows its contents
875
- Usage: demo db:COMMAND [ARGS]
944
+ Usage: hammer db:COMMAND [ARGS]
876
945
 
877
946
  Commands:
878
- demo db:migrate (alt: m) # Run pending migrations
947
+ hammer db:migrate (alt: m) # Run pending migrations
879
948
 
880
949
  users:
881
- demo db:users:list # List users
882
- demo db:users:create # Create a user
950
+ hammer db:users:list # List users
951
+ hammer db:users:create # Create a user
883
952
 
884
953
  $ hammer db:users:create -h
885
- Usage: demo db:users:create EMAIL [OPTIONS]
954
+ Usage: hammer db:users:create EMAIL [OPTIONS]
886
955
  Create a user
887
956
 
888
957
  Options:
@@ -909,7 +978,7 @@ class MyCli < Hammer
909
978
  end
910
979
 
911
980
  MyCli.start(ARGV) # or:
912
- MyCli.hammer_greet('dino', loud: true)
981
+ MyCli.hammer :greet, loud: true
913
982
  ```
914
983
 
915
984
  ## Development
@@ -941,7 +1010,7 @@ few small things that have been bugging me about both for years.
941
1010
  | Opts container | `Thor::CoreExt::HashWithIndifferentAccess` | plain `Hash` with symbol keys |
942
1011
  | Positional args | method positional params + `method_option`, two parallel systems | declared-order opts fill from positional, single system |
943
1012
  | Sub-namespaces | `register SubClass, 'name', '...'` (inheritance ceremony) | `namespace :name do ... end` (no classes needed) |
944
- | Cross-invoke | `invoke 'name', [args], opts` | `hammer_name(*args, **kwargs)` (looks like a method call) |
1013
+ | Cross-invoke | `invoke 'name', [args], opts` | `hammer :name, **opts` (looks like a method call) |
945
1014
  | Inline CLI | class only | class DSL **or** `Hammer.run do ... end` block DSL **or** a `Hammerfile` |
946
1015
 
947
1016
  **What hammer does better and why:**
@@ -955,7 +1024,7 @@ few small things that have been bugging me about both for years.
955
1024
  you into method params (which then clash with options) or makes you read
956
1025
  `ARGV` yourself. Hammer just says: opts you declared come first, leftover
957
1026
  goes to `opts[:args]`.
958
- * **Cross-invocation reads as Ruby.** `hammer_db_migrate(env: 'prod')`
1027
+ * **Cross-invocation reads as Ruby.** `hammer 'db:migrate', env: 'prod'`
959
1028
  looks like a method call. Thor's `invoke('db:migrate', [], env: 'prod')`
960
1029
  always feels like reflection.
961
1030
  * **No generator complexity.** Thor's other half is file scaffolding and
@@ -971,8 +1040,8 @@ few small things that have been bugging me about both for years.
971
1040
  | Namespacing | colon paths (`db:migrate`) | colon paths (`db:migrate`) - parity |
972
1041
  | Per-task options | `task[a,b,c]` positional only | typed `opt`s with flags, aliases, defaults, required |
973
1042
  | Help | `rake -T` (plain list) | bare `hammer` lists everything grouped by namespace; `hammer X -h` for per-command help with examples and defaults |
974
- | Cross-invoke | `Rake::Task['db:migrate'].invoke` | `hammer_db_migrate` |
975
- | Prerequisites | `task :build => [:clean, :compile]` (declarative DAG) | explicit - call `hammer_clean; hammer_compile` in the proc |
1043
+ | Cross-invoke | `Rake::Task['db:migrate'].invoke` | `hammer 'db:migrate'` |
1044
+ | Prerequisites | `task :build => [:clean, :compile]` (declarative DAG) | explicit - call `hammer :clean; hammer :compile` in the proc |
976
1045
  | File tasks | yes (mtime-based) | no |
977
1046
  | Aliases | none (workarounds via re-defined tasks) | `alt :short_name` |
978
1047
  | Split across files | `import 'other.rake'` | `load auto: true` (or explicit paths/globs) |
@@ -7,10 +7,6 @@ class Hammer
7
7
  @klass = klass
8
8
  end
9
9
 
10
- def program(name)
11
- @klass.program_name(name)
12
- end
13
-
14
10
  def define(name, &block)
15
11
  @klass.define(name, &block)
16
12
  end
data/lib/lux-hammer.rb CHANGED
@@ -11,15 +11,13 @@ require_relative 'hammer/command_builder'
11
11
  # Class DSL:
12
12
  #
13
13
  # class MyCli < Hammer
14
- # program_name 'mycli'
15
- #
16
14
  # define :build do
17
15
  # desc 'Build the project'
18
16
  # example 'build -v --env=prod'
19
17
  # opt :verbose, type: :boolean, alias: :v
20
18
  # opt :env, type: :string, default: 'dev'
21
19
  # proc do |opts|
22
- # say "building #{opts[:env]} args=#{opts[:args].inspect}", :green
20
+ # say.green "building #{opts[:env]} args=#{opts[:args].inspect}"
23
21
  # end
24
22
  # end
25
23
  # end
@@ -29,7 +27,6 @@ require_relative 'hammer/command_builder'
29
27
  # Block DSL is identical, just inside `Hammer.run`:
30
28
  #
31
29
  # Hammer.run(ARGV) do
32
- # program 'inline'
33
30
  # define :hello do
34
31
  # desc 'Greet someone'
35
32
  # opt :loud, type: :boolean, alias: :l
@@ -101,15 +98,17 @@ class Hammer
101
98
  @pending_needs = []
102
99
  end
103
100
 
104
- def program_name(name = nil)
105
- @program_name = name if name
106
- @program_name || default_program_name
101
+ # Resolved lazily on first read and memoized, so callers that need the
102
+ # cwd-relative form (see `default_program_name`) can warm the cache
103
+ # before chdir-ing elsewhere.
104
+ def program_name
105
+ @program_name ||= default_program_name
107
106
  end
108
107
 
109
- # Default shown in help/usage when `program_name` is not set:
110
- # the invocation path relative to cwd if the script lives inside it
111
- # (e.g. `bin/foo` when invoked from the project root), otherwise the
112
- # basename (e.g. `lux` for a globally installed bin in PATH).
108
+ # Program name shown in help/usage: the invocation path relative to cwd
109
+ # if the script lives inside it (e.g. `bin/foo` when invoked from the
110
+ # project root), otherwise the basename (e.g. `lux` for a globally
111
+ # installed bin in PATH).
113
112
  def default_program_name
114
113
  prog = $PROGRAM_NAME
115
114
  return File.basename(prog) unless prog.include?('/')
@@ -159,8 +158,8 @@ class Hammer
159
158
  end
160
159
 
161
160
  # Open a namespace (group of commands). Everything inside the block
162
- # (define, nested namespace, program_name override, ...) belongs to
163
- # that namespace, evaluated against an anonymous Hammer subclass.
161
+ # (define, nested namespace, ...) belongs to that namespace, evaluated
162
+ # against an anonymous Hammer subclass.
164
163
  #
165
164
  # namespace :db do
166
165
  # define :migrate do ... end
@@ -169,15 +168,16 @@ class Hammer
169
168
  def namespace(name, &block)
170
169
  sub = Class.new(Hammer)
171
170
  # Track the top-level CLI class so cross-invocation
172
- # (`hammer_<colon_path>`) from inside a namespaced command dispatches
171
+ # (`hammer 'ns:cmd'`) from inside a namespaced command dispatches
173
172
  # against the full tree, not just the current namespace.
174
173
  sub.instance_variable_set(:@root, root)
175
174
  # Parent link, so `before` hooks defined further up the namespace
176
175
  # tree can be collected and run outer -> inner before a command.
177
176
  sub.instance_variable_set(:@parent, self)
178
- # Inherit program_name so help banners show "myapp ns:cmd", not
179
- # whichever binary the namespace class fell back to.
180
- sub.program_name(program_name) if @program_name
177
+ # Share the parent's resolved program_name so help banners show
178
+ # "myapp ns:cmd" with the same prefix everywhere - and so the value
179
+ # captured pre-chdir (see `Hammer.cli`) survives into nested classes.
180
+ sub.instance_variable_set(:@program_name, program_name)
181
181
  sub.class_eval(&block) if block
182
182
  @namespaces[name.to_s] = sub
183
183
  end
@@ -188,7 +188,7 @@ class Hammer
188
188
  #
189
189
  # before { |opts| Dotenv.load }
190
190
  # namespace :db do
191
- # before { hammer_env }
191
+ # before { hammer :env }
192
192
  # define :migrate do ... end
193
193
  # end
194
194
  def before(&block)
@@ -257,15 +257,41 @@ class Hammer
257
257
  # Entry point. Parses ARGV, finds the right command, runs it.
258
258
  # Command names are Rake-style colon paths: "build", "db:migrate",
259
259
  # "db:users:list".
260
+ #
261
+ # Rake-style chained dispatch: `hammer build + deploy + notify`.
262
+ # A bare `+` argv token separates commands; `++` escapes to a literal
263
+ # `+` positional. Quoted shell args (`--foo="a + b"`) arrive as a
264
+ # single token and are not split.
260
265
  def start(argv = ARGV)
261
266
  # Track prereqs fired during this top-level invocation so a `needs`
262
267
  # chain runs each prereq at most once. Nested `start` calls (e.g.
263
- # `needs` -> `hammer_*` -> `start`) share the set; the outermost
264
- # call owns its lifetime.
268
+ # `needs` -> `hammer` -> `start`, or a `+` chain) share the set;
269
+ # the outermost call owns its lifetime.
265
270
  outer = Thread.current[:hammer_needs_ran].nil?
266
271
  Thread.current[:hammer_needs_ran] ||= {}
267
272
  Thread.current[:hammer_before_ran] ||= {}
268
273
 
274
+ split_chain(argv).each { |seg| dispatch(seg) }
275
+ ensure
276
+ Thread.current[:hammer_needs_ran] = nil if outer
277
+ Thread.current[:hammer_before_ran] = nil if outer
278
+ end
279
+
280
+ private
281
+
282
+ # Split argv on bare `+` tokens; unescape `++` -> `+` within each
283
+ # segment. Returns [argv] when no chain operator is present.
284
+ def split_chain(argv)
285
+ return [argv] unless argv.include?('+') || argv.include?('++')
286
+ segs = argv.slice_when { |a, b| a == '+' || b == '+' }
287
+ .map { |seg| seg.reject { |t| t == '+' } }
288
+ .map { |seg| seg.map { |t| t == '++' ? '+' : t } }
289
+ .reject(&:empty?)
290
+ segs.empty? ? [argv] : segs
291
+ end
292
+
293
+ # Run a single (already-split) command segment.
294
+ def dispatch(argv)
269
295
  argv = argv.dup
270
296
  name = argv.shift
271
297
 
@@ -283,11 +309,10 @@ class Hammer
283
309
  Shell.print_error("unknown command: #{name}")
284
310
  print_help
285
311
  exit 1
286
- ensure
287
- Thread.current[:hammer_needs_ran] = nil if outer
288
- Thread.current[:hammer_before_ran] = nil if outer
289
312
  end
290
313
 
314
+ public
315
+
291
316
  # Find a command by canonical name or alt within this class.
292
317
  def find_command(name)
293
318
  commands[name.to_s] || commands.values.find { |c| c.matches?(name) }
@@ -313,23 +338,22 @@ class Hammer
313
338
  klass
314
339
  end
315
340
 
316
- # MyCli.hammer_db_users_list("a", verbose: true) ->
317
- # MyCli.start(["db:users:list", "a", "--verbose"])
341
+ # Programmatic dispatch by name. Useful for scripting and tests.
342
+ #
343
+ # MyCli.hammer :build -> start(["build"])
344
+ # MyCli.hammer 'db:users:list' -> start(["db:users:list"])
345
+ # MyCli.hammer :eval, 'puts 42' -> start(["eval", "puts 42"])
346
+ # MyCli.hammer :build, env: 'prod' -> start(["build", "--env=prod"])
347
+ # MyCli.hammer :build, verbose: true -> start(["build", "--verbose"])
348
+ # MyCli.hammer :build, no_cache: true -> start(["build", "--no-cache"])
349
+ # MyCli.hammer :build, cache: false -> skipped (no-op)
318
350
  #
319
- # Useful for scripting and tests. Underscores in the method name map
320
- # to colons; underscores in kwarg keys map to dashes in the flag.
321
- def method_missing(name, *args, **kwargs, &block)
322
- str = name.to_s
323
- return super unless str.start_with?('hammer_')
324
-
325
- path = str.sub(/^hammer_/, '').tr('_', ':')
326
- argv = [path, *args.map(&:to_s)]
327
- # kwarg key mirrors the CLI flag literally:
328
- # verbose: true -> --verbose
329
- # no_cache: true -> --no-cache (just the general rule)
330
- # env: 'prod' -> --env=prod
331
- # anything: false -> skipped (no-op; use `no_x: true` to negate)
332
- kwargs.each do |k, v|
351
+ # Symbols are single-segment names; pass a string with colons for
352
+ # namespaced paths. Trailing positionals become positional ARGV.
353
+ # Underscores in option keys become dashes in flags.
354
+ def hammer(name, *args, **opts)
355
+ argv = [name.to_s, *args.map(&:to_s)]
356
+ opts.each do |k, v|
333
357
  next if v == false
334
358
  flag = "--#{k.to_s.tr('_', '-')}"
335
359
  argv << (v == true ? flag : "#{flag}=#{v}")
@@ -337,10 +361,6 @@ class Hammer
337
361
  start(argv)
338
362
  end
339
363
 
340
- def respond_to_missing?(name, include_private = false)
341
- name.to_s.start_with?('hammer_') || super
342
- end
343
-
344
364
  # Yield [full_colon_path, Command] for every command in this class
345
365
  # and all nested namespaces.
346
366
  def each_command(prefix = nil, &block)
@@ -442,7 +462,7 @@ class Hammer
442
462
  def print_command_list(klass, prefix = nil)
443
463
  rows = []
444
464
  # Commands without a `desc` are hidden from listings but still
445
- # dispatchable + `hammer_*`-callable - useful for private helpers
465
+ # dispatchable + `hammer`-callable - useful for private helpers
446
466
  # invoked from `before` hooks or other commands (e.g. `:env`, `:app`).
447
467
  klass.each_command(prefix) { |full, c| rows << [full, c] unless c.desc.empty? }
448
468
  return if rows.empty?
@@ -530,26 +550,21 @@ class Hammer
530
550
  #
531
551
  # define :deploy do
532
552
  # proc do |opts|
533
- # hammer_build
534
- # hammer_db_migrate(pretend: true)
553
+ # hammer :build
554
+ # hammer 'db:migrate', pretend: true
535
555
  # end
536
556
  # end
537
- def method_missing(name, *args, **kwargs, &block)
538
- return super unless name.to_s.start_with?('hammer_')
539
- # Dispatch from the root class so `hammer_a_b` resolves against the
540
- # full colon path "a:b" even when called inside a namespaced command
541
- # (where self.class would be the namespace subclass).
542
- self.class.root.send(name, *args, **kwargs, &block)
543
- end
544
-
545
- def respond_to_missing?(name, include_private = false)
546
- name.to_s.start_with?('hammer_') || super
557
+ #
558
+ # Dispatches from the root class so colon paths resolve against the
559
+ # full tree even when called from inside a namespaced command.
560
+ def hammer(name, *args, **opts)
561
+ self.class.root.hammer(name, *args, **opts)
547
562
  end
548
563
 
549
564
  # ----- block DSL -----------------------------------------------------
550
565
 
551
- # Define and run a CLI inline. Inside the block use `program`,
552
- # `define :name do ... end`, and `namespace`.
566
+ # Define and run a CLI inline. Inside the block use
567
+ # `define :name do ... end`, `namespace`, and `load`.
553
568
  #
554
569
  # Without a block: load ./Hammerfile if it exists, otherwise
555
570
  # auto-discover *_hammer.rb under Dir.pwd, then dispatch ARGV.
@@ -575,13 +590,27 @@ class Hammer
575
590
  path = find_hammerfile(Dir.pwd)
576
591
  unless path
577
592
  Shell.print_error "no Hammerfile found in #{Dir.pwd} or any parent directory"
593
+
594
+ # Heuristic: *.rb files referencing `Hammer.` are likely inline CLIs
595
+ # the user could promote into a Hammerfile.
596
+ excludes = %w[.git node_modules tmp vendor coverage dist build]
597
+ .map { |d| "--exclude-dir=#{d}" }.join(' ')
598
+ candidates = `grep -rl --include='*.rb' #{excludes} 'Hammer\\.' . 2>/dev/null`
599
+ .lines.map(&:strip).reject(&:empty?)
600
+ unless candidates.empty?
601
+ Shell.say "possible CLI implementation(s) - files referencing `Hammer.`:", :yellow
602
+ candidates.first(10).each { |f| Shell.say " #{f.sub(%r{\A\./}, '')}" }
603
+ Shell.say ''
604
+ end
605
+
578
606
  Shell.say "create one - example:"
607
+ puts
579
608
  Shell.say <<~RUBY
580
- program 'mycli'
581
-
582
609
  define :hello do
583
610
  desc 'say hello'
584
- proc { |opts| say "hello \#{opts[:args].first || 'world'}", :green }
611
+ proc do |opts|
612
+ say.green "hello \#{opts[:args].first || 'world'}"
613
+ end
585
614
  end
586
615
  RUBY
587
616
  exit 1
@@ -589,8 +618,8 @@ class Hammer
589
618
 
590
619
  klass = Class.new(Hammer)
591
620
  # Resolve before chdir so paths like `bin/foo` stay relative to the
592
- # cwd the user actually invoked from.
593
- klass.program_name(klass.default_program_name)
621
+ # cwd the user actually invoked from. `program_name` memoizes.
622
+ klass.program_name
594
623
 
595
624
  # chdir into the Hammerfile's directory for the entire run so commands
596
625
  # operate on the project root (Rake-style).
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.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dino Reic