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 +4 -4
- data/.version +1 -1
- data/README.md +108 -39
- data/lib/hammer/builder.rb +0 -4
- data/lib/lux-hammer.rb +92 -63
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae2c5c9dbcd3b17941440e327e04e84dbffaec7209e4ce15b205cf83525b3724
|
|
4
|
+
data.tar.gz: 516eb4a4cb0f8f02f489eb00d2e4500cfe70f600eaebe192fb1749c81fe37074
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d922e58738cb3afc95c54864fc79d9a6ec556e3634a94f258711652abf220ba7f436e1d0f0cf24f48ba9c6372af22339d8377b491c9de93f708e19a7ca13692d
|
|
7
|
+
data.tar.gz: 6ad385997b87bf70ba58ec3ae6d701b3717d5e9584ef00aae29ebeb5c9b3666a39f6032277e275987f6de94161809767eeb838e288718fda09287327009756b6
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.3
|
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.
|
|
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
|
-
|
|
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 {
|
|
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 `
|
|
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 {
|
|
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
|
-
`
|
|
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 `
|
|
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 {
|
|
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 (`
|
|
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
|
-
|
|
521
|
-
|
|
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
|
-
|
|
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
|
-
* `
|
|
530
|
-
|
|
531
|
-
*
|
|
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` (
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
916
|
+
Usage: hammer COMMAND [ARGS]
|
|
848
917
|
|
|
849
918
|
Commands:
|
|
850
|
-
|
|
851
|
-
|
|
919
|
+
hammer build # Build the project
|
|
920
|
+
hammer deploy (alt: ship) # Deploy to URL
|
|
852
921
|
|
|
853
922
|
db:
|
|
854
|
-
|
|
923
|
+
hammer db:migrate (alt: m) # Run pending migrations
|
|
855
924
|
|
|
856
925
|
db:users:
|
|
857
|
-
|
|
858
|
-
|
|
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:
|
|
944
|
+
Usage: hammer db:COMMAND [ARGS]
|
|
876
945
|
|
|
877
946
|
Commands:
|
|
878
|
-
|
|
947
|
+
hammer db:migrate (alt: m) # Run pending migrations
|
|
879
948
|
|
|
880
949
|
users:
|
|
881
|
-
|
|
882
|
-
|
|
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:
|
|
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.
|
|
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` | `
|
|
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.** `
|
|
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` | `
|
|
975
|
-
| Prerequisites | `task :build => [:clean, :compile]` (declarative DAG) | explicit - call `
|
|
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) |
|
data/lib/hammer/builder.rb
CHANGED
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}"
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
#
|
|
110
|
-
# the
|
|
111
|
-
# (e.g. `
|
|
112
|
-
#
|
|
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,
|
|
163
|
-
#
|
|
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
|
-
# (`
|
|
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
|
-
#
|
|
179
|
-
#
|
|
180
|
-
|
|
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 {
|
|
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` -> `
|
|
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
|
-
#
|
|
317
|
-
#
|
|
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
|
-
#
|
|
320
|
-
#
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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 + `
|
|
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
|
-
#
|
|
534
|
-
#
|
|
553
|
+
# hammer :build
|
|
554
|
+
# hammer 'db:migrate', pretend: true
|
|
535
555
|
# end
|
|
536
556
|
# 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
|
|
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
|
|
552
|
-
# `define :name do ... end`, and `
|
|
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
|
|
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
|
|
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).
|