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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75362aef2908fdd2a3f4e32ab8c6ce7a4c4ae52fce795f5bca5437325a92da49
4
- data.tar.gz: 0d318814ed2cdd0da35ca2b66e7637c62d5d2efece0cb24f60da1c5e02f4ad1f
3
+ metadata.gz: 7ea304583caa18c45db8f340aecf5e08803f23eebe4f9152eefec7ac77c64b18
4
+ data.tar.gz: 72eafeef47b8ae8c2ff41e226756199f085865899532c3d7669dd5025c4b4019
5
5
  SHA512:
6
- metadata.gz: 64a8ce40bf7fc4986b6b0796c72fb9a8e9a13b2acb70c4aaa76cd68d5f68158bc9fbde7d5a0a5f3e586978b97f849f712b5addef336754b896e901f71c9feec2
7
- data.tar.gz: 6f12a27b72098ff513ecc216ee8f2c6deb08e3c3a29cf4f1a7e9c6dfb7750a9b00ad13fe514f66fdda7b7a837b62e2526d1af04ca1af85deed1435e2d34b30a3
6
+ metadata.gz: 7cfa688c6d9d21acc72a6c8d614acefe2942746fb8b8b55f4f72fb08d9f1d37a6eccbd3326551348ea572889e3404790c4a59f64577d394f257e1ab826959c45
7
+ data.tar.gz: 535fb35b15ae31e9e861ad0ff32d9e216382b533cb877b12560185cd722724c0d01a0dde350facebf209178166d88c4109328e7882ba6a14ae0273eb111d2215
data/.version CHANGED
@@ -1 +1 @@
1
- 0.3.12
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 / a built-in task name). After evaluating the
160
- Hammerfile, registers the always-on core built-ins (`:default`,
161
- `:help`, `:update`, `:agents`, `:version`) via
162
- `Hammer::Builtins.register_core` - each guarded by
163
- `unless commands.key?(...)` so a user-defined task wins silently.
164
- No-project-only built-ins (`:recipes`, `:init`) are NOT registered
165
- when a Hammerfile loaded - reach them with `--system`. The
166
- `--system` flag is peeled off argv at the top and forces the
167
- no-Hammerfile branch. Not part of the user-facing surface - don't
168
- recommend it in examples; `Hammer.run` is what library users should
169
- reach for.
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` defines two registration entry points; both register
243
- **after** Hammerfile evaluation and skip each task when the user
244
- already defined it (`unless klass.commands.key?(name)` - no
245
- redefinition warning).
246
-
247
- * `register_core(klass)` - always called. Registers `:default`,
248
- `:help`, `:update`, `:agents`, `:version`. These coexist with
249
- Hammerfile tasks.
250
- * `register_no_project(klass)` - called only on the no-Hammerfile
251
- branch of `Hammer.cli` (which `--system` also routes through).
252
- Registers `:recipes` and `:init` - tasks that would collide too
253
- easily with user tasks inside a project.
254
-
255
- No reserved namespace. Names like `:self`, `:recipes`, `:update` can
256
- all be defined freely in a Hammerfile; the built-ins yield.
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). Just prints `self.class.root.print_help`
261
- (brief). Fires for bare `hammer` and leading-flag invocations where
262
- the first token starts with `-` and isn't `-h` / `--help` (see
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
- * `:help` - has a desc, shows in listings. Calls
266
- `self.class.root.print_help(opts[:args].first, extended: true)`.
267
- Fires for `help` / `-h` / `--help` at the top level.
268
- * `:update` / `:agents` / `:version` - thin wrappers around
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
- * `:recipes` - all recipe-management actions on one task, picked via
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
- * `:init` - writes `Hammer::STARTER_HAMMERFILE` to `./Hammerfile`;
280
+ * `h:init` - writes `Hammer::STARTER_HAMMERFILE` to `./Hammerfile`;
275
281
  refuses if one exists.
276
-
277
- Both `:default` and `:help` are invoked via `run_command(cmd, argv,
278
- full: name, quiet: true)` - the gray `> prog cmd ...` banner is
279
- suppressed because the user didn't type `hammer default` /
280
- `hammer help` literally.
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 (all overridable)
540
+ ## Built-in tasks
541
541
 
542
- The `hammer` binary auto-registers a small set of built-in tasks at
543
- the root of your CLI. Each one is guarded by `unless commands.key?` -
544
- defining your own `task :name` in a Hammerfile silently replaces the
545
- built-in.
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
- Always available (with or without a Hammerfile):
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. Just prints the brief help by default; override
551
- to wire up your own global flags.
552
- * `:help` - `hammer help [TARGET]`, `hammer -h`, `hammer --help`. Prints
553
- the extended help view; `TARGET` accepts a command path or a `ns:`
554
- prefix.
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`.)
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
- Only when no Hammerfile is loaded (or `--system` is passed - see below):
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
- * `:recipes` - list / install / show / edit recipes.
562
- * `:init` - write a starter Hammerfile in cwd (refuses if one exists).
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
- These two are skipped inside a project so they don't shadow user tasks.
565
- To reach them from inside a project, pass `--system`:
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 --system recipes # list recipes from anywhere
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. From inside a
1110
- project the task isn't registered (so it can't shadow user tasks);
1111
- pass `--system` to reach it from anywhere.
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 non-project dir
1117
- $ hammer --system recipes # from inside a project
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>
@@ -1,29 +1,37 @@
1
1
  class Hammer
2
- # Built-in tasks of the `hammer` binary - all live at the root level
3
- # (no reserved namespace). Two registration entry points:
4
- #
5
- # * register_core - tasks always available (subject to user override
6
- # via the `unless commands.key?(...)` guard): :default, :help,
7
- # :update, :agents, :version. These coexist with Hammerfile tasks.
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
- def register_core(klass)
17
- register_help(klass) unless klass.commands.key?('help')
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
- def register_no_project(klass)
25
- register_recipes(klass) unless klass.commands.key?('recipes')
26
- register_init(klass) unless klass.commands.key?('init')
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']
@@ -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 (val.start_with?('"') && val.end_with?('"')) ||
42
- (val.start_with?("'") && val.end_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
- def walk(dir, out)
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