lux-hammer 0.2.2 → 0.2.4
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 +128 -57
- data/lib/hammer/builder.rb +2 -2
- data/lib/hammer/command_builder.rb +1 -1
- data/lib/lux-hammer.rb +128 -61
- 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: 7007c5628c3466147f7776a2de86832fa8737623ef7d91c8c4dc4a4b3db7e557
|
|
4
|
+
data.tar.gz: 38d2781742e9cb9d8d12c22b3e8c416fe9f26901a7ef0bed678b206f872a9468
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3dd10a43530fd33c1535ea38086eb3a86ce8d3750e3f79acb765b390b667ab70048bda841e3ab3959effcc1438aa0557e07bfd7f24d727d64a8e963a3ed8b6c
|
|
7
|
+
data.tar.gz: e976dc67d490f64c44044fab9dcad7e2b5431570eefdada070feb2689daba6fea8af46572ba223a6f801c881917ff522f519d08cf19218bd530ac438f91d4759
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.4
|
data/README.md
CHANGED
|
@@ -1,9 +1,52 @@
|
|
|
1
1
|
# hammer
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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. AI LLM-s love `hammer`.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
namespace :db do # Rake-style colon paths
|
|
11
|
+
task :migrate do # Joshua-style task 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
|
+
*`task :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
|
|
|
@@ -24,7 +67,7 @@ This installs the `hammer` binary and exposes `require 'lux-hammer'`.
|
|
|
24
67
|
Create a `Hammerfile` in your project root:
|
|
25
68
|
|
|
26
69
|
```ruby
|
|
27
|
-
|
|
70
|
+
task :hello do
|
|
28
71
|
desc 'say hi'
|
|
29
72
|
proc do |opts|
|
|
30
73
|
say.green "hello #{opts[:args].first || 'world'}"
|
|
@@ -72,7 +115,7 @@ Hammer takes typed options, positional fill, and any common flag form:
|
|
|
72
115
|
|
|
73
116
|
```ruby
|
|
74
117
|
# Hammerfile
|
|
75
|
-
|
|
118
|
+
task :greet do
|
|
76
119
|
desc 'Say hello'
|
|
77
120
|
opt :name
|
|
78
121
|
opt :loud, type: :boolean, alias: :l
|
|
@@ -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
|
-
|
|
139
|
+
hammer 'db:migrate', env: 'prod'
|
|
97
140
|
```
|
|
98
141
|
|
|
99
142
|
### Thor: `desc` welded to a method, no aliases, two arg systems
|
|
@@ -116,7 +159,7 @@ end
|
|
|
116
159
|
|
|
117
160
|
```ruby
|
|
118
161
|
# hammer - one arg system, real aliases, no usage string to maintain
|
|
119
|
-
|
|
162
|
+
task :greet do
|
|
120
163
|
desc 'Say hello'
|
|
121
164
|
alt :g
|
|
122
165
|
opt :name
|
|
@@ -130,13 +173,13 @@ and there's one place to look for everything the command takes.
|
|
|
130
173
|
|
|
131
174
|
## The two styles
|
|
132
175
|
|
|
133
|
-
### `
|
|
176
|
+
### `task :name do ... end` (block DSL)
|
|
134
177
|
|
|
135
178
|
The block's **last expression must be `proc do |opts| ... end`**. That
|
|
136
179
|
proc is the handler. Everything before it is metadata.
|
|
137
180
|
|
|
138
181
|
```ruby
|
|
139
|
-
|
|
182
|
+
task :build do
|
|
140
183
|
desc 'Build the project'
|
|
141
184
|
example 'build prod -v'
|
|
142
185
|
opt :verbose, type: :boolean, alias: :v
|
|
@@ -328,7 +371,7 @@ Anything in ARGV without `-` / `--` fills the next un-set
|
|
|
328
371
|
**non-boolean** opt, in declaration order:
|
|
329
372
|
|
|
330
373
|
```ruby
|
|
331
|
-
|
|
374
|
+
task :deploy do
|
|
332
375
|
opt :url
|
|
333
376
|
opt :env, default: 'dev'
|
|
334
377
|
proc { |opts| ... }
|
|
@@ -360,7 +403,7 @@ Always a `Hash` with **symbol keys**. Keys present:
|
|
|
360
403
|
* `opts[:args]` - array of positional ARGV not absorbed by an opt
|
|
361
404
|
|
|
362
405
|
```ruby
|
|
363
|
-
|
|
406
|
+
task :show do
|
|
364
407
|
opt :env, default: 'dev'
|
|
365
408
|
opt :loud, type: :boolean
|
|
366
409
|
proc { |opts| p opts }
|
|
@@ -392,12 +435,12 @@ colon-paths from the root binary - just like `rake db:migrate`:
|
|
|
392
435
|
|
|
393
436
|
```ruby
|
|
394
437
|
namespace :db do
|
|
395
|
-
|
|
438
|
+
task :migrate do
|
|
396
439
|
proc { |opts| ... }
|
|
397
440
|
end
|
|
398
441
|
|
|
399
442
|
namespace :users do
|
|
400
|
-
|
|
443
|
+
task :list do
|
|
401
444
|
proc { |opts| ... }
|
|
402
445
|
end
|
|
403
446
|
end
|
|
@@ -410,7 +453,9 @@ Then:
|
|
|
410
453
|
hammer db:migrate
|
|
411
454
|
hammer db:users:list
|
|
412
455
|
hammer db # bare namespace lists everything under it
|
|
456
|
+
hammer db: # trailing colon: full per-task help for every command
|
|
413
457
|
hammer db:migrate -h # per-command help
|
|
458
|
+
hammer : # trailing colon at root: full help for every command
|
|
414
459
|
```
|
|
415
460
|
|
|
416
461
|
Namespaces nest to any depth. There is no per-level dispatch - the root
|
|
@@ -426,14 +471,14 @@ Hooks fire outer -> inner, then the command's handler:
|
|
|
426
471
|
before { Dotenv.load } # runs before every command
|
|
427
472
|
|
|
428
473
|
namespace :db do
|
|
429
|
-
before {
|
|
430
|
-
|
|
474
|
+
before { hammer :env } # runs before every db:* command
|
|
475
|
+
task :migrate do
|
|
431
476
|
proc { |opts| ... } # no boilerplate require inside
|
|
432
477
|
end
|
|
433
478
|
end
|
|
434
479
|
```
|
|
435
480
|
|
|
436
|
-
`before` is intentionally not available inside `
|
|
481
|
+
`before` is intentionally not available inside `task` - the proc body
|
|
437
482
|
*is* the command body, just put the setup line at the top of the proc.
|
|
438
483
|
|
|
439
484
|
Pairs naturally with hidden commands (next section): keep `:env` /
|
|
@@ -442,16 +487,16 @@ Pairs naturally with hidden commands (next section): keep `:env` /
|
|
|
442
487
|
## Hidden commands (no `desc`)
|
|
443
488
|
|
|
444
489
|
A command declared without a `desc` is **hidden from help listings**
|
|
445
|
-
but stays fully dispatchable and `
|
|
490
|
+
but stays fully dispatchable and `hammer`-callable:
|
|
446
491
|
|
|
447
492
|
```ruby
|
|
448
|
-
|
|
493
|
+
task :env do
|
|
449
494
|
proc { |_| require './config/env' } # no desc -> hidden
|
|
450
495
|
end
|
|
451
496
|
|
|
452
497
|
namespace :db do
|
|
453
|
-
before {
|
|
454
|
-
|
|
498
|
+
before { hammer :env } # call it from a hook
|
|
499
|
+
task :migrate do
|
|
455
500
|
desc 'Run migrations'
|
|
456
501
|
proc { |_| ... }
|
|
457
502
|
end
|
|
@@ -459,31 +504,31 @@ end
|
|
|
459
504
|
```
|
|
460
505
|
|
|
461
506
|
`hammer` and `hammer db` won't list `env`, but `hammer env`,
|
|
462
|
-
`
|
|
507
|
+
`hammer :env` from another proc, and `before { hammer :env }` all work.
|
|
463
508
|
|
|
464
509
|
## Prereqs (`needs`)
|
|
465
510
|
|
|
466
511
|
Declare commands that must run before this one (Rake-style task deps):
|
|
467
512
|
|
|
468
513
|
```ruby
|
|
469
|
-
|
|
514
|
+
task :env do
|
|
470
515
|
proc { |_| require './config/env' } # hidden helper
|
|
471
516
|
end
|
|
472
517
|
|
|
473
|
-
|
|
518
|
+
task :app do
|
|
474
519
|
needs :env # runs `env` first
|
|
475
520
|
desc 'start the app'
|
|
476
521
|
proc { |opts| App.start }
|
|
477
522
|
end
|
|
478
523
|
|
|
479
|
-
|
|
524
|
+
task :deploy do
|
|
480
525
|
needs :env, :build # multiple prereqs, in order
|
|
481
526
|
proc { |opts| ... }
|
|
482
527
|
end
|
|
483
528
|
```
|
|
484
529
|
|
|
485
530
|
Prereq names are colon paths resolved against the root class - same
|
|
486
|
-
lookup as `
|
|
531
|
+
lookup as `hammer`. Use `needs 'db:env'` to depend on a namespaced
|
|
487
532
|
command.
|
|
488
533
|
|
|
489
534
|
Each prereq fires **at most once per top-level invocation**, so if
|
|
@@ -491,7 +536,7 @@ Each prereq fires **at most once per top-level invocation**, so if
|
|
|
491
536
|
only once. Prereqs run with default options (no argv passed through).
|
|
492
537
|
|
|
493
538
|
`needs` vs `before`:
|
|
494
|
-
* `before {
|
|
539
|
+
* `before { hammer :env }` - fires for *every* command in a scope.
|
|
495
540
|
* `needs :env` - declared per command, deduped across the call chain.
|
|
496
541
|
|
|
497
542
|
## Command aliases (`alt`)
|
|
@@ -499,7 +544,7 @@ only once. Prereqs run with default options (no argv passed through).
|
|
|
499
544
|
`alt :short_name` (or several) registers extra names for a command:
|
|
500
545
|
|
|
501
546
|
```ruby
|
|
502
|
-
|
|
547
|
+
task :server do
|
|
503
548
|
alt :s, :srv
|
|
504
549
|
proc { |opts| ... }
|
|
505
550
|
end
|
|
@@ -509,36 +554,62 @@ Then `hammer server`, `hammer s`, and `hammer srv` all dispatch to the
|
|
|
509
554
|
same command. Alts work inside namespaces too: `alt :m` on `db:migrate`
|
|
510
555
|
makes `db:m` resolve.
|
|
511
556
|
|
|
512
|
-
## Cross-invocation (`
|
|
557
|
+
## Cross-invocation (`hammer`)
|
|
513
558
|
|
|
514
559
|
From inside any command's proc - or from outside via the class - you can
|
|
515
560
|
invoke other commands without re-shelling out:
|
|
516
561
|
|
|
517
562
|
```ruby
|
|
518
|
-
|
|
563
|
+
task :deploy do
|
|
519
564
|
proc do |opts|
|
|
520
|
-
|
|
521
|
-
|
|
565
|
+
hammer :build, env: 'prod', verbose: true
|
|
566
|
+
hammer 'db:migrate'
|
|
522
567
|
say.green 'deployed'
|
|
523
568
|
end
|
|
524
569
|
end
|
|
525
570
|
```
|
|
526
571
|
|
|
527
|
-
|
|
572
|
+
Signature: `hammer(name, *args, **opts)`. `name` is a symbol for a
|
|
573
|
+
single command or a colon-path string for namespaced commands. Trailing
|
|
574
|
+
positionals become positional ARGV; kwargs become CLI flags:
|
|
528
575
|
|
|
529
|
-
* `
|
|
530
|
-
|
|
531
|
-
*
|
|
576
|
+
* `hammer :foo` → `foo`
|
|
577
|
+
* `hammer 'db:users:list'` → `db:users:list`
|
|
578
|
+
* `hammer :evaluate, 'puts 42'` → `evaluate "puts 42"` (positional ARGV)
|
|
532
579
|
* `verbose: true` → `--verbose`
|
|
533
|
-
* `no_cache: true` → `--no-cache` (
|
|
534
|
-
the kwarg key become dashes)
|
|
580
|
+
* `no_cache: true` → `--no-cache` (underscores in the key become dashes)
|
|
535
581
|
* `dry_run: true` → `--dry-run`
|
|
536
582
|
* `env: 'prod'` → `--env=prod`
|
|
537
583
|
* `anything: false` → skipped (no-op; use `no_x: true` to negate)
|
|
538
584
|
|
|
539
|
-
`MyCli.
|
|
585
|
+
`MyCli.hammer 'db:users:list', verbose: true` also works at the
|
|
540
586
|
class level, useful for tests and scripting.
|
|
541
587
|
|
|
588
|
+
## Chained dispatch (`+`)
|
|
589
|
+
|
|
590
|
+
Run several commands in one shot, Rake-style, using a bare `+` token
|
|
591
|
+
as the separator:
|
|
592
|
+
|
|
593
|
+
```sh
|
|
594
|
+
hammer build + deploy + notify
|
|
595
|
+
hammer build prod -v + db:migrate --pretend + deploy --url=https://x.com
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Each segment is parsed and dispatched independently - its own opts,
|
|
599
|
+
its own positional args. `needs` prereqs dedupe across the whole
|
|
600
|
+
chain, so a shared `:env` step runs once even if every chained command
|
|
601
|
+
declares `needs :env`.
|
|
602
|
+
|
|
603
|
+
A `+` only acts as a separator when it arrives as its own argv token.
|
|
604
|
+
Quoting protects it: `--foo="a + b"` reaches the parser as a single
|
|
605
|
+
token and is left alone. To pass a literal `+` as a positional, double
|
|
606
|
+
it - `++` unescapes to a single `+`:
|
|
607
|
+
|
|
608
|
+
```sh
|
|
609
|
+
hammer echo ++ x # echo gets positional args ["+", "x"]
|
|
610
|
+
hammer echo + bar # runs echo, then runs bar
|
|
611
|
+
```
|
|
612
|
+
|
|
542
613
|
## Shell helpers
|
|
543
614
|
|
|
544
615
|
These are mixed into every handler (and also live on `Hammer::Shell` for
|
|
@@ -660,7 +731,7 @@ load auto: true # recursive scan for *_hammer.rb from here
|
|
|
660
731
|
```ruby
|
|
661
732
|
# tasks/db_hammer.rb
|
|
662
733
|
namespace :db do
|
|
663
|
-
|
|
734
|
+
task :migrate do
|
|
664
735
|
desc 'Run pending migrations'
|
|
665
736
|
opt :pretend, type: :boolean, alias: :p
|
|
666
737
|
proc { |o| say.green "migrating pretend=#{o[:pretend].inspect}" }
|
|
@@ -670,10 +741,10 @@ end
|
|
|
670
741
|
|
|
671
742
|
```ruby
|
|
672
743
|
# tasks/deploy_hammer.rb
|
|
673
|
-
|
|
744
|
+
task :deploy do
|
|
674
745
|
desc 'Deploy to prod'
|
|
675
746
|
proc do |_|
|
|
676
|
-
|
|
747
|
+
hammer 'db:migrate' # cross-file invocation just works
|
|
677
748
|
say.cyan 'deployed'
|
|
678
749
|
end
|
|
679
750
|
end
|
|
@@ -708,7 +779,7 @@ hidden directory.
|
|
|
708
779
|
### Fragment shape
|
|
709
780
|
|
|
710
781
|
A `*_hammer.rb` file is a **block-DSL fragment** - same surface as a
|
|
711
|
-
`Hammerfile`: `
|
|
782
|
+
`Hammerfile`: `task`, `namespace`, and nested `load`. Not a class
|
|
712
783
|
re-open. If you want to extend a `Hammer` subclass in the classic
|
|
713
784
|
`desc` + `def` style across files, use plain `require_relative`.
|
|
714
785
|
|
|
@@ -732,7 +803,7 @@ Same shape as a Hammerfile, just inline:
|
|
|
732
803
|
require 'lux-hammer'
|
|
733
804
|
|
|
734
805
|
Hammer.run(ARGV) do
|
|
735
|
-
|
|
806
|
+
task :hello do
|
|
736
807
|
desc 'say hi'
|
|
737
808
|
opt :loud, type: :boolean, alias: :l
|
|
738
809
|
proc do |opts|
|
|
@@ -776,7 +847,7 @@ end
|
|
|
776
847
|
|
|
777
848
|
```ruby
|
|
778
849
|
# Simple top-level command
|
|
779
|
-
|
|
850
|
+
task :build do
|
|
780
851
|
desc 'Build the project'
|
|
781
852
|
example 'build prod -v'
|
|
782
853
|
example 'build --env=staging'
|
|
@@ -791,14 +862,14 @@ define :build do
|
|
|
791
862
|
end
|
|
792
863
|
|
|
793
864
|
# Command that calls another command
|
|
794
|
-
|
|
865
|
+
task :deploy do
|
|
795
866
|
desc 'Deploy to URL'
|
|
796
867
|
alt :ship
|
|
797
868
|
opt :url, req: true
|
|
798
869
|
opt :force, type: :boolean
|
|
799
870
|
|
|
800
871
|
proc do |opts|
|
|
801
|
-
|
|
872
|
+
hammer :build, env: 'prod'
|
|
802
873
|
exit 0 unless yes? "deploy to #{opts[:url]}?" unless opts[:force]
|
|
803
874
|
say.yellow "deploying to #{opts[:url]}"
|
|
804
875
|
end
|
|
@@ -806,7 +877,7 @@ end
|
|
|
806
877
|
|
|
807
878
|
# Namespace with two levels of nesting
|
|
808
879
|
namespace :db do
|
|
809
|
-
|
|
880
|
+
task :migrate do
|
|
810
881
|
desc 'Run pending migrations'
|
|
811
882
|
alt :m
|
|
812
883
|
example 'db:migrate 3 --pretend'
|
|
@@ -819,7 +890,7 @@ namespace :db do
|
|
|
819
890
|
end
|
|
820
891
|
|
|
821
892
|
namespace :users do
|
|
822
|
-
|
|
893
|
+
task :list do
|
|
823
894
|
desc 'List users'
|
|
824
895
|
opt :role, default: 'all'
|
|
825
896
|
opt :limit, type: :integer, default: 100
|
|
@@ -829,7 +900,7 @@ namespace :db do
|
|
|
829
900
|
end
|
|
830
901
|
end
|
|
831
902
|
|
|
832
|
-
|
|
903
|
+
task :create do
|
|
833
904
|
desc 'Create a user'
|
|
834
905
|
opt :email, req: true
|
|
835
906
|
opt :admin, type: :boolean
|
|
@@ -899,7 +970,7 @@ directly. Useful for embedding or testing:
|
|
|
899
970
|
require 'lux-hammer'
|
|
900
971
|
|
|
901
972
|
class MyCli < Hammer
|
|
902
|
-
|
|
973
|
+
task :greet do
|
|
903
974
|
opt :loud, type: :boolean
|
|
904
975
|
proc do |opts|
|
|
905
976
|
msg = "hello #{opts[:args].first}"
|
|
@@ -909,7 +980,7 @@ class MyCli < Hammer
|
|
|
909
980
|
end
|
|
910
981
|
|
|
911
982
|
MyCli.start(ARGV) # or:
|
|
912
|
-
MyCli.
|
|
983
|
+
MyCli.hammer :greet, loud: true
|
|
913
984
|
```
|
|
914
985
|
|
|
915
986
|
## Development
|
|
@@ -937,11 +1008,11 @@ few small things that have been bugging me about both for years.
|
|
|
937
1008
|
| Lines of code | ~6,000 | ~400 |
|
|
938
1009
|
| Runtime deps | a few | zero |
|
|
939
1010
|
| Root constants | `Thor`, `Thor::Group`, `Thor::Shell`, `Thor::Actions`, ... | just `Hammer` |
|
|
940
|
-
| Command DSL | `desc 'usage', 'help'` + `method_option` + `def name(arg)` | `
|
|
1011
|
+
| Command DSL | `desc 'usage', 'help'` + `method_option` + `def name(arg)` | `task :name do ... proc do \|opts\| end end` (or classic `desc` + `def`) |
|
|
941
1012
|
| Opts container | `Thor::CoreExt::HashWithIndifferentAccess` | plain `Hash` with symbol keys |
|
|
942
1013
|
| Positional args | method positional params + `method_option`, two parallel systems | declared-order opts fill from positional, single system |
|
|
943
1014
|
| Sub-namespaces | `register SubClass, 'name', '...'` (inheritance ceremony) | `namespace :name do ... end` (no classes needed) |
|
|
944
|
-
| Cross-invoke | `invoke 'name', [args], opts` | `
|
|
1015
|
+
| Cross-invoke | `invoke 'name', [args], opts` | `hammer :name, **opts` (looks like a method call) |
|
|
945
1016
|
| Inline CLI | class only | class DSL **or** `Hammer.run do ... end` block DSL **or** a `Hammerfile` |
|
|
946
1017
|
|
|
947
1018
|
**What hammer does better and why:**
|
|
@@ -955,7 +1026,7 @@ few small things that have been bugging me about both for years.
|
|
|
955
1026
|
you into method params (which then clash with options) or makes you read
|
|
956
1027
|
`ARGV` yourself. Hammer just says: opts you declared come first, leftover
|
|
957
1028
|
goes to `opts[:args]`.
|
|
958
|
-
* **Cross-invocation reads as Ruby.** `
|
|
1029
|
+
* **Cross-invocation reads as Ruby.** `hammer 'db:migrate', env: 'prod'`
|
|
959
1030
|
looks like a method call. Thor's `invoke('db:migrate', [], env: 'prod')`
|
|
960
1031
|
always feels like reflection.
|
|
961
1032
|
* **No generator complexity.** Thor's other half is file scaffolding and
|
|
@@ -971,8 +1042,8 @@ few small things that have been bugging me about both for years.
|
|
|
971
1042
|
| Namespacing | colon paths (`db:migrate`) | colon paths (`db:migrate`) - parity |
|
|
972
1043
|
| Per-task options | `task[a,b,c]` positional only | typed `opt`s with flags, aliases, defaults, required |
|
|
973
1044
|
| 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` | `
|
|
975
|
-
| Prerequisites | `task :build => [:clean, :compile]` (declarative DAG) | explicit - call `
|
|
1045
|
+
| Cross-invoke | `Rake::Task['db:migrate'].invoke` | `hammer 'db:migrate'` |
|
|
1046
|
+
| Prerequisites | `task :build => [:clean, :compile]` (declarative DAG) | explicit - call `hammer :clean; hammer :compile` in the proc |
|
|
976
1047
|
| File tasks | yes (mtime-based) | no |
|
|
977
1048
|
| Aliases | none (workarounds via re-defined tasks) | `alt :short_name` |
|
|
978
1049
|
| Split across files | `import 'other.rake'` | `load auto: true` (or explicit paths/globs) |
|
data/lib/hammer/builder.rb
CHANGED
data/lib/lux-hammer.rb
CHANGED
|
@@ -11,13 +11,13 @@ require_relative 'hammer/command_builder'
|
|
|
11
11
|
# Class DSL:
|
|
12
12
|
#
|
|
13
13
|
# class MyCli < Hammer
|
|
14
|
-
#
|
|
14
|
+
# task :build do
|
|
15
15
|
# desc 'Build the project'
|
|
16
16
|
# example 'build -v --env=prod'
|
|
17
17
|
# opt :verbose, type: :boolean, alias: :v
|
|
18
18
|
# opt :env, type: :string, default: 'dev'
|
|
19
19
|
# proc do |opts|
|
|
20
|
-
# say "building #{opts[:env]} args=#{opts[:args].inspect}"
|
|
20
|
+
# say.green "building #{opts[:env]} args=#{opts[:args].inspect}"
|
|
21
21
|
# end
|
|
22
22
|
# end
|
|
23
23
|
# end
|
|
@@ -27,7 +27,7 @@ require_relative 'hammer/command_builder'
|
|
|
27
27
|
# Block DSL is identical, just inside `Hammer.run`:
|
|
28
28
|
#
|
|
29
29
|
# Hammer.run(ARGV) do
|
|
30
|
-
#
|
|
30
|
+
# task :hello do
|
|
31
31
|
# desc 'Greet someone'
|
|
32
32
|
# opt :loud, type: :boolean, alias: :l
|
|
33
33
|
# proc do |opts|
|
|
@@ -124,17 +124,17 @@ class Hammer
|
|
|
124
124
|
# return a Proc as its last expression. That proc is the handler and
|
|
125
125
|
# receives a single `opts` hash with symbol keys; positional ARGV
|
|
126
126
|
# lives at `opts[:args]`.
|
|
127
|
-
def
|
|
127
|
+
def task(name, &block)
|
|
128
128
|
cmd = Command.new(name: name.to_s)
|
|
129
129
|
handler = CommandBuilder.new(cmd).instance_eval(&block)
|
|
130
130
|
unless handler.is_a?(Proc)
|
|
131
131
|
raise Error, <<~MSG
|
|
132
|
-
|
|
132
|
+
task(:#{name}) block must end with a `proc do |opts| ... end`.
|
|
133
133
|
The proc's return value is what becomes the command handler.
|
|
134
134
|
|
|
135
135
|
Example:
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
task :#{name} do
|
|
138
138
|
desc 'what it does'
|
|
139
139
|
example '#{name} foo --env=prod'
|
|
140
140
|
opt :env, default: 'dev'
|
|
@@ -148,7 +148,7 @@ class Hammer
|
|
|
148
148
|
cmd.handler = handler
|
|
149
149
|
commands[cmd.name] = cmd
|
|
150
150
|
|
|
151
|
-
# `
|
|
151
|
+
# `task` ignores pending class-level state, but clear it so a
|
|
152
152
|
# later `def` doesn't accidentally consume stale metadata.
|
|
153
153
|
@pending_desc = nil
|
|
154
154
|
@pending_examples = []
|
|
@@ -158,17 +158,17 @@ class Hammer
|
|
|
158
158
|
end
|
|
159
159
|
|
|
160
160
|
# Open a namespace (group of commands). Everything inside the block
|
|
161
|
-
# (
|
|
161
|
+
# (task, nested namespace, ...) belongs to that namespace, evaluated
|
|
162
162
|
# against an anonymous Hammer subclass.
|
|
163
163
|
#
|
|
164
164
|
# namespace :db do
|
|
165
|
-
#
|
|
165
|
+
# task :migrate do ... end
|
|
166
166
|
# namespace :users do ... end
|
|
167
167
|
# end
|
|
168
168
|
def namespace(name, &block)
|
|
169
169
|
sub = Class.new(Hammer)
|
|
170
170
|
# Track the top-level CLI class so cross-invocation
|
|
171
|
-
# (`
|
|
171
|
+
# (`hammer 'ns:cmd'`) from inside a namespaced command dispatches
|
|
172
172
|
# against the full tree, not just the current namespace.
|
|
173
173
|
sub.instance_variable_set(:@root, root)
|
|
174
174
|
# Parent link, so `before` hooks defined further up the namespace
|
|
@@ -188,8 +188,8 @@ class Hammer
|
|
|
188
188
|
#
|
|
189
189
|
# before { |opts| Dotenv.load }
|
|
190
190
|
# namespace :db do
|
|
191
|
-
# before {
|
|
192
|
-
#
|
|
191
|
+
# before { hammer :env }
|
|
192
|
+
# task :migrate do ... end
|
|
193
193
|
# end
|
|
194
194
|
def before(&block)
|
|
195
195
|
before_hooks << 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` -> `
|
|
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
|
|
|
@@ -274,6 +300,19 @@ class Hammer
|
|
|
274
300
|
return print_help(target)
|
|
275
301
|
end
|
|
276
302
|
|
|
303
|
+
# Trailing colon ("db:") -> expanded namespace listing with full
|
|
304
|
+
# per-command help on every task. Bare ":" expands the root.
|
|
305
|
+
if name.end_with?(':') && name != ':'
|
|
306
|
+
bare = name.chomp(':')
|
|
307
|
+
ns = resolve_namespace(bare)
|
|
308
|
+
return print_namespace_help(bare, ns, full: true) if ns
|
|
309
|
+
Shell.print_error("unknown namespace: #{bare}")
|
|
310
|
+
print_help
|
|
311
|
+
exit 1
|
|
312
|
+
elsif name == ':'
|
|
313
|
+
return print_help(nil, full: true)
|
|
314
|
+
end
|
|
315
|
+
|
|
277
316
|
cmd, owner = resolve(name)
|
|
278
317
|
return owner.run_command(cmd, argv, full: name) if cmd
|
|
279
318
|
|
|
@@ -283,11 +322,10 @@ class Hammer
|
|
|
283
322
|
Shell.print_error("unknown command: #{name}")
|
|
284
323
|
print_help
|
|
285
324
|
exit 1
|
|
286
|
-
ensure
|
|
287
|
-
Thread.current[:hammer_needs_ran] = nil if outer
|
|
288
|
-
Thread.current[:hammer_before_ran] = nil if outer
|
|
289
325
|
end
|
|
290
326
|
|
|
327
|
+
public
|
|
328
|
+
|
|
291
329
|
# Find a command by canonical name or alt within this class.
|
|
292
330
|
def find_command(name)
|
|
293
331
|
commands[name.to_s] || commands.values.find { |c| c.matches?(name) }
|
|
@@ -313,23 +351,22 @@ class Hammer
|
|
|
313
351
|
klass
|
|
314
352
|
end
|
|
315
353
|
|
|
316
|
-
#
|
|
317
|
-
# MyCli.start(["db:users:list", "a", "--verbose"])
|
|
354
|
+
# Programmatic dispatch by name. Useful for scripting and tests.
|
|
318
355
|
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
356
|
+
# MyCli.hammer :build -> start(["build"])
|
|
357
|
+
# MyCli.hammer 'db:users:list' -> start(["db:users:list"])
|
|
358
|
+
# MyCli.hammer :eval, 'puts 42' -> start(["eval", "puts 42"])
|
|
359
|
+
# MyCli.hammer :build, env: 'prod' -> start(["build", "--env=prod"])
|
|
360
|
+
# MyCli.hammer :build, verbose: true -> start(["build", "--verbose"])
|
|
361
|
+
# MyCli.hammer :build, no_cache: true -> start(["build", "--no-cache"])
|
|
362
|
+
# MyCli.hammer :build, cache: false -> skipped (no-op)
|
|
363
|
+
#
|
|
364
|
+
# Symbols are single-segment names; pass a string with colons for
|
|
365
|
+
# namespaced paths. Trailing positionals become positional ARGV.
|
|
366
|
+
# Underscores in option keys become dashes in flags.
|
|
367
|
+
def hammer(name, *args, **opts)
|
|
368
|
+
argv = [name.to_s, *args.map(&:to_s)]
|
|
369
|
+
opts.each do |k, v|
|
|
333
370
|
next if v == false
|
|
334
371
|
flag = "--#{k.to_s.tr('_', '-')}"
|
|
335
372
|
argv << (v == true ? flag : "#{flag}=#{v}")
|
|
@@ -337,10 +374,6 @@ class Hammer
|
|
|
337
374
|
start(argv)
|
|
338
375
|
end
|
|
339
376
|
|
|
340
|
-
def respond_to_missing?(name, include_private = false)
|
|
341
|
-
name.to_s.start_with?('hammer_') || super
|
|
342
|
-
end
|
|
343
|
-
|
|
344
377
|
# Yield [full_colon_path, Command] for every command in this class
|
|
345
378
|
# and all nested namespaces.
|
|
346
379
|
def each_command(prefix = nil, &block)
|
|
@@ -409,29 +442,52 @@ class Hammer
|
|
|
409
442
|
scan.include?('-h') || scan.include?('--help')
|
|
410
443
|
end
|
|
411
444
|
|
|
412
|
-
def print_help(target = nil)
|
|
445
|
+
def print_help(target = nil, full: false)
|
|
413
446
|
if target
|
|
447
|
+
# `help ns:` is equivalent to `ns:` - expanded namespace listing.
|
|
448
|
+
if target.end_with?(':') && target != ':'
|
|
449
|
+
bare = target.chomp(':')
|
|
450
|
+
ns = resolve_namespace(bare)
|
|
451
|
+
return print_namespace_help(bare, ns, full: true) if ns
|
|
452
|
+
Shell.print_error("unknown: #{target}")
|
|
453
|
+
return
|
|
454
|
+
end
|
|
414
455
|
cmd, _ = resolve(target)
|
|
415
456
|
return print_command_help(cmd, target) if cmd
|
|
416
457
|
ns = resolve_namespace(target)
|
|
417
|
-
return print_namespace_help(target, ns) if ns
|
|
458
|
+
return print_namespace_help(target, ns, full: full) if ns
|
|
418
459
|
Shell.print_error("unknown: #{target}")
|
|
419
460
|
return
|
|
420
461
|
end
|
|
421
462
|
|
|
422
463
|
Shell.say "Usage: #{program_name} COMMAND [ARGS]", :cyan
|
|
423
|
-
|
|
424
|
-
|
|
464
|
+
if full
|
|
465
|
+
each_command { |path, c| print_full_block(path, c) unless c.desc.empty? }
|
|
466
|
+
else
|
|
467
|
+
Shell.say ''
|
|
468
|
+
print_command_list(self)
|
|
469
|
+
end
|
|
425
470
|
print_footer
|
|
426
471
|
end
|
|
427
472
|
|
|
428
|
-
def print_namespace_help(prefix, ns)
|
|
473
|
+
def print_namespace_help(prefix, ns, full: false)
|
|
429
474
|
Shell.say "Usage: #{program_name} #{prefix}:COMMAND [ARGS]", :cyan
|
|
430
|
-
|
|
431
|
-
|
|
475
|
+
if full
|
|
476
|
+
ns.each_command(prefix) { |path, c| print_full_block(path, c) unless c.desc.empty? }
|
|
477
|
+
else
|
|
478
|
+
Shell.say ''
|
|
479
|
+
print_command_list(ns, prefix)
|
|
480
|
+
end
|
|
432
481
|
print_footer
|
|
433
482
|
end
|
|
434
483
|
|
|
484
|
+
# One "task block" for the expanded listing: blank line separator
|
|
485
|
+
# then the standard per-command help (usage + desc + options + examples).
|
|
486
|
+
def print_full_block(path, cmd)
|
|
487
|
+
Shell.say ''
|
|
488
|
+
print_command_help(cmd, path)
|
|
489
|
+
end
|
|
490
|
+
|
|
435
491
|
HOMEPAGE ||= 'https://github.com/dux/hammer'.freeze
|
|
436
492
|
|
|
437
493
|
def print_footer
|
|
@@ -442,7 +498,7 @@ class Hammer
|
|
|
442
498
|
def print_command_list(klass, prefix = nil)
|
|
443
499
|
rows = []
|
|
444
500
|
# Commands without a `desc` are hidden from listings but still
|
|
445
|
-
# dispatchable + `
|
|
501
|
+
# dispatchable + `hammer`-callable - useful for private helpers
|
|
446
502
|
# invoked from `before` hooks or other commands (e.g. `:env`, `:app`).
|
|
447
503
|
klass.each_command(prefix) { |full, c| rows << [full, c] unless c.desc.empty? }
|
|
448
504
|
return if rows.empty?
|
|
@@ -528,28 +584,23 @@ class Hammer
|
|
|
528
584
|
|
|
529
585
|
# Inside a command's `proc do |opts| ... end`, call sibling commands:
|
|
530
586
|
#
|
|
531
|
-
#
|
|
587
|
+
# task :deploy do
|
|
532
588
|
# proc do |opts|
|
|
533
|
-
#
|
|
534
|
-
#
|
|
589
|
+
# hammer :build
|
|
590
|
+
# hammer 'db:migrate', pretend: true
|
|
535
591
|
# end
|
|
536
592
|
# end
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
|
593
|
+
#
|
|
594
|
+
# Dispatches from the root class so colon paths resolve against the
|
|
595
|
+
# full tree even when called from inside a namespaced command.
|
|
596
|
+
def hammer(name, *args, **opts)
|
|
597
|
+
self.class.root.hammer(name, *args, **opts)
|
|
547
598
|
end
|
|
548
599
|
|
|
549
600
|
# ----- block DSL -----------------------------------------------------
|
|
550
601
|
|
|
551
602
|
# Define and run a CLI inline. Inside the block use
|
|
552
|
-
# `
|
|
603
|
+
# `task :name do ... end`, `namespace`, and `load`.
|
|
553
604
|
#
|
|
554
605
|
# Without a block: load ./Hammerfile if it exists, otherwise
|
|
555
606
|
# auto-discover *_hammer.rb under Dir.pwd, then dispatch ARGV.
|
|
@@ -575,11 +626,27 @@ class Hammer
|
|
|
575
626
|
path = find_hammerfile(Dir.pwd)
|
|
576
627
|
unless path
|
|
577
628
|
Shell.print_error "no Hammerfile found in #{Dir.pwd} or any parent directory"
|
|
629
|
+
|
|
630
|
+
# Heuristic: *.rb files referencing `Hammer.` are likely inline CLIs
|
|
631
|
+
# the user could promote into a Hammerfile.
|
|
632
|
+
excludes = %w[.git node_modules tmp vendor coverage dist build]
|
|
633
|
+
.map { |d| "--exclude-dir=#{d}" }.join(' ')
|
|
634
|
+
candidates = `grep -rl --include='*.rb' #{excludes} 'Hammer\\.' . 2>/dev/null`
|
|
635
|
+
.lines.map(&:strip).reject(&:empty?)
|
|
636
|
+
unless candidates.empty?
|
|
637
|
+
Shell.say "possible CLI implementation(s) - files referencing `Hammer.`:", :yellow
|
|
638
|
+
candidates.first(10).each { |f| Shell.say " #{f.sub(%r{\A\./}, '')}" }
|
|
639
|
+
Shell.say ''
|
|
640
|
+
end
|
|
641
|
+
|
|
578
642
|
Shell.say "create one - example:"
|
|
643
|
+
puts
|
|
579
644
|
Shell.say <<~RUBY
|
|
580
|
-
|
|
645
|
+
task :hello do
|
|
581
646
|
desc 'say hello'
|
|
582
|
-
proc
|
|
647
|
+
proc do |opts|
|
|
648
|
+
say.green "hello \#{opts[:args].first || 'world'}"
|
|
649
|
+
end
|
|
583
650
|
end
|
|
584
651
|
RUBY
|
|
585
652
|
exit 1
|
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.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dino Reic
|
|
@@ -61,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
61
61
|
- !ruby/object:Gem::Version
|
|
62
62
|
version: '0'
|
|
63
63
|
requirements: []
|
|
64
|
-
rubygems_version: 4.0.
|
|
64
|
+
rubygems_version: 4.0.10
|
|
65
65
|
specification_version: 4
|
|
66
66
|
summary: Thor-inspired tiny CLI builder
|
|
67
67
|
test_files: []
|