hiiro 0.1.367 → 0.1.368
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +36 -48
- data/bin/h-alias +28 -0
- data/bin/h-env +25 -0
- data/bin/h-herdr +156 -0
- data/docs/h-herdr.md +41 -0
- data/docs/h-task.md +13 -170
- data/docs/h.md +1 -0
- data/docs/t.md +91 -388
- data/docs/why-t.md +21 -23
- data/herdr-plugin/herdr-plugin.toml +47 -0
- data/lib/hiiro/current_task.rb +3 -3
- data/lib/hiiro/task_cli.rb +279 -162
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +0 -1
- metadata +6 -2
- data/lib/hiiro/task_scope.rb +0 -51
data/lib/hiiro/task_cli.rb
CHANGED
|
@@ -4,39 +4,28 @@ require 'uri'
|
|
|
4
4
|
require 'shellwords'
|
|
5
5
|
|
|
6
6
|
class Hiiro
|
|
7
|
-
# Task
|
|
7
|
+
# Task CLI behind the `t`, `tt`, and `h task` executables.
|
|
8
8
|
#
|
|
9
9
|
# Hiiro.run(*ARGV, external_commands: false, builtin_commands: false) { Hiiro::TaskCli.setup(self) }
|
|
10
10
|
#
|
|
11
|
-
#
|
|
11
|
+
# Grammar: `t COMMAND [TASK] [ARGS...]`. Commands that act on a task read it from
|
|
12
|
+
# the first positional argument when that word names a task (exact or unique
|
|
13
|
+
# prefix); otherwise the current task is used and the word stays in the payload.
|
|
14
|
+
# `-t TASK` forces a task, `-f` picks one with a fuzzy finder, `.` is the current
|
|
15
|
+
# task, and `-` selects orphan todos in todo commands.
|
|
12
16
|
class TaskCli
|
|
13
17
|
def self.setup(hiiro)
|
|
14
18
|
hiiro.extend(Commands)
|
|
15
|
-
hiiro.
|
|
16
|
-
if reference.nil? || %w[ls list].include?(reference)
|
|
17
|
-
hiiro.no_args!(command_args)
|
|
18
|
-
hiiro.list_tasks
|
|
19
|
-
elsif reference == 'help'
|
|
20
|
-
hiiro.no_args!(command_args)
|
|
21
|
-
puts "Usage: t TASK [COMMAND ...]\n\nBare t, t ls, or t list lists tasks with open todo counts; t TASK shows one."
|
|
22
|
-
puts "Use . for the current task, or - for unassigned todos."
|
|
23
|
-
puts "Todo shortcut: tt TASK [COMMAND ...]\n\nTask commands:"
|
|
24
|
-
hiiro.run_task_scope('TASK', ['help'])
|
|
25
|
-
else
|
|
26
|
-
hiiro.run_task_scope(reference, command_args)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
19
|
+
hiiro.root_commands
|
|
29
20
|
end
|
|
30
21
|
|
|
31
|
-
# `tt
|
|
22
|
+
# `tt ARGS...` is `t todo ARGS...`.
|
|
32
23
|
def self.setup_todo(hiiro)
|
|
33
24
|
hiiro.extend(Commands)
|
|
34
|
-
hiiro.add_default do
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
else
|
|
39
|
-
hiiro.run_task_scope(reference || '.', ['todo', *todo_args])
|
|
25
|
+
hiiro.add_default do |*todo_args|
|
|
26
|
+
hiiro.run_child(:todo, todo_args, external_commands: false, builtin_commands: false) do
|
|
27
|
+
extend Commands
|
|
28
|
+
todo_commands
|
|
40
29
|
end
|
|
41
30
|
end
|
|
42
31
|
end
|
|
@@ -44,8 +33,76 @@ class Hiiro
|
|
|
44
33
|
module Commands
|
|
45
34
|
class Error < Hiiro::Error; end
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
TASK_OPTIONS = %i[task find].freeze
|
|
37
|
+
|
|
38
|
+
def task_options
|
|
39
|
+
add_option :task, short: :t, desc: 'Task name (exact or unique prefix)'
|
|
40
|
+
add_flag :find, short: :f, desc: 'Choose the task with a fuzzy finder'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def task_names
|
|
44
|
+
Hiiro::TaskRecord.all_as_list.map(&:name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Exact name, else unique prefix; ambiguous prefixes fail; nil when nothing matches.
|
|
48
|
+
def lookup_task(reference)
|
|
49
|
+
result = Hiiro::Matcher.new(Hiiro::TaskRecord.all_as_list, :name).by_prefix(reference.to_s)
|
|
50
|
+
match = result.resolved
|
|
51
|
+
raise Error, "Ambiguous task #{reference}: #{result.matches.map { |m| m.item.name }.join(', ')}" if !match && result.ambiguous?
|
|
52
|
+
match&.item
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def lookup_task!(reference)
|
|
56
|
+
lookup_task(reference) || raise(Error, "Task not found: #{reference}; run t new #{reference}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def current_task
|
|
60
|
+
Hiiro::CurrentTask.new(herdr: -> { herdr_client }, pin: true).resolve!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Old h task rule: [task, remaining_args]. The first positional is the task
|
|
64
|
+
# when it names one; otherwise the current task is used and args are untouched.
|
|
65
|
+
# `.` is the current task. With orphan: true, `-` yields a nil task (orphan todos).
|
|
66
|
+
# Passthrough commands pass options: nil and get -t/-f parsed from the raw args.
|
|
67
|
+
def take_task(args, orphan: false, options: opts)
|
|
68
|
+
args = args.dup
|
|
69
|
+
forced = options&.fetch(:task)
|
|
70
|
+
find = options&.fetch(:find)
|
|
71
|
+
if options.nil?
|
|
72
|
+
if %w[-t --task].include?(args.first) && args.length > 1
|
|
73
|
+
args.shift
|
|
74
|
+
forced = args.shift
|
|
75
|
+
elsif args.first.to_s.start_with?('--task=')
|
|
76
|
+
forced = args.shift.delete_prefix('--task=')
|
|
77
|
+
elsif %w[-f --find].include?(args.first)
|
|
78
|
+
args.shift
|
|
79
|
+
find = true
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
return [lookup_task!(forced), args] if forced
|
|
83
|
+
if find
|
|
84
|
+
name = fuzzyfind(task_names)
|
|
85
|
+
raise Error, 'No task selected' if name.nil? || name.strip.empty?
|
|
86
|
+
return [lookup_task!(name.strip), args]
|
|
87
|
+
end
|
|
88
|
+
first = args.first
|
|
89
|
+
if first == '.'
|
|
90
|
+
args.shift
|
|
91
|
+
return [current_task, args]
|
|
92
|
+
elsif first == '-' && orphan
|
|
93
|
+
args.shift
|
|
94
|
+
return [nil, args]
|
|
95
|
+
elsif first && !first.start_with?('-') && (task = lookup_task(first))
|
|
96
|
+
args.shift
|
|
97
|
+
return [task, args]
|
|
98
|
+
end
|
|
99
|
+
[current_task, args]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def take_task_only(args, options: opts)
|
|
103
|
+
task, rest = take_task(args, options: options)
|
|
104
|
+
no_args!(rest)
|
|
105
|
+
task
|
|
49
106
|
end
|
|
50
107
|
|
|
51
108
|
def single(args, optional: false)
|
|
@@ -65,9 +122,6 @@ class Hiiro
|
|
|
65
122
|
name
|
|
66
123
|
end
|
|
67
124
|
|
|
68
|
-
def select_task
|
|
69
|
-
selected_task_scope.task!
|
|
70
|
-
end
|
|
71
125
|
|
|
72
126
|
def save_current(task)
|
|
73
127
|
pin = Hiiro::PinRecord.find_key('t', 'current_task') || Hiiro::PinRecord.new(command: 't', key: 'current_task')
|
|
@@ -120,7 +174,7 @@ class Hiiro
|
|
|
120
174
|
def list_tasks
|
|
121
175
|
tasks = Hiiro::TaskRecord.all_as_list
|
|
122
176
|
if tasks.empty?
|
|
123
|
-
puts 'No tasks. Create one with t
|
|
177
|
+
puts 'No tasks. Create one with t new NAME.'
|
|
124
178
|
return
|
|
125
179
|
end
|
|
126
180
|
counts = open_todo_counts
|
|
@@ -145,33 +199,36 @@ class Hiiro
|
|
|
145
199
|
.order(:id)
|
|
146
200
|
end
|
|
147
201
|
|
|
148
|
-
def add_todo(args)
|
|
202
|
+
def add_todo(task, args)
|
|
149
203
|
text = args.join(' ')
|
|
150
|
-
raise Error, 'Usage: t
|
|
151
|
-
|
|
152
|
-
task, item = Hiiro::DB.connection.transaction(mode: :immediate) do
|
|
153
|
-
selected = scope.task
|
|
154
|
-
selected ||= create(scope.reference) unless scope.orphan?
|
|
155
|
-
[selected, Hiiro::TodoItem.create(text: text, task_name: selected&.name)]
|
|
156
|
-
end
|
|
204
|
+
raise Error, 'Usage: t todo add [TASK] TEXT...; todo text cannot be blank' if text.strip.empty?
|
|
205
|
+
item = Hiiro::TodoItem.create(text: text, task_name: task&.name)
|
|
157
206
|
puts "Added todo #{item.id} to #{task&.name || '-'}: #{item.text}"
|
|
158
207
|
end
|
|
159
208
|
|
|
160
|
-
def remove_todo(args)
|
|
209
|
+
def remove_todo(task, args)
|
|
161
210
|
id = single(args)
|
|
162
|
-
raise Error, 'Todo ID must be an exact decimal ID from t
|
|
163
|
-
scope = selected_task_scope
|
|
164
|
-
task = scope.orphan? ? nil : scope.task!
|
|
211
|
+
raise Error, 'Todo ID must be an exact decimal ID from t todo' unless id.match?(/\A\d+\z/)
|
|
165
212
|
deleted = task_todos(task).where(id: id.to_i).delete
|
|
166
213
|
name = task&.name || '-'
|
|
167
214
|
raise Error, "Todo #{id} does not belong to task #{name}" unless deleted == 1
|
|
168
215
|
puts "Removed todo #{id} from #{name}"
|
|
169
216
|
end
|
|
170
217
|
|
|
171
|
-
def list_todos
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
218
|
+
def list_todos(task, plain: false)
|
|
219
|
+
task_todos(task).each { |item| puts plain ? item.text : "Todo #{item.id} [#{item.status}]: #{item.text}" }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def find_todo(task, args)
|
|
223
|
+
id = single(args)
|
|
224
|
+
raise Error, 'Todo ID must be an exact decimal ID from t todo' unless id.match?(/\A\d+\z/)
|
|
225
|
+
item = task_todos(task).where(id: id.to_i).first
|
|
226
|
+
raise Error, "Todo #{id} does not belong to task #{task&.name || '-'}" unless item
|
|
227
|
+
item
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def show_todo(task, args)
|
|
231
|
+
puts find_todo(task, args).text
|
|
175
232
|
end
|
|
176
233
|
|
|
177
234
|
def show(task)
|
|
@@ -186,8 +243,7 @@ class Hiiro
|
|
|
186
243
|
task_todos(task).each { |item| puts "Todo #{item.id} [#{item.status}]: #{item.text}" }
|
|
187
244
|
end
|
|
188
245
|
|
|
189
|
-
def metadata(field, args)
|
|
190
|
-
task = select_task
|
|
246
|
+
def metadata(task, field, args)
|
|
191
247
|
raise Error, 'Use text or --clear, not both' if opts.clear && !args.empty?
|
|
192
248
|
if args.empty? && !opts.clear
|
|
193
249
|
puts task[field] if task[field]
|
|
@@ -235,8 +291,7 @@ class Hiiro
|
|
|
235
291
|
kind
|
|
236
292
|
end
|
|
237
293
|
|
|
238
|
-
def add_resource(group, target)
|
|
239
|
-
task = select_task
|
|
294
|
+
def add_resource(task, group, target)
|
|
240
295
|
kind = group == 'link' ? link_kind : group
|
|
241
296
|
if %w[directory file].include?(group)
|
|
242
297
|
target = existing_path(target, directory: group == 'directory')
|
|
@@ -304,8 +359,7 @@ class Hiiro
|
|
|
304
359
|
found.first
|
|
305
360
|
end
|
|
306
361
|
|
|
307
|
-
def open_resource(group, reference)
|
|
308
|
-
task = select_task
|
|
362
|
+
def open_resource(task, group, reference)
|
|
309
363
|
rows = resources(group, task)
|
|
310
364
|
candidates = rows.map { |row| [row.target, [row.id.to_s, row.target, row.label].compact] }
|
|
311
365
|
if group == 'file'
|
|
@@ -320,9 +374,8 @@ class Hiiro
|
|
|
320
374
|
"task-#{task.id}-"
|
|
321
375
|
end
|
|
322
376
|
|
|
323
|
-
def new_doc(args)
|
|
377
|
+
def new_doc(task, args)
|
|
324
378
|
name = safe_name!(args.shift&.delete_suffix('.md'))
|
|
325
|
-
task = select_task
|
|
326
379
|
path = File.join(ensure_home(task), "#{doc_prefix(task)}#{name}.md")
|
|
327
380
|
title = args.empty? ? name.tr('_-', ' ') : args.join(' ')
|
|
328
381
|
File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0o644) { |file| file.write("# #{title}\n\n") }
|
|
@@ -333,8 +386,7 @@ class Hiiro
|
|
|
333
386
|
home_files(task).select { |path| File.extname(path).downcase == '.md' }
|
|
334
387
|
end
|
|
335
388
|
|
|
336
|
-
def open_doc(reference)
|
|
337
|
-
task = select_task
|
|
389
|
+
def open_doc(task, reference)
|
|
338
390
|
candidates = documents(task).map do |path|
|
|
339
391
|
short_name = File.basename(path, '.md').delete_prefix(doc_prefix(task))
|
|
340
392
|
[path, [short_name, path.delete_prefix(task.home + '/'), File.basename(path), File.basename(path, '.md'), "#{short_name}.md", path]]
|
|
@@ -367,7 +419,7 @@ class Hiiro
|
|
|
367
419
|
raise Error, "Workspace label #{label} is shared by tasks: #{collisions.map(&:name).join(', ')}" unless collisions.one?
|
|
368
420
|
matches = client.workspaces.select { |workspace| workspace.name == label }
|
|
369
421
|
raise Error, "Multiple Herdr workspaces have label #{label}" if matches.length > 1
|
|
370
|
-
raise Error, "Task workspace is not open; run t #{task.name}
|
|
422
|
+
raise Error, "Task workspace is not open; run t switch #{task.name}" if required && matches.empty?
|
|
371
423
|
matches.first
|
|
372
424
|
end
|
|
373
425
|
|
|
@@ -376,17 +428,16 @@ class Hiiro
|
|
|
376
428
|
existing_path(path, directory: true)
|
|
377
429
|
end
|
|
378
430
|
|
|
379
|
-
def open_workspace
|
|
380
|
-
task = select_task
|
|
431
|
+
def open_workspace(task, save:)
|
|
381
432
|
open_task_workspace(task, start_directory(task))
|
|
382
|
-
save_current(task) if
|
|
433
|
+
save_current(task) if save
|
|
383
434
|
end
|
|
384
435
|
|
|
385
436
|
# Focus the task workspace or create it at directory. With optional: true,
|
|
386
437
|
# a stopped Herdr prints a hint instead of failing (used after worktree changes).
|
|
387
438
|
def open_task_workspace(task, directory, optional: false)
|
|
388
439
|
if optional && !herdr_client.server_running?
|
|
389
|
-
puts "Herdr is not running; run t #{task.name}
|
|
440
|
+
puts "Herdr is not running; run t switch #{task.name} to open the workspace"
|
|
390
441
|
return
|
|
391
442
|
end
|
|
392
443
|
workspace = workspace_for(task, required: false)
|
|
@@ -411,14 +462,18 @@ class Hiiro
|
|
|
411
462
|
end
|
|
412
463
|
|
|
413
464
|
def show_tree(task)
|
|
414
|
-
raise Error, "No worktree for #{task.name}; run t #{task.name}
|
|
465
|
+
raise Error, "No worktree for #{task.name}; run t tree new #{task.name}" unless task.tree
|
|
415
466
|
puts "#{task.tree}\t#{tree_path(task)}"
|
|
416
467
|
end
|
|
417
468
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
task =
|
|
421
|
-
|
|
469
|
+
# tree new [NAME]: NAME may be an existing task, a new task to create, or absent (current task).
|
|
470
|
+
def new_tree(args, app_name, sparse_groups)
|
|
471
|
+
task = if args.empty? then current_task
|
|
472
|
+
elsif (found = lookup_task(args.first)) then found
|
|
473
|
+
else create(safe_name!(args.first))
|
|
474
|
+
end
|
|
475
|
+
no_args!(args.drop(1))
|
|
476
|
+
raise Error, "#{task.name} already has worktree #{task.tree}; run t tree rm #{task.name} first" if task.tree
|
|
422
477
|
subtree = task.name.include?('/') ? task.name : "#{task.name}/main"
|
|
423
478
|
path = tree_manager.create_tree(subtree, sparse_groups: Array(sparse_groups))
|
|
424
479
|
raise Error, "Could not create worktree #{subtree}" unless path
|
|
@@ -441,8 +496,7 @@ class Hiiro
|
|
|
441
496
|
puts "Detached worktree #{tree} from #{task.name}; the directory stays for reuse or resume"
|
|
442
497
|
end
|
|
443
498
|
|
|
444
|
-
def resume_tree(reference)
|
|
445
|
-
task = select_task
|
|
499
|
+
def resume_tree(task, reference)
|
|
446
500
|
raise Error, "#{task.name} already has worktree #{task.tree}" if task.tree
|
|
447
501
|
used = Hiiro::TaskRecord.exclude(tree: nil).select_map(:tree)
|
|
448
502
|
available = Hiiro::Tree.all.reject { |tree| used.include?(tree.name) || used.include?(tree.path) }
|
|
@@ -470,8 +524,8 @@ class Hiiro
|
|
|
470
524
|
check_result(client.run_in_pane(pane, "cd #{start_directory(task).shellescape}"))
|
|
471
525
|
end
|
|
472
526
|
|
|
473
|
-
def show_workspace
|
|
474
|
-
workspace = workspace_for(
|
|
527
|
+
def show_workspace(task)
|
|
528
|
+
workspace = workspace_for(task)
|
|
475
529
|
puts workspace
|
|
476
530
|
client.tabs(workspace: workspace).each { |tab| puts " #{tab}" }
|
|
477
531
|
client.panes(workspace: workspace).each { |pane| puts " #{pane}\t#{pane.cwd}" }
|
|
@@ -481,16 +535,14 @@ class Hiiro
|
|
|
481
535
|
choose(kind, items.map { |item| [item, [item.id, item.name].compact] }, reference, hint: 'use its live ID')
|
|
482
536
|
end
|
|
483
537
|
|
|
484
|
-
def new_tab(label)
|
|
485
|
-
task = select_task
|
|
538
|
+
def new_tab(task, label)
|
|
486
539
|
result = client.new_tab(name: label, workspace: workspace_for(task), start_directory: start_directory(task), command: opts.command, focus: true)
|
|
487
540
|
check_result(result['tab'], 'Herdr did not create the tab')
|
|
488
541
|
puts result['tab']['tab_id']
|
|
489
542
|
end
|
|
490
543
|
|
|
491
|
-
def pane_action(action, args)
|
|
544
|
+
def pane_action(task, action, args)
|
|
492
545
|
reference = args.shift
|
|
493
|
-
task = select_task
|
|
494
546
|
pane = live_item('pane', client.panes(workspace: workspace_for(task)), reference)
|
|
495
547
|
no_args!(args) unless action == 'run'
|
|
496
548
|
case action
|
|
@@ -506,6 +558,7 @@ class Hiiro
|
|
|
506
558
|
check_result(created, 'Herdr did not split the pane')
|
|
507
559
|
puts created
|
|
508
560
|
when 'run'
|
|
561
|
+
args.shift if args.first == '--'
|
|
509
562
|
raise Error, 'A command is required' if args.empty?
|
|
510
563
|
check_result(client.run_in_pane(pane.id, args.shelljoin))
|
|
511
564
|
end
|
|
@@ -515,8 +568,7 @@ class Hiiro
|
|
|
515
568
|
raise Error, message unless result
|
|
516
569
|
end
|
|
517
570
|
|
|
518
|
-
def run_ai(tool, argv)
|
|
519
|
-
task = select_task
|
|
571
|
+
def run_ai(task, tool, argv)
|
|
520
572
|
directory = start_directory(task)
|
|
521
573
|
workspace = workspace_for(task, required: false)
|
|
522
574
|
workspace ||= client.new_workspace(workspace_label(task), start_directory: directory, focus: true)
|
|
@@ -524,126 +576,128 @@ class Hiiro
|
|
|
524
576
|
puts Hiiro::TaskSessions.new(client, workspace: workspace, directory: directory).run(tool, argv)
|
|
525
577
|
end
|
|
526
578
|
|
|
527
|
-
def
|
|
528
|
-
|
|
529
|
-
run_child(reference, command_args, external_commands: false, builtin_commands: false) do
|
|
579
|
+
def child(name, child_args, &block)
|
|
580
|
+
run_child(name, child_args, external_commands: false, builtin_commands: false) do
|
|
530
581
|
extend Commands
|
|
531
|
-
|
|
582
|
+
instance_eval(&block)
|
|
532
583
|
end
|
|
533
584
|
end
|
|
534
585
|
|
|
535
|
-
def
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
show(select_task)
|
|
539
|
-
end
|
|
586
|
+
def root_commands
|
|
587
|
+
task_options
|
|
588
|
+
add_default { |*unexpected| no_args!(unexpected); list_tasks }
|
|
540
589
|
add_cmd(:help) { help }
|
|
541
|
-
add_cmd(:
|
|
542
|
-
add_cmd(:
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
590
|
+
add_cmd(:list, :ls) { no_args!(opts.args); list_tasks }
|
|
591
|
+
add_cmd(:show, args: ['task?'], opts: TASK_OPTIONS) { show(take_task_only(opts.args)) }
|
|
592
|
+
add_cmd(:current, args: ['task?'], opts: TASK_OPTIONS) { puts take_task_only(opts.args).name }
|
|
593
|
+
add_cmd(:use, :pin, args: %i[task], opts: TASK_OPTIONS) do
|
|
594
|
+
task = take_task_only(opts.args)
|
|
595
|
+
save_current(task)
|
|
546
596
|
puts task.name
|
|
547
597
|
end
|
|
548
|
-
add_cmd(:new) {
|
|
549
|
-
add_cmd(:next, args: ['text...'], opts:
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
598
|
+
add_cmd(:new, args: %i[name]) { create(single(opts.args)) }
|
|
599
|
+
add_cmd(:next, args: ['task?', 'text...'], opts: [*TASK_OPTIONS, :clear]) do
|
|
600
|
+
task, rest = take_task(opts.args)
|
|
601
|
+
metadata(task, :next_action, rest)
|
|
602
|
+
end
|
|
603
|
+
add_cmd(:waiting, args: ['task?', 'text...'], opts: [*TASK_OPTIONS, :clear]) do
|
|
604
|
+
task, rest = take_task(opts.args)
|
|
605
|
+
metadata(task, :waiting_on, rest)
|
|
606
|
+
end
|
|
607
|
+
add_cmd(:status, args: ['task?', 'state?'], opts: TASK_OPTIONS) do
|
|
608
|
+
task, rest = take_task(opts.args)
|
|
609
|
+
state = single(rest, optional: true)
|
|
610
|
+
state ? set_status(task, state) : puts(task.task_status)
|
|
554
611
|
end
|
|
555
612
|
%w[done archive].each do |name|
|
|
556
|
-
add_cmd(name) do
|
|
557
|
-
|
|
558
|
-
set_status(select_task, name == 'archive' ? 'archived' : 'done')
|
|
613
|
+
add_cmd(name, args: ['task?'], opts: TASK_OPTIONS) do
|
|
614
|
+
set_status(take_task_only(opts.args), name == 'archive' ? 'archived' : 'done')
|
|
559
615
|
end
|
|
560
616
|
end
|
|
561
617
|
|
|
562
|
-
add_cmd(:todo, args: ['command?'], passthrough: true)
|
|
563
|
-
run_child(:todo, args, external_commands: false, builtin_commands: false) do
|
|
564
|
-
extend Commands
|
|
565
|
-
add_default { |*unexpected| no_args!(unexpected); list_todos }
|
|
566
|
-
add_cmd(:help) { help }
|
|
567
|
-
add_cmd(:list, :ls) { no_args!(opts.args); list_todos }
|
|
568
|
-
add_cmd(:add, args: ['text...'], passthrough: true) do
|
|
569
|
-
if %w[-h --help].include?(args.first)
|
|
570
|
-
puts options.select([]).parse([]).help_text
|
|
571
|
-
else
|
|
572
|
-
add_todo(args)
|
|
573
|
-
end
|
|
574
|
-
end
|
|
575
|
-
add_cmd(:rm, args: %i[id]) { remove_todo(opts.args) }
|
|
576
|
-
end
|
|
577
|
-
end
|
|
618
|
+
add_cmd(:todo, args: ['command?'], passthrough: true) { child(:todo, args) { todo_commands } }
|
|
578
619
|
|
|
579
620
|
%w[directory link pr file].each do |group|
|
|
580
|
-
add_cmd(group, args: %i[command], passthrough: true)
|
|
581
|
-
run_child(group, args, external_commands: false, builtin_commands: false) do
|
|
582
|
-
extend Commands
|
|
583
|
-
add_option :label, desc: 'Resource label'
|
|
584
|
-
add_option :kind, desc: 'Link kind: general, issue, or thread' if group == 'link'
|
|
585
|
-
add_options = %i[label]
|
|
586
|
-
add_options << :primary if group == 'directory'
|
|
587
|
-
add_options << :kind if group == 'link'
|
|
588
|
-
read_options = group == 'link' ? %i[kind] : []
|
|
589
|
-
add_cmd(:help) { help }
|
|
590
|
-
add_cmd(:add, args: %i[target], opts: add_options) { add_resource(group, single(opts.args)) }
|
|
591
|
-
add_cmd(:list, :ls, opts: read_options) { no_args!(opts.args); list_resources(group, select_task) }
|
|
592
|
-
add_cmd(:open, args: ['reference?'], opts: read_options) { open_resource(group, single(opts.args, optional: true)) }
|
|
593
|
-
end
|
|
594
|
-
end
|
|
621
|
+
add_cmd(group, args: %i[command], passthrough: true) { child(group, args) { resource_commands(group) } }
|
|
595
622
|
end
|
|
596
623
|
|
|
597
624
|
add_cmd(:doc, args: %i[command], passthrough: true) do
|
|
598
|
-
|
|
599
|
-
|
|
625
|
+
child(:doc, args) do
|
|
626
|
+
task_options
|
|
600
627
|
add_cmd(:help) { help }
|
|
601
|
-
add_cmd(:new, args: ['name', 'title...']
|
|
602
|
-
|
|
603
|
-
|
|
628
|
+
add_cmd(:new, args: ['task?', 'name', 'title...'], opts: TASK_OPTIONS) do
|
|
629
|
+
task, rest = take_task(opts.args)
|
|
630
|
+
new_doc(task, rest)
|
|
631
|
+
end
|
|
632
|
+
add_cmd(:list, :ls, args: ['task?'], opts: TASK_OPTIONS) { documents(take_task_only(opts.args)).each { |path| puts path } }
|
|
633
|
+
add_cmd(:open, args: ['task?', 'name?'], opts: TASK_OPTIONS) do
|
|
634
|
+
task, rest = take_task(opts.args)
|
|
635
|
+
open_doc(task, single(rest, optional: true))
|
|
636
|
+
end
|
|
604
637
|
end
|
|
605
638
|
end
|
|
606
639
|
|
|
607
640
|
add_cmd(:tree, args: ['command?'], passthrough: true) do
|
|
608
|
-
|
|
609
|
-
|
|
641
|
+
child(:tree, args) do
|
|
642
|
+
task_options
|
|
610
643
|
add_option :app, desc: 'App directory to open in the workspace'
|
|
611
644
|
add_option :sparse, desc: 'Sparse checkout group (repeatable)', multi: true
|
|
612
|
-
add_default { |*
|
|
645
|
+
add_default { |*rest| show_tree(take_task_only(rest, options: nil)) }
|
|
613
646
|
add_cmd(:help) { help }
|
|
614
|
-
add_cmd(:new,
|
|
615
|
-
|
|
616
|
-
|
|
647
|
+
add_cmd(:new, args: ['name?'], opts: [*TASK_OPTIONS, :app, :sparse]) do
|
|
648
|
+
if opts.task || opts.find
|
|
649
|
+
new_tree([take_task_only(opts.args).name], opts.app, opts.sparse)
|
|
650
|
+
else
|
|
651
|
+
new_tree(opts.args, opts.app, opts.sparse)
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
add_cmd(:rm, :remove, args: ['task?'], opts: TASK_OPTIONS) { remove_tree(take_task_only(opts.args)) }
|
|
655
|
+
add_cmd(:resume, args: ['task?', 'worktree?'], opts: TASK_OPTIONS) do
|
|
656
|
+
task, rest = take_task(opts.args)
|
|
657
|
+
resume_tree(task, single(rest, optional: true))
|
|
658
|
+
end
|
|
617
659
|
end
|
|
618
660
|
end
|
|
619
661
|
|
|
620
|
-
add_cmd(:path
|
|
621
|
-
add_cmd(:branch
|
|
622
|
-
add_cmd(:cd) {
|
|
623
|
-
add_cmd(:sh, args: ['command...'], passthrough: true)
|
|
662
|
+
add_cmd(:path, args: ['task?'], opts: TASK_OPTIONS) { puts start_directory(take_task_only(opts.args)) }
|
|
663
|
+
add_cmd(:branch, args: ['task?'], opts: TASK_OPTIONS) { puts current_branch(take_task_only(opts.args)) }
|
|
664
|
+
add_cmd(:cd, args: ['task?'], opts: TASK_OPTIONS) { cd_to(take_task_only(opts.args)) }
|
|
665
|
+
add_cmd(:sh, args: ['task?', 'command...'], passthrough: true) do
|
|
666
|
+
task, rest = take_task(args, options: nil)
|
|
667
|
+
run_shell(task, rest)
|
|
668
|
+
end
|
|
624
669
|
|
|
625
670
|
add_option :directory, desc: 'Existing start directory'
|
|
626
|
-
add_cmd(:switch, :workspace, opts:
|
|
627
|
-
|
|
628
|
-
opts.
|
|
671
|
+
add_cmd(:switch, :workspace, args: ['task?'], opts: [*TASK_OPTIONS, :directory, :show]) do
|
|
672
|
+
explicit = opts.task || opts.find || (opts.args.first && opts.args.first != '.')
|
|
673
|
+
task = take_task_only(opts.args)
|
|
674
|
+
opts.show ? show_workspace(task) : open_workspace(task, save: explicit)
|
|
629
675
|
end
|
|
630
676
|
|
|
631
677
|
[%w[omp], %w[codex cdx], %w[claude cld]].each do |names|
|
|
632
|
-
add_cmd(*names, args: ['cli-args...'], passthrough: true)
|
|
678
|
+
add_cmd(*names, args: ['task?', 'cli-args...'], passthrough: true) do
|
|
679
|
+
task, rest = take_task(args, options: nil)
|
|
680
|
+
run_ai(task, names.first, rest)
|
|
681
|
+
end
|
|
633
682
|
end
|
|
634
683
|
|
|
635
684
|
add_cmd(:tab, args: %i[command], passthrough: true) do
|
|
636
|
-
|
|
637
|
-
|
|
685
|
+
child(:tab, args) do
|
|
686
|
+
task_options
|
|
638
687
|
add_option :directory, desc: 'Existing start directory'
|
|
639
688
|
add_option :command, desc: 'Command to run in the new tab'
|
|
640
689
|
add_cmd(:help) { help }
|
|
641
|
-
add_cmd(:list, :ls
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
690
|
+
add_cmd(:list, :ls, args: ['task?'], opts: TASK_OPTIONS) do
|
|
691
|
+
client.tabs(workspace: workspace_for(take_task_only(opts.args))).each { |tab| puts tab }
|
|
692
|
+
end
|
|
693
|
+
add_cmd(:new, args: ['task?', 'label?'], opts: [*TASK_OPTIONS, :directory, :command]) do
|
|
694
|
+
task, rest = take_task(opts.args)
|
|
695
|
+
new_tab(task, single(rest, optional: true))
|
|
696
|
+
end
|
|
697
|
+
add_cmd(:open, args: ['task?', 'reference?'], opts: TASK_OPTIONS) do
|
|
698
|
+
task, rest = take_task(opts.args)
|
|
699
|
+
workspace = workspace_for(task)
|
|
700
|
+
tab = live_item('tab', client.tabs(workspace: workspace), single(rest, optional: true))
|
|
647
701
|
check_result(client.focus_workspace(workspace.id))
|
|
648
702
|
check_result(client.focus_tab(tab.id))
|
|
649
703
|
end
|
|
@@ -651,20 +705,83 @@ class Hiiro
|
|
|
651
705
|
end
|
|
652
706
|
|
|
653
707
|
add_cmd(:pane, args: %i[command], passthrough: true) do
|
|
654
|
-
|
|
655
|
-
|
|
708
|
+
child(:pane, args) do
|
|
709
|
+
task_options
|
|
656
710
|
add_option :directory, desc: 'Existing start directory'
|
|
657
711
|
add_option :command, desc: 'Command to run in the new pane'
|
|
658
712
|
add_option :direction, default: 'right', desc: 'Split direction: right or down'
|
|
659
713
|
add_cmd(:help) { help }
|
|
660
|
-
add_cmd(:list, :ls
|
|
714
|
+
add_cmd(:list, :ls, args: ['task?'], opts: TASK_OPTIONS) do
|
|
715
|
+
client.panes(workspace: workspace_for(take_task_only(opts.args))).each { |pane| puts pane }
|
|
716
|
+
end
|
|
661
717
|
%w[open read].each do |action|
|
|
662
|
-
add_cmd(action, args: ['pane?']
|
|
718
|
+
add_cmd(action, args: ['task?', 'pane?'], opts: TASK_OPTIONS) do
|
|
719
|
+
task, rest = take_task(opts.args)
|
|
720
|
+
pane_action(task, action, rest)
|
|
721
|
+
end
|
|
722
|
+
end
|
|
723
|
+
add_cmd(:run, args: ['task?', 'pane', 'command...'], passthrough: true) do
|
|
724
|
+
task, rest = take_task(args, options: nil)
|
|
725
|
+
pane_action(task, 'run', rest)
|
|
663
726
|
end
|
|
664
|
-
add_cmd(:
|
|
665
|
-
|
|
727
|
+
add_cmd(:split, args: ['task?', 'pane'], opts: [*TASK_OPTIONS, :directory, :command, :direction]) do
|
|
728
|
+
task, rest = take_task(opts.args)
|
|
729
|
+
pane_action(task, 'split', rest)
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def todo_commands
|
|
736
|
+
task_options
|
|
737
|
+
add_option :plain, type: :flag, desc: 'Print only todo text, one per line'
|
|
738
|
+
add_default do |*rest|
|
|
739
|
+
task, rest = take_task(rest, orphan: true, options: nil)
|
|
740
|
+
no_args!(rest)
|
|
741
|
+
list_todos(task)
|
|
742
|
+
end
|
|
743
|
+
add_cmd(:help) { help }
|
|
744
|
+
add_cmd(:list, :ls, args: ['task?'], opts: [*TASK_OPTIONS, :plain]) do
|
|
745
|
+
task, rest = take_task(opts.args, orphan: true)
|
|
746
|
+
no_args!(rest)
|
|
747
|
+
list_todos(task, plain: opts.plain)
|
|
748
|
+
end
|
|
749
|
+
add_cmd(:show, args: ['task?', 'id'], opts: TASK_OPTIONS) do
|
|
750
|
+
task, rest = take_task(opts.args, orphan: true)
|
|
751
|
+
show_todo(task, rest)
|
|
752
|
+
end
|
|
753
|
+
add_cmd(:add, args: ['task?', 'text...'], passthrough: true) do
|
|
754
|
+
task, rest = %w[-h --help].include?(args.first) ? [nil, args] : take_task(args, orphan: true, options: nil)
|
|
755
|
+
if %w[-h --help].include?(rest.first)
|
|
756
|
+
puts options.select(TASK_OPTIONS).parse([]).help_text
|
|
757
|
+
else
|
|
758
|
+
add_todo(task, rest)
|
|
666
759
|
end
|
|
667
760
|
end
|
|
761
|
+
add_cmd(:rm, args: ['task?', 'id'], opts: TASK_OPTIONS) do
|
|
762
|
+
task, rest = take_task(opts.args, orphan: true)
|
|
763
|
+
remove_todo(task, rest)
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def resource_commands(group)
|
|
768
|
+
task_options
|
|
769
|
+
add_option :label, desc: 'Resource label'
|
|
770
|
+
add_option :kind, desc: 'Link kind: general, issue, or thread' if group == 'link'
|
|
771
|
+
add_options = [*TASK_OPTIONS, :label]
|
|
772
|
+
add_options << :primary if group == 'directory'
|
|
773
|
+
add_options << :kind if group == 'link'
|
|
774
|
+
read_options = group == 'link' ? [*TASK_OPTIONS, :kind] : TASK_OPTIONS
|
|
775
|
+
add_cmd(:help) { help }
|
|
776
|
+
add_cmd(:add, args: ['task?', 'target'], opts: add_options) do
|
|
777
|
+
task, rest = take_task(opts.args)
|
|
778
|
+
add_resource(task, group, single(rest))
|
|
779
|
+
end
|
|
780
|
+
add_cmd(:list, :ls, args: ['task?'], opts: read_options) { list_resources(group, take_task_only(opts.args)) }
|
|
781
|
+
add_cmd(:open, args: ['task?', 'reference?'], opts: read_options) do
|
|
782
|
+
task, rest = take_task(opts.args)
|
|
783
|
+
open_resource(task, group, single(rest, optional: true))
|
|
784
|
+
end
|
|
668
785
|
end
|
|
669
786
|
end
|
|
670
787
|
end
|