hiiro 0.1.362 → 0.1.363

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: a175bf67aa12a51e5cfefdcd556c47466f47d86e633fdb015eab163225db1c21
4
- data.tar.gz: adda6458c9cde507d21f49d133171d7d96a2dbb03360d518adaad7e6d74eacf7
3
+ metadata.gz: f52debf7400300975671eefe3d6bac6766179c179f40cecdafd91d611ae9500b
4
+ data.tar.gz: 2a5edf1448073cffb3afa07620c92a5e478fd057537f8f16c15d8234697bc28f
5
5
  SHA512:
6
- metadata.gz: 6db9a4fbf07b07006a1c4ce5a4b03f8dd8f918e64a7fd2055f45c581eae06107552189e5598e1fdc38ed42e40b5bd8962e03c723aaecd41ddc0ef820578a93e2
7
- data.tar.gz: 23d2ecee15514029997c3447ce1796a2692e3362dbe52161d3f128bc04d7931623c71dc410cf5e9d7d7ccc1aa706a4cda0c5064814368f107ca1d31e4b9fc859
6
+ metadata.gz: 22521e0b7c2be0f03b371e5471840eb81e504557b81b0d2a8f45242871160fa1960406d742d79ffc704f3333a3aa606be03519253bc0bf98beca3a9b49fd7ef7
7
+ data.tar.gz: 6c102bd975d77c9eadaeee63810bc04fefceb09f77b046caa1c7e05486c48928287be23815f9bd36665ad065324724e636e67d3ab0856c36cbd0a5634b7b13d4
data/.DS_Store CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -3,10 +3,14 @@
3
3
  ## [Unreleased]
4
4
 
5
5
  ### Added
6
+ - Add the `t` task CLI with `--task`/`-t` selection, durable task status and next actions, document homes, resource references, and Herdr workspace/tab/pane commands.
7
+ - Allow block-only command dispatch with `external_commands: false`, keeping legacy `t-*` executables out of the task CLI.
6
8
  - Add `Hiiro::Herdr`, a JSON-backed adapter for Herdr workspaces, tabs, panes, notifications, and command execution.
7
9
  - Persist Herdr workspace/tab/pane IDs for invocations, branches, queued prompts, services, and tracked PRs, with read fallbacks for legacy metadata.
8
10
 
9
11
  ### Changed
12
+ - Keep task command definitions and helpers in `~/bin/t`, using `add_cmd` and native Hiiro help instead of a library-level TaskCLI and custom help template.
13
+ - Preserve task records and resource references when detaching or pruning worktrees.
10
14
  - Move task, project, queue, service, background, Claude, app navigation, PR attach, title, and notification workflows from tmux to Herdr.
11
15
  - Keep `h session` and `h window` as compatibility commands for Herdr workspaces and tabs.
12
16
 
@@ -15,6 +19,10 @@
15
19
  - Remove the old `Hiiro::Tmux` adapter and tmux-specific command tests.
16
20
 
17
21
  ### Fixed
22
+ - Preserve `add_cmd` declaration locations and argument metadata in generated help; allow command groups to pass arguments and help through to child commands.
23
+ - Make undeclared `add_cmd opts:` entries boolean flags without consuming positional arguments; preserve explicit options and reserve conflicting short aliases.
24
+ - Show selected command options for `add_cmd -h`/`--help` without executing the command block.
25
+ - Focus the exact Herdr pane through the socket API and read the CLI's plain-text pane output without JSON parsing.
18
26
  - Restore the missing `Hiiro::Bins` helper so `require "hiiro"` boots and commands like `h jumplist record` dispatch correctly.
19
27
  - Make Hiiro's Ruby requirement explicit as Ruby 3.2+ and have rbenv-wide gem installs skip incompatible Ruby versions.
20
28
  - Update the publish script to preserve the Ruby support constant, run only on supported Ruby, and install releases only into compatible rbenv versions.
data/README.md CHANGED
@@ -59,6 +59,23 @@ h ping
59
59
  # => pong
60
60
  ```
61
61
 
62
+ ### Task CLI
63
+
64
+ `~/bin/t` contains the task commands and helpers, using Hiiro's `add_cmd` DSL
65
+ and existing task records. `t new NAME` creates a task and its notes directory
66
+ without creating a Git worktree. Select a task with `--task NAME` or `-t NAME`.
67
+ Root and group help are generated by Hiiro; leaf command help lists its selected options.
68
+
69
+ ```sh
70
+ t new investigation
71
+ t next -t investigation "Inspect the failing request"
72
+ t doc new findings -t investigation
73
+ t doc open findings -t investigation
74
+ t show investigation
75
+ ```
76
+
77
+ See [Task commands](docs/h-task.md) for the full command reference.
78
+
62
79
  ## Subcommands
63
80
 
64
81
  ### Base Commands
@@ -181,6 +198,44 @@ add_subcmd(:pwd) do |*args|
181
198
  end
182
199
  ```
183
200
 
201
+ ### Commands with declared options
202
+
203
+ Use `add_cmd` to expose a command's options in help. Undeclared names in `opts:`
204
+ become boolean flags with a default of `false`. A flag does not consume the next
205
+ positional argument.
206
+
207
+ ```ruby
208
+ Hiiro.run do
209
+ add_option :task, short: :t, desc: 'Task name'
210
+
211
+ add_cmd :test, opts: %i[a b c d task] do
212
+ puts 'a was set' if opts.a
213
+ puts opts.task if opts.task
214
+ puts opts.args
215
+ end
216
+ end
217
+ ```
218
+
219
+ Here, `test -a payload -t investigation` sets `opts.a` to `true`, leaves
220
+ `payload` in `opts.args`, and sets `opts.task` to `investigation`.
221
+ `test -h` and `test --help` display the selected options without running the
222
+ command block.
223
+
224
+ Automatic flags use their first letter as a short alias only when it is unique
225
+ among the command's automatic flags and does not conflict with a selected
226
+ explicit option or `-h`. Every automatic flag has a long form. Explicit flags
227
+ and value options keep their definitions.
228
+
229
+ For a command that delegates to `make_child`, use `add_cmd(..., passthrough: true)`.
230
+ It forwards control without parsing the parent's arguments or intercepting the
231
+ child's `--help`. The child declares and parses its own options. Generated help
232
+ uses the original command block's source location and declared `args:`, not the
233
+ internal wrapper's signature.
234
+
235
+ For a CLI that should dispatch only registered blocks, pass
236
+ `external_commands: false` to `Hiiro.run` and its `make_child` calls. This
237
+ prevents unrelated same-prefix executables on `PATH` from taking precedence.
238
+
184
239
  ## Writing Plugins
185
240
 
186
241
  Plugins are Ruby modules that extend Hiiro instances:
data/docs/h-task.md CHANGED
@@ -1,8 +1,111 @@
1
- # h task
1
+ # Task commands
2
2
 
3
- Manage top-level tasks. Each task pairs a worktree with a Herdr workspace. `h task start` creates worktrees under Hiiro's default work root; `h task from` can register an existing worktree from any repo or path.
3
+ `t` manages durable tasks. A task can be a coding project, an investigation, or administrative work. It does not need Git or Herdr.
4
4
 
5
- Tasks are stored in `~/.config/hiiro/tasks/tasks.yml` (SQLite-backed with YAML backup).
5
+ Task records and resource references live in `~/.config/hiiro/hiiro.db`, in the existing `tasks` table and the `task_resources` table. `h task` uses the same records. Its YAML file is a backup, not a separate task store.
6
+
7
+ ## t
8
+
9
+ The command implementation lives in `~/bin/t`, not a `Hiiro::TaskCLI` library class. Commands use `add_cmd` with per-command argument and option declarations. Running `t` or `t doc` displays Hiiro's generated subcommand table, including declaration locations. Leaf help, such as `t directory add --help`, displays only that command's options without executing it. There is no separate task help template. `t` does not run `h task` or discover legacy `t-*` executables.
10
+
11
+ Every task command accepts `-t TASK` or `--task TASK` before or after the command. Explicit task names are exact and take precedence over the current directory or workspace. Conflicting explicit names are errors.
12
+
13
+ Without a selector, `t` considers the current directory inside a task home, an attached directory, or a legacy worktree. In a Herdr terminal, it also queries the current workspace. If the contexts identify different tasks, the command fails without changing task data. A shared directory therefore requires an explicit selector. Outside Herdr, an unrelated focused workspace does not affect task selection.
14
+
15
+ ### Task records
16
+
17
+ ```text
18
+ t list [--all]
19
+ t show [TASK]
20
+ t current
21
+ t new TASK
22
+ t next [TEXT...] [--clear]
23
+ t status [active|waiting|done|archived]
24
+ t waiting [TEXT...] [--clear]
25
+ t done
26
+ t archive
27
+ ```
28
+
29
+ `list` shows active and waiting tasks. `--all` includes completed and archived tasks. `next`, `waiting`, and `status` without arguments display the current value.
30
+
31
+ `new TASK` creates a record and `~/notes/work/TASK`. It never creates a Git worktree, moves code, or launches Herdr. Repeating `new` keeps the existing record and ensures its home exists. Names contain 1-120 ASCII letters, digits, dots, underscores, or hyphens and start with a letter or digit. Existing names that contain other characters remain usable, with those characters percent-encoded in the computed home directory name.
32
+
33
+ Setting waiting text changes status to `waiting`. Clearing that text changes a waiting task back to `active`. `done` and `archive` change status and record timestamps. They never remove a task home, a file, a directory, a link, or a workspace. `status active` reopens a task.
34
+
35
+ ```bash
36
+ t new audit-invoices
37
+ t next -t audit-invoices 'Compare the September export'
38
+ t waiting -t audit-invoices 'Finance approval'
39
+ t waiting -t audit-invoices --clear
40
+ t done -t audit-invoices
41
+ t show audit-invoices
42
+ ```
43
+
44
+ ### Directory, link, PR, and file references
45
+
46
+ ```text
47
+ t directory add PATH [--primary] [--label LABEL]
48
+ t directory list
49
+ t directory open [ID|PATH|LABEL]
50
+ t link add URL [--kind general|issue|thread] [--label LABEL]
51
+ t link list [--kind general|issue|thread]
52
+ t link open [ID|URL|LABEL]
53
+ t pr add URL [--label LABEL]
54
+ t pr list
55
+ t pr open [ID|URL|LABEL]
56
+ t file add PATH [--label LABEL]
57
+ t file list
58
+ t file open [ID|PATH|LABEL]
59
+ ```
60
+
61
+ Directory and file attachments must already exist. `add` stores their canonical paths without moving or copying anything. Repeating an identical attachment does not create another reference. `--primary` marks an attached directory as the default code directory for new workspace tabs and panes.
62
+
63
+ Links must be absolute HTTP or HTTPS URLs. `link list` includes PR references unless a kind filter is present. PRs use the same resource storage as other links and do not require a Git repository or provider API.
64
+
65
+ `file list` also discovers files in the task home, including documents, without registration. Home files can be opened by a relative path or an unambiguous basename. An omitted open selector works only when exactly one resource matches. Otherwise, the command requires an ID, path, URL, or unique label.
66
+
67
+ `open` uses the operating system's default application. Attachments remain references even after task completion.
68
+
69
+ ### Documents
70
+
71
+ ```text
72
+ t doc new NAME [TITLE...]
73
+ t doc list
74
+ t doc open [NAME]
75
+ ```
76
+
77
+ `doc new` creates a Markdown file in the task home with an initial heading. It never overwrites an existing file. Documents have a stable task-ID prefix, such as `task-42-investigation.md`, to avoid collisions in `mdoc`'s shared HTML output directory. `doc open investigation` accepts the short name and invokes `mdoc`. Existing Markdown files in the task home also appear without registration.
78
+
79
+ Task creation, metadata, references, and document creation/listing work without Git or Herdr. Document reading requires `mdoc` on `PATH` and its existing configuration.
80
+
81
+ ### Herdr workspaces, tabs, and panes
82
+
83
+ ```text
84
+ t workspace open [--directory PATH]
85
+ t workspace show
86
+ t tab list
87
+ t tab new [LABEL] [--directory PATH] [--command COMMAND]
88
+ t tab open ID|LABEL
89
+ t pane list
90
+ t pane open ID|LABEL
91
+ t pane read ID|LABEL
92
+ t pane run ID|LABEL COMMAND...
93
+ t pane split ID|LABEL [--direction right|down] [--directory PATH] [--command COMMAND]
94
+ ```
95
+
96
+ These commands require a running Herdr server. `workspace open` focuses the workspace with the task's label or creates one. A new workspace starts in the explicit directory, the primary code directory, the legacy worktree, or the task home, in that order. `--directory` changes that operation's start directory without changing stored attachments.
97
+
98
+ `workspace show` queries the current tabs and panes. Tab and pane selectors must belong to the selected task's workspace. Duplicate labels require a live ID. No pane or tab ID is stored as durable task identity. Task workspace labels follow Herdr's dot-to-underscore normalization. Colliding task or workspace labels are errors rather than fuzzy matches.
99
+
100
+ Use `--` before literal command arguments that begin with a dash:
101
+
102
+ ```bash
103
+ t pane run -t audit-invoices PANE_ID -- printf '%s\n' --example
104
+ ```
105
+
106
+ ## h task
107
+
108
+ The existing `h task` commands below retain their coding-worktree operations. Unlike `t new`, `h task start` can create a worktree for a new task. For a task without a worktree, path resolution uses its task home. Starting an existing noncoding task switches to that home without creating a worktree.
6
109
 
7
110
  ## Synopsis
8
111
 
@@ -225,19 +328,19 @@ h task path -a feat bug # tasks whose name starts with "feat" or "bug"
225
328
 
226
329
  ### prune
227
330
 
228
- Drop task records whose worktree directory is missing on disk. Useful after deleting worktrees out-of-band (e.g. `git worktree remove` on stale branches). Defaults to a dry-run; pass `-f` to actually remove.
331
+ Detach worktree associations whose directories are missing. The task record, metadata, and resource references remain. Tasks without worktrees are not pruned. Defaults to a dry-run.
229
332
 
230
333
  **Options**
231
334
 
232
335
  | Flag | Short | Description | Default |
233
336
  |------|-------|-------------|---------|
234
- | `--force` | `-f` | Actually delete (default is dry-run) | false |
337
+ | `--force` | `-f` | Detach missing worktrees | false |
235
338
 
236
339
  **Examples**
237
340
 
238
341
  ```bash
239
342
  h task prune # show what would be pruned
240
- h task prune -f # actually remove the missing-worktree task records
343
+ h task prune -f # detach missing worktrees, retaining task records
241
344
  ```
242
345
 
243
346
  ---
@@ -264,7 +367,7 @@ h task queue hadd
264
367
 
265
368
  ### resume
266
369
 
267
- Re-register an available (unassigned) worktree as a new task and switch to it. With no argument, opens a fuzzyfind selector over available worktrees.
370
+ Associate an available worktree with a new task or an existing task whose worktree was detached, then switch to it. Existing task metadata is preserved. With no argument, opens a fuzzyfind selector over available worktrees.
268
371
 
269
372
  **Examples**
270
373
 
@@ -438,7 +541,7 @@ h task st
438
541
 
439
542
  ### stop
440
543
 
441
- Remove a task from the task list (preserves the worktree for future reuse via `resume`). With no arguments, opens a fuzzyfind selector.
544
+ Detach a task's worktree association and those of its subtasks. The task records, metadata, workspace labels, and resources remain. The former worktree path becomes a directory reference, and the worktree is available for reuse through `resume`. With no arguments, opens a fuzzyfind selector.
442
545
 
443
546
  **Examples**
444
547
 
data/lib/hiiro/herdr.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require 'json'
2
+ require 'socket'
3
+ require 'timeout'
2
4
 
3
5
  class Hiiro
4
6
  class Herdr
@@ -487,11 +489,20 @@ class Hiiro
487
489
  alias kill_pane close_pane
488
490
 
489
491
  def focus_pane(ref)
490
- pane = get_pane(ref)
491
- return false unless pane
492
-
493
- focus_workspace(pane.workspace_id)
494
- focus_tab(pane.tab_id)
492
+ status = @executor.capture('herdr', 'status', 'server')
493
+ socket_path = status.lines.find { |line| line.start_with?('socket: ') }&.delete_prefix('socket: ')&.strip
494
+ return false unless socket_path
495
+
496
+ request = { id: 'hiiro:pane:focus', method: 'pane.focus', params: { pane_id: ref } }
497
+ Timeout.timeout(5) do
498
+ UNIXSocket.open(socket_path) do |socket|
499
+ socket.puts(JSON.generate(request))
500
+ response = JSON.parse(socket.gets || '{}')
501
+ response.dig('result', 'pane', 'focused') == true
502
+ end
503
+ end
504
+ rescue SystemCallError, IOError, JSON::ParserError, Timeout::Error
505
+ false
495
506
  end
496
507
  alias select_pane focus_pane
497
508
 
@@ -546,7 +557,7 @@ class Hiiro
546
557
 
547
558
  args = ['pane', 'read', pane_id, '--source', source]
548
559
  args += ['--lines', lines.to_s] if lines
549
- capture_result(*args).dig('read', 'text')
560
+ @executor.capture('herdr', *args)
550
561
  end
551
562
  alias capture_pane read_pane
552
563
 
data/lib/hiiro/options.rb CHANGED
@@ -86,13 +86,18 @@ class Hiiro
86
86
  public
87
87
 
88
88
  def select(names)
89
+ names = names.map(&:to_sym).uniq
89
90
  subset = self.class.setup {}
91
+ reserved_shorts = ['h'] + names.filter_map { |name| @definitions[name]&.short }
92
+ auto_shorts = names.reject { |name| @definitions.key?(name) }.map { |name| name.to_s[0] }.tally
90
93
  names.each do |name|
91
- defn = @definitions[name.to_sym]
94
+ defn = @definitions[name]
92
95
  if defn
93
- subset.definitions[name.to_sym] = defn
96
+ subset.definitions[name] = defn
94
97
  else
95
- subset.definitions[name.to_sym] = Definition.new(name, short: name.to_s.chars.first, desc: "auto-created flag: #{name}")
98
+ short = name.to_s[0]
99
+ short = nil if reserved_shorts.include?(short) || auto_shorts[short] > 1
100
+ subset.flag(name, short: short, desc: "auto-created flag: #{name}")
96
101
  end
97
102
  end
98
103
  subset
@@ -1,9 +1,13 @@
1
1
  require 'sequel'
2
+ require 'uri'
2
3
 
3
4
  class Hiiro
4
5
  class TaskRecord < Sequel::Model(:tasks)
5
6
  Hiiro::DB.register(self)
6
7
 
8
+ METADATA_COLUMNS = %i[status next_action waiting_on primary_directory updated_at completed_at archived_at].freeze
9
+ STATUSES = %w[active waiting done archived].freeze
10
+
7
11
  def self.create_table!(db)
8
12
  db.create_table?(:tasks) do
9
13
  primary_key :id
@@ -17,6 +21,37 @@ class Hiiro
17
21
  end
18
22
  end
19
23
 
24
+
25
+ def self.migrate!(db)
26
+ columns = db.schema(:tasks).map(&:first)
27
+ METADATA_COLUMNS.each do |column|
28
+ db.alter_table(:tasks) { add_column column, String } unless columns.include?(column)
29
+ end
30
+ end
31
+
32
+ def self.home_for(name)
33
+ component = URI::DEFAULT_PARSER.escape(name.to_s, /[^A-Za-z0-9._-]/)
34
+ component = component.gsub('.', '%2E') if %w[. ..].include?(component)
35
+ raise ArgumentError, 'Task name cannot be empty' if component.empty?
36
+
37
+ File.join(Dir.home, 'notes', 'work', component)
38
+ end
39
+
40
+ def home
41
+ self.class.home_for(name)
42
+ end
43
+
44
+ def task_status
45
+ status || 'active'
46
+ end
47
+
48
+ def task_attributes
49
+ values.reject { |key, _| key == :id }.compact
50
+ end
51
+
52
+ def resources
53
+ TaskResource.where(task_id: id).order(:id)
54
+ end
20
55
  def self.top_level
21
56
  where(Sequel.~(Sequel.like(:name, '%/%')))
22
57
  end
@@ -34,4 +69,20 @@ class Hiiro
34
69
  order(:name).all
35
70
  end
36
71
  end
72
+
73
+ class TaskResource < Sequel::Model(:task_resources)
74
+ Hiiro::DB.register(self)
75
+
76
+ def self.create_table!(db)
77
+ db.create_table?(:task_resources) do
78
+ primary_key :id
79
+ foreign_key :task_id, :tasks, null: false, on_delete: :cascade
80
+ String :kind, null: false
81
+ String :target, null: false
82
+ String :label
83
+ String :created_at
84
+ unique [:task_id, :kind, :target]
85
+ end
86
+ end
87
+ end
37
88
  end
data/lib/hiiro/tasks.rb CHANGED
@@ -89,6 +89,7 @@ class Hiiro
89
89
  return nil unless target
90
90
 
91
91
  return target.path if target.is_a?(FallbackTarget)
92
+ return target.home unless target.tree_name
92
93
 
93
94
  tree = environment.find_tree(target.tree_name)
94
95
  return tree.path if tree
@@ -250,8 +251,8 @@ class Hiiro
250
251
  return
251
252
  end
252
253
 
253
- config.remove_task(task.name)
254
- subtasks(task).each { |st| config.remove_task(st.name) }
254
+ config.detach_tree(task.name)
255
+ subtasks(task).each { |st| config.detach_tree(st.name) }
255
256
 
256
257
  puts "Stopped task '#{task.name}' (worktree available for reuse)"
257
258
  end
@@ -265,12 +266,14 @@ class Hiiro
265
266
  # Derive a default task name from the tree name: "foo/main" -> "foo"
266
267
  task_name ||= tree.name.end_with?('/main') ? tree.name.chomp('/main') : tree.name
267
268
 
268
- if task_by_name(task_name)
269
- puts "Task '#{task_name}' already exists"
269
+ existing = task_by_name(task_name)
270
+ if existing&.tree_name
271
+ puts "Task '#{task_name}' already has a worktree"
270
272
  return
271
273
  end
272
274
 
273
- task = Task.new(name: task_name, tree: tree.name, session: task_name)
275
+ attributes = existing ? existing.to_h : { name: task_name }
276
+ task = Task.new(**attributes.merge(tree: tree.name, session: task_name))
274
277
  config.save_task(task)
275
278
  puts "Resumed task '#{task_name}' from worktree '#{tree.name}'"
276
279
 
@@ -706,27 +709,26 @@ class Hiiro
706
709
  end
707
710
 
708
711
  def save_task(task)
709
- TaskRecord.find_or_create(name: task.name) do |r|
710
- r.tree = task.tree_name
711
- r.session = task.session_name
712
- r.app = task.respond_to?(:app) ? task.app : nil
713
- r.color_index = task.color_index
714
- end.update(
715
- tree: task.tree_name,
716
- session: task.session_name,
717
- app: task.respond_to?(:app) ? task.app : nil,
718
- color_index: task.color_index
719
- )
712
+ record = TaskRecord.find_by_name(task.name) || TaskRecord.new(name: task.name, created_at: Time.now.iso8601)
713
+ record.set(task.to_h.merge(tree: task.tree_name, session: task.session_name, color_index: task.color_index))
714
+ record.save
720
715
  save_tasks_yaml_backup
721
716
  rescue => e
722
717
  warn "Failed to save task to DB: #{e}"
723
718
  end
724
719
 
725
- def remove_task(name)
726
- TaskRecord.where(name: name).delete
720
+ def detach_tree(name)
721
+ record = TaskRecord.find_by_name(name)
722
+ return unless record&.tree
723
+
724
+ path = record.tree.start_with?('/') ? record.tree : File.join(Hiiro::WORK_DIR, record.tree)
725
+ Hiiro::DB.connection.transaction do
726
+ TaskResource.find_or_create(task_id: record.id, kind: 'directory', target: path)
727
+ record.update(tree: nil, updated_at: Time.now.iso8601)
728
+ end
727
729
  save_tasks_yaml_backup
728
730
  rescue => e
729
- warn "Failed to remove task from DB: #{e}"
731
+ warn "Failed to detach task worktree: #{e}"
730
732
  end
731
733
 
732
734
  private
@@ -735,7 +737,7 @@ class Hiiro
735
737
  rows = TaskRecord.all_as_list
736
738
  return fallback_load_tasks_from_yaml if rows.empty?
737
739
  { 'tasks' => rows.map { |r|
738
- { 'name' => r.name, 'tree' => r.tree, 'session' => r.session, 'app' => r.app, 'color_index' => r.color_index }.compact
740
+ r.task_attributes.transform_keys(&:to_s)
739
741
  }}
740
742
  rescue => e
741
743
  warn "Failed to load tasks from DB: #{e}. Falling back to YAML."
@@ -801,7 +803,7 @@ class Hiiro
801
803
 
802
804
  def save_tasks_yaml_backup(data = nil)
803
805
  data ||= { 'tasks' => TaskRecord.all_as_list.map { |r|
804
- { 'name' => r.name, 'tree' => r.tree, 'session' => r.session, 'app' => r.app, 'color_index' => r.color_index }.compact
806
+ r.task_attributes.transform_keys(&:to_s)
805
807
  }}
806
808
  FileUtils.mkdir_p(File.dirname(tasks_file))
807
809
  File.write(tasks_file, YAML.dump(data))
@@ -1213,33 +1215,33 @@ class Hiiro
1213
1215
 
1214
1216
  h.add_subcmd(:prune) do |*raw_args|
1215
1217
  opts = Hiiro::Options.parse(raw_args) {
1216
- flag(:force, short: :f, desc: 'Actually delete (default is dry-run)')
1218
+ flag(:force, short: :f, desc: 'Detach missing worktrees (default is dry-run)')
1217
1219
  }
1218
1220
 
1219
1221
  to_remove = tm.environment.all_tasks.select do |task|
1220
- next true unless task.tree_name
1222
+ next false unless task.tree_name
1221
1223
  path = tm.resolve_path(task)
1222
1224
  path.nil? || !Dir.exist?(path)
1223
1225
  end
1224
1226
 
1225
1227
  if to_remove.empty?
1226
- puts "No tasks to prune"
1228
+ puts "No missing task worktrees"
1227
1229
  next
1228
1230
  end
1229
1231
 
1230
1232
  to_remove.each do |task|
1231
1233
  path = task.tree_name ? tm.resolve_path(task) : '(no tree)'
1232
1234
  if opts.force
1233
- tm.config.remove_task(task.name)
1234
- puts "Pruned: #{task.name} (#{path})"
1235
+ tm.config.detach_tree(task.name)
1236
+ puts "Detached missing worktree: #{task.name} (#{path}); task retained"
1235
1237
  else
1236
- puts "Would prune: #{task.name} (#{path})"
1238
+ puts "Would detach missing worktree: #{task.name} (#{path})"
1237
1239
  end
1238
1240
  end
1239
1241
 
1240
1242
  unless opts.force
1241
1243
  puts
1242
- puts "Re-run with -f to actually delete."
1244
+ puts "Re-run with -f to detach these worktree references."
1243
1245
  end
1244
1246
  end
1245
1247
 
@@ -1486,13 +1488,19 @@ class Hiiro
1486
1488
  end
1487
1489
 
1488
1490
  class Task
1489
- attr_reader :name, :tree_name, :session_name, :color_index
1491
+ attr_reader :name, :tree_name, :session_name, :color_index, :app, :created_at, *TaskRecord::METADATA_COLUMNS
1490
1492
 
1491
- def initialize(name:, tree: nil, session: nil, color_index: nil, **_)
1493
+ def initialize(name:, tree: nil, session: nil, color_index: nil, **attributes)
1492
1494
  @name = name
1493
1495
  @tree_name = tree
1494
1496
  @session_name = session || name
1495
1497
  @color_index = color_index
1498
+ @attributes = attributes.slice(:app, :created_at, *TaskRecord::METADATA_COLUMNS)
1499
+ @attributes.each { |key, value| instance_variable_set("@#{key}", value) }
1500
+ end
1501
+
1502
+ def home
1503
+ TaskRecord.home_for(name)
1496
1504
  end
1497
1505
 
1498
1506
  def parent_name
@@ -1537,7 +1545,7 @@ class Hiiro
1537
1545
  end
1538
1546
 
1539
1547
  def to_h
1540
- h = { name: name }
1548
+ h = @attributes.merge(name: name)
1541
1549
  h[:tree] = tree_name if tree_name
1542
1550
  h[:session] = session_name if session_name != name
1543
1551
  h[:color_index] = color_index unless color_index.nil?
data/lib/hiiro/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  class Hiiro
2
- VERSION = "0.1.362"
2
+ VERSION = "0.1.363"
3
3
  SUPPORTED_RUBY_VERSION = ">= 3.2.0"
4
4
  end
data/lib/hiiro.rb CHANGED
@@ -355,11 +355,16 @@ class Hiiro
355
355
  end
356
356
  end
357
357
 
358
- def add_cmd(*names, args: [], opts: [], &block)
358
+ def add_cmd(*names, args: [], opts: [], passthrough: false, &block)
359
359
  cmd_opts = options.select(opts)
360
360
 
361
361
  wrapper = lambda do |*raw_args|
362
+ next instance_eval(&block) if passthrough
362
363
  @opts = cmd_opts.parse(raw_args)
364
+ if @opts.help?
365
+ puts @opts.help_text
366
+ next
367
+ end
363
368
  instance_eval(&block)
364
369
  end
365
370
 
@@ -368,7 +373,8 @@ class Hiiro
368
373
  name, wrapper,
369
374
  subcmd_args: args,
370
375
  subcmd_opts: cmd_opts,
371
- **global_values
376
+ **global_values,
377
+ source_location: block.source_location&.join(':')
372
378
  )
373
379
  end
374
380
  end
@@ -605,6 +611,8 @@ class Hiiro
605
611
  end
606
612
 
607
613
  def all_bins
614
+ return [] unless hiiro.global_values.fetch(:external_commands, true)
615
+
608
616
  pattern = format('{%s}/%s-*', paths.join(?,), bin_name)
609
617
 
610
618
  Dir.glob(pattern).map { |path| Bin.new(bin_name, path) }
@@ -740,13 +748,14 @@ class Hiiro
740
748
  end
741
749
 
742
750
  def location
743
- handler.source_location&.join(':')
751
+ values[:source_location] || handler.source_location&.join(':')
744
752
  end
745
753
 
746
754
  def params_string
747
755
  if subcmd_args.any?
748
756
  return subcmd_args.map { |a| "<#{a}>" }.join(' ')
749
757
  end
758
+ return nil if subcmd_opts
750
759
 
751
760
  return nil unless handler.respond_to?(:parameters)
752
761
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.362
4
+ version: 0.1.363
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-12 00:00:00.000000000 Z
11
+ date: 2026-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry