ace-git-worktree 0.26.1 → 0.27.1

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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/nav/protocols/skill-sources/ace-git-worktree.yml +0 -0
  3. data/.ace-defaults/nav/protocols/wfi-sources/ace-git-worktree.yml +0 -0
  4. data/CHANGELOG.md +62 -0
  5. data/LICENSE +0 -0
  6. data/README.md +0 -0
  7. data/Rakefile +0 -0
  8. data/docs/demo/ace-git-worktree-getting-started.gif +0 -0
  9. data/docs/demo/ace-git-worktree-getting-started.tape.yml +0 -0
  10. data/docs/demo/fixtures/README.md +0 -0
  11. data/docs/demo/fixtures/sample.txt +0 -0
  12. data/docs/getting-started.md +7 -4
  13. data/docs/handbook.md +0 -0
  14. data/docs/usage.md +153 -24
  15. data/handbook/agents/worktree.ag.md +10 -6
  16. data/handbook/skills/as-git-worktree/SKILL.md +0 -0
  17. data/handbook/skills/as-git-worktree-cleanup/SKILL.md +0 -0
  18. data/handbook/skills/as-git-worktree-create/SKILL.md +0 -0
  19. data/handbook/skills/as-git-worktree-manage/SKILL.md +0 -0
  20. data/handbook/workflow-instructions/git/worktree-create.wf.md +3 -3
  21. data/handbook/workflow-instructions/git/worktree-manage.wf.md +6 -6
  22. data/handbook/workflow-instructions/git/worktree.wf.md +2 -2
  23. data/lib/ace/git/worktree/atoms/duration_parser.rb +81 -0
  24. data/lib/ace/git/worktree/atoms/git_command.rb +0 -0
  25. data/lib/ace/git/worktree/atoms/path_expander.rb +0 -0
  26. data/lib/ace/git/worktree/atoms/relative_age_formatter.rb +59 -0
  27. data/lib/ace/git/worktree/atoms/slug_generator.rb +0 -0
  28. data/lib/ace/git/worktree/atoms/task_id_extractor.rb +20 -1
  29. data/lib/ace/git/worktree/atoms/text_truncator.rb +67 -0
  30. data/lib/ace/git/worktree/cli/commands/bootstrap.rb +0 -0
  31. data/lib/ace/git/worktree/cli/commands/cleanup.rb +0 -0
  32. data/lib/ace/git/worktree/cli/commands/config.rb +0 -0
  33. data/lib/ace/git/worktree/cli/commands/create.rb +0 -0
  34. data/lib/ace/git/worktree/cli/commands/list.rb +29 -8
  35. data/lib/ace/git/worktree/cli/commands/prune.rb +0 -0
  36. data/lib/ace/git/worktree/cli/commands/remove.rb +0 -0
  37. data/lib/ace/git/worktree/cli/commands/shared_helpers.rb +0 -0
  38. data/lib/ace/git/worktree/cli/commands/switch.rb +0 -0
  39. data/lib/ace/git/worktree/cli.rb +1 -1
  40. data/lib/ace/git/worktree/commands/bootstrap_command.rb +0 -0
  41. data/lib/ace/git/worktree/commands/list_command.rb +90 -94
  42. data/lib/ace/git/worktree/commands/prune_command.rb +0 -0
  43. data/lib/ace/git/worktree/commands/remove_command.rb +0 -0
  44. data/lib/ace/git/worktree/commands/switch_command.rb +0 -0
  45. data/lib/ace/git/worktree/configuration.rb +0 -0
  46. data/lib/ace/git/worktree/models/task_index.rb +186 -0
  47. data/lib/ace/git/worktree/models/worktree_info.rb +52 -3
  48. data/lib/ace/git/worktree/models/worktree_metadata.rb +0 -0
  49. data/lib/ace/git/worktree/molecules/bootstrap_executor.rb +0 -0
  50. data/lib/ace/git/worktree/molecules/cleanup_applier.rb +0 -0
  51. data/lib/ace/git/worktree/molecules/config_loader.rb +0 -0
  52. data/lib/ace/git/worktree/molecules/current_task_linker.rb +0 -0
  53. data/lib/ace/git/worktree/molecules/hook_executor.rb +0 -0
  54. data/lib/ace/git/worktree/molecules/parent_task_resolver.rb +0 -0
  55. data/lib/ace/git/worktree/molecules/pr_status_resolver.rb +172 -0
  56. data/lib/ace/git/worktree/molecules/recent_branch_counter.rb +70 -0
  57. data/lib/ace/git/worktree/molecules/task_association_resolver.rb +121 -0
  58. data/lib/ace/git/worktree/molecules/task_fetcher.rb +0 -0
  59. data/lib/ace/git/worktree/molecules/task_index_builder.rb +121 -0
  60. data/lib/ace/git/worktree/molecules/task_pusher.rb +0 -0
  61. data/lib/ace/git/worktree/molecules/task_status_updater.rb +0 -0
  62. data/lib/ace/git/worktree/molecules/toolchain_truster.rb +0 -0
  63. data/lib/ace/git/worktree/molecules/worktree_footer_formatter.rb +43 -0
  64. data/lib/ace/git/worktree/molecules/worktree_lister.rb +22 -93
  65. data/lib/ace/git/worktree/molecules/worktree_probe.rb +196 -0
  66. data/lib/ace/git/worktree/molecules/worktree_remover.rb +0 -0
  67. data/lib/ace/git/worktree/molecules/worktree_table_formatter.rb +241 -0
  68. data/lib/ace/git/worktree/organisms/worktree_list_report.rb +315 -0
  69. data/lib/ace/git/worktree/organisms/worktree_manager.rb +22 -20
  70. data/lib/ace/git/worktree/organisms/worktree_probe_runner.rb +320 -0
  71. data/lib/ace/git/worktree/version.rb +1 -1
  72. data/lib/ace/git/worktree.rb +14 -0
  73. metadata +15 -2
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Git
5
+ module Worktree
6
+ module Atoms
7
+ # Formats a commit age as one compact unit for the `Age` table column.
8
+ #
9
+ # Ladder (floor of the current unit, no `ago` suffix):
10
+ # 0-59m -> "Nm" 1-23h -> "Nh"
11
+ # 1-13d -> "Nd" 14-59d -> "Nw"
12
+ # 60-364d -> "Nmo" >=365d -> "Ny"
13
+ #
14
+ # `nil` means the branch has no commits ("never"); clock skew/future
15
+ # timestamps render "0m" so the row still reads as active.
16
+ #
17
+ # @example
18
+ # RelativeAgeFormatter.format(9 * 24 * 3600) # => "9d"
19
+ # RelativeAgeFormatter.format(nil) # => "never"
20
+ module RelativeAgeFormatter
21
+ MINUTE = 60
22
+ HOUR = 60 * MINUTE
23
+ DAY = 24 * HOUR
24
+ WEEK = 7 * DAY
25
+
26
+ # Unit thresholds in seconds, largest unit last.
27
+ # The day->week switch is at 14 days and week->month at 60 days so
28
+ # short-lived worktrees keep a day count (e.g. "9d") while older
29
+ # ones step up to weeks ("3w") and then months ("2mo").
30
+ WEEK_THRESHOLD = 14 * DAY
31
+ MONTH_THRESHOLD = 60 * DAY
32
+ MONTH = 30 * DAY
33
+ YEAR_THRESHOLD = 365 * DAY
34
+
35
+ class << self
36
+ # @param age_seconds [Numeric, nil] age in seconds (nil = no commits)
37
+ # @return [String] compact age label
38
+ def format(age_seconds)
39
+ return "never" if age_seconds.nil?
40
+
41
+ seconds = age_seconds.to_f
42
+ return "0m" if seconds.negative?
43
+
44
+ case seconds
45
+ when 0...MINUTE then "0m"
46
+ when MINUTE...HOUR then "#{(seconds / MINUTE).floor}m"
47
+ when HOUR...DAY then "#{(seconds / HOUR).floor}h"
48
+ when DAY...WEEK_THRESHOLD then "#{(seconds / DAY).floor}d"
49
+ when WEEK_THRESHOLD...MONTH_THRESHOLD then "#{(seconds / WEEK).floor}w"
50
+ when MONTH_THRESHOLD...YEAR_THRESHOLD then "#{(seconds / MONTH).floor}mo"
51
+ else "#{(seconds / YEAR_THRESHOLD).floor}y"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
File without changes
@@ -53,6 +53,22 @@ module Ace
53
53
  task_data[:task_number]&.to_s || "unknown"
54
54
  end
55
55
 
56
+ # Derive the short form of a full Base36 task id.
57
+ #
58
+ # The short form is the three-character suffix plus an optional
59
+ # subtask segment (numeric or single character).
60
+ #
61
+ # @param full_id [String, nil] formatted task id (e.g. "8xg.t.k9y",
62
+ # "8wr.t.qk0.3")
63
+ # @return [String, nil] short id (e.g. "k9y", "qk0.3") or nil when the
64
+ # argument is not a full task id
65
+ def self.short_id(full_id)
66
+ match = full_id.to_s.strip.match(/\A[0-9a-z]{3}\.[a-z]\.([0-9a-z]{3})(?:\.([0-9a-z]+))?\z/)
67
+ return nil unless match
68
+
69
+ match[2] ? "#{match[1]}.#{match[2]}" : match[1]
70
+ end
71
+
56
72
  # Normalize a task reference string to simple ID format
57
73
  #
58
74
  # @param task_ref [String] Task reference (e.g., "121.01", "v.0.9.0+task.121.01")
@@ -63,7 +79,7 @@ module Ace
63
79
 
64
80
  # Regex patterns for recognized ACE task ID formats
65
81
  # B36TS full ID: "8pp.t.hy4" -> "hy4", or subtask "8pp.t.hy4.a" -> "hy4.a"
66
- if match = ref.match(/\A[0-9a-z]{3}\.[a-z]\.([0-9a-z]{3}(?:\.[a-z0-9])?)\z/)
82
+ if match = ref.match(/\A[0-9a-z]{3}\.[a-z]\.([0-9a-z]{3}(?:\.[0-9a-z]+)?)\z/)
67
83
  match[1]
68
84
  # B36TS short ref: exactly 3 lowercase alphanumeric chars (e.g., "hy4")
69
85
  elsif match = ref.match(/\A([0-9a-z]{3})\z/)
@@ -72,6 +88,9 @@ module Ace
72
88
  # Try hierarchical pattern first (e.g., "121.01", "task.121.01")
73
89
  elsif match = ref.match(/(\d{3})\.(\d{2})(?:\b|$)/)
74
90
  "#{match[1]}.#{match[2]}"
91
+ # B36TS short ref with subtask segment (e.g., "qk0.3", "hy4.a")
92
+ elsif match = ref.match(/\A([0-9a-z]{3})\.([0-9a-z]+)\z/)
93
+ "#{match[1]}.#{match[2]}"
75
94
  # Try ace-task/ace-t or task/t directory at end of path (e.g., "ace-task.hy4", "/path/to/task.121")
76
95
  elsif match = ref.match(/(?:ace-)?(?:task|t)\.([0-9a-z]{3})\/?$/)
77
96
  match[1]
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Git
5
+ module Worktree
6
+ module Atoms
7
+ # Truncates single-column text using the single-character ellipsis `…`.
8
+ #
9
+ # Each helper returns a string no wider than `width` columns (the
10
+ # ellipsis counts as one column). Text that already fits is returned
11
+ # unchanged. Widths below 1 render an empty string; width 1 renders
12
+ # only the ellipsis.
13
+ #
14
+ # @example Branches keep both ends
15
+ # TextTruncator.middle("codex/ei9-campaign-composition-join", 25)
16
+ # # => "codex/ei9-ca…osition-join"
17
+ module TextTruncator
18
+ ELLIPSIS = "…"
19
+
20
+ class << self
21
+ # Keep the prefix and suffix; elide the middle.
22
+ # @param text [String, nil]
23
+ # @param width [Integer] target column width
24
+ # @return [String]
25
+ def middle(text, width)
26
+ value = text.to_s
27
+ return "" if width <= 0
28
+ return value if value.length <= width
29
+ return ELLIPSIS if width == 1
30
+
31
+ remaining = width - 1
32
+ prefix_length = (remaining + 1) / 2
33
+ suffix_length = remaining - prefix_length
34
+ "#{value[0, prefix_length]}#{ELLIPSIS}#{value[-suffix_length, suffix_length]}"
35
+ end
36
+
37
+ # Keep the prefix; truncate the tail.
38
+ # @param text [String, nil]
39
+ # @param width [Integer] target column width
40
+ # @return [String]
41
+ def right(text, width)
42
+ value = text.to_s
43
+ return "" if width <= 0
44
+ return value if value.length <= width
45
+ return ELLIPSIS if width == 1
46
+
47
+ "#{value[0, width - 1]}#{ELLIPSIS}"
48
+ end
49
+
50
+ # Keep the suffix; elide the head.
51
+ # @param text [String, nil]
52
+ # @param width [Integer] target column width
53
+ # @return [String]
54
+ def left(text, width)
55
+ value = text.to_s
56
+ return "" if width <= 0
57
+ return value if value.length <= width
58
+ return ELLIPSIS if width == 1
59
+
60
+ "#{ELLIPSIS}#{value[-(width - 1), width - 1]}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
File without changes
File without changes
File without changes
File without changes
@@ -12,28 +12,42 @@ module Ace
12
12
  class List < Ace::Support::Cli::Command
13
13
  include SharedHelpers
14
14
 
15
- desc "List all worktrees with optional filtering"
15
+ desc <<~DESC.chomp
16
+ List active worktrees with task, activity, dirty-state, and PR context
17
+ Columns: Task, Branch, Age, Commit, Changes, PR, Path. A worktree is
18
+ active when its last commit is inside --active-within (default 18h) or
19
+ it has uncommitted changes. Task ids are inferred from recorded task
20
+ metadata, the _current symlink, validated path/branch names, or
21
+ .ace-tasks files changed in the worktree; PRs match by branch, then
22
+ HEAD commit sha, then a unique task reference. Columns shrink to the
23
+ terminal width and Path drops on very narrow terminals. Probing runs
24
+ in isolated child processes with a 5 second per-worktree budget;
25
+ unreadable probes render "?".
26
+ DESC
16
27
 
17
28
  example [
18
- " # List all worktrees",
19
- "--show-tasks # Include task associations",
20
- "--format json # JSON output",
29
+ " # Worktrees active in the last 18h",
30
+ "--full # Every worktree with absolute paths",
31
+ "--active-within 3d # Wider activity window",
32
+ "--format json # Machine-readable output",
21
33
  "--search auth # Filter by branch pattern"
22
34
  ]
23
35
 
24
36
  option :format, desc: "Output format: table, json, simple", aliases: [], default: "table"
25
- option :show_tasks, desc: "Include task associations", type: :boolean
37
+ option :compact, desc: "Active worktrees only (default)", type: :boolean
38
+ option :full, desc: "List every worktree with absolute paths", type: :boolean
39
+ option :active_within, desc: "Activity window (Nm, Nh, Nd, or bare hours; default 18h)", aliases: []
26
40
  option :task_associated, desc: "Show only task-associated worktrees", type: :boolean
27
41
  option :usable, desc: "Show only usable worktrees", type: :boolean
28
42
  option :search, desc: "Filter by branch name pattern", aliases: []
29
- option :quiet, type: :boolean, aliases: ["-q"], desc: "Suppress non-essential output"
43
+ option :quiet, type: :boolean, aliases: ["-q"], desc: "Suppress the count footer"
30
44
  option :verbose, type: :boolean, aliases: ["-v"], desc: "Show verbose output"
31
45
  option :debug, type: :boolean, aliases: ["-d"], desc: "Show debug output"
32
46
 
33
47
  def call(**options)
34
48
  display_config_summary("list", options) unless options[:format] == "json"
35
49
 
36
- # Keep explicit false values as --no-* flags so legacy parser receives filters.
50
+ # Keep explicit false values as --no-* flags so the parser receives filters.
37
51
  args = list_options_to_args(options)
38
52
 
39
53
  Ace::Git::Worktree::Commands::ListCommand.new.run(args)
@@ -44,7 +58,14 @@ module Ace
44
58
  def list_options_to_args(options)
45
59
  args = []
46
60
  args.concat(format_arg(options))
47
- args << "--show-tasks" if options[:show_tasks]
61
+ args << "--compact" if options[:compact]
62
+ args << "--full" if options[:full]
63
+ args << "--quiet" if options[:quiet]
64
+
65
+ if options[:active_within].is_a?(String) && !options[:active_within].empty?
66
+ args << "--active-within"
67
+ args << options[:active_within]
68
+ end
48
69
 
49
70
  unless options[:task_associated].nil?
50
71
  args << (options[:task_associated] ? "--task-associated" : "--no-task-associated")
File without changes
File without changes
File without changes
File without changes
@@ -38,7 +38,7 @@ module Ace
38
38
 
39
39
  HELP_EXAMPLES = [
40
40
  "ace-git-worktree create --task 148 # Isolated worktree for task",
41
- "ace-git-worktree list --show-tasks # Worktrees with task context",
41
+ "ace-git-worktree list --full # Every worktree with task context",
42
42
  "ace-git-worktree switch 148 # Get path for cd",
43
43
  "ace-git-worktree prune --dry-run # Preview stale cleanup"
44
44
  ].freeze
File without changes
@@ -6,21 +6,29 @@ module Ace
6
6
  module Commands
7
7
  # List command
8
8
  #
9
- # Lists worktrees with various formatting and filtering options.
10
- # Supports task-aware listing and search capabilities.
9
+ # Prints the active-worktree table: one enriched row per worktree whose
10
+ # last activity falls inside the activity window (committed or dirty),
11
+ # plus a two-line count footer. `--full` lists every worktree,
12
+ # `--format json` is the machine surface, `--format simple` is the
13
+ # branch-at-path listing.
11
14
  #
12
- # @example List all worktrees
15
+ # @example Daily driver (last 18h)
13
16
  # ListCommand.new.run([])
14
17
  #
15
- # @example List with JSON output
16
- # ListCommand.new.run(["--format", "json"])
18
+ # @example Wider window in JSON
19
+ # ListCommand.new.run(["--active-within", "3d", "--format", "json"])
17
20
  #
18
- # @example List only task-associated worktrees
19
- # ListCommand.new.run(["--task-associated"])
21
+ # @example Exhaustive listing
22
+ # ListCommand.new.run(["--full"])
20
23
  class ListCommand
24
+ # Supported output formats
25
+ FORMATS = %w[table json simple].freeze
26
+
21
27
  # Initialize a new ListCommand
22
- def initialize
23
- @manager = Organisms::WorktreeManager.new
28
+ #
29
+ # @param reporter [Organisms::WorktreeListReport, nil] injectable report
30
+ def initialize(reporter: nil)
31
+ @reporter = reporter
24
32
  end
25
33
 
26
34
  # Run the list command
@@ -33,13 +41,10 @@ module Ace
33
41
 
34
42
  validate_options(options)
35
43
 
36
- # Convert format to symbol for WorktreeLister compatibility
37
- options[:format] = options[:format].to_sym if options[:format]
38
-
39
- result = @manager.list_all(options)
44
+ result = reporter.run(options)
40
45
 
41
46
  if result[:success]
42
- display_list_result(result, options)
47
+ puts result[:output] unless result[:output].to_s.empty?
43
48
  0
44
49
  else
45
50
  puts "Failed to list worktrees: #{result[:error]}"
@@ -47,8 +52,6 @@ module Ace
47
52
  end
48
53
  rescue ArgumentError => e
49
54
  puts "Error: #{e.message}"
50
- puts
51
- show_help
52
55
  1
53
56
  rescue => e
54
57
  puts "Error: #{e.message}"
@@ -63,49 +66,61 @@ module Ace
63
66
  ace-git-worktree list - List worktrees
64
67
 
65
68
  USAGE:
66
- ace-git-worktree list [OPTIONS]
69
+ ace-git-worktree list [--compact | --full] [--active-within DURATION] [OPTIONS]
70
+
71
+ VIEW:
72
+ --compact Active worktrees only (default)
73
+ --full List every worktree with absolute paths
74
+ --active-within <dur> Activity window: Nm, Nh, Nd, or bare hours (default: 18h)
67
75
 
68
- OUTPUT FORMATS:
76
+ OUTPUT:
69
77
  --format <format> Output format: table, json, simple (default: table)
70
- --show-tasks Include task associations
78
+ -q, --quiet Suppress the count footer
71
79
 
72
80
  FILTERING:
73
- --task-associated Show only task-associated worktrees
74
- --no-task-associated Show only non-task worktrees
75
- --usable Show only usable worktrees
76
- --no-usable Show only unusable worktrees
81
+ --task-associated Show only worktrees with a resolved task id
82
+ --no-task-associated Show only worktrees without a task id
83
+ --usable Show only usable worktrees
84
+ --no-usable Show only unusable worktrees
77
85
  --search <pattern> Filter by branch name pattern
78
86
 
79
87
  EXAMPLES:
80
- # List all worktrees in table format
88
+ # Worktrees active in the last 18 hours
81
89
  ace-git-worktree list
82
90
 
83
- # List with task associations in JSON format
84
- ace-git-worktree list --show-tasks --format json
85
-
86
- # List only task-associated worktrees
87
- ace-git-worktree list --task-associated
88
-
89
- # Search for worktrees with "auth" in branch name
90
- ace-git-worktree list --search auth
91
-
92
- # List only usable worktrees
93
- ace-git-worktree list --usable
94
-
95
- OUTPUT:
96
- Table format columns:
97
- - Task: Task ID (or - for non-task worktrees)
98
- - Branch: Git branch name
99
- - Path: Worktree directory path
100
- - Status: worktree status (task, normal, bare, detached, etc.)
101
-
102
- JSON format includes full worktree details and metadata.
91
+ # Everything, including inactive worktrees
92
+ ace-git-worktree list --full
93
+
94
+ # After a weekend
95
+ ace-git-worktree list --active-within 3d
96
+
97
+ # Machine-readable output
98
+ ace-git-worktree list --format json
99
+
100
+ COLUMNS:
101
+ Task Resolved task id (inferred from metadata, the _current symlink,
102
+ validated names, or task files changed in the worktree)
103
+ Branch Checked-out branch (middle-elided when truncated)
104
+ Age Age of the last commit, one compact unit (8m, 3h, 9d, 3w, 2mo, 1y)
105
+ Commit Last commit subject (right-truncated)
106
+ Changes Uncommitted file count (staged + unstaged + untracked)
107
+ PR #N, #N draft, #N merged, #N closed, or - (matched by branch,
108
+ HEAD commit, or a unique task reference)
109
+ Path Worktree path (~ shortened; --full prints it absolute)
110
+
111
+ Columns shrink to fit the terminal width; Path is dropped on very
112
+ narrow terminals. Probing runs in isolated child processes with a
113
+ 5 second per-worktree budget; unreadable probes render "?".
103
114
  HELP
104
115
  0
105
116
  end
106
117
 
107
118
  private
108
119
 
120
+ def reporter
121
+ @reporter || Organisms::WorktreeListReport.new(env: ENV)
122
+ end
123
+
109
124
  # Parse command line arguments
110
125
  #
111
126
  # @param args [Array<String>] Command arguments
@@ -113,10 +128,15 @@ module Ace
113
128
  def parse_arguments(args)
114
129
  options = {
115
130
  format: "table",
116
- show_tasks: false,
131
+ compact: false,
132
+ full: false,
133
+ active_within: nil,
117
134
  task_associated: nil,
118
135
  usable: nil,
119
136
  search: nil,
137
+ quiet: false,
138
+ verbose: false,
139
+ debug: false,
120
140
  help: false
121
141
  }
122
142
 
@@ -128,13 +148,17 @@ module Ace
128
148
  when "--format"
129
149
  i += 1
130
150
  format = args[i]&.downcase
131
- if %w[table json simple].include?(format)
132
- options[:format] = format
133
- else
151
+ unless FORMATS.include?(format)
134
152
  raise ArgumentError, "Invalid format: #{format}. Use: table, json, simple"
135
153
  end
136
- when "--show-tasks"
137
- options[:show_tasks] = true
154
+ options[:format] = format
155
+ when "--compact"
156
+ options[:compact] = true
157
+ when "--full"
158
+ options[:full] = true
159
+ when "--active-within"
160
+ i += 1
161
+ options[:active_within] = args[i]
138
162
  when "--task-associated"
139
163
  options[:task_associated] = true
140
164
  when "--no-task-associated"
@@ -146,6 +170,12 @@ module Ace
146
170
  when "--search"
147
171
  i += 1
148
172
  options[:search] = args[i]
173
+ when "--quiet", "-q"
174
+ options[:quiet] = true
175
+ when "--verbose", "-v"
176
+ options[:verbose] = true
177
+ when "--debug", "-d"
178
+ options[:debug] = true
149
179
  when "--help", "-h"
150
180
  options[:help] = true
151
181
  when /^--/
@@ -164,14 +194,23 @@ module Ace
164
194
  #
165
195
  # @param options [Hash] Parsed options
166
196
  def validate_options(options)
197
+ if options[:compact] && options[:full]
198
+ raise ArgumentError, "--compact and --full are mutually exclusive"
199
+ end
200
+
167
201
  if options[:search] && options[:search].empty?
168
202
  raise ArgumentError, "Search pattern cannot be empty"
169
203
  end
170
204
 
171
- if options[:format] && !%w[table json simple].include?(options[:format])
205
+ if options[:format] && !FORMATS.include?(options[:format])
172
206
  raise ArgumentError, "Invalid format: #{options[:format]}"
173
207
  end
174
208
 
209
+ # Parse eagerly so invalid windows fail before any output.
210
+ if options[:active_within]
211
+ Atoms::DurationParser.parse(options[:active_within])
212
+ end
213
+
175
214
  # Security validation for search patterns
176
215
  if options[:search] && contains_dangerous_patterns?(options[:search])
177
216
  raise ArgumentError, "Search pattern contains potentially dangerous characters"
@@ -197,49 +236,6 @@ module Ace
197
236
 
198
237
  dangerous_patterns.any? { |pattern| value.match?(pattern) }
199
238
  end
200
-
201
- # Display list result
202
- #
203
- # @param result [Hash] List result
204
- # @param options [Hash] Command options
205
- def display_list_result(result, options)
206
- if result[:worktrees].empty?
207
- puts "No worktrees found."
208
- return
209
- end
210
-
211
- # Display the formatted output
212
- puts result[:formatted_output]
213
-
214
- # Display summary if requested or if not JSON format
215
- if options[:format] != :json
216
- display_summary(result, options)
217
- end
218
- end
219
-
220
- # Display summary information
221
- #
222
- # @param result [Hash] List result
223
- # @param options [Hash] Command options
224
- def display_summary(result, options)
225
- stats = result[:statistics]
226
- puts "\nSummary:"
227
- puts " Total worktrees: #{stats[:total]}"
228
- puts " Task-associated: #{stats[:task_associated]}"
229
- puts " Usable: #{stats[:usable]}"
230
-
231
- if options[:show_tasks] && stats[:task_ids].any?
232
- puts " Tasks with worktrees: #{stats[:task_ids].join(", ")}"
233
- end
234
-
235
- if stats[:branches].any?
236
- puts " Branches: #{stats[:branches].join(", ")}"
237
- end
238
-
239
- # Show active worktrees count
240
- active_count = result[:worktrees].count { |wt| wt.exists? && wt.usable? }
241
- puts " Active worktrees: #{active_count}"
242
- end
243
239
  end
244
240
  end
245
241
  end
File without changes
File without changes
File without changes
File without changes