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,961 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Git
5
+ module Worktree
6
+ module Commands
7
+ # Create command
8
+ #
9
+ # Handles worktree creation with support for both task-aware and traditional
10
+ # worktree creation. Provides various options for customization.
11
+ #
12
+ # @example Task-aware worktree creation
13
+ # CreateCommand.new.run(["--task", "081"])
14
+ #
15
+ # @example Traditional worktree creation
16
+ # CreateCommand.new.run(["feature-branch"])
17
+ #
18
+ # @example Dry run
19
+ # CreateCommand.new.run(["--task", "081", "--dry-run"])
20
+ class CreateCommand
21
+ # Pattern for validating PR numbers (digits only)
22
+ PR_NUMBER_PATTERN = /^\d+$/
23
+
24
+ # Initialize a new CreateCommand
25
+ #
26
+ # @param manager [Object] Optional manager dependency for testing
27
+ def initialize(manager: nil)
28
+ @manager = manager || Ace::Git::Worktree::Organisms::WorktreeManager.new
29
+ end
30
+
31
+ # Run the create command
32
+ #
33
+ # @param args [Array<String>] Command arguments
34
+ # @return [Integer] Exit code (0 for success, 1 for error)
35
+ def run(args = [])
36
+ # Show help if no arguments provided
37
+ return show_help if args.empty?
38
+
39
+ options = parse_arguments(args)
40
+ return show_help if options[:help]
41
+
42
+ validate_options(options)
43
+
44
+ if options[:task]
45
+ create_task_worktree(options)
46
+ elsif options[:pr]
47
+ create_pr_worktree(options)
48
+ elsif options[:branch]
49
+ create_branch_worktree(options)
50
+ else
51
+ create_traditional_worktree(options)
52
+ end
53
+ rescue ArgumentError => e
54
+ puts "Error: #{e.message}"
55
+ puts
56
+ show_help
57
+ 1
58
+ rescue Ace::Git::PrNotFoundError,
59
+ Ace::Git::GhAuthenticationError,
60
+ Ace::Git::GhNotInstalledError,
61
+ Ace::Git::TimeoutError => e
62
+ puts "Error: #{e.message}"
63
+ 1
64
+ rescue Ace::Git::Error => e
65
+ # Catch other ace-git specific errors
66
+ puts "Error: #{e.message}"
67
+ puts "Debug: #{e.class}" if ENV["DEBUG"]
68
+ 1
69
+ end
70
+
71
+ # Show help for the create command
72
+ #
73
+ # @return [Integer] Exit code
74
+ def show_help
75
+ puts <<~HELP
76
+ ace-git-worktree create - Create a new worktree
77
+
78
+ USAGE:
79
+ ace-git-worktree create <branch-name> [OPTIONS]
80
+ ace-git-worktree create --task <task-id> [OPTIONS]
81
+ ace-git-worktree create --pr <pr-number> [OPTIONS]
82
+ ace-git-worktree create --branch <branch> [OPTIONS]
83
+
84
+ TASK-AWARE CREATION:
85
+ --task <task-id> Create worktree for a specific task
86
+ Task ID formats: 081, task.081, v.0.9.0+081
87
+
88
+ PR-AWARE CREATION:
89
+ --pr <number> Create worktree for a GitHub pull request
90
+ --pull-request <number> (alias for --pr)
91
+ Requires gh CLI to be installed and authenticated
92
+
93
+ BRANCH-AWARE CREATION:
94
+ -b <branch> Create worktree from a branch (local or remote)
95
+ --branch <branch> (alias for -b)
96
+ Remote branches: origin/feature, upstream/main
97
+ Local branches: feature-name
98
+
99
+ OPTIONS:
100
+ --path <path> Custom worktree path (default: from config)
101
+ --source <ref> Git ref to use as branch start-point (default: current branch)
102
+ Examples: main, origin/develop, HEAD~3, commit-sha
103
+ --dry-run Show what would be created without creating
104
+ --no-status-update Skip marking task as in-progress (task mode only)
105
+ --no-commit Skip committing task changes (task mode only)
106
+ --no-push Skip pushing task changes to remote (task mode only)
107
+ --no-upstream Skip pushing worktree branch with upstream tracking (task mode only)
108
+ --no-pr Skip creating draft PR (task mode only)
109
+ --push-remote <name> Remote to push to (default: origin) (task mode only)
110
+ --no-auto-navigate Stay in current directory (default: navigate to worktree)
111
+ --commit-message <msg> Custom commit message for task updates (task mode only)
112
+ --force Create even if worktree already exists
113
+ --help, -h Show this help message
114
+
115
+ EXAMPLES:
116
+ # Create task-aware worktree
117
+ ace-git-worktree create --task 081
118
+
119
+ # Create task worktree based on main instead of current branch
120
+ ace-git-worktree create --task 081 --source main
121
+
122
+ # Create PR worktree
123
+ ace-git-worktree create --pr 26
124
+
125
+ # Create worktree from remote branch
126
+ ace-git-worktree create -b origin/feature/auth
127
+
128
+ # Create worktree from local branch
129
+ ace-git-worktree create -b my-feature
130
+
131
+ # Create traditional worktree
132
+ ace-git-worktree create feature-branch
133
+
134
+ # Create traditional worktree based on specific commit
135
+ ace-git-worktree create feature-branch --source HEAD~3
136
+
137
+ # Custom path and dry run
138
+ ace-git-worktree create --pr 26 --path ~/worktrees --dry-run
139
+
140
+ CONFIGURATION:
141
+ Worktree creation is controlled by .ace/git/worktree.yml:
142
+ - root_path: Default worktree root directory
143
+ - task.auto_*: Automation settings for task workflows
144
+ - pr.directory_format: PR worktree naming format (default: ace-pr-{number})
145
+ - pr.branch_format: PR branch naming format (default: pr-{number}-{slug})
146
+ Variables: {number}, {slug}, {title_slug}, {base_branch}
147
+ - hooks.after_create: Commands to run after worktree creation
148
+
149
+ REQUIREMENTS:
150
+ PR-aware creation requires GitHub CLI (gh):
151
+ - Install: brew install gh
152
+ - Authenticate: gh auth login
153
+ HELP
154
+ 0
155
+ end
156
+
157
+ private
158
+
159
+ # Parse command line arguments
160
+ #
161
+ # @param args [Array<String>] Command arguments
162
+ # @return [Hash] Parsed options
163
+ def parse_arguments(args)
164
+ options = {
165
+ task: nil,
166
+ pr: nil,
167
+ branch: nil,
168
+ path: nil,
169
+ source: nil,
170
+ dry_run: false,
171
+ no_status_update: false,
172
+ no_commit: false,
173
+ no_push: false,
174
+ no_upstream: false,
175
+ no_pr: false,
176
+ push_remote: nil,
177
+ no_auto_navigate: false,
178
+ commit_message: nil,
179
+ target_branch: nil,
180
+ force: false,
181
+ help: false
182
+ }
183
+
184
+ i = 0
185
+ while i < args.length
186
+ arg = args[i]
187
+
188
+ case arg
189
+ when "--task"
190
+ i += 1
191
+ options[:task] = args[i]
192
+ when "--pr", "--pull-request"
193
+ i += 1
194
+ options[:pr] = args[i]
195
+ when "-b", "--branch"
196
+ i += 1
197
+ options[:branch] = args[i]
198
+ when "--path"
199
+ i += 1
200
+ options[:path] = args[i]
201
+ when "--source"
202
+ i += 1
203
+ options[:source] = args[i]
204
+ when "--dry-run"
205
+ options[:dry_run] = true
206
+ when "--no-status-update"
207
+ options[:no_status_update] = true
208
+ when "--no-commit"
209
+ options[:no_commit] = true
210
+ when "--no-push"
211
+ options[:no_push] = true
212
+ when "--no-upstream"
213
+ options[:no_upstream] = true
214
+ when "--no-pr"
215
+ options[:no_pr] = true
216
+ when "--push-remote"
217
+ i += 1
218
+ options[:push_remote] = args[i]
219
+ when "--no-auto-navigate"
220
+ options[:no_auto_navigate] = true
221
+ when "--commit-message"
222
+ i += 1
223
+ options[:commit_message] = args[i]
224
+ when "--target-branch"
225
+ i += 1
226
+ options[:target_branch] = args[i]
227
+ when "--force"
228
+ options[:force] = true
229
+ when "--no-mise-trust"
230
+ options[:no_mise_trust] = true
231
+ when "--help", "-h"
232
+ options[:help] = true
233
+ when /^--/
234
+ raise ArgumentError, "Unknown option: #{arg}"
235
+ else
236
+ # Positional argument - branch name for traditional creation
237
+ if options[:branch_name]
238
+ raise ArgumentError, "Multiple branch names specified: #{options[:branch_name]} and #{arg}"
239
+ end
240
+ options[:branch_name] = arg
241
+ end
242
+
243
+ i += 1
244
+ end
245
+
246
+ options
247
+ end
248
+
249
+ # Detect which creation mode(s) are specified
250
+ #
251
+ # @param options [Hash] Parsed options
252
+ # @return [Array<Symbol>] List of detected modes (:task, :pr, :branch, :traditional)
253
+ def detect_creation_modes(options)
254
+ modes = []
255
+ modes << :task if options[:task]
256
+ modes << :pr if options[:pr]
257
+ modes << :branch if options[:branch]
258
+ modes << :traditional if options[:branch_name]
259
+ modes
260
+ end
261
+
262
+ # Validate parsed options
263
+ #
264
+ # @param options [Hash] Parsed options
265
+ def validate_options(options)
266
+ # Detect creation modes
267
+ modes = detect_creation_modes(options)
268
+
269
+ # Check for conflicts
270
+ if modes.length > 1
271
+ raise ArgumentError, "Cannot use multiple creation modes: #{modes.join(", ")}. " \
272
+ "Use only one of --task, --pr, --branch, or <branch-name>"
273
+ end
274
+
275
+ # Require at least one mode
276
+ if modes.empty?
277
+ raise ArgumentError, "Must specify either --task <task-id>, --pr <number>, --branch <branch>, or <branch-name>"
278
+ end
279
+
280
+ # Validate each mode's input
281
+ if options[:task] && options[:task].empty?
282
+ raise ArgumentError, "Task ID cannot be empty"
283
+ end
284
+
285
+ # Security validation for task IDs
286
+ if options[:task] && contains_dangerous_patterns?(options[:task])
287
+ raise ArgumentError, "Task ID contains potentially dangerous characters"
288
+ end
289
+
290
+ if options[:pr] && options[:pr].empty?
291
+ raise ArgumentError, "PR number cannot be empty"
292
+ end
293
+
294
+ if options[:branch] && options[:branch].empty?
295
+ raise ArgumentError, "Branch name cannot be empty"
296
+ end
297
+
298
+ if options[:branch_name] && options[:branch_name].empty?
299
+ raise ArgumentError, "Branch name cannot be empty"
300
+ end
301
+
302
+ # Validate PR number is numeric
303
+ if options[:pr] && !options[:pr].match?(/^\d+$/)
304
+ raise ArgumentError, "PR number must be a positive integer"
305
+ end
306
+
307
+ if options[:commit_message] && options[:commit_message].empty?
308
+ raise ArgumentError, "Commit message cannot be empty"
309
+ end
310
+
311
+ # Security validation for paths
312
+ if options[:path] && contains_dangerous_patterns?(options[:path])
313
+ raise ArgumentError, "Path contains potentially dangerous characters"
314
+ end
315
+ end
316
+
317
+ # Check if a string contains dangerous patterns
318
+ #
319
+ # Matches TaskFetcher's validation to ensure consistent security boundaries.
320
+ # Rejects shell metacharacters, null bytes, newlines, redirects, and path traversal.
321
+ #
322
+ # @param value [String] Value to check
323
+ # @return [Boolean] true if dangerous patterns found
324
+ def contains_dangerous_patterns?(value)
325
+ return false if value.nil?
326
+
327
+ # Patterns from TaskFetcher.valid_task_reference? for consistency
328
+ dangerous_patterns = [
329
+ /[;&|`$(){}\[\]]/, # Shell metacharacters
330
+ /\x00/, # Null bytes
331
+ /[\r\n]/, # Newlines
332
+ /[<>]/, # Redirects
333
+ /\.\./ # Path traversal
334
+ ]
335
+
336
+ dangerous_patterns.any? { |pattern| value.match?(pattern) }
337
+ end
338
+
339
+ # Create a task-aware worktree
340
+ #
341
+ # @param options [Hash] Command options
342
+ # @return [Integer] Exit code
343
+ def create_task_worktree(options)
344
+ @options = options
345
+ puts "Creating worktree for task: #{options[:task]}"
346
+
347
+ # Check ace-task availability first
348
+ availability_check = check_task_dependency_availability
349
+ unless availability_check[:available]
350
+ puts "Error: ace-task is required for task-aware worktree creation."
351
+ puts
352
+ puts availability_check[:message]
353
+ puts
354
+ puts "Alternative: Use traditional worktree creation:"
355
+ puts " ace-git-worktree create <branch-name>"
356
+ puts
357
+ return 1
358
+ end
359
+
360
+ # Prepare creation options
361
+ creation_options = {
362
+ path: options[:path],
363
+ source: options[:source],
364
+ dry_run: options[:dry_run],
365
+ no_status_update: options[:no_status_update],
366
+ no_commit: options[:no_commit],
367
+ no_push: options[:no_push],
368
+ no_upstream: options[:no_upstream],
369
+ no_pr: options[:no_pr],
370
+ push_remote: options[:push_remote],
371
+ commit_message: options[:commit_message],
372
+ target_branch: options[:target_branch],
373
+ no_mise_trust: options[:no_mise_trust],
374
+ force: options[:force]
375
+ }.compact
376
+
377
+ # Create the worktree
378
+ result = @manager.create_task(options[:task], creation_options)
379
+
380
+ if result[:success]
381
+ display_task_creation_result(result, options[:dry_run])
382
+ 0
383
+ else
384
+ puts "Failed to create worktree: #{result[:error]}"
385
+ display_warnings(result[:warnings]) if result[:warnings]
386
+
387
+ # Provide helpful guidance based on error type
388
+ if result[:error]&.include?("ace-task")
389
+ puts
390
+ puts "ace-task issue detected. Check that:"
391
+ puts " 1. ace-task is installed: gem install ace-task"
392
+ puts " 2. ace-task is in your PATH: which ace-task"
393
+ puts " 3. Task '#{options[:task]}' exists"
394
+ elsif result[:error]&.include?("not found")
395
+ puts
396
+ puts "Task not found suggestions:"
397
+ puts " 1. Check task ID format (try: #{options[:task]})"
398
+ puts " 2. List available tasks: ace-task list"
399
+ puts " 3. Verify you're in the correct project directory"
400
+ end
401
+
402
+ 1
403
+ end
404
+ end
405
+
406
+ # Create a PR worktree
407
+ #
408
+ # @param options [Hash] Command options
409
+ # @return [Integer] Exit code
410
+ def create_pr_worktree(options)
411
+ @options = options
412
+ pr_input = options[:pr].to_s.strip
413
+
414
+ # Validate PR number is numeric to provide clear error message
415
+ unless pr_input.match?(PR_NUMBER_PATTERN)
416
+ puts "Error: Invalid PR number '#{pr_input}'. Please provide a numeric PR number."
417
+ return 1
418
+ end
419
+
420
+ pr_number = pr_input.to_i
421
+
422
+ # Validate PR number is within reasonable bounds
423
+ if pr_number <= 0 || pr_number > 999_999
424
+ puts "Error: PR number must be between 1 and 999999."
425
+ return 1
426
+ end
427
+
428
+ puts "Creating worktree for PR ##{pr_number}..."
429
+
430
+ # Check gh CLI availability using ace-git's PrMetadataFetcher
431
+ unless Ace::Git::Molecules::PrMetadataFetcher.gh_installed?
432
+ puts "Error: gh CLI is required for PR worktree creation."
433
+ puts
434
+ puts gh_not_available_message
435
+ return 1
436
+ end
437
+
438
+ unless Ace::Git::Molecules::PrMetadataFetcher.gh_authenticated?
439
+ puts "Error: gh CLI is not authenticated."
440
+ puts
441
+ puts "Authenticate with: gh auth login"
442
+ return 1
443
+ end
444
+
445
+ # Fetch PR data
446
+ puts "Fetching PR information..."
447
+ begin
448
+ result = Ace::Git::Molecules::PrMetadataFetcher.fetch_metadata(pr_number.to_s)
449
+
450
+ unless result[:success]
451
+ puts "Error: #{result[:error]}"
452
+ return 1
453
+ end
454
+
455
+ metadata = result[:metadata]
456
+
457
+ # Convert to pr_data format expected by worktree creator
458
+ pr_data = pr_data_from_metadata(metadata)
459
+
460
+ # Show PR details
461
+ puts "PR ##{pr_data[:number]}: #{pr_data[:title]}"
462
+ puts "Branch: #{pr_data[:head_branch]} -> #{pr_data[:base_branch]}"
463
+
464
+ # Warn about fork PRs
465
+ if pr_data[:is_cross_repository]
466
+ puts
467
+ puts "Warning: This PR is from a fork (#{pr_data[:head_repository_owner]})."
468
+ puts "You will not be able to push to the PR branch directly."
469
+ end
470
+ puts
471
+
472
+ # Prepare creation options
473
+ creation_options = {
474
+ path: options[:path],
475
+ dry_run: options[:dry_run],
476
+ no_mise_trust: options[:no_mise_trust],
477
+ force: options[:force]
478
+ }.compact
479
+
480
+ # Create the worktree
481
+ result = @manager.create_pr(pr_number, pr_data, creation_options)
482
+
483
+ if result[:success]
484
+ display_pr_creation_result(result, options[:dry_run])
485
+ 0
486
+ else
487
+ puts "Failed to create worktree: #{result[:error]}"
488
+ display_warnings(result[:warnings]) if result[:warnings]
489
+ 1
490
+ end
491
+ rescue Ace::Git::PrNotFoundError, Ace::Git::GhAuthenticationError,
492
+ Ace::Git::GhNotInstalledError => e
493
+ # Let specific ace-git errors be handled by handle_pr_fetch_error
494
+ # Other errors will bubble up to the top-level rescue in run()
495
+ handle_pr_fetch_error(e, pr_number)
496
+ end
497
+ end
498
+
499
+ # Create a branch worktree
500
+ #
501
+ # @param options [Hash] Command options
502
+ # @return [Integer] Exit code
503
+ def create_branch_worktree(options)
504
+ @options = options
505
+ branch_name = options[:branch]
506
+ puts "Creating worktree for branch: #{branch_name}..."
507
+
508
+ # Detect if remote branch
509
+ require_relative "../molecules/worktree_creator"
510
+ creator = Molecules::WorktreeCreator.new
511
+ remote_info = creator.send(:detect_remote_branch, branch_name)
512
+
513
+ if remote_info
514
+ puts "Detected remote branch: #{remote_info[:remote]}/#{remote_info[:branch]}"
515
+ puts "Will create with tracking..."
516
+ else
517
+ puts "Creating worktree from local branch..."
518
+ end
519
+ puts
520
+
521
+ # Prepare creation options
522
+ creation_options = {
523
+ path: options[:path],
524
+ dry_run: options[:dry_run],
525
+ no_mise_trust: options[:no_mise_trust],
526
+ force: options[:force]
527
+ }.compact
528
+
529
+ # Create the worktree
530
+ result = @manager.create_branch(branch_name, creation_options)
531
+
532
+ if result[:success]
533
+ display_branch_creation_result(result, options[:dry_run])
534
+ 0
535
+ else
536
+ puts "Failed to create worktree: #{result[:error]}"
537
+ display_warnings(result[:warnings]) if result[:warnings]
538
+
539
+ # Provide helpful guidance
540
+ if result[:error]&.include?("not found")
541
+ puts
542
+ puts "Branch not found suggestions:"
543
+ if remote_info
544
+ puts " 1. Fetch the remote: git fetch #{remote_info[:remote]}"
545
+ puts " 2. List remote branches: git branch -r"
546
+ puts " 3. Verify branch name: git ls-remote #{remote_info[:remote]}"
547
+ else
548
+ puts " 1. List local branches: git branch"
549
+ puts " 2. Create the branch: git branch #{branch_name}"
550
+ puts " 3. Or use a remote branch: -b origin/#{branch_name}"
551
+ end
552
+ end
553
+
554
+ 1
555
+ end
556
+ end
557
+
558
+ # Check if task dependencies are available with helpful messages
559
+ #
560
+ # @return [Hash] { available: boolean, message: string }
561
+ def check_task_dependency_availability
562
+ require_relative "../molecules/task_fetcher"
563
+ fetcher = Ace::Git::Worktree::Molecules::TaskFetcher.new
564
+
565
+ if fetcher.ace_task_available?
566
+ {available: true, message: "ace-task is available"}
567
+ else
568
+ {available: false, message: "ace-task is not available. Install with: gem install ace-task"}
569
+ end
570
+ end
571
+
572
+ # Create a traditional worktree
573
+ #
574
+ # @param options [Hash] Command options
575
+ # @return [Integer] Exit code
576
+ def create_traditional_worktree(options)
577
+ @options = options
578
+ puts "Creating worktree for branch: #{options[:branch_name]}"
579
+
580
+ # Prepare creation options
581
+ creation_options = {
582
+ path: options[:path],
583
+ source: options[:source],
584
+ dry_run: options[:dry_run],
585
+ no_mise_trust: options[:no_mise_trust],
586
+ force: options[:force]
587
+ }.compact
588
+
589
+ # Create the worktree
590
+ result = @manager.create(options[:branch_name], creation_options)
591
+
592
+ if result[:success]
593
+ display_traditional_creation_result(result, options[:dry_run])
594
+ 0
595
+ else
596
+ puts "Failed to create worktree: #{result[:error]}"
597
+ 1
598
+ end
599
+ end
600
+
601
+ # Display task worktree creation result
602
+ #
603
+ # @param result [Hash] Creation result
604
+ # @param dry_run [Boolean] Whether this was a dry run
605
+ def display_task_creation_result(result, dry_run = false)
606
+ if dry_run
607
+ puts "\nDry run - no changes made:"
608
+ puts "Would create worktree at: #{result[:would_create][:worktree_path]}"
609
+ puts "Would create branch: #{result[:would_create][:branch]}"
610
+ puts "Target branch: #{result[:would_create][:target_branch]}" if result[:would_create][:target_branch]
611
+ puts "Task ID: #{result[:task_id]}"
612
+ puts "Task title: #{result[:task_title]}"
613
+ if result[:would_create][:task_push]
614
+ puts "Would push to: #{result[:would_create][:push_remote]}"
615
+ end
616
+ if result[:would_create][:upstream_push]
617
+ puts "Would setup upstream: #{result[:would_create][:push_remote]}/#{result[:would_create][:branch]}"
618
+ end
619
+ if result[:would_create][:create_pr]
620
+ puts "Would create draft PR: #{result[:would_create][:pr_title]}"
621
+ end
622
+ puts "\nPlanned steps:"
623
+ result[:steps_planned].each_with_index do |step, i|
624
+ puts " #{i + 1}. #{step.tr("_", " ")}"
625
+ end
626
+ else
627
+ puts "\nWorktree created successfully!"
628
+ puts "Task ID: #{result[:task_id]}"
629
+ puts "Task title: #{result[:task_title]}" if result[:task_title]
630
+ puts "Worktree path: #{result[:worktree_path]}"
631
+ puts "Branch: #{result[:branch]}"
632
+ puts "Directory: #{result[:directory_name]}" if result[:directory_name]
633
+ puts "Start point: #{result[:start_point]}" if result[:start_point]
634
+ puts "Pushed to: #{result[:pushed_to]}" if result[:pushed_to]
635
+
636
+ # Display PR info if created
637
+ if result[:pr_number]
638
+ existing_label = result[:pr_existing] ? " (existing)" : ""
639
+ puts "PR: ##{result[:pr_number]}#{existing_label} - #{result[:pr_url]}"
640
+ end
641
+
642
+ puts "\nSteps completed:"
643
+ result[:steps_completed].each_with_index do |step, i|
644
+ puts " ✓ #{step.tr("_", " ")}"
645
+ end
646
+
647
+ # Display hooks results if available
648
+ if result[:hooks_results] && result[:hooks_results].any?
649
+ puts "\nHooks executed:"
650
+ result[:hooks_results].each do |hook_result|
651
+ status = hook_result[:success] ? "✓" : "✗"
652
+ command = (hook_result[:command].length > 60) ? "#{hook_result[:command][0..57]}..." : hook_result[:command]
653
+ puts " #{status} #{command}"
654
+ unless hook_result[:success]
655
+ puts " Error: #{hook_result[:error]}" if hook_result[:error]
656
+ end
657
+ end
658
+ end
659
+ end
660
+
661
+ display_warnings(result[:warnings]) if result[:warnings]
662
+
663
+ # Display cd command or launch tmux
664
+ unless dry_run
665
+ puts ""
666
+ launch_tmux_or_display_cd(result[:worktree_path])
667
+ end
668
+ end
669
+
670
+ # Display PR worktree creation result
671
+ #
672
+ # @param result [Hash] Creation result
673
+ # @param dry_run [Boolean] Whether this was a dry run
674
+ def display_pr_creation_result(result, dry_run = false)
675
+ if dry_run
676
+ puts "\nDry run - no changes made:"
677
+ puts "Would create worktree at: #{result[:would_create][:worktree_path]}"
678
+ puts "Would create branch: #{result[:would_create][:branch]}"
679
+ puts "Would track: #{result[:would_create][:tracking]}"
680
+ puts "PR: ##{result[:pr_number]} - #{result[:pr_title]}"
681
+ else
682
+ puts "\nWorktree created successfully!"
683
+ puts "✓ PR ##{result[:pr_number]}: #{result[:pr_title]}" if result[:pr_title]
684
+ puts "✓ Remote branch: #{result[:tracking]}" if result[:tracking]
685
+ puts "✓ Created worktree: #{result[:directory_name]}" if result[:directory_name]
686
+ puts "✓ Branch: #{result[:branch]} tracking #{result[:tracking]}"
687
+ puts "✓ Location: #{result[:worktree_path]}"
688
+ puts
689
+
690
+ # Display hooks results if available
691
+ if result[:hooks_results] && result[:hooks_results].any?
692
+ puts "Hooks executed:"
693
+ result[:hooks_results].each do |hook_result|
694
+ status = hook_result[:success] ? "✓" : "✗"
695
+ command = (hook_result[:command].length > 60) ? "#{hook_result[:command][0..57]}..." : hook_result[:command]
696
+ puts " #{status} #{command}"
697
+ unless hook_result[:success]
698
+ puts " Error: #{hook_result[:error]}" if hook_result[:error]
699
+ end
700
+ end
701
+ puts
702
+ end
703
+ end
704
+
705
+ display_warnings(result[:warnings]) if result[:warnings]
706
+
707
+ # Display cd command or launch tmux
708
+ unless dry_run
709
+ launch_tmux_or_display_cd(result[:worktree_path])
710
+ end
711
+ end
712
+
713
+ # Display branch worktree creation result
714
+ #
715
+ # @param result [Hash] Creation result
716
+ # @param dry_run [Boolean] Whether this was a dry run
717
+ def display_branch_creation_result(result, dry_run = false)
718
+ if dry_run
719
+ puts "\nDry run - no changes made:"
720
+ puts "Would create worktree at: #{result[:would_create][:worktree_path]}"
721
+ puts "Would create branch: #{result[:would_create][:branch]}"
722
+ if result[:would_create][:tracking]
723
+ puts "Would track: #{result[:would_create][:tracking]}"
724
+ else
725
+ puts "Local branch (no tracking)"
726
+ end
727
+ else
728
+ puts "\nWorktree created successfully!"
729
+ puts "✓ Created worktree: #{result[:directory_name]}" if result[:directory_name]
730
+ if result[:tracking]
731
+ puts "✓ Branch: #{result[:branch]} tracking #{result[:tracking]}"
732
+ else
733
+ puts "✓ Branch: #{result[:branch]} (local, no tracking)"
734
+ end
735
+ puts "✓ Location: #{result[:worktree_path]}"
736
+ puts
737
+
738
+ # Display hooks results if available
739
+ if result[:hooks_results] && result[:hooks_results].any?
740
+ puts "Hooks executed:"
741
+ result[:hooks_results].each do |hook_result|
742
+ status = hook_result[:success] ? "✓" : "✗"
743
+ command = (hook_result[:command].length > 60) ? "#{hook_result[:command][0..57]}..." : hook_result[:command]
744
+ puts " #{status} #{command}"
745
+ unless hook_result[:success]
746
+ puts " Error: #{hook_result[:error]}" if hook_result[:error]
747
+ end
748
+ end
749
+ puts
750
+ end
751
+ end
752
+
753
+ display_warnings(result[:warnings]) if result[:warnings]
754
+
755
+ # Display cd command or launch tmux
756
+ unless dry_run
757
+ launch_tmux_or_display_cd(result[:worktree_path])
758
+ end
759
+ end
760
+
761
+ # Display traditional worktree creation result
762
+ #
763
+ # @param result [Hash] Creation result
764
+ # @param dry_run [Boolean] Whether this was a dry run
765
+ def display_traditional_creation_result(result, dry_run = false)
766
+ if dry_run
767
+ puts "\nDry run - no changes made:"
768
+ puts "Would create worktree at: #{result[:would_create][:worktree_path]}"
769
+ puts "Would create branch: #{result[:would_create][:branch]}"
770
+ puts "Branch exists: #{result[:would_create][:branch_exists]}"
771
+ puts "Source: #{result[:would_create][:source]}"
772
+ else
773
+ puts "\nWorktree created successfully!"
774
+ puts "Worktree path: #{result[:worktree_path]}"
775
+ puts "Branch: #{result[:branch]}"
776
+ puts "Git root: #{result[:git_root]}"
777
+
778
+ display_warnings(result[:warnings]) if result[:warnings]
779
+ puts ""
780
+ launch_tmux_or_display_cd(result[:worktree_path])
781
+ end
782
+ end
783
+
784
+ # Display warnings
785
+ #
786
+ # @param warnings [Array<String>] Array of warning messages
787
+ def display_warnings(warnings)
788
+ return unless warnings&.any?
789
+
790
+ puts "\nWarnings:"
791
+ warnings.each { |warning| puts " ⚠️ #{warning}" }
792
+ end
793
+
794
+ # Check if auto-navigation should be performed
795
+ #
796
+ # @return [Boolean] true if auto-navigation should be performed
797
+ def should_auto_navigate?
798
+ # Check CLI flag first
799
+ return false if @options[:no_auto_navigate]
800
+
801
+ # Then check configuration by loading it directly
802
+ begin
803
+ require_relative "../molecules/config_loader"
804
+ config_loader = Ace::Git::Worktree::Molecules::ConfigLoader.new
805
+ config = config_loader.load
806
+ return false unless config
807
+
808
+ config.auto_navigate?
809
+ rescue
810
+ # If configuration loading fails, default to no auto-navigation
811
+ false
812
+ end
813
+ end
814
+
815
+ # Launch tmux session or display cd hint for navigation
816
+ #
817
+ # When tmux config is enabled and ace-tmux is available, launches a tmux
818
+ # session rooted at the worktree path (replaces current process).
819
+ # Otherwise falls back to displaying the cd command.
820
+ #
821
+ # @param worktree_path [String] Path to the worktree
822
+ def launch_tmux_or_display_cd(worktree_path)
823
+ return unless worktree_path
824
+
825
+ if tmux_enabled?
826
+ if ace_tmux_available?
827
+ Kernel.exec("ace-tmux", "--root", worktree_path)
828
+ else
829
+ puts "Warning: tmux is enabled in config but ace-tmux is not installed."
830
+ puts "Install ace-tmux or disable tmux in .ace/git/worktree.yml"
831
+ puts "cd #{worktree_path}"
832
+ end
833
+ else
834
+ puts "cd #{worktree_path}"
835
+ end
836
+ end
837
+
838
+ # Check if tmux integration is enabled in config
839
+ #
840
+ # @return [Boolean] true if tmux is enabled
841
+ def tmux_enabled?
842
+ require_relative "../molecules/config_loader"
843
+ config_loader = Ace::Git::Worktree::Molecules::ConfigLoader.new
844
+ config = config_loader.load
845
+ return false unless config
846
+
847
+ config.tmux?
848
+ rescue
849
+ false
850
+ end
851
+
852
+ # Check if ace-tmux binary is available on PATH
853
+ #
854
+ # @return [Boolean] true if ace-tmux is installed
855
+ def ace_tmux_available?
856
+ system("which ace-tmux > /dev/null 2>&1")
857
+ end
858
+
859
+ # Convert PR metadata from gh CLI to internal pr_data format
860
+ #
861
+ # Anti-corruption layer that translates gh CLI JSON output to internal format.
862
+ # This isolates worktree creation from gh CLI output structure changes.
863
+ #
864
+ # Expected metadata schema from ace-git PrMetadataFetcher (via gh pr view --json):
865
+ # {
866
+ # "number" => Integer,
867
+ # "title" => String,
868
+ # "headRefName" => String (PR source branch),
869
+ # "baseRefName" => String (PR target branch),
870
+ # "isCrossRepository" => Boolean (true for fork PRs),
871
+ # "headRepositoryOwner" => { "login" => String } (fork owner info)
872
+ # }
873
+ #
874
+ # @param metadata [Hash] PR metadata from gh CLI
875
+ # @return [Hash] pr_data format expected by worktree creator
876
+ def pr_data_from_metadata(metadata)
877
+ {
878
+ number: metadata["number"],
879
+ title: metadata["title"],
880
+ head_branch: metadata["headRefName"],
881
+ base_branch: metadata["baseRefName"],
882
+ is_cross_repository: metadata["isCrossRepository"] || false,
883
+ head_repository_owner: metadata.dig("headRepositoryOwner", "login") || "unknown"
884
+ }
885
+ end
886
+
887
+ # Handle errors during PR metadata fetch
888
+ #
889
+ # @param error [Exception] The error that occurred
890
+ # @param pr_number [Integer] PR number for context in error messages
891
+ # @return [Integer] Exit code (always 1 for errors)
892
+ def handle_pr_fetch_error(error, pr_number)
893
+ case error
894
+ when Ace::Git::PrNotFoundError
895
+ handle_pr_not_found(error, pr_number)
896
+ when Ace::Git::GhAuthenticationError
897
+ handle_gh_auth_error(error)
898
+ when Ace::Git::GhNotInstalledError
899
+ handle_gh_not_installed(error)
900
+ else
901
+ handle_unknown_error(error)
902
+ end
903
+ 1
904
+ end
905
+
906
+ # Handle PR not found error with helpful suggestions
907
+ def handle_pr_not_found(error, pr_number)
908
+ puts "Error: #{error.message}"
909
+ puts
910
+ puts "Suggestions:"
911
+ puts " 1. Verify the PR number is correct"
912
+ puts " 2. Check if the PR exists: gh pr view #{pr_number}"
913
+ puts " 3. Ensure you're in the correct repository"
914
+ end
915
+
916
+ # Handle GitHub authentication error with troubleshooting steps
917
+ def handle_gh_auth_error(error)
918
+ puts "Error: #{error.message}"
919
+ puts
920
+ puts "Troubleshooting:"
921
+ puts " 1. Verify GitHub authentication: gh auth status"
922
+ puts " 2. Re-authenticate if needed: gh auth login"
923
+ puts " 3. Check repository access permissions"
924
+ end
925
+
926
+ # Handle gh CLI not installed error with installation guidance
927
+ def handle_gh_not_installed(error)
928
+ puts "Error: #{error.message}"
929
+ puts
930
+ puts gh_not_available_message
931
+ end
932
+
933
+ # Handle unknown/unexpected errors with debug info
934
+ def handle_unknown_error(error)
935
+ puts "Error: #{error.message}"
936
+ if ENV["DEBUG"]
937
+ puts "Debug: #{error.class}"
938
+ puts "Debug: #{error.backtrace&.first}" if error.backtrace
939
+ end
940
+ end
941
+
942
+ # Get helpful error message when gh CLI is unavailable
943
+ #
944
+ # @return [String] User-friendly error message with installation guidance
945
+ def gh_not_available_message
946
+ <<~MESSAGE
947
+ gh CLI is required for PR worktrees but is not installed.
948
+
949
+ Install gh CLI:
950
+ - macOS: brew install gh
951
+ - Linux: See https://github.com/cli/cli#installation
952
+ - Windows: See https://github.com/cli/cli#installation
953
+
954
+ After installation, authenticate with: gh auth login
955
+ MESSAGE
956
+ end
957
+ end
958
+ end
959
+ end
960
+ end
961
+ end