ace-git-worktree 0.19.0

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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/git/worktree.yml +250 -0
  3. data/.ace-defaults/nav/protocols/wfi-sources/ace-git-worktree.yml +19 -0
  4. data/CHANGELOG.md +957 -0
  5. data/LICENSE +21 -0
  6. data/README.md +40 -0
  7. data/Rakefile +14 -0
  8. data/docs/demo/ace-git-worktree-getting-started.gif +0 -0
  9. data/docs/demo/ace-git-worktree-getting-started.tape.yml +28 -0
  10. data/docs/demo/fixtures/README.md +3 -0
  11. data/docs/demo/fixtures/sample.txt +1 -0
  12. data/docs/getting-started.md +114 -0
  13. data/docs/handbook.md +38 -0
  14. data/docs/usage.md +334 -0
  15. data/exe/ace-git-worktree +24 -0
  16. data/handbook/agents/worktree.ag.md +189 -0
  17. data/handbook/skills/as-git-worktree/SKILL.md +27 -0
  18. data/handbook/skills/as-git-worktree-create/SKILL.md +21 -0
  19. data/handbook/skills/as-git-worktree-manage/SKILL.md +20 -0
  20. data/handbook/workflow-instructions/git/worktree-create.wf.md +262 -0
  21. data/handbook/workflow-instructions/git/worktree-manage.wf.md +384 -0
  22. data/handbook/workflow-instructions/git/worktree.wf.md +224 -0
  23. data/lib/ace/git/worktree/atoms/git_command.rb +121 -0
  24. data/lib/ace/git/worktree/atoms/path_expander.rb +189 -0
  25. data/lib/ace/git/worktree/atoms/slug_generator.rb +235 -0
  26. data/lib/ace/git/worktree/atoms/task_id_extractor.rb +91 -0
  27. data/lib/ace/git/worktree/cli/commands/config.rb +50 -0
  28. data/lib/ace/git/worktree/cli/commands/create.rb +80 -0
  29. data/lib/ace/git/worktree/cli/commands/list.rb +76 -0
  30. data/lib/ace/git/worktree/cli/commands/prune.rb +43 -0
  31. data/lib/ace/git/worktree/cli/commands/remove.rb +48 -0
  32. data/lib/ace/git/worktree/cli/commands/shared_helpers.rb +66 -0
  33. data/lib/ace/git/worktree/cli/commands/switch.rb +44 -0
  34. data/lib/ace/git/worktree/cli.rb +103 -0
  35. data/lib/ace/git/worktree/commands/config_command.rb +351 -0
  36. data/lib/ace/git/worktree/commands/create_command.rb +961 -0
  37. data/lib/ace/git/worktree/commands/list_command.rb +247 -0
  38. data/lib/ace/git/worktree/commands/prune_command.rb +260 -0
  39. data/lib/ace/git/worktree/commands/remove_command.rb +522 -0
  40. data/lib/ace/git/worktree/commands/switch_command.rb +249 -0
  41. data/lib/ace/git/worktree/configuration.rb +167 -0
  42. data/lib/ace/git/worktree/models/worktree_config.rb +502 -0
  43. data/lib/ace/git/worktree/models/worktree_info.rb +303 -0
  44. data/lib/ace/git/worktree/models/worktree_metadata.rb +294 -0
  45. data/lib/ace/git/worktree/molecules/config_loader.rb +125 -0
  46. data/lib/ace/git/worktree/molecules/current_task_linker.rb +136 -0
  47. data/lib/ace/git/worktree/molecules/hook_executor.rb +361 -0
  48. data/lib/ace/git/worktree/molecules/parent_task_resolver.rb +186 -0
  49. data/lib/ace/git/worktree/molecules/pr_creator.rb +253 -0
  50. data/lib/ace/git/worktree/molecules/task_committer.rb +329 -0
  51. data/lib/ace/git/worktree/molecules/task_fetcher.rb +244 -0
  52. data/lib/ace/git/worktree/molecules/task_pusher.rb +183 -0
  53. data/lib/ace/git/worktree/molecules/task_status_updater.rb +447 -0
  54. data/lib/ace/git/worktree/molecules/worktree_creator.rb +832 -0
  55. data/lib/ace/git/worktree/molecules/worktree_lister.rb +337 -0
  56. data/lib/ace/git/worktree/molecules/worktree_remover.rb +416 -0
  57. data/lib/ace/git/worktree/organisms/task_worktree_orchestrator.rb +906 -0
  58. data/lib/ace/git/worktree/organisms/worktree_manager.rb +714 -0
  59. data/lib/ace/git/worktree/version.rb +9 -0
  60. data/lib/ace/git/worktree.rb +215 -0
  61. metadata +218 -0
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Ace
6
+ module Git
7
+ module Worktree
8
+ module Atoms
9
+ # Path expansion and validation for worktree operations
10
+ #
11
+ # Simplified implementation focused on worktree-specific needs without
12
+ # over-engineered security patterns.
13
+ #
14
+ # @example Expand a user path
15
+ # PathExpander.expand("~") # => "/home/user"
16
+ #
17
+ # @example Check if a directory is writable
18
+ # PathExpander.writable?("/tmp/worktree") # => true/false
19
+ class PathExpander
20
+ class << self
21
+ # Expand a path using standard Ruby path expansion
22
+ #
23
+ # @param path [String] Path to expand
24
+ # @param base [String, nil] Base directory for relative paths (default: current directory)
25
+ # @return [String] Expanded absolute path
26
+ def expand(path, base = nil)
27
+ return "" if path.nil? || path.empty?
28
+ base ? File.expand_path(path, base) : File.expand_path(path)
29
+ end
30
+
31
+ # Resolve a path relative to a base directory
32
+ #
33
+ # @param path [String] Path to resolve
34
+ # @param base [String] Base directory (default: current directory)
35
+ # @return [String] Resolved absolute path
36
+ def resolve(path, base = Dir.pwd)
37
+ expand_path = expand(path)
38
+ expanded_base = expand(base)
39
+
40
+ if File.absolute_path?(expand_path)
41
+ expand_path
42
+ else
43
+ File.expand_path(path, expanded_base)
44
+ end
45
+ end
46
+
47
+ # Check if a path is writable
48
+ #
49
+ # @param path [String] Path to check
50
+ # @param create_if_missing [Boolean] Create directory if it doesn't exist
51
+ # @return [Boolean] true if path is writable
52
+ def writable?(path, create_if_missing: false)
53
+ expanded_path = expand(path)
54
+
55
+ begin
56
+ # Check if path exists
57
+ unless File.exist?(expanded_path)
58
+ if create_if_missing
59
+ FileUtils.mkdir_p(expanded_path)
60
+ else
61
+ return false
62
+ end
63
+ end
64
+
65
+ # Test writability by trying to create a temp file
66
+ temp_file = File.join(expanded_path, ".ace_git_worktree_test_#{Time.now.to_i}_#{$$}")
67
+ File.write(temp_file, "test")
68
+ File.delete(temp_file)
69
+ true
70
+ rescue Errno::EACCES, Errno::EPERM
71
+ false
72
+ rescue
73
+ false
74
+ end
75
+ end
76
+
77
+ # Validate a path for worktree creation
78
+ #
79
+ # @param path [String] Path to validate
80
+ # @param git_root [String] Git repository root
81
+ # @return [Hash] Validation result with :valid, :error, :expanded_path keys
82
+ def validate_for_worktree(path, git_root = nil)
83
+ expanded_path = expand(path)
84
+
85
+ # Check if parent directory exists and is writable
86
+ parent_dir = File.dirname(expanded_path)
87
+ unless File.exist?(parent_dir)
88
+ return {
89
+ valid: false,
90
+ error: "Parent directory does not exist: #{parent_dir}",
91
+ expanded_path: expanded_path
92
+ }
93
+ end
94
+
95
+ unless writable?(parent_dir)
96
+ return {
97
+ valid: false,
98
+ error: "Parent directory is not writable: #{parent_dir}",
99
+ expanded_path: expanded_path
100
+ }
101
+ end
102
+
103
+ # Check if worktree is being created directly in git root (not allowed)
104
+ # Allow worktrees in subdirectories or outside git root entirely
105
+ if git_root
106
+ git_root_expanded = File.expand_path(git_root)
107
+ expanded_path_abs = File.expand_path(expanded_path)
108
+
109
+ # Only prevent creation directly at git root, not in subdirectories
110
+ if expanded_path_abs == git_root_expanded
111
+ return {
112
+ valid: false,
113
+ error: "Worktree cannot be created at git repository root. Use a subdirectory or different location.",
114
+ expanded_path: expanded_path
115
+ }
116
+ end
117
+ end
118
+
119
+ # Check if path already exists
120
+ if File.exist?(expanded_path)
121
+ if File.directory?(expanded_path) && !Dir.empty?(expanded_path)
122
+ return {
123
+ valid: false,
124
+ error: "Directory already exists and is not empty: #{expanded_path}",
125
+ expanded_path: expanded_path
126
+ }
127
+ elsif File.file?(expanded_path)
128
+ return {
129
+ valid: false,
130
+ error: "Path exists but is a file: #{expanded_path}",
131
+ expanded_path: expanded_path
132
+ }
133
+ end
134
+ end
135
+
136
+ {
137
+ valid: true,
138
+ error: nil,
139
+ expanded_path: expanded_path
140
+ }
141
+ end
142
+
143
+ # Get a relative path from git root
144
+ #
145
+ # @param path [String] Absolute path
146
+ # @param git_root [String] Git repository root
147
+ # @return [String] Relative path from git root
148
+ def relative_to_git_root(path, git_root)
149
+ expanded_path = expand(path)
150
+ expanded_root = expand(git_root)
151
+
152
+ # Use File.expand_path for consistent comparison
153
+ normalized_path = File.expand_path(expanded_path)
154
+ normalized_root = File.expand_path(expanded_root)
155
+
156
+ if normalized_path.start_with?(normalized_root + "/") || normalized_path == normalized_root
157
+ if normalized_path == normalized_root
158
+ "."
159
+ else
160
+ relative_path = normalized_path[normalized_root.length..-1]
161
+ relative_path.start_with?("/") ? relative_path[1..-1] : relative_path
162
+ end
163
+ else
164
+ expanded_path
165
+ end
166
+ end
167
+
168
+ # Simple path safety validation
169
+ # Only checks for obviously dangerous patterns
170
+ #
171
+ # @param path [String] Path to validate
172
+ # @return [Boolean] true if path appears safe
173
+ def safe_path?(path)
174
+ return false if path.nil? || path.empty?
175
+
176
+ path_str = path.to_s
177
+
178
+ # Check for null bytes and obviously dangerous patterns
179
+ return false if path_str.include?("\x00")
180
+ return false if path_str.include?("../../../")
181
+
182
+ true
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Git
5
+ module Worktree
6
+ module Atoms
7
+ # Slug generation atom
8
+ #
9
+ # Converts task titles and other text into URL-safe slugs suitable
10
+ # for git branch names and directory names.
11
+ #
12
+ # @example Generate a slug from a task title
13
+ # SlugGenerator.from_title("Fix authentication bug in login flow")
14
+ # # => "fix-authentication-bug-in-login-flow"
15
+ #
16
+ # @example Generate a slug with custom max length
17
+ # SlugGenerator.from_title("Very long task title...", max_length: 20)
18
+ # # => "very-long-task-title"
19
+ class SlugGenerator
20
+ # Default maximum length for slugs
21
+ DEFAULT_MAX_LENGTH = 50
22
+
23
+ # Minimum length to avoid empty or too-short slugs
24
+ MIN_LENGTH = 3
25
+
26
+ # Characters that are not allowed in git branch names
27
+ FORBIDDEN_CHARS = /[~\^*?\[\]:]/
28
+
29
+ # Characters to replace with hyphens
30
+ SEPARATORS = /[ ._\/\\()]+/
31
+
32
+ # Multiple consecutive non-word characters (but allow single hyphens)
33
+ MULTIPLE_SEPARATORS = /-{2,}|[^a-zA-Z0-9-]+/
34
+
35
+ # Leading/trailing separators and hyphens
36
+ TRIM_SEPARATORS = /^[-_]+|[-_]+$/
37
+
38
+ class << self
39
+ # Generate a slug from a task title
40
+ #
41
+ # @param title [String] Task title to convert
42
+ # @param max_length [Integer] Maximum length of the slug
43
+ # @param fallback [String] Fallback string if slug is too short/empty
44
+ # @return [String] URL-safe slug
45
+ #
46
+ # @example
47
+ # SlugGenerator.from_title("Fix authentication bug")
48
+ # # => "fix-authentication-bug"
49
+ #
50
+ # SlugGenerator.from_title("Add user:profile endpoint", max_length: 20)
51
+ # # => "add-user-profile-end"
52
+ def from_title(title, max_length: DEFAULT_MAX_LENGTH, fallback: "task")
53
+ return fallback if title.nil? || title.empty?
54
+
55
+ # Convert to string and strip whitespace
56
+ title_str = title.to_s.strip
57
+
58
+ # Remove forbidden characters that git doesn't allow in branch names
59
+ cleaned = title_str.gsub(FORBIDDEN_CHARS, "")
60
+
61
+ # Replace separators (spaces, dots, underscores, slashes) with hyphens
62
+ normalized = cleaned.gsub(SEPARATORS, "-")
63
+
64
+ # Remove any remaining non-alphanumeric characters except hyphens
65
+ sanitized = normalized.gsub(MULTIPLE_SEPARATORS, "-")
66
+
67
+ # Remove leading/trailing hyphens and underscores
68
+ trimmed = sanitized.gsub(TRIM_SEPARATORS, "")
69
+
70
+ # Convert to lowercase
71
+ slug = trimmed.downcase
72
+
73
+ # Handle empty or too-short result
74
+ if slug.length < MIN_LENGTH
75
+ slug = fallback
76
+ end
77
+
78
+ # Truncate to max length, avoiding cutting in the middle of a word if possible
79
+ if slug.length > max_length
80
+ slug = truncate_slug(slug, max_length)
81
+ end
82
+
83
+ # Final validation - ensure we still have a valid slug
84
+ if slug.empty? || slug.length < MIN_LENGTH
85
+ slug = fallback
86
+ end
87
+
88
+ slug
89
+ end
90
+
91
+ # Generate a slug for a task ID
92
+ #
93
+ # @param task_id [String] Task ID (e.g., "081", "task.081", "v.0.9.0+081")
94
+ # @return [String] Simple task ID slug
95
+ #
96
+ # @example
97
+ # SlugGenerator.from_task_id("081") # => "task-081"
98
+ # SlugGenerator.from_task_id("v.0.9.0+081") # => "task-081"
99
+ def from_task_id(task_id)
100
+ return "task" if task_id.nil? || task_id.empty?
101
+
102
+ # Extract the numeric part from various task ID formats
103
+ numeric_part = task_id.to_s.match(/(\d+)$/)
104
+ return "task-#{task_id}" unless numeric_part
105
+
106
+ "task-#{numeric_part[1]}"
107
+ end
108
+
109
+ # Generate a combined slug from task ID and title
110
+ #
111
+ # @param task_id [String] Task ID
112
+ # @param title [String] Task title
113
+ # @param max_length [Integer] Maximum length of the combined slug
114
+ # @return [String] Combined slug like "081-fix-authentication-bug"
115
+ #
116
+ # @example
117
+ # SlugGenerator.combined("081", "Fix authentication bug")
118
+ # # => "081-fix-authentication-bug"
119
+ def combined(task_id, title, max_length: DEFAULT_MAX_LENGTH)
120
+ task_slug = from_task_id(task_id).gsub(/^task-/, "")
121
+ title_slug = from_title(title, max_length: max_length - task_slug.length - 1)
122
+
123
+ "#{task_slug}-#{title_slug}"
124
+ end
125
+
126
+ # Sanitize an existing slug
127
+ #
128
+ # @param slug [String] Slug to sanitize
129
+ # @return [String] Sanitized slug
130
+ #
131
+ # @example
132
+ # SlugGenerator.sanitize("invalid@branch#name") # => "invalid-branch-name"
133
+ def sanitize(slug)
134
+ return "task" if slug.nil? || slug.empty?
135
+
136
+ slug_str = slug.to_s.strip
137
+
138
+ # Remove forbidden characters
139
+ cleaned = slug_str.gsub(FORBIDDEN_CHARS, "")
140
+
141
+ # Replace separators with hyphens
142
+ normalized = cleaned.gsub(SEPARATORS, "-")
143
+
144
+ # Remove multiple consecutive separators
145
+ sanitized = normalized.gsub(MULTIPLE_SEPARATORS, "-")
146
+
147
+ # Trim leading/trailing separators
148
+ trimmed = sanitized.gsub(TRIM_SEPARATORS, "")
149
+
150
+ # Convert to lowercase
151
+ slug = trimmed.downcase
152
+
153
+ # Fallback if result is empty
154
+ slug.empty? ? "task" : slug
155
+ end
156
+
157
+ # Convert a branch name to a safe directory name
158
+ #
159
+ # Sanitizes branch names for use as directory names by replacing
160
+ # characters that are not alphanumeric, hyphens, or underscores with hyphens.
161
+ # Also handles slash-separated branch names (e.g., "origin/feature/auth").
162
+ #
163
+ # @param branch_name [String] Branch name to convert
164
+ # @return [String] Directory-safe name
165
+ #
166
+ # @example Simple branch name
167
+ # SlugGenerator.to_directory_name("feature-branch")
168
+ # # => "feature-branch"
169
+ #
170
+ # @example Branch with slashes (remote or hierarchical)
171
+ # SlugGenerator.to_directory_name("origin/feature/auth")
172
+ # # => "origin-feature-auth"
173
+ #
174
+ # @example Branch with special characters
175
+ # SlugGenerator.to_directory_name("fix:bug#123")
176
+ # # => "fix-bug-123"
177
+ def to_directory_name(branch_name)
178
+ return "worktree" if branch_name.nil? || branch_name.empty?
179
+
180
+ # Replace slashes and any non-alphanumeric/hyphen/underscore with hyphens
181
+ branch_name.to_s.tr("/", "-").gsub(/[^a-zA-Z0-9\-_]/, "-")
182
+ end
183
+
184
+ # Check if a slug is valid for git branch names
185
+ #
186
+ # @param slug [String] Slug to validate
187
+ # @return [Boolean] true if slug is valid
188
+ #
189
+ # @example
190
+ # SlugGenerator.valid?("valid-branch-name") # => true
191
+ # SlugGenerator.valid?("invalid@branch") # => false
192
+ def valid?(slug)
193
+ return false if slug.nil? || slug.empty?
194
+ return false if slug.length > 255 # Git branch name limit
195
+ return false if slug.match?(FORBIDDEN_CHARS)
196
+ return false if slug.start_with?("-") || slug.end_with?("-")
197
+ return false if slug.include?(".") # Dots not allowed in git branch names
198
+ return false if slug.include?(" ") # Spaces not allowed
199
+
200
+ # Check for invalid git branch name patterns
201
+ return false if slug == "HEAD"
202
+ return false if slug.include?("..")
203
+ return false if slug.include?("@")
204
+
205
+ true
206
+ end
207
+
208
+ private
209
+
210
+ # Truncate slug intelligently to avoid cutting words
211
+ #
212
+ # @param slug [String] Slug to truncate
213
+ # @param max_length [Integer] Maximum length
214
+ # @return [String] Truncated slug
215
+ def truncate_slug(slug, max_length)
216
+ return slug if slug.length <= max_length
217
+
218
+ # Try to truncate at a hyphen to avoid cutting words
219
+ truncated = slug[0, max_length]
220
+
221
+ # Find the last hyphen before the cutoff
222
+ last_hyphen = truncated.rindex("-")
223
+ if last_hyphen && last_hyphen > max_length * 0.7 # Don't cut too much
224
+ truncated = slug[0, last_hyphen]
225
+ end
226
+
227
+ # Remove trailing hyphen
228
+ truncated.gsub(/-$/, "")
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Git
5
+ module Worktree
6
+ module Atoms
7
+ # Extracts task IDs from task data, preserving hierarchical subtask notation
8
+ #
9
+ # This atom provides a single source of truth for task ID extraction across
10
+ # ace-git-worktree, ensuring consistent handling of hierarchical task IDs
11
+ # (e.g., "121.01" for subtasks).
12
+ #
13
+ # @example Extract from task data hash
14
+ # TaskIDExtractor.extract({id: "v.0.9.0+task.121.01"}) # => "121.01"
15
+ # TaskIDExtractor.extract({id: "v.0.9.0+task.121"}) # => "121"
16
+ # TaskIDExtractor.extract({id: "v.0.9.0+ace-t.121.01"}) # => "121.01"
17
+ # TaskIDExtractor.extract({id: "v.0.9.0+t.121"}) # => "121"
18
+ #
19
+ # @example Normalize a task reference string
20
+ # TaskIDExtractor.normalize("v.0.9.0+task.121.01") # => "121.01"
21
+ # TaskIDExtractor.normalize("task.121") # => "121"
22
+ # TaskIDExtractor.normalize("ace-t.121") # => "121"
23
+ # TaskIDExtractor.normalize("121.01") # => "121.01"
24
+ class TaskIDExtractor
25
+ # Extract task ID from task data hash
26
+ #
27
+ # @param task_data [Hash] Task data with :id and/or :task_number
28
+ # @return [String] Task ID (e.g., "121" or "121.01")
29
+ def self.extract(task_data)
30
+ return "unknown" unless task_data
31
+
32
+ # Regex with subtask support
33
+ if task_data[:id]
34
+ # B36TS full ID: "8pp.t.hy4" -> "hy4", or subtask "8pp.t.hy4.a" -> "hy4.a"
35
+ if match = task_data[:id].match(/\A[0-9a-z]{3}\.[a-z]\.([0-9a-z]{3}(?:\.[a-z0-9])?)\z/)
36
+ return match[1]
37
+ end
38
+ # Try subtask pattern first (e.g., "v.0.9.0+task.121.01" -> "121.01")
39
+ if match = task_data[:id].match(/(?:ace-)?(?:task|t)\.(\d+)\.(\d{2})$/)
40
+ return "#{match[1]}.#{match[2]}"
41
+ end
42
+ # Then simple pattern (e.g., "v.0.9.0+task.094" -> "094")
43
+ if match = task_data[:id].match(/(?:ace-)?(?:task|t)\.(\d+)$/)
44
+ return match[1]
45
+ end
46
+ # Fallback for partial patterns (e.g., "task.121.1" -> "121", ignoring invalid suffix)
47
+ if match = task_data[:id].match(/(?:ace-)?(?:task|t)\.(\d+)/)
48
+ return match[1]
49
+ end
50
+ end
51
+
52
+ # Last resort: use task_number (doesn't include subtask suffix)
53
+ task_data[:task_number]&.to_s || "unknown"
54
+ end
55
+
56
+ # Normalize a task reference string to simple ID format
57
+ #
58
+ # @param task_ref [String] Task reference (e.g., "121.01", "v.0.9.0+task.121.01")
59
+ # @return [String, nil] Normalized ID or nil if invalid
60
+ def self.normalize(task_ref)
61
+ ref = task_ref.to_s.strip
62
+ return nil if ref.empty?
63
+
64
+ # Regex patterns for recognized ACE task ID formats
65
+ # 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/)
67
+ match[1]
68
+ # B36TS short ref: exactly 3 lowercase alphanumeric chars (e.g., "hy4")
69
+ elsif match = ref.match(/\A([0-9a-z]{3})\z/)
70
+ match[1]
71
+ # ACE task IDs are 3-digit zero-padded (081, 121, etc.)
72
+ # Try hierarchical pattern first (e.g., "121.01", "task.121.01")
73
+ elsif match = ref.match(/(\d{3})\.(\d{2})(?:\b|$)/)
74
+ "#{match[1]}.#{match[2]}"
75
+ # Try ace-task/ace-t or task/t directory at end of path (e.g., "ace-task.hy4", "/path/to/task.121")
76
+ elsif match = ref.match(/(?:ace-)?(?:task|t)\.([0-9a-z]{3})\/?$/)
77
+ match[1]
78
+ # Try task./t. prefix in middle of path (e.g., ".cache/test/task.121")
79
+ # Use negative lookbehind to skip ace-task.XXX / ace-t.XXX parent directories
80
+ elsif match = ref.match(/(?<!ace-)(?:task|t)\.(\d{3})(?:\b|$)/)
81
+ match[1]
82
+ # Try bare 3-digit task ID (e.g., "121", "081")
83
+ elsif match = ref.match(/\b(\d{3})\b/)
84
+ match[1]
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "shared_helpers"
5
+ require_relative "../../commands/config_command"
6
+
7
+ module Ace
8
+ module Git
9
+ module Worktree
10
+ module CLI
11
+ module Commands
12
+ class Config < Ace::Support::Cli::Command
13
+ include SharedHelpers
14
+
15
+ desc "Show and manage worktree configuration"
16
+
17
+ example [
18
+ " # Show current configuration",
19
+ "--show # Show current configuration",
20
+ "--validate # Validate configuration",
21
+ "--files # Show config file locations"
22
+ ]
23
+
24
+ # Accept extra positional arguments for backward compatibility
25
+ # (e.g., "show", "validate" as positional args instead of flags)
26
+ argument :subcommand, required: false, desc: "Subcommand (show, validate, files)"
27
+
28
+ option :show, desc: "Show current configuration", type: :boolean, aliases: []
29
+ option :validate, desc: "Validate configuration", type: :boolean, aliases: []
30
+ option :files, desc: "Show configuration file locations", type: :boolean, aliases: []
31
+ option :verbose, desc: "Show verbose output", type: :boolean, aliases: ["-v"]
32
+ option :quiet, type: :boolean, aliases: ["-q"], desc: "Suppress non-essential output"
33
+ option :debug, type: :boolean, aliases: ["-d"], desc: "Show debug output"
34
+
35
+ def call(subcommand: nil, **options)
36
+ display_config_summary("config", options)
37
+
38
+ # Convert ace-support-cli options to args array format
39
+ args = options_to_args(options)
40
+ # Add subcommand as positional argument if provided
41
+ args << subcommand if subcommand
42
+
43
+ Ace::Git::Worktree::Commands::ConfigCommand.new.run(args)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require_relative "shared_helpers"
5
+ require_relative "../../commands/create_command"
6
+
7
+ module Ace
8
+ module Git
9
+ module Worktree
10
+ module CLI
11
+ module Commands
12
+ class Create < Ace::Support::Cli::Command
13
+ include SharedHelpers
14
+
15
+ desc <<~DESC.strip
16
+ Create a new worktree
17
+
18
+ Supports task-aware, PR, and traditional worktree creation.
19
+
20
+ Task-Aware:
21
+ ace-git-worktree --task 081
22
+ ace-git-worktree --task 081 --dry-run
23
+
24
+ PR-Aware:
25
+ ace-git-worktree --pr 123
26
+
27
+ Traditional:
28
+ ace-git-worktree feature-branch
29
+ ace-git-worktree create --from origin/feature
30
+ DESC
31
+
32
+ example [
33
+ "--task 081 # Create worktree for task",
34
+ "--pr 123 # Create worktree for PR",
35
+ "--from origin/feature # Create from remote branch",
36
+ "feature/new-auth # Create with branch name"
37
+ ]
38
+
39
+ argument :branch, required: false, desc: "Branch name for traditional creation"
40
+
41
+ option :task, desc: "Task ID for task-aware worktree", aliases: []
42
+ option :pr, desc: "PR number for PR-aware worktree", aliases: ["--pull-request"]
43
+ option :from, desc: "Create from specific branch (local or remote)", aliases: ["-b"]
44
+ option :path, desc: "Custom worktree path", aliases: []
45
+ option :source, desc: "Git ref to use as start-point", aliases: []
46
+ option :dry_run, desc: "Show what would be created", type: :boolean, aliases: ["--dry-run"]
47
+ option :no_status_update, desc: "Skip marking task as in-progress", type: :boolean, aliases: ["--no-status-update"]
48
+ option :no_commit, desc: "Skip committing task changes", type: :boolean, aliases: ["--no-commit"]
49
+ option :no_push, desc: "Skip pushing task changes", type: :boolean, aliases: ["--no-push"]
50
+ option :no_upstream, desc: "Skip pushing with upstream tracking", type: :boolean, aliases: ["--no-upstream"]
51
+ option :no_pr, desc: "Skip creating draft PR", type: :boolean, aliases: ["--no-pr"]
52
+ option :push_remote, desc: "Remote to push to", aliases: []
53
+ option :no_auto_navigate, desc: "Stay in current directory", type: :boolean, aliases: ["--no-auto-navigate"]
54
+ option :commit_message, desc: "Custom commit message", aliases: []
55
+ option :target_branch, desc: "Override PR target branch (default: auto-detect from parent)", aliases: []
56
+ option :force, desc: "Create even if worktree exists", type: :boolean, aliases: []
57
+ option :quiet, type: :boolean, aliases: ["-q"], desc: "Suppress non-essential output"
58
+ option :verbose, type: :boolean, aliases: ["-v"], desc: "Show verbose output"
59
+ option :debug, type: :boolean, aliases: ["-d"], desc: "Show debug output"
60
+
61
+ def call(branch: nil, **options)
62
+ display_config_summary("create", options)
63
+
64
+ # Convert --from to --source for the underlying command
65
+ if options[:from]
66
+ options[:source] = options.delete(:from)
67
+ end
68
+
69
+ # Convert ace-support-cli options hash to args array format
70
+ args = options_to_args(options)
71
+ args << branch if branch
72
+
73
+ Ace::Git::Worktree::Commands::CreateCommand.new.run(args)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end