lux-hammer 0.3.12 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/AGENTS.md +45 -39
- data/README.md +47 -44
- data/lib/hammer/builtins.rb +40 -33
- data/lib/hammer/dotenv.rb +3 -2
- data/lib/hammer/loader.rb +13 -7
- data/lib/hammer/option.rb +2 -0
- data/lib/hammer/parser.rb +40 -10
- data/lib/hammer/recipe.rb +2 -2
- data/lib/hammer/shell.rb +5 -2
- data/lib/lux-hammer.rb +69 -35
- data/recipes/deploy.rb +32 -0
- data/recipes/lib/deploy/boot.rb +52 -0
- data/recipes/lib/deploy/commands.rb +555 -0
- data/recipes/lib/deploy/config.rb +62 -0
- data/recipes/lib/deploy/context.rb +149 -0
- data/recipes/lib/deploy/doctor.rb +238 -0
- data/recipes/lib/deploy/hammer.rb +168 -0
- data/recipes/lib/deploy/manifest.rb +169 -0
- data/recipes/lib/deploy/ssh.rb +129 -0
- data/recipes/lib/deploy/template.rb +39 -0
- metadata +12 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 411174afd97cefbe9e7e3309c25233f465b9458ee0dc710e67077725658876c0
|
|
4
|
+
data.tar.gz: f4b64f7897fd8c67412fdbe418ad51b96eeac1ebb3e87ce9df5b97651a4a9ff6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba1364fcff70fefa529a2e7586ee33f7826b43ee7cd3ebd5e454f0bce82131b427cf320d85d8d911edc3a11762196de7db9964c6877959548eb4c6753e5ac063
|
|
7
|
+
data.tar.gz: 5b21c08ac766b1af02b1a0d49b7fb3129fffd94b0c13fd07c7a5995851c21447e6a437848f25ef30f364bfccb94f1bac51046630dbbd574aaa109a61a738e893
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.13
|
data/AGENTS.md
CHANGED
|
@@ -156,17 +156,18 @@ Runtime cross-invocation:
|
|
|
156
156
|
errors if none found anywhere up the tree (unless the invocation
|
|
157
157
|
routes to a built-in - see `dispatches_to_builtin?` /
|
|
158
158
|
`looks_like_builtin?`, true for bare invocation / leading flag /
|
|
159
|
-
explicit help /
|
|
160
|
-
Hammerfile, registers the
|
|
161
|
-
`:
|
|
162
|
-
`
|
|
163
|
-
`
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
159
|
+
explicit help / an `h:`-namespace path). After evaluating the
|
|
160
|
+
Hammerfile, registers the built-ins via `Hammer::Builtins.register`:
|
|
161
|
+
`:default` at the root (backs bare invocation, carries `--version`)
|
|
162
|
+
plus the `h:` namespace (`h:help`, `h:update`, `h:agents`,
|
|
163
|
+
`h:version`, `h:recipes`, `h:init`). The full set registers in every
|
|
164
|
+
context - living under `h:` means they can't collide with project
|
|
165
|
+
root tasks, so there's no core/no-project split. Each is guarded by
|
|
166
|
+
`unless commands.key?(...)` so a user who reopens `namespace :h` wins
|
|
167
|
+
silently. The `--system` flag is peeled off argv at the top and
|
|
168
|
+
forces the no-Hammerfile branch. Not part of the user-facing surface
|
|
169
|
+
- don't recommend it in examples; `Hammer.run` is what library users
|
|
170
|
+
should reach for.
|
|
170
171
|
* `Hammer.recipe(name, argv = ARGV)` - entry for recipe stubs in PATH.
|
|
171
172
|
Loads `<gem>/recipes/<name>.rb` (or its user-dir override), runs as
|
|
172
173
|
a standalone CLI with `program_name = name`. No Hammerfile lookup,
|
|
@@ -239,45 +240,50 @@ explicit ADR-level discussion. Keys:
|
|
|
239
240
|
|
|
240
241
|
## Built-in tasks (user-overridable)
|
|
241
242
|
|
|
242
|
-
`Hammer::Builtins`
|
|
243
|
-
**after** Hammerfile evaluation and
|
|
244
|
-
already defined it (`unless
|
|
245
|
-
redefinition warning).
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
* `
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
243
|
+
`Hammer::Builtins.register(klass)` is the single registration entry
|
|
244
|
+
point. It runs **after** Hammerfile evaluation and skips each task when
|
|
245
|
+
the user already defined it (`unless commands.key?(name)` - no
|
|
246
|
+
redefinition warning). The same set registers in every context (project
|
|
247
|
+
or bare binary): `:default` at the root, everything else under the
|
|
248
|
+
reserved `h:` namespace.
|
|
249
|
+
|
|
250
|
+
* Root: `:default` (hidden, carries `--version` / `-v`).
|
|
251
|
+
* `h:` namespace: `h:help`, `h:update`, `h:agents`, `h:version`,
|
|
252
|
+
`h:recipes`, `h:init`.
|
|
253
|
+
|
|
254
|
+
`h:` is the reserved built-in namespace - keeping the tool-meta commands
|
|
255
|
+
there means they never collide with a project's root tasks, so there's
|
|
256
|
+
no core/no-project split and no hidden-desc bookkeeping. Other names
|
|
257
|
+
(`:self`, `:recipes` at the root, ...) stay free for Hammerfiles. The
|
|
258
|
+
`h:` subclass is flagged `@builtin_namespace`, which prunes it from the
|
|
259
|
+
compact listing - the built-ins are always dispatchable but only show
|
|
260
|
+
(under an `h:` section) in the extended `--help` view.
|
|
257
261
|
|
|
258
262
|
Task contracts:
|
|
259
263
|
|
|
260
|
-
* `:default` - hidden (no desc)
|
|
261
|
-
(brief)
|
|
262
|
-
|
|
264
|
+
* `:default` - hidden (no desc), at the root. Prints
|
|
265
|
+
`self.class.root.print_help` (brief) and exposes a `--version` flag.
|
|
266
|
+
Fires for bare `hammer` and leading-flag invocations where the first
|
|
267
|
+
token starts with `-` and isn't `-h` / `--help` (see
|
|
263
268
|
`Hammer.dispatch`). User overrides typically declare flag opts and
|
|
264
269
|
fall through to `hammer :help` when none matched.
|
|
265
|
-
*
|
|
266
|
-
`self.class.root.print_help(opts[:args].first, extended: true)`.
|
|
267
|
-
|
|
268
|
-
|
|
270
|
+
* `h:help` - has a desc; shows in the `--help` listing. Calls
|
|
271
|
+
`self.class.root.print_help(opts[:args].first, extended: true)`. The
|
|
272
|
+
conventional `help` / `-h` / `--help` at the top level still reach it
|
|
273
|
+
(handled directly in `Hammer.dispatch`).
|
|
274
|
+
* `h:update` / `h:agents` / `h:version` - thin wrappers around
|
|
269
275
|
`Hammer.self_update` / `Hammer.print_ai_help` / `puts Hammer::VERSION`.
|
|
270
|
-
*
|
|
276
|
+
* `h:recipes` - all recipe-management actions on one task, picked via
|
|
271
277
|
boolean opts: `--install [NAME [TARGET]]`, `--show NAME`,
|
|
272
278
|
`--path NAME`, `--edit NAME`, `--run NAME [ARGS]` (use `--` to
|
|
273
279
|
forward flags through). Bare invocation lists.
|
|
274
|
-
*
|
|
280
|
+
* `h:init` - writes `Hammer::STARTER_HAMMERFILE` to `./Hammerfile`;
|
|
275
281
|
refuses if one exists.
|
|
276
282
|
|
|
277
|
-
|
|
278
|
-
full: name, quiet: true)` - the
|
|
279
|
-
suppressed because the user didn't type
|
|
280
|
-
`hammer help` literally.
|
|
283
|
+
The `:default` task and the `help` / `-h` / `--help` requests are
|
|
284
|
+
invoked via `run_command(cmd, argv, full: name, quiet: true)` - the
|
|
285
|
+
gray `> prog cmd ...` banner is suppressed because the user didn't type
|
|
286
|
+
`hammer default` / `hammer h:help` literally.
|
|
281
287
|
* Commands listed flat with colon paths, grouped by top-level namespace.
|
|
282
288
|
* Bare namespace (`hammer db`) prints the same listing scoped to that
|
|
283
289
|
namespace.
|
data/README.md
CHANGED
|
@@ -73,7 +73,7 @@ Clones into `~/.local/share/lux-hammer` (override with `LUX_HAMMER_DIR=`),
|
|
|
73
73
|
builds the gem, and installs it. Re-run any time, or use:
|
|
74
74
|
|
|
75
75
|
```sh
|
|
76
|
-
hammer update # git pull main + rebuild + reinstall
|
|
76
|
+
hammer h:update # git pull main + rebuild + reinstall
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
## Quick start
|
|
@@ -537,41 +537,43 @@ end
|
|
|
537
537
|
`hammer` and `hammer db` won't list `env`, but `hammer env`,
|
|
538
538
|
`hammer :env` from another proc, and `before { hammer :env }` all work.
|
|
539
539
|
|
|
540
|
-
## Built-in tasks
|
|
540
|
+
## Built-in tasks
|
|
541
541
|
|
|
542
|
-
The `hammer` binary auto-registers a small set of built-in tasks
|
|
543
|
-
the
|
|
544
|
-
|
|
545
|
-
|
|
542
|
+
The `hammer` binary auto-registers a small set of built-in tasks. All
|
|
543
|
+
the tool-meta commands live under the reserved `h:` namespace so they
|
|
544
|
+
can never collide with your project's own root tasks. They register the
|
|
545
|
+
same way with or without a Hammerfile and are always dispatchable, but
|
|
546
|
+
to keep the bare listing focused on your project they only show up
|
|
547
|
+
(under an `h:` section) in the extended `hammer --help` view.
|
|
546
548
|
|
|
547
|
-
|
|
549
|
+
Handled at the root (so the conventional invocations keep working):
|
|
548
550
|
|
|
549
551
|
* `:default` - fires on bare `hammer` and on leading-flag invocations.
|
|
550
|
-
Hidden from listings.
|
|
551
|
-
to wire up your own global flags.
|
|
552
|
-
*
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
* `:update` - rebuild + reinstall lux-hammer from main.
|
|
556
|
-
* `:agents` - dump AGENTS.md (AI-friendly Hammerfile authoring docs).
|
|
557
|
-
* `:version` - print the lux-hammer version.
|
|
552
|
+
Hidden from listings. Prints the brief help by default and carries a
|
|
553
|
+
`--version` / `-v` flag; override to wire up your own global flags.
|
|
554
|
+
* `hammer help [TARGET]`, `hammer -h`, `hammer --help` - prints the
|
|
555
|
+
extended help view; `TARGET` accepts a command path or a `ns:` prefix.
|
|
556
|
+
(Also available as `hammer h:help`.)
|
|
558
557
|
|
|
559
|
-
|
|
558
|
+
Under the `h:` namespace:
|
|
560
559
|
|
|
561
|
-
*
|
|
562
|
-
*
|
|
560
|
+
* `h:update` - rebuild + reinstall lux-hammer from main.
|
|
561
|
+
* `h:agents` - dump AGENTS.md (AI-friendly Hammerfile authoring docs).
|
|
562
|
+
* `h:version` - print the lux-hammer version.
|
|
563
|
+
* `h:recipes` - list / install / show / edit recipes.
|
|
564
|
+
* `h:init` - write a starter Hammerfile in cwd (refuses if one exists).
|
|
563
565
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
+
Each is guarded by `unless commands.key?` within the namespace, so you
|
|
567
|
+
can override one by reopening `namespace :h` in your Hammerfile.
|
|
568
|
+
|
|
569
|
+
`--system` forces the no-Hammerfile branch - the Hammerfile in the
|
|
570
|
+
current tree (if any) isn't loaded for that invocation:
|
|
566
571
|
|
|
567
572
|
```sh
|
|
568
|
-
hammer --system recipes # list recipes
|
|
569
|
-
hammer --system recipes --install srt ~/bin/srt
|
|
573
|
+
hammer --system h:recipes # list recipes, ignoring any local Hammerfile
|
|
574
|
+
hammer --system h:recipes --install srt ~/bin/srt
|
|
570
575
|
```
|
|
571
576
|
|
|
572
|
-
`--system` forces the no-Hammerfile branch - the Hammerfile in the
|
|
573
|
-
current tree (if any) isn't loaded for that invocation.
|
|
574
|
-
|
|
575
577
|
Customize bare `hammer` by replacing `:default`:
|
|
576
578
|
|
|
577
579
|
```ruby
|
|
@@ -1106,20 +1108,21 @@ A **recipe** is a standalone Hammerfile-style script bundled inside the
|
|
|
1106
1108
|
top-level binary in your `PATH` - so `srt` becomes a real command, not
|
|
1107
1109
|
`hammer srt:shift`.
|
|
1108
1110
|
|
|
1109
|
-
Recipe management lives under the `recipes` task
|
|
1110
|
-
|
|
1111
|
-
|
|
1111
|
+
Recipe management lives under the `h:recipes` task, reachable from
|
|
1112
|
+
anywhere (inside a project the `h:` namespace can't shadow your root
|
|
1113
|
+
tasks). `--system` skips loading a local Hammerfile if you want a clean
|
|
1114
|
+
environment.
|
|
1112
1115
|
|
|
1113
1116
|
Listing what's available:
|
|
1114
1117
|
|
|
1115
1118
|
```sh
|
|
1116
|
-
$ hammer recipes # from any
|
|
1117
|
-
$ hammer --system recipes #
|
|
1119
|
+
$ hammer h:recipes # from any directory
|
|
1120
|
+
$ hammer --system h:recipes # ignoring any local Hammerfile
|
|
1118
1121
|
gem:
|
|
1119
1122
|
srt # Subtitle (.srt) toolkit - shift timestamps, show stats
|
|
1120
|
-
[install: hammer recipes --install srt]
|
|
1123
|
+
[install: hammer h:recipes --install srt]
|
|
1121
1124
|
llm # personal LLM utility CLI (memory store, prompt-token expander, ...)
|
|
1122
|
-
[install: hammer recipes --install llm]
|
|
1125
|
+
[install: hammer h:recipes --install llm]
|
|
1123
1126
|
```
|
|
1124
1127
|
|
|
1125
1128
|
Installing one. With no TARGET, the stub is printed to stdout (you
|
|
@@ -1127,8 +1130,8 @@ redirect it yourself); with a TARGET path, lux-hammer writes the file
|
|
|
1127
1130
|
and chmods +x in one step:
|
|
1128
1131
|
|
|
1129
1132
|
```sh
|
|
1130
|
-
$ hammer recipes --install srt ~/bin/srt # write + chmod
|
|
1131
|
-
$ hammer recipes --install srt > ~/bin/srt && chmod +x ~/bin/srt
|
|
1133
|
+
$ hammer h:recipes --install srt ~/bin/srt # write + chmod
|
|
1134
|
+
$ hammer h:recipes --install srt > ~/bin/srt && chmod +x ~/bin/srt
|
|
1132
1135
|
$ srt --help
|
|
1133
1136
|
Usage: srt COMMAND [ARGS]
|
|
1134
1137
|
|
|
@@ -1145,7 +1148,7 @@ so the recipe always runs the version currently in the gem.
|
|
|
1145
1148
|
### Authoring your own
|
|
1146
1149
|
|
|
1147
1150
|
Drop a plain `.rb` file in `~/.config/hammer/recipes/`. The first
|
|
1148
|
-
`# desc: ...` comment is what shows in `hammer recipes`. The file body
|
|
1151
|
+
`# desc: ...` comment is what shows in `hammer h:recipes`. The file body
|
|
1149
1152
|
uses the same DSL as a Hammerfile - `task`, `namespace`, `before`,
|
|
1150
1153
|
`load`. Example `~/.config/hammer/recipes/json.rb`:
|
|
1151
1154
|
|
|
@@ -1165,22 +1168,22 @@ end
|
|
|
1165
1168
|
Install it the same way:
|
|
1166
1169
|
|
|
1167
1170
|
```sh
|
|
1168
|
-
$ hammer recipes --install json ~/bin/json
|
|
1171
|
+
$ hammer h:recipes --install json ~/bin/json
|
|
1169
1172
|
```
|
|
1170
1173
|
|
|
1171
1174
|
User-dir recipes override gem recipes with the same name, so you can
|
|
1172
1175
|
fork without forking.
|
|
1173
1176
|
|
|
1174
|
-
### Other `recipes` actions
|
|
1177
|
+
### Other `h:recipes` actions
|
|
1175
1178
|
|
|
1176
1179
|
```sh
|
|
1177
|
-
hammer recipes # list all
|
|
1178
|
-
hammer recipes --install # interactive picker, then prints stub
|
|
1179
|
-
hammer recipes --show NAME # cat the recipe source
|
|
1180
|
-
hammer recipes --path NAME # absolute path
|
|
1181
|
-
hammer recipes --edit NAME # open in $EDITOR (copies gem -> user dir first)
|
|
1182
|
-
hammer recipes --run NAME [ARGS] # run without installing its bin
|
|
1183
|
-
hammer recipes --run NAME -- --help # `--` forwards flags to the recipe
|
|
1180
|
+
hammer h:recipes # list all
|
|
1181
|
+
hammer h:recipes --install # interactive picker, then prints stub
|
|
1182
|
+
hammer h:recipes --show NAME # cat the recipe source
|
|
1183
|
+
hammer h:recipes --path NAME # absolute path
|
|
1184
|
+
hammer h:recipes --edit NAME # open in $EDITOR (copies gem -> user dir first)
|
|
1185
|
+
hammer h:recipes --run NAME [ARGS] # run without installing its bin
|
|
1186
|
+
hammer h:recipes --run NAME -- --help # `--` forwards flags to the recipe
|
|
1184
1187
|
```
|
|
1185
1188
|
|
|
1186
1189
|
## Programmatic use
|
data/lib/hammer/builtins.rb
CHANGED
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
class Hammer
|
|
2
|
-
# Built-in tasks of the `hammer` binary
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# * register_no_project - tasks meaningful only when no Hammerfile is
|
|
10
|
-
# loaded (or `--system` was passed): :recipes, :init. These would
|
|
11
|
-
# collide too easily with user tasks if always registered, so the
|
|
12
|
-
# Hammerfile branch skips them.
|
|
2
|
+
# Built-in tasks of the `hammer` binary. Everything but :default lives
|
|
3
|
+
# under the reserved `h:` namespace (e.g. `hammer h:update`,
|
|
4
|
+
# `hammer h:recipes`) so the built-ins never collide with a project's
|
|
5
|
+
# own root tasks. Only the bare `hammer` invocation and the
|
|
6
|
+
# `-h`/`--help`/`help` request are handled at root - bare fires
|
|
7
|
+
# :default, which also carries the `--version` convenience flag.
|
|
13
8
|
module Builtins
|
|
14
9
|
module_function
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
# Single registration entry point - identical in every context
|
|
12
|
+
# (project Hammerfile or bare `hammer` binary). Namespacing under
|
|
13
|
+
# `h:` removes the old collision worries, so there's no core vs
|
|
14
|
+
# no-project split and no hidden-desc dance: the same tasks register
|
|
15
|
+
# everywhere, always dispatchable, and surface in the listing only
|
|
16
|
+
# under the extended `--help` view (the `@builtin_namespace` flag).
|
|
17
|
+
def register(klass)
|
|
18
18
|
register_default(klass) unless klass.commands.key?('default')
|
|
19
|
-
register_update(klass) unless klass.commands.key?('update')
|
|
20
|
-
register_agents(klass) unless klass.commands.key?('agents')
|
|
21
|
-
register_version(klass) unless klass.commands.key?('version')
|
|
22
|
-
end
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
existed = klass.namespaces.key?('h')
|
|
21
|
+
klass.namespace(:h) {} # ensure/reopen the reserved subclass
|
|
22
|
+
h = klass.namespaces['h']
|
|
23
|
+
# Flag the tree so the compact listing prunes it - the built-ins
|
|
24
|
+
# only show under the extended `--help` view (always dispatchable).
|
|
25
|
+
# Skip when the user already owns an `h:` namespace, so their own
|
|
26
|
+
# tasks stay visible in the bare listing.
|
|
27
|
+
h.instance_variable_set(:@builtin_namespace, true) unless existed
|
|
28
|
+
register_help(h) unless h.commands.key?('help')
|
|
29
|
+
register_update(h) unless h.commands.key?('update')
|
|
30
|
+
register_agents(h) unless h.commands.key?('agents')
|
|
31
|
+
register_version(h) unless h.commands.key?('version')
|
|
32
|
+
register_recipes(h) unless h.commands.key?('recipes')
|
|
33
|
+
register_init(h) unless h.commands.key?('init')
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
def register_help(klass)
|
|
@@ -37,9 +44,9 @@ class Hammer
|
|
|
37
44
|
per-command help; with a namespace prefix prints that
|
|
38
45
|
namespace's command listing.
|
|
39
46
|
TXT
|
|
40
|
-
example 'help'
|
|
41
|
-
example 'help build'
|
|
42
|
-
example 'help db:'
|
|
47
|
+
example 'h:help'
|
|
48
|
+
example 'h:help build'
|
|
49
|
+
example 'h:help db:'
|
|
43
50
|
proc do |opts|
|
|
44
51
|
self.class.root.print_help(opts[:args].first, extended: true)
|
|
45
52
|
end
|
|
@@ -109,7 +116,7 @@ class Hammer
|
|
|
109
116
|
# invocation lists; opts pick the action and positional args carry
|
|
110
117
|
# the recipe name (and optional target path for --install). Run via
|
|
111
118
|
# `--run NAME [ARGS]` - use `--` to forward flags to the recipe
|
|
112
|
-
# itself (e.g. `hammer recipes --run srt -- --help`).
|
|
119
|
+
# itself (e.g. `hammer h:recipes --run srt -- --help`).
|
|
113
120
|
def register_recipes(klass)
|
|
114
121
|
klass.class_eval do
|
|
115
122
|
task :recipes do
|
|
@@ -123,12 +130,12 @@ class Hammer
|
|
|
123
130
|
opt :path, type: :boolean, desc: 'print recipe abs path'
|
|
124
131
|
opt :edit, type: :boolean, desc: 'open recipe in $EDITOR (copies gem -> user dir first)'
|
|
125
132
|
opt :run, type: :boolean, desc: 'run a recipe without installing its bin (forwards remaining args)'
|
|
126
|
-
example 'recipes'
|
|
127
|
-
example 'recipes --install srt ~/bin/srt # write + chmod in one shot'
|
|
128
|
-
example 'recipes --install srt > ~/bin/srt && chmod +x $_'
|
|
129
|
-
example 'recipes --show srt'
|
|
130
|
-
example 'recipes --run srt extract movie.mp4'
|
|
131
|
-
example 'recipes --run srt -- --help # -- forwards flags to the recipe'
|
|
133
|
+
example 'h:recipes'
|
|
134
|
+
example 'h:recipes --install srt ~/bin/srt # write + chmod in one shot'
|
|
135
|
+
example 'h:recipes --install srt > ~/bin/srt && chmod +x $_'
|
|
136
|
+
example 'h:recipes --show srt'
|
|
137
|
+
example 'h:recipes --run srt extract movie.mp4'
|
|
138
|
+
example 'h:recipes --run srt -- --help # -- forwards flags to the recipe'
|
|
132
139
|
proc do |opts|
|
|
133
140
|
args = opts[:args]
|
|
134
141
|
if opts[:install]
|
|
@@ -170,7 +177,7 @@ class Hammer
|
|
|
170
177
|
|
|
171
178
|
def require_name!(name, action)
|
|
172
179
|
return name if name
|
|
173
|
-
Shell.print_error "missing recipe name (usage: recipes --#{action} NAME)"
|
|
180
|
+
Shell.print_error "missing recipe name (usage: h:recipes --#{action} NAME)"
|
|
174
181
|
exit 1
|
|
175
182
|
end
|
|
176
183
|
|
|
@@ -192,7 +199,7 @@ class Hammer
|
|
|
192
199
|
Shell.say "#{source}:", :yellow
|
|
193
200
|
items.each_key do |name|
|
|
194
201
|
_n, desc, installed = rows.find { |r| r.first == name }
|
|
195
|
-
status = installed ? "(installed: #{installed})" : "[install: hammer recipes --install #{name}]"
|
|
202
|
+
status = installed ? "(installed: #{installed})" : "[install: hammer h:recipes --install #{name}]"
|
|
196
203
|
line = " #{name.ljust(width)} # #{desc}"
|
|
197
204
|
Shell.say line
|
|
198
205
|
Shell.say " #{' ' * width} #{status}", :gray
|
|
@@ -242,7 +249,7 @@ class Hammer
|
|
|
242
249
|
end
|
|
243
250
|
|
|
244
251
|
# For a gem recipe, offer to copy to user dir first so edits
|
|
245
|
-
# survive `hammer update`. Then exec $EDITOR on the file.
|
|
252
|
+
# survive `hammer h:update`. Then exec $EDITOR on the file.
|
|
246
253
|
def edit(name)
|
|
247
254
|
path = Hammer::Recipe.path(name) or fail_unknown(name)
|
|
248
255
|
editor = ENV['EDITOR'] || ENV['VISUAL']
|
data/lib/hammer/dotenv.rb
CHANGED
|
@@ -38,8 +38,9 @@ class Hammer
|
|
|
38
38
|
key = key.strip
|
|
39
39
|
next if key.empty?
|
|
40
40
|
val = val.strip
|
|
41
|
-
if
|
|
42
|
-
(val.start_with?("'
|
|
41
|
+
if val.length >= 2 &&
|
|
42
|
+
((val.start_with?('"') && val.end_with?('"')) ||
|
|
43
|
+
(val.start_with?("'") && val.end_with?("'")))
|
|
43
44
|
val = val[1..-2]
|
|
44
45
|
end
|
|
45
46
|
out[key] = val
|
data/lib/hammer/loader.rb
CHANGED
|
@@ -38,31 +38,37 @@ class Hammer
|
|
|
38
38
|
|
|
39
39
|
def resolve_pattern(anchor, pattern)
|
|
40
40
|
abs = File.expand_path(pattern, anchor)
|
|
41
|
+
# A real file/dir wins over glob interpretation, so a literal path
|
|
42
|
+
# whose name contains [ ] { } * ? still resolves. Directory: discover
|
|
43
|
+
# *_hammer.rb under it (empty result is OK - an app may have none).
|
|
44
|
+
return [abs] if File.file?(abs)
|
|
45
|
+
return discover(abs) if File.directory?(abs)
|
|
41
46
|
if abs.match?(/[\*\?\[\{]/)
|
|
42
47
|
matches = Dir.glob(abs).sort
|
|
43
48
|
raise Hammer::Error, "load: no files matched #{pattern.inspect}" if matches.empty?
|
|
44
49
|
return matches
|
|
45
50
|
end
|
|
46
|
-
# Directory: discover *_hammer.rb under it. Empty result is OK
|
|
47
|
-
# (an app may legitimately have no fragments).
|
|
48
|
-
return discover(abs) if File.directory?(abs)
|
|
49
|
-
return [abs] if File.file?(abs)
|
|
50
51
|
raise Hammer::Error, "load: no files matched #{pattern.inspect}"
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
def discover(dir)
|
|
54
55
|
return [] unless File.directory?(dir)
|
|
55
56
|
out = []
|
|
56
|
-
walk(dir, out)
|
|
57
|
+
walk(dir, out, {})
|
|
57
58
|
out.sort
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
# `seen` tracks visited real paths so a cyclic directory symlink
|
|
62
|
+
# doesn't recurse forever.
|
|
63
|
+
def walk(dir, out, seen)
|
|
64
|
+
real = File.realpath(dir) rescue dir
|
|
65
|
+
return if seen[real]
|
|
66
|
+
seen[real] = true
|
|
61
67
|
Dir.each_child(dir) do |entry|
|
|
62
68
|
full = File.join(dir, entry)
|
|
63
69
|
if File.directory?(full)
|
|
64
70
|
next if SKIP_DIRS.include?(entry) || entry.start_with?('.')
|
|
65
|
-
walk(full, out)
|
|
71
|
+
walk(full, out, seen)
|
|
66
72
|
elsif entry.end_with?('_hammer.rb')
|
|
67
73
|
out << full
|
|
68
74
|
end
|
data/lib/hammer/option.rb
CHANGED
data/lib/hammer/parser.rb
CHANGED
|
@@ -8,9 +8,9 @@ class Hammer
|
|
|
8
8
|
@options = options
|
|
9
9
|
@by_switch = {}
|
|
10
10
|
options.each do |opt|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
opt.aliases.each { |a|
|
|
11
|
+
register_switch(opt.switch, opt)
|
|
12
|
+
register_switch(opt.negation, opt) if opt.boolean?
|
|
13
|
+
opt.aliases.each { |a| register_switch(a, opt) }
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -31,7 +31,11 @@ class Hammer
|
|
|
31
31
|
if token.start_with?('--') && token.include?('=')
|
|
32
32
|
key, val = token.split('=', 2)
|
|
33
33
|
opt = lookup!(key)
|
|
34
|
-
|
|
34
|
+
if opt.boolean? && key.start_with?('--no-')
|
|
35
|
+
values[opt.name] = !opt.cast(val) # `--no-x=false` -> true
|
|
36
|
+
else
|
|
37
|
+
values[opt.name] = opt.cast(val)
|
|
38
|
+
end
|
|
35
39
|
i += 1
|
|
36
40
|
next
|
|
37
41
|
end
|
|
@@ -50,7 +54,18 @@ class Hammer
|
|
|
50
54
|
next
|
|
51
55
|
end
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
# Glued short flag with value: `-pVALUE` (non-boolean short opts only).
|
|
58
|
+
if token.start_with?('-') && !token.start_with?('--') && token.length > 2
|
|
59
|
+
if (opt = @by_switch[token[0, 2]]) && !opt.boolean?
|
|
60
|
+
values[opt.name] = opt.cast(token[2..])
|
|
61
|
+
i += 1
|
|
62
|
+
next
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Dash-led and not a negative number -> a genuinely unknown flag.
|
|
67
|
+
# Bare `-` and negative numbers (`-5`) fall through to positionals.
|
|
68
|
+
if token.start_with?('-') && token.length > 1 && token !~ /\A-\d/
|
|
54
69
|
raise Error, "unknown option: #{token}"
|
|
55
70
|
end
|
|
56
71
|
|
|
@@ -59,15 +74,21 @@ class Hammer
|
|
|
59
74
|
end
|
|
60
75
|
|
|
61
76
|
# Fill un-set non-boolean opts from positional args in declaration
|
|
62
|
-
# order. Booleans always need an explicit flag.
|
|
77
|
+
# order. Booleans always need an explicit flag. Scalars take one
|
|
78
|
+
# positional each; an :array opt slurps whatever's left, so it's
|
|
79
|
+
# filled last and never starves a later-declared scalar opt.
|
|
80
|
+
array_opt = nil
|
|
63
81
|
@options.each do |opt|
|
|
64
|
-
break if positional.empty?
|
|
65
82
|
next if opt.boolean? || values.key?(opt.name)
|
|
66
83
|
if opt.type == :array
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
values[opt.name] = opt.cast(positional.shift)
|
|
84
|
+
array_opt = opt
|
|
85
|
+
next
|
|
70
86
|
end
|
|
87
|
+
break if positional.empty?
|
|
88
|
+
values[opt.name] = opt.cast(positional.shift)
|
|
89
|
+
end
|
|
90
|
+
if array_opt && !positional.empty?
|
|
91
|
+
values[array_opt.name] = array_opt.cast(positional.shift(positional.size))
|
|
71
92
|
end
|
|
72
93
|
|
|
73
94
|
@options.each do |opt|
|
|
@@ -80,6 +101,15 @@ class Hammer
|
|
|
80
101
|
|
|
81
102
|
private
|
|
82
103
|
|
|
104
|
+
# Map a flag string to its option, refusing silent shadowing when two
|
|
105
|
+
# options would claim the same switch/negation/alias.
|
|
106
|
+
def register_switch(flag, opt)
|
|
107
|
+
if (prev = @by_switch[flag]) && prev != opt
|
|
108
|
+
raise Error, "flag #{flag} claimed by both :#{prev.name} and :#{opt.name}"
|
|
109
|
+
end
|
|
110
|
+
@by_switch[flag] = opt
|
|
111
|
+
end
|
|
112
|
+
|
|
83
113
|
def lookup!(key)
|
|
84
114
|
@by_switch[key] or raise Error, "unknown option: #{key}"
|
|
85
115
|
end
|
data/lib/hammer/recipe.rb
CHANGED
|
@@ -49,14 +49,14 @@ class Hammer
|
|
|
49
49
|
''
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
# Ruby wrapper text printed by `hammer recipes --install`. User
|
|
52
|
+
# Ruby wrapper text printed by `hammer h:recipes --install`. User
|
|
53
53
|
# redirects it to a file in PATH and chmods +x. The leading comment
|
|
54
54
|
# documents the canonical install command. Name is passed as a
|
|
55
55
|
# string literal so hyphenated names (`git-helper`) work too.
|
|
56
56
|
def stub(name)
|
|
57
57
|
<<~RUBY
|
|
58
58
|
#!/usr/bin/env ruby
|
|
59
|
-
# install: hammer recipes --install #{name} > ~/bin/#{name} && chmod +x $_
|
|
59
|
+
# install: hammer h:recipes --install #{name} > ~/bin/#{name} && chmod +x $_
|
|
60
60
|
require 'lux-hammer'
|
|
61
61
|
Hammer.recipe('#{name}', ARGV)
|
|
62
62
|
RUBY
|
data/lib/hammer/shell.rb
CHANGED
|
@@ -13,8 +13,11 @@ class Hammer
|
|
|
13
13
|
module_function
|
|
14
14
|
|
|
15
15
|
def color?
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# Only an explicit color!(value) override is sticky; otherwise the
|
|
17
|
+
# tty decision is recomputed so a redirected $stdout (tests, capture
|
|
18
|
+
# blocks) is honored instead of frozen at first read.
|
|
19
|
+
return @color if defined?(@color) && !@color.nil?
|
|
20
|
+
$stdout.tty? && ENV['NO_COLOR'].nil?
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def color!(value)
|