hiiro 0.1.378 → 0.1.379

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: bc8c7a68d7b0fc2d7d6182b1d307978ba4ed185d4b6162800821ef12c8fa5d4e
4
- data.tar.gz: 3708447b7e35a09e59deb4cc570a770619054c299852e3ead032f6240180cfcf
3
+ metadata.gz: 437599f5b96592172456a116919ccc4774cae2ecd248537f098f9c49f2e08446
4
+ data.tar.gz: 9a892f045ab4722668b7d89fc7412ec51c3fc954ffc4dd4c1d7f60c516cdaa1e
5
5
  SHA512:
6
- metadata.gz: 0c4a5a117425f0b879607a1040d7a757b696b0b4773d8ff1cc4747a403123029217e3ec4744031ff45b14a3f5565727c8a3a2d40097a017cf8b6bcbd68b4c93f
7
- data.tar.gz: 219ff3e0aa32313353c2aad202c745788d1a6327816a90b54e9b9adee90a975709c64a36ce40c7f5425510af1e979240eb213f496ddd81da151e39e4249ee550
6
+ metadata.gz: c55442904262705529cbc815ece19453841348d65a9c2de7601edd268b06d81cc9b8140823a4c2e55e7085169379d838543a54f11ff0f404973d4a7bf55d77c7
7
+ data.tar.gz: 0c6e2780c3500669cad8748280cd3da8ecf1d19a909ef547245467ef2a9feb03441d1430696732c926b27a4c86074db4c86190961eba652387f7c2edc44009ea
data/CHANGELOG.md CHANGED
@@ -10,6 +10,9 @@
10
10
  - `t path`, `t cd`, `t start`, and `t switch` accept optional `APP` argument to select a configured relative directory beneath the task root
11
11
  - APP resolves by exact name or unique case-sensitive prefix; fails when ambiguous or missing
12
12
 
13
+ ### Added
14
+ - `t switch` / `t workspace` and `t cd` accept an optional APP positional (or `--app`) resolving through the apps registry by exact name or unique prefix; the workspace or pane cd targets the app directory under the task worktree, e.g. `t switch aldi cpm` opens `~/work/aldi/main/retailer-tools/content-page-migrator`
15
+
13
16
  ## [0.1.376] - 2026-09-14
14
17
 
15
18
  ### Changed
data/README.md CHANGED
@@ -92,8 +92,11 @@ prefix or a missing current task opens the fuzzy finder; non-interactive runs fa
92
92
 
93
93
  The current task is the calling Herdr workspace, then the current directory
94
94
  inside a task home, code directory, worktree, or registered directory, then the
95
- task saved with `t use TASK`. `t switch TASK` focuses or creates the task
96
- workspace and also saves it. `t current` only prints.
95
+ task saved with `t use TASK`. `t switch TASK [APP]` focuses or creates the task
96
+ workspace and also saves it; APP (exact name or unique prefix from the apps
97
+ registry) opens the workspace in that app's directory under the task worktree.
98
+ `t cd TASK [APP]` sends the same `cd` to the calling Herdr pane. `t current`
99
+ only prints.
97
100
 
98
101
  ```sh
99
102
  t new investigation
@@ -105,7 +108,7 @@ t show investigation
105
108
  t todo rm 42 # an ID printed by show or todo list
106
109
  t use investigation
107
110
  t switch investigation
108
- t switch investigation frontend # app name or unique prefix
111
+ t switch investigation frontend # open in an app directory (unique prefix)
109
112
  t switch --show
110
113
  t next . "Write the handoff"
111
114
  t start investigation frontend # tree new + app start directory
data/bin/hh ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'hiiro'
4
+
5
+ Hiiro.run do
6
+ add_cmd(:ls) do
7
+ cmd = 'herdr', 'session', 'list', *opts.original_args
8
+ temp = Tempfile.new('asdf')
9
+ system(*cmd)
10
+ end
11
+
12
+ add_cmd(:switch, :start) do
13
+ cmd = 'herdr', 'session', 'list'
14
+ temp = Tempfile.new('asdf')
15
+ system(*cmd, out: temp.path)
16
+
17
+ lines = File.readlines(temp.path, chomp: true)
18
+
19
+ sessions = lines.filter_map.with_index{|x, i| next if i == 0; [x, x.split(/\s+/, 2).first] }.to_h
20
+
21
+ ses =
22
+ if opts.args.empty?
23
+ fuzzyfind_from_map(sessions)
24
+ else
25
+ opts.args.first
26
+ end
27
+
28
+ use = sessions.values.find{|ss| ss.start_with?(ses) }
29
+
30
+ system('herdr', '--session', use)
31
+ end
32
+
33
+ add_cmd(:kill, :stop, :rm, :remove) do
34
+ tmp = Tempfile.new 'asdf'
35
+ result = system 'herdr', 'session' ,'stop', *opts.args, err: tmp.path
36
+
37
+ if result
38
+ result = system 'herdr', 'session' ,'delete', *opts.args
39
+ else
40
+ reason = File.read tmp.path
41
+ if reason[/running/]
42
+ result = system 'herdr', 'session' ,'delete', *opts.args
43
+ end
44
+ end
45
+ end
46
+
47
+ add_default do |*args|
48
+ system 'herdr', *args
49
+ end
50
+ end
data/docs/t.md CHANGED
@@ -95,7 +95,7 @@ t doc open [TASK] [NAME]
95
95
  | `t path [TASK] [APP]` | Start directory, optionally beneath the configured relative directory for APP |
96
96
  | `t branch [TASK]` | Worktree branch or `(detached)` |
97
97
  | `t sh [TASK] [CMD...]` | Changes to the start directory and execs a shell or CMD |
98
- | `t cd [TASK] [APP]` | Sends `cd` to the calling Herdr pane |
98
+ | `t cd [TASK] [APP]` | Sends `cd` to the calling Herdr pane; APP narrows to a registry app directory under the task worktree |
99
99
 
100
100
  Worktree creation is `Hiiro::TaskManager#create_tree`, shared with the legacy `h task start` code path.
101
101
 
@@ -104,7 +104,7 @@ APP resolves against configured Hiiro apps by exact name or unique case-sensitiv
104
104
  ## Herdr workspaces, tabs, and panes
105
105
 
106
106
  ```text
107
- t switch [TASK] [APP] [--directory PATH] [--show] alias: workspace
107
+ t switch [TASK] [APP] [--directory PATH] [--show] alias: workspace
108
108
  t tab ls [TASK]
109
109
  t tab new [TASK] [LABEL] [--directory PATH] [--command COMMAND]
110
110
  t tab open [TASK] [ID|LABEL]
@@ -117,6 +117,10 @@ t pane split [TASK] ID|LABEL [--direction right|down] [--directory PATH] [--comm
117
117
 
118
118
  These require a running Herdr server. The workspace label is the task session or name with `.` replaced by `_`. For a task, optional APP selects its configured relative directory when creating the workspace; when the workspace already exists, `switch` focuses it and sends `cd` to its focused pane. APP and `--directory` cannot be combined. `switch` also accepts the name of a live Herdr workspace that belongs to no task, exactly or by unique prefix, and focuses it; APP is invalid for loose workspaces and panes. A task with the same name wins. An exact pane ID such as `w6:p2` focuses that pane, and the picker lists every live pane with its directory and foreground command. When the name is ambiguous, or no task or context is given, a terminal gets a fuzzy finder over tasks and loose workspaces, with duplicate workspace names numbered in the label only. Commands that jump somewhere never assume the current task: `t switch`, `t tab open`, and `t pane open` with no task open the picker instead (or fail when stdin is not a terminal); use `.` explicitly for the current task.
119
119
 
120
+ ### App directories
121
+
122
+ `t switch TASK APP` and `t cd TASK APP` accept APP as an exact name or unique prefix from the apps registry (`Hiiro::AppRecord`); `--app APP` is the flag form. The workspace opens (or the pane cds) into the app's directory under the task worktree, e.g. with `aldi` registered as `retailer-tools/content-page-migrator`, `t switch aldi cpm` opens `~/work/aldi/main/retailer-tools/content-page-migrator`. An ambiguous prefix is an error; APP only applies to task workspaces, not panes or loose workspaces.
123
+
120
124
  ## Native AI sessions
121
125
 
122
126
  `t omp [TASK] [ARGS...]`, `t codex` / `cdx`, and `t claude` / `cld` create a new focused tab in the task workspace, creating the workspace if needed, and run the native tool with ARGS forwarded verbatim. A leading argument that is a prefix of `resume` turns into the tool's native resume flag; bare `resume` with exactly one running pane of that tool focuses it instead.
@@ -63,6 +63,18 @@ class Hiiro
63
63
  lookup_task(reference) || raise(Error, "Task not found: #{reference}; run t new #{reference}")
64
64
  end
65
65
 
66
+ # [name, relative_path] from the apps registry: exact name wins, else a
67
+ # unique prefix; ambiguity and no-match are errors.
68
+ def lookup_app!(reference)
69
+ apps = Hiiro::AppRecord.all_as_hash
70
+ raise Error, 'No apps are configured' if apps.empty?
71
+ return [reference, apps[reference]] if apps[reference]
72
+ found = Hiiro::Matcher.by_prefix(apps.keys.sort, reference).matches.map(&:item)
73
+ raise Error, "No app matches #{reference}" if found.empty?
74
+ raise Error, "Ambiguous app #{reference}: #{found.join(', ')}" unless found.one?
75
+ [found.first, apps[found.first]]
76
+ end
77
+
66
78
  # Current task from Herdr, directory, or the saved pin. With no context at all,
67
79
  # a TTY gets the fuzzy finder over every task; ambiguous or stale context still fails.
68
80
  def current_task
@@ -469,6 +481,26 @@ class Hiiro
469
481
  herdr
470
482
  end
471
483
  end
484
+ # App reference (positional or --app) resolves through the apps registry
485
+ # under the task worktree; explicit param wins over the option.
486
+ def start_directory(task, app: nil)
487
+ app = opts&.fetch(:app) if app.nil?
488
+ path = opts&.fetch(:directory) || (app && app_directory(task, app)) || code_directory(task) || ensure_home(task)
489
+ existing_path(path, directory: true)
490
+ end
491
+
492
+ # Registry app directory under the task worktree.
493
+ def app_directory(task, reference)
494
+ _name, relative = lookup_app!(reference)
495
+ root = code_directory(task)
496
+ raise Error, "#{task.name} has no worktree; run t tree new #{task.name} first" unless root
497
+ File.join(root, relative)
498
+ end
499
+
500
+ def open_workspace(task, save:, app: nil)
501
+ open_task_workspace(task, start_directory(task, app: app))
502
+ save_current(task) if save
503
+ end
472
504
 
473
505
  def workspace_for(task, required: true)
474
506
  label = workspace_label(task)
@@ -480,29 +512,6 @@ class Hiiro
480
512
  matches.first
481
513
  end
482
514
 
483
- def app_directory(path, app_name)
484
- return path unless app_name
485
- result = Hiiro::Matcher.new(Hiiro::AppRecord.all, :name).by_prefix(app_name)
486
- app = result.resolved&.item
487
- if !app && result.ambiguous?
488
- raise Error, "Ambiguous app #{app_name}: #{result.matches.map { |match| match.item.name }.join(', ')}"
489
- end
490
- raise Error, "App not found: #{app_name}" unless app
491
- File.join(path, app.path)
492
- end
493
-
494
- def start_directory(task, app_name = nil)
495
- path = opts&.fetch(:directory)
496
- raise Error, 'Use an app or --directory, not both' if path && app_name
497
- path ||= code_directory(task) || ensure_home(task)
498
- existing_path(app_directory(path, app_name), directory: true)
499
- end
500
-
501
- def open_workspace(task, save:, app_name: nil)
502
- open_task_workspace(task, start_directory(task, app_name), chdir: !app_name.nil?)
503
- save_current(task) if save
504
- end
505
-
506
515
  # Focus the task workspace or create it at directory. With optional: true,
507
516
  # a stopped Herdr prints a hint instead of failing (used after worktree changes).
508
517
  def open_task_workspace(task, directory, optional: false, chdir: false)
@@ -556,51 +565,47 @@ class Hiiro
556
565
  puts pane_line(pane)
557
566
  end
558
567
 
559
- # [target, explicit, app_name]. A task wins over a workspace with the same name.
568
+ # [target, explicit, rest]. A task wins over a workspace with the same name.
569
+ # The first positional names the target; any remaining positional is the app.
560
570
  def switch_target(args)
561
571
  args = args.dup
562
- return [lookup_task!(opts.task), true, single(args, optional: true)] if opts.task
563
- return [pick_switch_target(Hiiro::TaskRecord.all_as_list, loose_workspaces), true, single(args, optional: true)] if opts.find
564
- first = args.first
572
+ first = args.shift
573
+ rest = args
574
+ if opts.task || opts.find
575
+ # Flag forces the target, so the first positional is the app.
576
+ app = [first].compact
577
+ no_args!(rest)
578
+ return [lookup_task!(opts.task), true, app] if opts.task
579
+ return [pick_switch_target(Hiiro::TaskRecord.all_as_list, loose_workspaces), true, app] if opts.find
580
+ end
581
+ no_args!(rest.drop(1))
565
582
  if first && first != '.' && !first.start_with?('-')
566
- args.shift
567
- app_name = single(args, optional: true)
568
583
  tasks = Hiiro::TaskRecord.all_as_list
569
584
  workspaces = loose_workspaces
570
585
  exact = tasks.find { |task| task.name == first }
571
- return [exact, true, app_name] if exact
586
+ return [exact, true, rest] if exact
572
587
  pane = live_panes.values.flatten.find { |candidate| candidate.id == first }
573
- raise Error, 'An app applies only to task workspaces' if pane && app_name
574
- return [pane, false, nil] if pane
588
+ return [pane, false, rest] if pane
575
589
  exact_ws = workspaces.select { |workspace| workspace.name == first }
576
- if exact_ws.one?
577
- raise Error, 'An app applies only to task workspaces' if app_name
578
- return [exact_ws.first, false, nil]
579
- end
590
+ return [exact_ws.first, false, rest] if exact_ws.one?
580
591
  task_matches = tasks.select { |task| task.name.start_with?(first) }
581
592
  ws_matches = exact_ws.any? ? exact_ws : workspaces.select { |workspace| workspace.name.start_with?(first) }
582
- return [task_matches.first, true, app_name] if task_matches.one? && ws_matches.empty?
583
- if ws_matches.one? && task_matches.empty?
584
- raise Error, 'An app applies only to task workspaces' if app_name
585
- return [ws_matches.first, false, nil]
586
- end
593
+ return [task_matches.first, true, rest] if task_matches.one? && ws_matches.empty?
594
+ return [ws_matches.first, false, rest] if ws_matches.one? && task_matches.empty?
587
595
  raise Error, "No task or workspace matches #{first}" if task_matches.empty? && ws_matches.empty?
588
596
  names = (task_matches.map(&:name) + ws_matches.map { |w| "#{w.name} (workspace)" }).join(', ')
589
597
  raise Error, "Ambiguous #{first}: #{names}" unless $stdin.tty?
590
598
  target = pick_switch_target(task_matches, ws_matches, panes: [])
591
- raise Error, 'An app applies only to task workspaces' if app_name && !target.is_a?(Hiiro::TaskRecord)
592
- return [target, target.is_a?(Hiiro::TaskRecord), app_name]
599
+ return [target, target.is_a?(Hiiro::TaskRecord), rest]
593
600
  end
601
+ no_args!(rest)
594
602
  if first == '.'
595
- args.shift
596
- app_name = single(args, optional: true)
597
- return [Hiiro::CurrentTask.new(herdr: -> { herdr_client }, pin: true).resolve!, false, app_name]
603
+ return [Hiiro::CurrentTask.new(herdr: -> { herdr_client }, pin: true).resolve!, false, rest]
598
604
  end
599
- no_args!(args)
600
605
  # Jumping somewhere never assumes the current task: pick a destination.
601
606
  raise Error, 'Name a task, workspace, or pane to switch to, or use . for the current task' unless $stdin.tty?
602
607
  target = pick_switch_target(Hiiro::TaskRecord.all_as_list, loose_workspaces)
603
- [target, target.is_a?(Hiiro::TaskRecord), nil]
608
+ [target, target.is_a?(Hiiro::TaskRecord), rest]
604
609
  end
605
610
 
606
611
  def require_picker!(what)
@@ -707,10 +712,10 @@ class Hiiro
707
712
  command.empty? ? exec(ENV['SHELL'] || 'zsh') : exec(*command)
708
713
  end
709
714
 
710
- def cd_to(task, app_name = nil)
715
+ def cd_to(task, app: nil)
711
716
  pane = ENV['HERDR_PANE_ID']
712
717
  raise Error, 'Not running inside a Herdr pane' unless pane
713
- check_result(client.run_in_pane(pane, "cd #{start_directory(task, app_name).shellescape}"))
718
+ check_result(client.run_in_pane(pane, "cd #{start_directory(task, app: app).shellescape}"))
714
719
  end
715
720
 
716
721
  def show_workspace(task)
@@ -850,45 +855,36 @@ class Hiiro
850
855
  end
851
856
  end
852
857
 
853
- add_cmd(:path, args: ['task?', 'app?'], opts: TASK_OPTIONS) do
854
- task, rest = take_task(opts.args)
855
- puts start_directory(task, single(rest, optional: true))
856
- end
857
- add_cmd(:branch, args: ['task?'], opts: TASK_OPTIONS) { puts current_branch(take_task_only(opts.args)) }
858
- add_cmd(:cd, args: ['task?', 'app?'], opts: TASK_OPTIONS) do
859
- task, rest = take_task(opts.args)
860
- cd_to(task, single(rest, optional: true))
861
- end
862
- add_cmd(:sh, args: ['task?', 'command...'], passthrough: true) do
863
- task, rest = take_task(args, options: nil)
864
- run_shell(task, rest)
865
- end
866
-
867
858
  add_option :directory, desc: 'Existing start directory'
868
- add_option :app, desc: 'App directory to open in the workspace'
869
- add_option :sparse, short: :s, desc: 'Sparse checkout group (repeatable)', multi: true
870
- add_cmd(:start, args: %i[name app?], opts: [*TASK_OPTIONS, :app, :sparse]) do
871
- if opts.task
872
- new_tree([lookup_task!(opts.task).name, *opts.args], opts.app, opts.sparse)
873
- elsif opts.find
874
- new_tree([pick_task(Hiiro::TaskRecord.all_as_list).name, *opts.args], opts.app, opts.sparse)
875
- else
876
- new_tree(opts.args, opts.app, opts.sparse)
877
- end
878
- end
879
- add_cmd(:switch, :workspace, args: ['task-or-workspace?', 'app?'], opts: [*TASK_OPTIONS, :directory, :show]) do
880
- target, explicit, app_name = switch_target(opts.args)
859
+ add_option :app, desc: 'Registry app directory under the task worktree'
860
+ add_cmd(:switch, :workspace, args: ['task-or-workspace?', 'app?'], opts: [*TASK_OPTIONS, :directory, :app, :show]) do
861
+ target, explicit, rest = switch_target(opts.args)
862
+ app = opts.app || rest.first
881
863
  if target.is_a?(Hiiro::Herdr::Pane)
882
864
  raise Error, '--show applies to task workspaces' if opts.show
865
+ raise Error, 'An APP only applies to task workspaces' if app
883
866
  focus_pane_target(target)
884
867
  elsif target.is_a?(Hiiro::Herdr::Workspace)
885
868
  raise Error, '--show applies to task workspaces' if opts.show
869
+ raise Error, 'An APP only applies to task workspaces' if app
886
870
  check_result(client.focus_workspace(target.id))
887
871
  puts target
888
872
  else
889
- opts.show ? show_workspace(target) : open_workspace(target, save: explicit, app_name: app_name)
873
+ opts.show ? show_workspace(target) : open_workspace(target, save: explicit, app: app)
890
874
  end
891
875
  end
876
+ add_cmd(:path, args: ['task?'], opts: TASK_OPTIONS) { puts start_directory(take_task_only(opts.args)) }
877
+ add_cmd(:branch, args: ['task?'], opts: TASK_OPTIONS) { puts current_branch(take_task_only(opts.args)) }
878
+ add_cmd(:cd, args: ['task?', 'app?'], opts: TASK_OPTIONS) do
879
+ task, rest = take_task(opts.args)
880
+ no_args!(rest.drop(1))
881
+ cd_to(task, app: rest.first)
882
+ end
883
+
884
+ add_cmd(:sh, args: ['task?', 'command...'], passthrough: true) do
885
+ task, rest = take_task(args, options: nil)
886
+ run_shell(task, rest)
887
+ end
892
888
 
893
889
  [%w[omp], %w[codex cdx], %w[claude cld]].each do |names|
894
890
  add_cmd(*names, args: ['task?', 'cli-args...'], passthrough: true) do
data/lib/hiiro/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  class Hiiro
2
- VERSION = "0.1.378"
2
+ VERSION = "0.1.379"
3
3
  SUPPORTED_RUBY_VERSION = ">= 3.2.0"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.378
4
+ version: 0.1.379
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
@@ -219,6 +219,7 @@ files:
219
219
  - bin/h-wtree
220
220
  - bin/h-xxserve
221
221
  - bin/h-zosubtask
222
+ - bin/hh
222
223
  - bin/t
223
224
  - bin/tt
224
225
  - demo.sh