aidp 0.49.9 → 0.49.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 152b6f4bd772dffcdd49bff974dc989b987e16e1d5a2a6cbc28ff0017eb654ad
4
- data.tar.gz: 6163f77e46235f25748344bd675e8ccd221a6ce56d0322d63da1e93969ffc903
3
+ metadata.gz: 2521930bf3c6ad6ed74527be97a75ada7645500392b018335b3063f90e045885
4
+ data.tar.gz: 332b8f38ea7282420195209d115eb066311f92fac1d481b74fb8c87304df136e
5
5
  SHA512:
6
- metadata.gz: fcde2af278ae33285b683948d102eecb16b38bd40d121135c80fceab6ed01908be325ce7cb2ff266d5e4399a9f1127b6aae538ad7eedf8442c46bd7630c3bdfe
7
- data.tar.gz: a2d8a65e046f8bb81bcb4ec50bbf6838fe6119beb4872bec33f4249db67c107809fa12e65aeaa4452bd8b02e41be3bde74f2956a8faa6b0c70e0bfd4b3240f06
6
+ metadata.gz: 1f33ce70e9679f0844e008b03856da81f6de8bc0a7820eeedb25d47d5f72b93e0274d03143bd31980348ca4bd08fd0a1ee2888fb86a18a0ace1be47e1b2f322e
7
+ data.tar.gz: 7ea709e5742560ee4fa8617c1bdbec13143661c67e32417314a4efce07359566c188ae723ae9cc9f597a5de0977b9734077b88a72a5ac3d5578debcfbb20be9f
@@ -342,12 +342,13 @@ module Aidp
342
342
  return 0
343
343
  end
344
344
 
345
- # Run RSpec with timeout protection
346
- cmd = "bundle exec rspec #{rspec_path} --format failures"
347
- @prompt.say("$ #{cmd}\n")
345
+ # Run RSpec with timeout protection, passing arguments without shell
346
+ # interpretation of the library-provided project path
347
+ command_args = ["bundle", "exec", "rspec", rspec_path, "--format", "failures"]
348
+ @prompt.say("$ #{command_args.join(" ")}\n")
348
349
  @prompt.say("(timeout: #{AUDIT_TIMEOUT_SECONDS / 60} minutes)\n")
349
350
 
350
- exit_status = run_with_timeout(cmd, AUDIT_TIMEOUT_SECONDS)
351
+ exit_status = run_with_timeout(command_args, AUDIT_TIMEOUT_SECONDS)
351
352
 
352
353
  case exit_status
353
354
  when 0
@@ -365,11 +366,11 @@ module Aidp
365
366
  private
366
367
 
367
368
  # Run a command with timeout protection
368
- # @param cmd [String] The command to execute
369
+ # @param command_args [Array<String>] The command and its arguments
369
370
  # @param timeout [Integer] Timeout in seconds
370
371
  # @return [Integer, Symbol] Exit status or :timeout
371
- def run_with_timeout(cmd, timeout)
372
- pid = spawn(cmd)
372
+ def run_with_timeout(command_args, timeout)
373
+ pid = Process.spawn(*command_args)
373
374
  start_time = Time.now
374
375
 
375
376
  loop do
@@ -3,6 +3,7 @@
3
3
  require "yaml"
4
4
  require "time"
5
5
  require "json"
6
+ require "open3"
6
7
  require "aidp/rescue_logging"
7
8
 
8
9
  module Aidp
@@ -183,7 +184,8 @@ module Aidp
183
184
  def run_rubocop_check
184
185
  # Run rubocop and parse output to get a quality score
185
186
  # This is a simplified version - could be enhanced
186
- result = `cd #{@project_dir} && rubocop --format json 2>/dev/null`
187
+ stdout, _stderr, _status = Open3.capture3("rubocop", "--format", "json", chdir: @project_dir)
188
+ result = stdout
187
189
  return nil if result.empty?
188
190
 
189
191
  data = JSON.parse(result)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "tty-prompt"
4
4
  require "tty-spinner"
5
+ require "open3"
5
6
  require_relative "async_work_loop_runner"
6
7
  require_relative "repl_macros"
7
8
  require_relative "../rescue_logging"
@@ -307,9 +308,9 @@ module Aidp
307
308
  }
308
309
  end
309
310
 
310
- # Execute reset
311
- output = `git reset --hard HEAD~#{count} 2>&1`
312
- success = $?.success?
311
+ # Execute reset without shell interpretation of the rollback count
312
+ output, status = Open3.capture2e("git", "reset", "--hard", "HEAD~#{count}")
313
+ success = status.success?
313
314
 
314
315
  {
315
316
  success: success,
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
3
4
  require_relative "prompt_manager"
4
5
  require_relative "prompt_evaluator"
5
6
  require_relative "checkpoint"
@@ -2359,8 +2360,9 @@ module Aidp
2359
2360
 
2360
2361
  stats = {}
2361
2362
  files.each do |file|
2362
- # Use git diff to get line counts
2363
- output = `git diff --numstat HEAD -- "#{file}" 2>/dev/null`.strip
2363
+ # Use git diff to get line counts without shell interpretation of file names
2364
+ stdout, _stderr, _status = Open3.capture3("git", "diff", "--numstat", "HEAD", "--", file.to_s)
2365
+ output = stdout.strip
2364
2366
  next if output.empty?
2365
2367
 
2366
2368
  parts = output.split("\t")
@@ -64,27 +64,27 @@ module Aidp
64
64
  test_commands = detect_test_commands
65
65
  return true if test_commands.empty?
66
66
 
67
- test_commands.any? do |cmd|
68
- system("cd #{@project_dir} && #{cmd} > /dev/null 2>&1")
69
- end
67
+ test_commands.any? { |cmd| run_project_command(cmd) }
70
68
  end
71
69
 
72
70
  def linting_clean?
73
71
  lint_commands = detect_lint_commands
74
72
  return true if lint_commands.empty?
75
73
 
76
- lint_commands.all? do |cmd|
77
- system("cd #{@project_dir} && #{cmd} > /dev/null 2>&1")
78
- end
74
+ lint_commands.all? { |cmd| run_project_command(cmd) }
79
75
  end
80
76
 
81
77
  def build_successful?
82
78
  build_commands = detect_build_commands
83
79
  return true if build_commands.empty?
84
80
 
85
- build_commands.any? do |cmd|
86
- system("cd #{@project_dir} && #{cmd} > /dev/null 2>&1")
87
- end
81
+ build_commands.any? { |cmd| run_project_command(cmd) }
82
+ end
83
+
84
+ # Run a detected command in the project directory without building a
85
+ # shell command string from the library-provided project path
86
+ def run_project_command(cmd)
87
+ system(cmd, chdir: @project_dir, out: File::NULL, err: File::NULL)
88
88
  end
89
89
 
90
90
  def documentation_complete?
@@ -357,9 +357,9 @@ module Aidp
357
357
 
358
358
  next unless tool_command
359
359
 
360
- # Check if command exists
360
+ # Check if command exists without shell interpretation
361
361
  begin
362
- unless system("which #{tool_command} > /dev/null 2>&1")
362
+ unless system("which", tool_command, out: File::NULL, err: File::NULL)
363
363
  warnings << " ⚠️ #{format_tool(tool_data[:tool])} detected but command '#{tool_command}' not found in PATH"
364
364
  end
365
365
  rescue IOError
@@ -1,6 +1,5 @@
1
1
  require "json"
2
2
  require "fileutils"
3
- require "shellwords"
4
3
 
5
4
  module Aidp
6
5
  # Manages worktrees specifically for Pull Request branches
@@ -151,17 +150,18 @@ module Aidp
151
150
  branch == "* #{base_branch}"
152
151
  end
153
152
 
154
- # Enhance branch tracking and fetching
153
+ # Enhance branch tracking and fetching; argv execution keeps the
154
+ # library-provided branch name from being interpreted by a shell
155
155
  unless base_branch_exists
156
156
  # Try multiple fetch strategies
157
157
  fetch_commands = [
158
- "git fetch origin #{base_branch}:#{base_branch} 2>/dev/null",
159
- "git fetch origin 2>/dev/null",
160
- "git fetch --all 2>/dev/null"
158
+ ["git", "fetch", "origin", "#{base_branch}:#{base_branch}"],
159
+ ["git", "fetch", "origin"],
160
+ ["git", "fetch", "--all"]
161
161
  ]
162
162
 
163
- fetch_commands.each do |fetch_cmd|
164
- @shell_executor.system(fetch_cmd)
163
+ fetch_commands.each do |fetch_args|
164
+ @shell_executor.system(*fetch_args, err: File::NULL)
165
165
  branch_list_output = `git branch -a`.split("\n").map(&:strip)
166
166
  base_branch_exists = branch_list_output.any? do |branch|
167
167
  branch.end_with?("/#{base_branch}", "remotes/origin/#{base_branch}") ||
@@ -174,14 +174,15 @@ module Aidp
174
174
 
175
175
  raise ArgumentError, "Base branch '#{base_branch}' does not exist in the repository" unless base_branch_exists
176
176
 
177
- # Robust worktree creation with enhanced error handling and logging
178
- worktree_create_command = "git worktree add #{Shellwords.escape(worktree_path)} -b #{Shellwords.escape(head_branch)} #{Shellwords.escape(base_branch)}"
179
- unless @shell_executor.system(worktree_create_command)
177
+ # Robust worktree creation with enhanced error handling and logging;
178
+ # argv execution keeps library-provided branches and paths literal
179
+ worktree_create_args = ["git", "worktree", "add", worktree_path, "-b", head_branch, base_branch]
180
+ unless @shell_executor.system(*worktree_create_args)
180
181
  error_details = {
181
182
  pr_number: pr_number,
182
183
  base_branch: base_branch,
183
184
  head_branch: head_branch,
184
- command: worktree_create_command
185
+ command: worktree_create_args.join(" ")
185
186
  }
186
187
  Aidp.log_error(
187
188
  "pr_worktree_manager", "worktree_creation_failed",
@@ -377,9 +378,9 @@ module Aidp
377
378
  end
378
379
  end
379
380
 
380
- # Stage only successfully modified files
381
+ # Stage only successfully modified files without shell interpretation
381
382
  unless successful_files.empty?
382
- @shell_executor.system("git add #{successful_files.map { |f| Shellwords.escape(f) }.join(" ")}")
383
+ @shell_executor.system("git", "add", "--", *successful_files)
383
384
  end
384
385
  end
385
386
 
@@ -428,25 +429,22 @@ module Aidp
428
429
 
429
430
  Dir.chdir(worktree_path) do
430
431
  # Check staged changes with more robust capture
431
- staged_changes_output = @shell_executor.run("git diff --staged --name-only").strip
432
+ staged_changes_output = @shell_executor.run_argv("git", "diff", "--staged", "--name-only").stdout.strip
432
433
 
433
434
  if !staged_changes_output.empty?
434
435
  push_result[:git_actions][:staged_changes] = true
435
436
  push_result[:changed_files] = staged_changes_output.split("\n")
436
437
 
437
- # More robust commit command with additional logging
438
- commit_message = "Changes applied via AIDP request-changes workflow for PR ##{pr_number}"
439
- commit_command = "git commit -m '#{commit_message}' 2>&1"
440
- commit_output = @shell_executor.run(commit_command).strip
438
+ # Commit staged changes without shell interpretation of library input
439
+ commit_output = commit_staged_changes(pr_number)
441
440
 
442
- if @shell_executor.success?
441
+ if commit_output.success?
443
442
  push_result[:git_actions][:committed] = true
444
443
 
445
- # Enhanced push with verbose tracking
446
- push_command = "git push origin #{head_branch} 2>&1"
447
- push_output = @shell_executor.run(push_command).strip
444
+ # Push the registry-sourced head branch without shell interpretation
445
+ push_output = push_head_branch(head_branch)
448
446
 
449
- if @shell_executor.success?
447
+ if push_output.success?
450
448
  push_result[:git_actions][:pushed] = true
451
449
  push_result[:success] = true
452
450
 
@@ -458,22 +456,22 @@ module Aidp
458
456
  )
459
457
  else
460
458
  # Detailed push error logging
461
- push_result[:errors] << "Push failed: #{push_output}"
459
+ push_result[:errors] << "Push failed: #{push_output.output.strip}"
462
460
  Aidp.log_error(
463
461
  "pr_worktree_manager", "push_changes_failed",
464
462
  pr_number: pr_number,
465
463
  branch: head_branch,
466
- error_details: push_output
464
+ error_details: push_output.output.strip
467
465
  )
468
466
  end
469
467
  else
470
468
  # Detailed commit error logging
471
- push_result[:errors] << "Commit failed: #{commit_output}"
469
+ push_result[:errors] << "Commit failed: #{commit_output.output.strip}"
472
470
  Aidp.log_error(
473
471
  "pr_worktree_manager", "commit_changes_failed",
474
472
  pr_number: pr_number,
475
473
  branch: head_branch,
476
- error_details: commit_output
474
+ error_details: commit_output.output.strip
477
475
  )
478
476
  end
479
477
  else
@@ -496,8 +494,10 @@ module Aidp
496
494
  existing_worktree = @worktrees[pr_number.to_s]
497
495
  return false unless existing_worktree
498
496
 
499
- # Remove git worktree
500
- @shell_executor.system("git worktree remove #{existing_worktree["path"]}") if File.exist?(existing_worktree["path"])
497
+ # Remove git worktree without shell interpretation of registry data
498
+ if File.exist?(existing_worktree["path"])
499
+ @shell_executor.system("git", "worktree", "remove", existing_worktree["path"])
500
+ end
501
501
 
502
502
  # Remove from registry and save
503
503
  @worktrees.delete(pr_number.to_s)
@@ -532,6 +532,19 @@ module Aidp
532
532
 
533
533
  private
534
534
 
535
+ # Commit currently staged changes; argv execution keeps externally
536
+ # sourced values (pr_number) from being interpreted by a shell
537
+ def commit_staged_changes(pr_number)
538
+ commit_message = "Changes applied via AIDP request-changes workflow for PR ##{pr_number}"
539
+ @shell_executor.run_argv("git", "commit", "-m", commit_message)
540
+ end
541
+
542
+ # Push the head branch; argv execution keeps registry-sourced branch
543
+ # names from being interpreted by a shell
544
+ def push_head_branch(head_branch)
545
+ @shell_executor.run_argv("git", "push", "origin", "--", head_branch)
546
+ end
547
+
535
548
  # Load the worktree registry from file
536
549
  def load_registry
537
550
  return {} unless File.exist?(worktree_registry_path)
@@ -3,8 +3,9 @@
3
3
  module Aidp
4
4
  # Shell command executor wrapper for testability
5
5
  #
6
- # Provides two modes of execution:
7
- # 1. `run(command)` - Captures output silently via backticks
6
+ # Commands are always executed in argument form so shell
7
+ # metacharacters in any value are treated as literal data:
8
+ # 1. `run_argv(*command)` - Captures output without shell interpretation
8
9
  # 2. `system(*args)` - Wraps Kernel.system() with optional output suppression
9
10
  #
10
11
  # In tests, set `ShellExecutor.suppress_output = true` to suppress all
@@ -25,19 +26,45 @@ module Aidp
25
26
  end
26
27
  self.suppress_output = false
27
28
 
28
- # Run a command and capture its output
29
- #
30
- # @param command [String] The shell command to run
31
- # @return [String] The command's stdout output
32
- def run(command)
33
- `#{command}`
29
+ # Immutable result of a shell-free command execution
30
+ class Result
31
+ attr_reader :stdout, :stderr, :exit_status
32
+
33
+ # @param stdout [String] standard output from the command
34
+ # @param stderr [String] standard error from the command
35
+ # @param exit_status [Integer] exit status code
36
+ def initialize(stdout:, stderr:, exit_status:)
37
+ @stdout = stdout.to_s.freeze
38
+ @stderr = stderr.to_s.freeze
39
+ @exit_status = exit_status || 1
40
+ freeze
41
+ end
42
+
43
+ # @return [Boolean] true if exit_status is 0
44
+ def success?
45
+ @exit_status.zero?
46
+ end
47
+
48
+ # Combined output, equivalent to shell `2>&1` redirection
49
+ # @return [String]
50
+ def output
51
+ [@stdout, @stderr].reject(&:empty?).join("\n")
52
+ end
34
53
  end
35
54
 
36
- # Check if the last command succeeded
55
+ # Run a command without shell interpretation and capture its output
56
+ #
57
+ # The command and its arguments are passed directly to the operating
58
+ # system, so shell metacharacters in any argument are treated as
59
+ # literal data. Prefer this over #run whenever an argument may contain
60
+ # externally supplied values.
37
61
  #
38
- # @return [Boolean] true if last command exited with status 0
39
- def success?
40
- $?.success?
62
+ # @param command [Array<String>] the command and its arguments
63
+ # @return [Result] captured stdout/stderr with the exit status
64
+ def run_argv(*command)
65
+ require "open3"
66
+ stdout, stderr, status = Open3.capture3(*command.map(&:to_s))
67
+ Result.new(stdout: stdout, stderr: stderr, exit_status: status.exitstatus)
41
68
  end
42
69
 
43
70
  # Run a command via system(), optionally suppressing output
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.49.9"
4
+ VERSION = "0.49.11"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aidp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.49.9
4
+ version: 0.49.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan