lux-hammer 0.3.12 → 0.3.14
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 +49 -40
- data/README.md +61 -44
- data/gui/Hammer.app/Contents/Info.plist +16 -0
- data/gui/Hammer.app/Contents/MacOS/HammerGUI +0 -0
- data/lib/hammer/builtins.rb +64 -33
- data/lib/hammer/command.rb +20 -0
- data/lib/hammer/dotenv.rb +3 -2
- data/lib/hammer/loader.rb +13 -7
- data/lib/hammer/option.rb +21 -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 +149 -64
- data/recipes/deploy.rb +32 -0
- data/recipes/git-helper.rb +2 -2
- 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 +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ea304583caa18c45db8f340aecf5e08803f23eebe4f9152eefec7ac77c64b18
|
|
4
|
+
data.tar.gz: 72eafeef47b8ae8c2ff41e226756199f085865899532c3d7669dd5025c4b4019
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7cfa688c6d9d21acc72a6c8d614acefe2942746fb8b8b55f4f72fb08d9f1d37a6eccbd3326551348ea572889e3404790c4a59f64577d394f257e1ab826959c45
|
|
7
|
+
data.tar.gz: 535fb35b15ae31e9e861ad0ff32d9e216382b533cb877b12560185cd722724c0d01a0dde350facebf209178166d88c4109328e7882ba6a14ae0273eb111d2215
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.14
|
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,53 @@ 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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
`
|
|
282
|
+
* `h:json` - `puts JSON` of `root.export_spec` (tasks grouped exactly
|
|
283
|
+
like the bare listing via `section_for`, root group keyed `__root`).
|
|
284
|
+
`--all` keeps the `h:` tree, `--compact` minifies.
|
|
285
|
+
|
|
286
|
+
The `:default` task and the `help` / `-h` / `--help` requests are
|
|
287
|
+
invoked via `run_command(cmd, argv, full: name, quiet: true)` - the
|
|
288
|
+
gray `> prog cmd ...` banner is suppressed because the user didn't type
|
|
289
|
+
`hammer default` / `hammer h:help` literally.
|
|
281
290
|
* Commands listed flat with colon paths, grouped by top-level namespace.
|
|
282
291
|
* Bare namespace (`hammer db`) prints the same listing scoped to that
|
|
283
292
|
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,57 @@ 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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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`.)
|
|
557
|
+
|
|
558
|
+
Under the `h:` namespace:
|
|
559
|
+
|
|
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).
|
|
565
|
+
* `h:json` - dump the CLI definition as JSON (tasks grouped like the
|
|
566
|
+
bare listing, with desc/options/examples/aliases/needs); `--all`
|
|
567
|
+
includes the `h:` tasks, `--compact` minifies. Output is plain stdout
|
|
568
|
+
(the run banner goes to stderr), so `hammer h:json | jq` just works.
|
|
569
|
+
|
|
570
|
+
Each is guarded by `unless commands.key?` within the namespace, so you
|
|
571
|
+
can override one by reopening `namespace :h` in your Hammerfile.
|
|
558
572
|
|
|
559
|
-
|
|
573
|
+
`--system` forces the no-Hammerfile branch - the Hammerfile in the
|
|
574
|
+
current tree (if any) isn't loaded for that invocation:
|
|
560
575
|
|
|
561
|
-
|
|
562
|
-
|
|
576
|
+
```sh
|
|
577
|
+
hammer --system h:recipes # list recipes, ignoring any local Hammerfile
|
|
578
|
+
hammer --system h:recipes --install srt ~/bin/srt
|
|
579
|
+
```
|
|
563
580
|
|
|
564
|
-
|
|
565
|
-
|
|
581
|
+
`--gui` opens a native macOS window for the current project: a sidebar of
|
|
582
|
+
your tasks (grouped like the listing), a form per task built from its
|
|
583
|
+
options, and a Run button that streams the task's output. It reads the
|
|
584
|
+
project via `h:json` and runs each task as a `hammer <path> ...`
|
|
585
|
+
subprocess. macOS / arm64 only.
|
|
566
586
|
|
|
567
587
|
```sh
|
|
568
|
-
hammer --
|
|
569
|
-
hammer --system recipes --install srt ~/bin/srt
|
|
588
|
+
hammer --gui # open the runner for the nearest Hammerfile
|
|
570
589
|
```
|
|
571
590
|
|
|
572
|
-
`--system` forces the no-Hammerfile branch - the Hammerfile in the
|
|
573
|
-
current tree (if any) isn't loaded for that invocation.
|
|
574
|
-
|
|
575
591
|
Customize bare `hammer` by replacing `:default`:
|
|
576
592
|
|
|
577
593
|
```ruby
|
|
@@ -1106,20 +1122,21 @@ A **recipe** is a standalone Hammerfile-style script bundled inside the
|
|
|
1106
1122
|
top-level binary in your `PATH` - so `srt` becomes a real command, not
|
|
1107
1123
|
`hammer srt:shift`.
|
|
1108
1124
|
|
|
1109
|
-
Recipe management lives under the `recipes` task
|
|
1110
|
-
|
|
1111
|
-
|
|
1125
|
+
Recipe management lives under the `h:recipes` task, reachable from
|
|
1126
|
+
anywhere (inside a project the `h:` namespace can't shadow your root
|
|
1127
|
+
tasks). `--system` skips loading a local Hammerfile if you want a clean
|
|
1128
|
+
environment.
|
|
1112
1129
|
|
|
1113
1130
|
Listing what's available:
|
|
1114
1131
|
|
|
1115
1132
|
```sh
|
|
1116
|
-
$ hammer recipes # from any
|
|
1117
|
-
$ hammer --system recipes #
|
|
1133
|
+
$ hammer h:recipes # from any directory
|
|
1134
|
+
$ hammer --system h:recipes # ignoring any local Hammerfile
|
|
1118
1135
|
gem:
|
|
1119
1136
|
srt # Subtitle (.srt) toolkit - shift timestamps, show stats
|
|
1120
|
-
[install: hammer recipes --install srt]
|
|
1137
|
+
[install: hammer h:recipes --install srt]
|
|
1121
1138
|
llm # personal LLM utility CLI (memory store, prompt-token expander, ...)
|
|
1122
|
-
[install: hammer recipes --install llm]
|
|
1139
|
+
[install: hammer h:recipes --install llm]
|
|
1123
1140
|
```
|
|
1124
1141
|
|
|
1125
1142
|
Installing one. With no TARGET, the stub is printed to stdout (you
|
|
@@ -1127,8 +1144,8 @@ redirect it yourself); with a TARGET path, lux-hammer writes the file
|
|
|
1127
1144
|
and chmods +x in one step:
|
|
1128
1145
|
|
|
1129
1146
|
```sh
|
|
1130
|
-
$ hammer recipes --install srt ~/bin/srt # write + chmod
|
|
1131
|
-
$ hammer recipes --install srt > ~/bin/srt && chmod +x ~/bin/srt
|
|
1147
|
+
$ hammer h:recipes --install srt ~/bin/srt # write + chmod
|
|
1148
|
+
$ hammer h:recipes --install srt > ~/bin/srt && chmod +x ~/bin/srt
|
|
1132
1149
|
$ srt --help
|
|
1133
1150
|
Usage: srt COMMAND [ARGS]
|
|
1134
1151
|
|
|
@@ -1145,7 +1162,7 @@ so the recipe always runs the version currently in the gem.
|
|
|
1145
1162
|
### Authoring your own
|
|
1146
1163
|
|
|
1147
1164
|
Drop a plain `.rb` file in `~/.config/hammer/recipes/`. The first
|
|
1148
|
-
`# desc: ...` comment is what shows in `hammer recipes`. The file body
|
|
1165
|
+
`# desc: ...` comment is what shows in `hammer h:recipes`. The file body
|
|
1149
1166
|
uses the same DSL as a Hammerfile - `task`, `namespace`, `before`,
|
|
1150
1167
|
`load`. Example `~/.config/hammer/recipes/json.rb`:
|
|
1151
1168
|
|
|
@@ -1165,22 +1182,22 @@ end
|
|
|
1165
1182
|
Install it the same way:
|
|
1166
1183
|
|
|
1167
1184
|
```sh
|
|
1168
|
-
$ hammer recipes --install json ~/bin/json
|
|
1185
|
+
$ hammer h:recipes --install json ~/bin/json
|
|
1169
1186
|
```
|
|
1170
1187
|
|
|
1171
1188
|
User-dir recipes override gem recipes with the same name, so you can
|
|
1172
1189
|
fork without forking.
|
|
1173
1190
|
|
|
1174
|
-
### Other `recipes` actions
|
|
1191
|
+
### Other `h:recipes` actions
|
|
1175
1192
|
|
|
1176
1193
|
```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
|
|
1194
|
+
hammer h:recipes # list all
|
|
1195
|
+
hammer h:recipes --install # interactive picker, then prints stub
|
|
1196
|
+
hammer h:recipes --show NAME # cat the recipe source
|
|
1197
|
+
hammer h:recipes --path NAME # absolute path
|
|
1198
|
+
hammer h:recipes --edit NAME # open in $EDITOR (copies gem -> user dir first)
|
|
1199
|
+
hammer h:recipes --run NAME [ARGS] # run without installing its bin
|
|
1200
|
+
hammer h:recipes --run NAME -- --help # `--` forwards flags to the recipe
|
|
1184
1201
|
```
|
|
1185
1202
|
|
|
1186
1203
|
## Programmatic use
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleName</key><string>Hammer</string>
|
|
6
|
+
<key>CFBundleDisplayName</key><string>Hammer</string>
|
|
7
|
+
<key>CFBundleIdentifier</key><string>com.lux-hammer.gui</string>
|
|
8
|
+
<key>CFBundleVersion</key><string>1</string>
|
|
9
|
+
<key>CFBundleShortVersionString</key><string>1.0</string>
|
|
10
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
11
|
+
<key>CFBundleExecutable</key><string>HammerGUI</string>
|
|
12
|
+
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
|
13
|
+
<key>NSHighResolutionCapable</key><true/>
|
|
14
|
+
<key>NSPrincipalClass</key><string>NSApplication</string>
|
|
15
|
+
</dict>
|
|
16
|
+
</plist>
|
|
Binary file
|
data/lib/hammer/builtins.rb
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
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')
|
|
34
|
+
register_json(h) unless h.commands.key?('json')
|
|
27
35
|
end
|
|
28
36
|
|
|
29
37
|
def register_help(klass)
|
|
@@ -37,9 +45,9 @@ class Hammer
|
|
|
37
45
|
per-command help; with a namespace prefix prints that
|
|
38
46
|
namespace's command listing.
|
|
39
47
|
TXT
|
|
40
|
-
example 'help'
|
|
41
|
-
example 'help build'
|
|
42
|
-
example 'help db:'
|
|
48
|
+
example 'h:help'
|
|
49
|
+
example 'h:help build'
|
|
50
|
+
example 'h:help db:'
|
|
43
51
|
proc do |opts|
|
|
44
52
|
self.class.root.print_help(opts[:args].first, extended: true)
|
|
45
53
|
end
|
|
@@ -105,11 +113,34 @@ class Hammer
|
|
|
105
113
|
end
|
|
106
114
|
end
|
|
107
115
|
|
|
116
|
+
def register_json(klass)
|
|
117
|
+
klass.class_eval do
|
|
118
|
+
task :json do
|
|
119
|
+
desc <<~TXT
|
|
120
|
+
Dump the CLI definition as JSON: tasks grouped exactly like
|
|
121
|
+
the bare-`hammer` listing, each with desc, options, examples,
|
|
122
|
+
aliases, needs. Consumed by the macOS GUI and any tooling
|
|
123
|
+
that wants the full Hammerfile spec.
|
|
124
|
+
TXT
|
|
125
|
+
example 'h:json'
|
|
126
|
+
example 'h:json --all # include the reserved h: tasks'
|
|
127
|
+
example 'h:json --compact # minified, single line'
|
|
128
|
+
opt :all, type: :boolean, desc: 'include reserved built-in h: tasks'
|
|
129
|
+
opt :compact, type: :boolean, desc: 'minified JSON (default: pretty)'
|
|
130
|
+
proc do |opts|
|
|
131
|
+
require 'json'
|
|
132
|
+
spec = self.class.root.export_spec(include_builtins: opts[:all])
|
|
133
|
+
puts opts[:compact] ? JSON.generate(spec) : JSON.pretty_generate(spec)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
108
139
|
# `:recipes` rolls all recipe-management actions into one task. Bare
|
|
109
140
|
# invocation lists; opts pick the action and positional args carry
|
|
110
141
|
# the recipe name (and optional target path for --install). Run via
|
|
111
142
|
# `--run NAME [ARGS]` - use `--` to forward flags to the recipe
|
|
112
|
-
# itself (e.g. `hammer recipes --run srt -- --help`).
|
|
143
|
+
# itself (e.g. `hammer h:recipes --run srt -- --help`).
|
|
113
144
|
def register_recipes(klass)
|
|
114
145
|
klass.class_eval do
|
|
115
146
|
task :recipes do
|
|
@@ -123,12 +154,12 @@ class Hammer
|
|
|
123
154
|
opt :path, type: :boolean, desc: 'print recipe abs path'
|
|
124
155
|
opt :edit, type: :boolean, desc: 'open recipe in $EDITOR (copies gem -> user dir first)'
|
|
125
156
|
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'
|
|
157
|
+
example 'h:recipes'
|
|
158
|
+
example 'h:recipes --install srt ~/bin/srt # write + chmod in one shot'
|
|
159
|
+
example 'h:recipes --install srt > ~/bin/srt && chmod +x $_'
|
|
160
|
+
example 'h:recipes --show srt'
|
|
161
|
+
example 'h:recipes --run srt extract movie.mp4'
|
|
162
|
+
example 'h:recipes --run srt -- --help # -- forwards flags to the recipe'
|
|
132
163
|
proc do |opts|
|
|
133
164
|
args = opts[:args]
|
|
134
165
|
if opts[:install]
|
|
@@ -170,7 +201,7 @@ class Hammer
|
|
|
170
201
|
|
|
171
202
|
def require_name!(name, action)
|
|
172
203
|
return name if name
|
|
173
|
-
Shell.print_error "missing recipe name (usage: recipes --#{action} NAME)"
|
|
204
|
+
Shell.print_error "missing recipe name (usage: h:recipes --#{action} NAME)"
|
|
174
205
|
exit 1
|
|
175
206
|
end
|
|
176
207
|
|
|
@@ -192,7 +223,7 @@ class Hammer
|
|
|
192
223
|
Shell.say "#{source}:", :yellow
|
|
193
224
|
items.each_key do |name|
|
|
194
225
|
_n, desc, installed = rows.find { |r| r.first == name }
|
|
195
|
-
status = installed ? "(installed: #{installed})" : "[install: hammer recipes --install #{name}]"
|
|
226
|
+
status = installed ? "(installed: #{installed})" : "[install: hammer h:recipes --install #{name}]"
|
|
196
227
|
line = " #{name.ljust(width)} # #{desc}"
|
|
197
228
|
Shell.say line
|
|
198
229
|
Shell.say " #{' ' * width} #{status}", :gray
|
|
@@ -242,7 +273,7 @@ class Hammer
|
|
|
242
273
|
end
|
|
243
274
|
|
|
244
275
|
# For a gem recipe, offer to copy to user dir first so edits
|
|
245
|
-
# survive `hammer update`. Then exec $EDITOR on the file.
|
|
276
|
+
# survive `hammer h:update`. Then exec $EDITOR on the file.
|
|
246
277
|
def edit(name)
|
|
247
278
|
path = Hammer::Recipe.path(name) or fail_unknown(name)
|
|
248
279
|
editor = ENV['EDITOR'] || ENV['VISUAL']
|
data/lib/hammer/command.rb
CHANGED
|
@@ -63,6 +63,26 @@ class Hammer
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Structured form for JSON export (`h:json`). `path` is the full
|
|
67
|
+
# colon path supplied by the tree walk - a Command doesn't know its
|
|
68
|
+
# own namespace prefix. `hidden` follows the help rule: a task with
|
|
69
|
+
# no `desc` still dispatches but is hidden from listings.
|
|
70
|
+
def to_h(path = name)
|
|
71
|
+
{
|
|
72
|
+
name: name,
|
|
73
|
+
path: path,
|
|
74
|
+
desc: desc,
|
|
75
|
+
brief: brief,
|
|
76
|
+
hidden: desc.empty?,
|
|
77
|
+
redefined: !prev_location.nil?,
|
|
78
|
+
location: location,
|
|
79
|
+
alts: alts,
|
|
80
|
+
needs: needs,
|
|
81
|
+
examples: examples,
|
|
82
|
+
options: options.map(&:to_h)
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
66
86
|
private
|
|
67
87
|
|
|
68
88
|
def short_flag?(switch)
|
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
|
@@ -59,6 +59,8 @@ class Hammer
|
|
|
59
59
|
when :array then value.is_a?(Array) ? value : value.to_s.split(',')
|
|
60
60
|
else value.to_s
|
|
61
61
|
end
|
|
62
|
+
rescue ArgumentError, TypeError
|
|
63
|
+
raise Hammer::Parser::Error, "invalid #{type} value for --#{name}: #{value.inspect}"
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
def usage
|
|
@@ -76,5 +78,24 @@ class Hammer
|
|
|
76
78
|
return '' if default.nil?
|
|
77
79
|
"(default: #{default.inspect})"
|
|
78
80
|
end
|
|
81
|
+
|
|
82
|
+
# Structured form for JSON export (`h:json`). The GUI maps `type`
|
|
83
|
+
# to a form widget; `default`/`required`/`desc` decorate it. Mirrors
|
|
84
|
+
# the data behind `usage`. `negation` is only meaningful for booleans.
|
|
85
|
+
def to_h
|
|
86
|
+
h = {
|
|
87
|
+
name: name.to_s,
|
|
88
|
+
type: type.to_s,
|
|
89
|
+
default: default,
|
|
90
|
+
required: required,
|
|
91
|
+
desc: desc,
|
|
92
|
+
placeholder: placeholder,
|
|
93
|
+
switch: switch,
|
|
94
|
+
aliases: aliases,
|
|
95
|
+
usage: usage.strip
|
|
96
|
+
}
|
|
97
|
+
h[:negation] = negation if boolean?
|
|
98
|
+
h
|
|
99
|
+
end
|
|
79
100
|
end
|
|
80
101
|
end
|