hiiro 0.1.391 → 0.1.393

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: 9c562c5f430e154b0d7c2d177077dd4350323160eea9f0578c58e8e4c9fa0967
4
- data.tar.gz: e3bd6d848935c9e31d4b32a3ad4701b063952d064d5d59ab17ea2a4dba749c6f
3
+ metadata.gz: ead58a6f2c9467256f546afcf8ef417f755717c6f6c346a31f7ae44290fd9d44
4
+ data.tar.gz: c5d4759a675c8f464bc8c6efda43e24abeb8945ef782ca7890365f3bfadbd66a
5
5
  SHA512:
6
- metadata.gz: 9b58f7c8c8aab4f69c4f5019a849693697c7f85fbd7487c756573cbedd5f7c330891b5751910fdc39917e50dc1cd3d96e0cdce917ad968bd52ed1107d97f73ea
7
- data.tar.gz: 672103c549e704a81680a3998be922d310445a5fe6109be7120e9eef54dedf21b8c158c9e12beebc582a068fbd0841b5e92ebcc6f6d793f2e689b63b4e4c45c3
6
+ metadata.gz: db0fca7b6671893efcb5d6636c49b87cdca6ef0af063ef27be6cb67e1b6b02c5bd36646499adbcdad6b9318a489c51fd6fa8765b3868b7d490b09634442110c8
7
+ data.tar.gz: 6eb79e4d02b4ce0c2d3bf50770b0ed5ad8410ad035d90ae27f5df85039ded7d3491c3ee0e355a92680219ec73825c6cff531dae858d5634608012248aa304a6b
data/AGENTS.md CHANGED
@@ -24,6 +24,18 @@ Tests are organized under `test/`:
24
24
 
25
25
  The `Hiiro::TestHarness` class (in `test/test_helper.rb`) enables testing bin files by capturing the block passed to `Hiiro.run` and evaluating it in a test context with stubbed `system` calls.
26
26
 
27
+
28
+ ## Making Changes
29
+
30
+ Hiiro is extensible, and when working on a bin or subcommand, there could be
31
+ multiple variations of a bun and which one the user assumes is latest might not
32
+ be accurate. The common places for these bins are:
33
+
34
+ - ~/bin/
35
+ - ~/proj/phome/bin/
36
+ - ~/proj/hiiro/bin/
37
+ - any version of the gem installed via rbenv
38
+
27
39
  ## Architecture
28
40
 
29
41
  ### Core Components (bin/h)
@@ -239,6 +251,59 @@ end
239
251
  - `h db migrate` — re-run YAML import (if not yet migrated)
240
252
  - `h db restore` — restore YAML files from SQLite data
241
253
 
254
+ ### Hiiro::Table / Hiiro::TableScope / Hiiro::TableActions (lib/hiiro/table*.rb)
255
+
256
+ A **table** is one named key/value list, stored as `registry.resource_type`; an **entry** is one `RegistryEntry` row. `RegistryEntry` has a nullable `task_id` and `tags` / `tag(*names)` / `untag(*names)` through `Hiiro::Tag` (taggable_type `RegistryEntry`).
257
+
258
+ ```ruby
259
+ Hiiro::Table.all # every table, sorted by name
260
+ t = Hiiro::Table.named('link') # exists? is false until a row is added
261
+ t.add('gh', value: 'https://github.com', short: 'g', desc: '', meta: {}, task_id: nil)
262
+ t.entries(scope: Hiiro::TableScope.global) # rows ordered by name; count(scope:) ; find(ref)
263
+ t.rename('links') # moves every row; raises Hiiro::Error if the target exists
264
+
265
+ scope = Hiiro::TableScope.current # task scope when CurrentTask resolves one, else global
266
+ scope.task? / scope.global? / scope.label # "task(@NAME)" | "global"
267
+ scope.toggle # task -> global keeps the task; global -> task re-uses or resolves it
268
+ scope.apply(dataset) # the only place that knows task scope means where(task_id:)
269
+
270
+ acts = Hiiro::TableActions.load # ~/.config/hiiro/tables.yml: { 'link' => { 'c' => 'cd {value}' } }
271
+ acts.for(entry) # DEFAULTS (o open, y pbcopy) merged with the table's letters
272
+ acts.command('o', entry) # {value} {name} {table} shell-escaped; nil when no action
273
+ acts.run('o', entry) # system('/bin/sh', '-c', cmd); returns cmd
274
+ ```
275
+
276
+ `bin/h-table` is the TUI (`Hiiro::TableScreen` in lib/hiiro/table_screen.rb: `TableScreen.new(scope:, table:, actions:)`, `reload!`, `marked`) and the scriptable CLI (`tables`, `ls`, `add`, `rm`, `mv`, `set`, `get`, `tag`, `untag`) over these tables; `h registry` is the same rows. See `docs/h-table.md`.
277
+
278
+ `t ui` is the second screen on the toolkit: `Hiiro::TaskScreen` (lib/hiiro/task_screen.rb, `TaskScreen.new(scope: :global, current:, show_closed: false, ops:)`, `reload!`, `expanded`) is a tree of task/subtask/todo nodes; edits go through `TaskRecord#set_next!` / `#set_waiting!` / `#set_status!`, `TodoItem#update_status`, and `TaskScreen::Ops` (create, save_current, open_workspace over `TaskCli::Commands`, stdout captured). See `docs/t.md`.
279
+
280
+ `h project ui` is the third screen on the toolkit: `Hiiro::ProjectCatalog.load(roots:, aliases:)` (lib/hiiro/project_catalog.rb; `default_roots` is `~/proj` and `WORK_DIR`, `rows` / `repos` / `reload` / `repo_for(row)`; a `Row` is `{ name, path, kind: :tree | :dir | :alias, repo, branch, head }`, `key` is the path, `main?` is the repo's first worktree) lists one row per git worktree under the roots plus plain dirs and `project` aliases, grouping a repo's worktrees by `Git::Worktrees.fetch` so an extra tree reads `repo ▸ tree`. `Hiiro::ProjectScreen.new(catalog:, scope:, current:, facts:, opener:, fetcher:)` (lib/hiiro/project_screen.rb, `reload!`, `rebuild!`) draws it; `Facts` caches `status[tree_path]` / `branches[repo_path]` / `prs[repo_path]` fetched on `s` / `b` / `p` through `Fetcher` (git and `gh pr list`), `Opener#open(row)` runs `tuios new-window` under `TUIOS_ENV=1` (`command(row)` is the argv) and `Herdr.open_workspace` otherwise, and `a` / `d` add and remove worktrees through git. See `docs/h-project.md`.
281
+
282
+ ### Hiiro::Tui (lib/hiiro/tui.rb)
283
+
284
+ Screen toolkit for in-process TUIs. One engine for every hiiro TUI; do not add curses or shell out to a fuzzy finder for a screen (see `docs/plans/hiiro-tui.md`).
285
+
286
+ ```ruby
287
+ class MyScreen < Hiiro::Tui::Screen
288
+ key 'o', :open_item # key map is a table; subclasses add or override bindings
289
+ def header_lines = ["title #{state.query}"]
290
+ def detail_lines(item) = ["name #{item.name}"] # nil (default) means no detail pane
291
+ def pick_candidates = %w[link sound] # '@' opens a prefix-filtered picker in the status line
292
+ def picked(candidate) = (self.message = candidate)
293
+ def open_item = (self.message = "open #{current_item}")
294
+ def unhandled_key(key) = nil # list-mode keys the key map does not bind
295
+ def ask = prompt('name', default: 'x') { |text| prompt('value') { |v| save(text, v) } }
296
+ end
297
+ MyScreen.new(items: rows).run # j/k/h/l move, '/' searches, '@' picks, q/Esc/Ctrl-C quit
298
+ ```
299
+
300
+ - `state` is one `Tui::State` (`mode` :list|:search|:pick|:prompt, `cursor`, `top`, `horizontal_offset`, `query`, `pick_query`, `message`, `prompt_label`, `prompt_text`). Read it; never keep a parallel boolean.
301
+ - `prompt(label, default: '') { |text| ... }` reads one line in the status line (`label: text_`). Enter leaves the mode and calls the block, Esc leaves without calling it, Ctrl-C quits. The block runs after the prompt closes, so it may call `prompt` again to chain questions.
302
+ - `unhandled_key(key)` is called from list mode for any key the key map does not bind (default no-op); `TableScreen` uses it for per-table action letters.
303
+ - `frame(rows:, cols:)` returns plain strings with no ANSI, one per row, which is what tests assert against literally. `render` adds the highlight.
304
+ - `Tui::Pane.new(width:, height:).lines(texts)` pads and truncates to an exact rectangle.
305
+ - `ListScreen` is `Screen` without a detail pane and keeps the `Showing a-b of n` footer.
306
+
242
307
  ### Hiiro::Fuzzyfind (lib/hiiro/fuzzyfind.rb)
243
308
 
244
309
  Integration with `sk` (skim) or `fzf` fuzzy finders:
@@ -371,9 +436,9 @@ loc.target # pane_id || tab_id || workspace_i
371
436
  loc.to_meta # inverse of from_meta (queue/service metadata)
372
437
  ```
373
438
 
374
- ### Hiiro::Link / Hiiro::URLTemplate (lib/hiiro/link.rb)
439
+ ### Hiiro::URLTemplate (lib/hiiro/link.rb)
375
440
 
376
- `bin/h-link` uses `Hiiro::Link` directly (its inline `LinkManager::Link` copy is gone). `Link.from_hash(yaml_row)` builds an unsaved instance; `to_h` / `to_yaml` give the links.yml shape.
441
+ Saved links are the `link` registry table (url → value, shorthand → short_name, description, created_at for the numbered order); `bin/h-link` reads and writes them through `Hiiro::Table.named('link')` and wraps each `RegistryEntry` in its own `LinkPresenter`. `h link path` prints the database path. `DB.import_tables!` imported the legacy `links` table (kept for the installed gem) and `links.yml` (renamed `.bak`) once; `Hiiro::DB.link_name(url, shorthand, description)` picks the entry name.
377
442
 
378
443
  ```ruby
379
444
  t = Hiiro::URLTemplate.new('https://x/{org}/{repo}', values: { 'org' => 'a b' })
@@ -660,16 +725,17 @@ ALWAYS update `README.md` and any files in `docs/` or other markdown files that
660
725
 
661
726
  All config lives in `~/.config/hiiro/`:
662
727
  - `plugins/` - Auto-loaded plugin files
663
- - `pins/` - Per-command YAML key-value storage
728
+ - `pins/` - Legacy per-command pin files. Pins now live in registry tables named after the command (e.g. table `t`, entry `current_task`); `pins/*` files and `sounds.yml` are imported once into tables `<name>` and `sound` by `Hiiro::DB.import_tables!` and renamed `.bak`
664
729
  - `tasks/` - Task metadata for worktree management
665
730
  - `queue/` - Prompt queue (wip, pending, running, done, failed)
666
731
  - `services/` - Service runtime state
667
732
  - `jumplist/` - Per-client tmux navigation history
668
733
  - `env_templates/` - Base .env template files for services
669
- - `projects.yml` - Project directory aliases
734
+ - `projects.yml` - Legacy project aliases; imported once into registry table `project` and renamed `.bak`. Aliases now live in that table; `h project edit` edits it through a YAML buffer
670
735
  - `apps.yml` - App directory mappings for task plugin
671
736
  - `services.yml` - Service and service group definitions
672
737
  - `tools.yml` - Runner tool definitions
738
+ - `tables.yml` - Per-table action letters for `h table`
673
739
  - `app_files.yml` - Per-app tracked file lists
674
740
 
675
741
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.393] - 2026-10-12
4
+
5
+ ### Added
6
+ - `h json` subcommand (`bin/h-json`)
7
+
8
+ ## [0.1.392] - 2026-10-12
9
+
10
+ ### Changed
11
+ - `add_cmd` accepts `opts`, `flags`, and `mopts` arrays for value options, boolean flags, and repeated value options. Existing definitions are reused, and new short names use the first available alphanumeric character in the full name.
12
+ - Automatic command definitions are registered for reuse by later commands. Existing boolean callers now use `flags`.
13
+
3
14
  ## [0.1.390] - 2026-10-07
4
15
 
5
16
  ### Added
@@ -18,6 +29,9 @@
18
29
 
19
30
  ## Unreleased
20
31
 
32
+ ### Changed
33
+ - `h strip ctrl` replaces bare `h strip` for stripping ASCII terminal escapes and C0/DEL controls from files, stdin, or text. Tabs, line feeds, UTF-8 text, and raw invalid bytes are preserved.
34
+
21
35
  ### Fixed
22
36
  - Fix `PsProcess::Snapshot.capture` constant lookup, which raised `NameError` in `h ps byport`, `h ps indir`, and parent/child process lookups.
23
37
 
data/README.md CHANGED
@@ -123,6 +123,23 @@ printf '%s\n' '"src/my file.rb:12:3"' | h extract files --no-check
123
123
  printf '%s\n' 'missing/output/' | h extract dirs --no-check
124
124
  ```
125
125
 
126
+ ### Strip terminal escapes and controls
127
+
128
+ `h strip ctrl` removes terminal escape sequences and control characters from
129
+ files, piped stdin, or literal text. Tabs, newlines, and UTF-8 text are preserved.
130
+
131
+ ```sh
132
+ h strip ctrl terminal.log > clean.log
133
+ printf '\033[31mhello\033[0m\n' | h strip ctrl
134
+ h strip ctrl hello world
135
+ h strip ctrl --help
136
+ ```
137
+
138
+ The command writes to stdout without adding a newline and reads the whole input
139
+ into memory. Redirect to a different file, not the input file. The command does
140
+ not reconstruct a terminal screen. See [the strip reference](docs/h-misc.md#h-strip-ctrl)
141
+ for input selection and supported escape sequences.
142
+
126
143
  ### Task CLI
127
144
 
128
145
  `t` and `tt` are gem executables in `exe/`, installed alongside `h`. They are thin
@@ -203,7 +220,7 @@ See the [task command reference](docs/t.md) for all commands and the [workflow i
203
220
  | `h edit` | Open the h script in your editor |
204
221
  | `h alert` | macOS desktop notifications via terminal-notifier |
205
222
  | `h herdr` | Herdr task popups plus Vim-style pane focus, movement, resize, swap, and zoom controls (`h herdr install`) |
206
- | `h task` | Same program as `t`: task records, todos, worktrees (`t NAME tree new`), and Herdr workspaces |
223
+ | `h task` | Same program as `t`: task records, todos, worktrees (`t NAME tree new`), and Herdr workspaces; `t ui` is a tree TUI over tasks, subtasks, and todos |
207
224
 
208
225
  ### External Subcommands
209
226
 
@@ -222,13 +239,16 @@ See the [task command reference](docs/t.md) for all commands and the [workflow i
222
239
  | `h pane` | Herdr pane management |
223
240
  | `h plugin` | Manage hiiro plugins (list, edit, search) |
224
241
  | `h pr` | GitHub PR management via gh CLI |
225
- | `h project` | Project navigation with Herdr workspace management |
242
+ | [`h project`](docs/h-project.md) | Project navigation with Herdr workspace management; `h project ui` is the projects screen over every worktree, directory, and alias |
226
243
  | `h queue` | Claude prompt queue with Herdr-based task execution |
244
+ | `h registry` | Named resources by type with short aliases; the same rows as `h table` |
227
245
  | `h service` | Manage background dev services with env variations and groups |
228
246
  | `h file` | Track and open frequently-used files per app |
229
247
  | `h run` | Run dev tools (lint/test/format) against changed files |
230
248
  | `h session` | Compatibility commands for Herdr workspaces |
231
249
  | `h sha` | Extract short SHA from git log |
250
+ | [`h strip ctrl`](docs/h-misc.md#h-strip-ctrl) | Strip terminal escapes and controls from files, stdin, or text |
251
+ | [`h table`](docs/h-table.md) | TUI and scriptable CLI over registry tables (links, sounds, pins, ...) with task scope, tags, and per-table actions |
232
252
  | `h todo` | Todo list management with tags and task association |
233
253
  | `h window` | Compatibility commands for Herdr tabs |
234
254
  | `h wtree` | Git worktree management |
@@ -346,15 +366,18 @@ end
346
366
 
347
367
  ### Commands with declared options
348
368
 
349
- Use `add_cmd` to expose a command's options in help. Undeclared names in `opts:`
350
- become boolean flags with a default of `false`. A flag does not consume the next
351
- positional argument.
369
+ Use `add_cmd` to expose a command's options in help. Choose a declaration bucket
370
+ for each undeclared name:
371
+
372
+ - `opts:` creates a string-valued option with a default of `nil`.
373
+ - `flags:` creates a boolean flag with a default of `false`. Flags do not consume the next positional argument.
374
+ - `mopts:` creates a repeatable string-valued option with a default of `[]`.
352
375
 
353
376
  ```ruby
354
377
  Hiiro.run do
355
378
  add_option :task, short: :t, desc: 'Task name'
356
379
 
357
- add_cmd :test, opts: %i[a b c d task] do
380
+ add_cmd :test, opts: %i[task], flags: %i[a b c d], mopts: %i[tag] do
358
381
  puts 'a was set' if opts.a
359
382
  puts opts.task if opts.task
360
383
  puts opts.args
@@ -362,15 +385,20 @@ Hiiro.run do
362
385
  end
363
386
  ```
364
387
 
365
- Here, `test -a payload -t investigation` sets `opts.a` to `true`, leaves
366
- `payload` in `opts.args`, and sets `opts.task` to `investigation`.
388
+ Here, `test -a payload -t investigation --tag ruby --tag cli` sets `opts.a`
389
+ to `true`, leaves `payload` in `opts.args`, sets `opts.task` to
390
+ `investigation`, and sets `opts.tag` to `['ruby', 'cli']`.
367
391
  `test -h` and `test --help` display the selected options without running the
368
392
  command block.
369
393
 
370
- Automatic flags use their first letter as a short alias only when it is unique
371
- among the command's automatic flags and does not conflict with a selected
372
- explicit option or `-h`. Every automatic flag has a long form. Explicit flags
373
- and value options keep their definitions.
394
+ Names accept strings or symbols and are deduplicated across the buckets.
395
+ Existing definitions keep their type, default, short alias, and description,
396
+ regardless of the bucket used to select them. New definitions are
397
+ registered on the parent option set so later commands reuse them.
398
+
399
+ Automatic short aliases use the first unused alphanumeric character in the full
400
+ name, scanning left to right. All existing short aliases and `-h` are reserved;
401
+ aliases are never reassigned. A name with no available character has only its long form.
374
402
 
375
403
  For a command that delegates to `run_child`, use `add_cmd(..., passthrough: true)`.
376
404
  It forwards control without parsing the parent's arguments or intercepting the
@@ -440,12 +468,13 @@ All configuration lives in `~/.config/hiiro/`:
440
468
  ```
441
469
  ~/.config/hiiro/
442
470
  plugins/ # Plugin files (auto-loaded)
443
- pins/ # Pin storage (per command)
471
+ pins/ # Legacy pin files; imported once into registry tables named after the command (h-sha -> sha), then renamed .bak
472
+ sounds.yml # Legacy; imported once into registry table sound, then renamed .bak
444
473
  services/ # Service runtime state
445
474
  bg-history.txt # Background command history
446
475
  env_templates/ # Base .env templates for services
447
476
  tasks/ # Task metadata
448
- projects.yml # Project aliases
477
+ projects.yml # Legacy; imported once into registry table project, then renamed .bak (edit aliases with h project edit)
449
478
  apps.yml # App directory mappings
450
479
  services.yml # Service and service group definitions
451
480
  tools.yml # Runner tool definitions
data/bin/h-alias CHANGED
@@ -5,7 +5,7 @@ require 'hiiro'
5
5
  Hiiro.run do
6
6
  add_flag :global, short: :g, desc: 'Create a global zsh alias'
7
7
 
8
- add_cmd :add, args: ['NAME', 'COMMAND...'], opts: [:global] do
8
+ add_cmd :add, args: ['NAME', 'COMMAND...'], flags: [:global] do
9
9
  abort 'Usage: h alias add [--global|-g] NAME COMMAND...' unless opts.args.length >= 2
10
10
  name, *command_args = opts.args
11
11
  abort 'Invalid alias name' unless /\A[A-Za-z0-9_.][A-Za-z0-9_.-]*\z/.match?(name)
data/bin/h-date CHANGED
@@ -8,13 +8,13 @@ Hiiro.run(*ARGV) do
8
8
  add_flag(:utc, short: :u, desc: 'Output in UTC')
9
9
 
10
10
  # h date to_unix [date-string] -> unix seconds (or ms with --ms)
11
- add_cmd(:to_unix, :unix, :ts, opts: %i[ms]) do
11
+ add_cmd(:to_unix, :unix, :ts, flags: %i[ms]) do
12
12
  t = Hiiro::TimeInput.parse(@opts.args.join(' '))
13
13
  puts @opts.ms ? t.unix_milliseconds : t.unix_seconds
14
14
  end
15
15
 
16
16
  # h date from_unix <timestamp> -> human date (auto-detects ms by digit count)
17
- add_cmd(:from_unix, :parse, opts: %i[ms utc]) do
17
+ add_cmd(:from_unix, :parse, flags: %i[ms utc]) do
18
18
  raw = @opts.args.first or abort 'usage: h date from_unix <timestamp> [--ms] [--utc]'
19
19
  t = Hiiro::TimeInput.parse(raw, ms: @opts.ms ? true : nil)
20
20
  t = t.utc if @opts.utc
@@ -22,7 +22,7 @@ Hiiro.run(*ARGV) do
22
22
  end
23
23
 
24
24
  # h date fmt Y-m-d H:M:S -> strftime with % auto-inserted before each char
25
- add_cmd(:fmt, :format, :strftime, opts: %i[utc]) do
25
+ add_cmd(:fmt, :format, :strftime, flags: %i[utc]) do
26
26
  fmt = @opts.args.join(' ')
27
27
  abort 'usage: h date fmt <strftime-chars-without-percent>' if fmt.empty?
28
28
  with_percent = fmt.gsub(/[A-Za-z%]/) { |c| c == '%' ? '%%' : "%#{c}" }
@@ -30,20 +30,20 @@ Hiiro.run(*ARGV) do
30
30
  puts t.strftime(with_percent)
31
31
  end
32
32
 
33
- add_cmd(:now, opts: %i[utc]) do
33
+ add_cmd(:now, flags: %i[utc]) do
34
34
  t = @opts.utc ? Time.now.utc : Time.now
35
35
  puts t.iso8601
36
36
  end
37
37
 
38
38
  # h date dur <seconds> -> 1d23h12m10s (--ms to interpret input as milliseconds)
39
- add_cmd(:dur, :duration, :human, opts: %i[ms]) do
39
+ add_cmd(:dur, :duration, :human, flags: %i[ms]) do
40
40
  raw = @opts.args.first or abort 'usage: h date dur <seconds> [--ms]'
41
41
  n = Rational(raw.to_s)
42
42
  puts (@opts.ms ? Hiiro::Duration.milliseconds(n) : Hiiro::Duration.seconds(n)).compact
43
43
  end
44
44
 
45
45
  # h date diff <a> <b> -> how far apart two dates are (dates or unix timestamps)
46
- add_cmd(:diff, opts: %i[ms]) do
46
+ add_cmd(:diff, flags: %i[ms]) do
47
47
  a, b = @opts.args
48
48
  abort 'usage: h date diff <date-a> <date-b> (each: date string or unix timestamp)' unless a && b
49
49
  ta = Hiiro::TimeInput.parse(a, ms: @opts.ms ? true : nil)
data/bin/h-db CHANGED
@@ -188,7 +188,6 @@ Hiiro.run(*ARGV) do
188
188
  apps: %w[name],
189
189
  projects: %w[name],
190
190
  assignments: %w[worktree],
191
- pins: %w[command key],
192
191
  pane_homes: %w[name],
193
192
  check_runs: %w[pr_number name],
194
193
  reminders: %w[text],
data/bin/h-git CHANGED
@@ -215,7 +215,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
215
215
  end
216
216
  end
217
217
 
218
- add_cmd(:commit, opts: %i[dry_run yes model]) do
218
+ add_cmd(:commit, opts: %i[model], flags: %i[dry_run yes]) do
219
219
  unless system('git', 'rev-parse', '--is-inside-work-tree', out: File::NULL, err: File::NULL)
220
220
  puts 'Not inside a git work tree.'
221
221
  next false
data/bin/h-json ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'hiiro'
4
+
5
+ Hiiro.run do
6
+ add_cmd :structure, args: ['[FILE...]'] do
7
+ if opts.args.empty? && $stdin.tty?
8
+ abort 'Usage: h-json structure [FILE...] (or pipe JSON into stdin)'
9
+ end
10
+
11
+ filter = <<~JQ
12
+ def structure:
13
+ if type == "object" then
14
+ with_entries(.value |= structure)
15
+ elif type == "array" then
16
+ map(structure) | unique
17
+ else
18
+ type
19
+ end;
20
+
21
+ structure
22
+ JQ
23
+
24
+ exec('jq', '--', filter, *opts.args)
25
+ rescue Errno::ENOENT
26
+ abort 'h-json structure requires jq on PATH'
27
+ end
28
+ end