aidp 0.1.0 → 0.5.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +59 -4
  3. data/bin/aidp +2 -2
  4. data/lib/aidp/analyze/agent_personas.rb +1 -1
  5. data/lib/aidp/analyze/data_retention_manager.rb +2 -2
  6. data/lib/aidp/analyze/database.rb +99 -82
  7. data/lib/aidp/analyze/error_handler.rb +12 -76
  8. data/lib/aidp/analyze/focus_guidance.rb +2 -2
  9. data/lib/aidp/analyze/large_analysis_progress.rb +2 -2
  10. data/lib/aidp/analyze/metrics_storage.rb +336 -0
  11. data/lib/aidp/analyze/prioritizer.rb +4 -4
  12. data/lib/aidp/analyze/repository_chunker.rb +15 -13
  13. data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
  14. data/lib/aidp/analyze/runner.rb +107 -191
  15. data/lib/aidp/analyze/steps.rb +29 -30
  16. data/lib/aidp/analyze/storage.rb +234 -172
  17. data/lib/aidp/cli/jobs_command.rb +489 -0
  18. data/lib/aidp/cli/terminal_io.rb +52 -0
  19. data/lib/aidp/cli.rb +227 -0
  20. data/lib/aidp/config.rb +33 -0
  21. data/lib/aidp/core_ext/class_attribute.rb +36 -0
  22. data/lib/aidp/database/pg_adapter.rb +148 -0
  23. data/lib/aidp/database_config.rb +69 -0
  24. data/lib/aidp/database_connection.rb +72 -0
  25. data/lib/aidp/database_migration.rb +158 -0
  26. data/lib/aidp/execute/runner.rb +65 -92
  27. data/lib/aidp/execute/steps.rb +81 -82
  28. data/lib/aidp/job_manager.rb +41 -0
  29. data/lib/aidp/jobs/base_job.rb +47 -0
  30. data/lib/aidp/jobs/provider_execution_job.rb +96 -0
  31. data/lib/aidp/project_detector.rb +117 -0
  32. data/lib/aidp/provider_manager.rb +25 -0
  33. data/lib/aidp/providers/agent_supervisor.rb +348 -0
  34. data/lib/aidp/providers/anthropic.rb +187 -0
  35. data/lib/aidp/providers/base.rb +162 -0
  36. data/lib/aidp/providers/cursor.rb +304 -0
  37. data/lib/aidp/providers/gemini.rb +187 -0
  38. data/lib/aidp/providers/macos_ui.rb +24 -0
  39. data/lib/aidp/providers/supervised_base.rb +317 -0
  40. data/lib/aidp/providers/supervised_cursor.rb +22 -0
  41. data/lib/aidp/sync.rb +13 -0
  42. data/lib/aidp/util.rb +39 -0
  43. data/lib/aidp/{shared/version.rb → version.rb} +1 -3
  44. data/lib/aidp/workspace.rb +19 -0
  45. data/lib/aidp.rb +36 -45
  46. data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
  47. metadata +89 -45
  48. data/lib/aidp/shared/cli.rb +0 -117
  49. data/lib/aidp/shared/config.rb +0 -35
  50. data/lib/aidp/shared/project_detector.rb +0 -119
  51. data/lib/aidp/shared/providers/anthropic.rb +0 -26
  52. data/lib/aidp/shared/providers/base.rb +0 -17
  53. data/lib/aidp/shared/providers/cursor.rb +0 -102
  54. data/lib/aidp/shared/providers/gemini.rb +0 -26
  55. data/lib/aidp/shared/providers/macos_ui.rb +0 -26
  56. data/lib/aidp/shared/sync.rb +0 -15
  57. data/lib/aidp/shared/util.rb +0 -41
  58. data/lib/aidp/shared/workspace.rb +0 -21
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Aidp
6
+ module Providers
7
+ class Gemini < Base
8
+ def self.available?
9
+ !!Aidp::Util.which("gemini")
10
+ end
11
+
12
+ def name = "gemini"
13
+
14
+ def send(prompt:, session: nil)
15
+ raise "gemini CLI not available" unless self.class.available?
16
+
17
+ require "open3"
18
+
19
+ # Use Gemini CLI for non-interactive mode
20
+ cmd = ["gemini", "--print"]
21
+
22
+ puts "📝 Sending prompt to gemini..."
23
+
24
+ # Smart timeout calculation
25
+ timeout_seconds = calculate_timeout
26
+
27
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait|
28
+ # Send the prompt to stdin
29
+ stdin.puts prompt
30
+ stdin.close
31
+
32
+ # Start stuck detection thread
33
+ stuck_detection_thread = Thread.new do
34
+ loop do
35
+ sleep 10 # Check every 10 seconds
36
+
37
+ if stuck?
38
+ puts "⚠️ gemini appears stuck (no activity for #{stuck_timeout} seconds)"
39
+ puts " You can:"
40
+ puts " 1. Wait longer (press Enter)"
41
+ puts " 2. Abort (Ctrl+C)"
42
+
43
+ # Give user a chance to respond
44
+ begin
45
+ Timeout.timeout(30) do
46
+ gets
47
+ puts "🔄 Continuing to wait..."
48
+ end
49
+ rescue Timeout::Error
50
+ puts "⏰ No response received, continuing to wait..."
51
+ rescue Interrupt
52
+ puts "🛑 Aborting gemini..."
53
+ Process.kill("TERM", wait.pid)
54
+ raise Interrupt, "User aborted gemini execution"
55
+ end
56
+ end
57
+
58
+ # Stop checking if the process is done
59
+ break if wait.value
60
+ rescue
61
+ break
62
+ end
63
+ end
64
+
65
+ # Wait for completion with timeout
66
+ begin
67
+ Timeout.timeout(timeout_seconds) do
68
+ result = wait.value
69
+
70
+ # Stop stuck detection thread
71
+ stuck_detection_thread&.kill
72
+
73
+ if result.success?
74
+ output = stdout.read
75
+ puts "✅ Gemini analysis completed"
76
+ mark_completed
77
+ return output.empty? ? :ok : output
78
+ else
79
+ error_output = stderr.read
80
+ mark_failed("gemini failed with exit code #{result.exitstatus}: #{error_output}")
81
+ raise "gemini failed with exit code #{result.exitstatus}: #{error_output}"
82
+ end
83
+ end
84
+ rescue Timeout::Error
85
+ # Stop stuck detection thread
86
+ stuck_detection_thread&.kill
87
+
88
+ # Kill the process if it's taking too long
89
+ begin
90
+ Process.kill("TERM", wait.pid)
91
+ rescue
92
+ nil
93
+ end
94
+
95
+ mark_failed("gemini timed out after #{timeout_seconds} seconds")
96
+ raise Timeout::Error, "gemini timed out after #{timeout_seconds} seconds"
97
+ rescue Interrupt
98
+ # Stop stuck detection thread
99
+ stuck_detection_thread&.kill
100
+
101
+ # Kill the process
102
+ begin
103
+ Process.kill("TERM", wait.pid)
104
+ rescue
105
+ nil
106
+ end
107
+
108
+ mark_failed("gemini execution was interrupted")
109
+ raise
110
+ end
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ def calculate_timeout
117
+ # Priority order for timeout calculation:
118
+ # 1. Quick mode (for testing)
119
+ # 2. Environment variable override
120
+ # 3. Adaptive timeout based on step type
121
+ # 4. Default timeout
122
+
123
+ if ENV["AIDP_QUICK_MODE"]
124
+ puts "⚡ Quick mode enabled - 2 minute timeout"
125
+ return 120
126
+ end
127
+
128
+ if ENV["AIDP_GEMINI_TIMEOUT"]
129
+ return ENV["AIDP_GEMINI_TIMEOUT"].to_i
130
+ end
131
+
132
+ # Adaptive timeout based on step type
133
+ step_timeout = get_adaptive_timeout
134
+ if step_timeout
135
+ puts "🧠 Using adaptive timeout: #{step_timeout} seconds"
136
+ return step_timeout
137
+ end
138
+
139
+ # Default timeout (5 minutes for interactive use)
140
+ puts "📋 Using default timeout: 5 minutes"
141
+ 300
142
+ end
143
+
144
+ def get_adaptive_timeout
145
+ # Try to get timeout recommendations from metrics storage
146
+ begin
147
+ require_relative "../analyze/metrics_storage"
148
+ storage = Aidp::Analyze::MetricsStorage.new(Dir.pwd)
149
+ recommendations = storage.calculate_timeout_recommendations
150
+
151
+ # Get current step name from environment or context
152
+ step_name = ENV["AIDP_CURRENT_STEP"] || "unknown"
153
+
154
+ if recommendations[step_name]
155
+ recommended = recommendations[step_name][:recommended_timeout]
156
+ # Add 20% buffer for safety
157
+ return (recommended * 1.2).ceil
158
+ end
159
+ rescue => e
160
+ puts "⚠️ Could not get adaptive timeout: #{e.message}" if ENV["AIDP_DEBUG"]
161
+ end
162
+
163
+ # Fallback timeouts based on step type patterns
164
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
165
+
166
+ case step_name
167
+ when /REPOSITORY_ANALYSIS/
168
+ 180 # 3 minutes - repository analysis can be quick
169
+ when /ARCHITECTURE_ANALYSIS/
170
+ 600 # 10 minutes - architecture analysis needs more time
171
+ when /TEST_ANALYSIS/
172
+ 300 # 5 minutes - test analysis is moderate
173
+ when /FUNCTIONALITY_ANALYSIS/
174
+ 600 # 10 minutes - functionality analysis is complex
175
+ when /DOCUMENTATION_ANALYSIS/
176
+ 300 # 5 minutes - documentation analysis is moderate
177
+ when /STATIC_ANALYSIS/
178
+ 450 # 7.5 minutes - static analysis can be intensive
179
+ when /REFACTORING_RECOMMENDATIONS/
180
+ 600 # 10 minutes - refactoring recommendations are complex
181
+ else
182
+ nil # Use default
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Aidp
6
+ module Providers
7
+ class MacOSUI < Base
8
+ def self.available?
9
+ RUBY_PLATFORM.include?("darwin")
10
+ end
11
+
12
+ def name = "macos"
13
+
14
+ def send(prompt:, session: nil)
15
+ raise "macOS UI not available on this platform" unless self.class.available?
16
+
17
+ # Use macOS UI for interactive mode
18
+ cmd = ["osascript", "-e", "display dialog \"#{prompt}\" with title \"Aidp\" buttons {\"OK\"} default button \"OK\""]
19
+ system(*cmd)
20
+ :ok
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,317 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "agent_supervisor"
4
+
5
+ module Aidp
6
+ module Providers
7
+ # Base class for providers that use the agent supervisor
8
+ class SupervisedBase
9
+ # Timeout constants are now configurable via environment variables for flexibility
10
+ DEFAULT_TIMEOUT = Integer(ENV.fetch("AIDP_DEFAULT_TIMEOUT", "300")) # 5 minutes for general operations
11
+ QUICK_MODE_TIMEOUT = Integer(ENV.fetch("AIDP_QUICK_MODE_TIMEOUT", "120")) # 2 minutes for testing
12
+ REPOSITORY_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_REPOSITORY_ANALYSIS_TIMEOUT", "180")) # 3 minutes
13
+ ARCHITECTURE_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_ARCHITECTURE_ANALYSIS_TIMEOUT", "600")) # 10 minutes
14
+ TEST_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_TEST_ANALYSIS_TIMEOUT", "300")) # 5 minutes
15
+ FUNCTIONALITY_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_FUNCTIONALITY_ANALYSIS_TIMEOUT", "600")) # 10 minutes
16
+ DOCUMENTATION_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_DOCUMENTATION_ANALYSIS_TIMEOUT", "300")) # 5 minutes
17
+ STATIC_ANALYSIS_TIMEOUT = Integer(ENV.fetch("AIDP_STATIC_ANALYSIS_TIMEOUT", "450")) # 7.5 minutes
18
+ REFACTORING_RECOMMENDATIONS_TIMEOUT = Integer(ENV.fetch("AIDP_REFACTORING_RECOMMENDATIONS_TIMEOUT", "600")) # 10 minutes
19
+ ADAPTIVE_TIMEOUT_BUFFER = Float(ENV.fetch("AIDP_ADAPTIVE_TIMEOUT_BUFFER", "1.2")) # 20% buffer for adaptive timeouts
20
+ attr_reader :name, :last_execution_result, :metrics
21
+
22
+ def initialize
23
+ @last_execution_result = nil
24
+ @metrics = {
25
+ total_executions: 0,
26
+ successful_executions: 0,
27
+ timeout_count: 0,
28
+ failure_count: 0,
29
+ average_duration: 0.0,
30
+ total_duration: 0.0
31
+ }
32
+ @job_context = nil
33
+ end
34
+
35
+ # Abstract method - must be implemented by subclasses
36
+ def command
37
+ raise NotImplementedError, "#{self.class} must implement #command"
38
+ end
39
+
40
+ # Abstract method - must be implemented by subclasses
41
+ def provider_name
42
+ raise NotImplementedError, "#{self.class} must implement #provider_name"
43
+ end
44
+
45
+ # Set job context for background execution
46
+ def set_job_context(job_id:, execution_id:, job_manager:)
47
+ @job_context = {
48
+ job_id: job_id,
49
+ execution_id: execution_id,
50
+ job_manager: job_manager
51
+ }
52
+ end
53
+
54
+ # Execute with supervision and recovery
55
+ def send(prompt:, session: nil)
56
+ timeout_seconds = calculate_timeout
57
+ debug = ENV["AIDP_DEBUG"] == "1"
58
+
59
+ log_info("Executing with #{provider_name} provider (timeout: #{timeout_seconds}s)")
60
+
61
+ # Create supervisor
62
+ supervisor = AgentSupervisor.new(
63
+ command,
64
+ timeout_seconds: timeout_seconds,
65
+ debug: debug
66
+ )
67
+
68
+ begin
69
+ # Execute with supervision
70
+ result = supervisor.execute(prompt)
71
+
72
+ # Update metrics
73
+ update_metrics(supervisor, result)
74
+
75
+ # Store result for debugging
76
+ @last_execution_result = result
77
+
78
+ if result[:success]
79
+ log_info("#{provider_name} completed successfully in #{format_duration(result[:duration])}")
80
+ result[:output]
81
+ else
82
+ handle_execution_failure(result, supervisor)
83
+ end
84
+ rescue => e
85
+ log_error("#{provider_name} execution error: #{e.message}")
86
+
87
+ # Try to kill the process if it's still running
88
+ supervisor.kill! if supervisor.active?
89
+
90
+ raise
91
+ end
92
+ end
93
+
94
+ # Get execution statistics
95
+ def stats
96
+ @metrics.dup
97
+ end
98
+
99
+ # Reset statistics
100
+ def reset_stats!
101
+ @metrics = {
102
+ total_executions: 0,
103
+ successful_executions: 0,
104
+ timeout_count: 0,
105
+ failure_count: 0,
106
+ average_duration: 0.0,
107
+ total_duration: 0.0
108
+ }
109
+ end
110
+
111
+ # Check if provider supports activity monitoring
112
+ def supports_activity_monitoring?
113
+ true # Supervised providers always support activity monitoring
114
+ end
115
+
116
+ # Get activity summary for metrics (compatibility with old interface)
117
+ def activity_summary
118
+ return {} unless @last_execution_result
119
+
120
+ {
121
+ provider: provider_name,
122
+ step_name: ENV["AIDP_CURRENT_STEP"],
123
+ start_time: @last_execution_result[:start_time],
124
+ end_time: @last_execution_result[:end_time],
125
+ duration: @last_execution_result[:duration],
126
+ final_state: @last_execution_result[:state],
127
+ stuck_detected: false, # Supervisor handles this differently
128
+ output_count: @last_execution_result[:output_count] || 0
129
+ }
130
+ end
131
+
132
+ # Compatibility methods for old activity monitoring interface
133
+ def setup_activity_monitoring(step_name, callback = nil, timeout = nil)
134
+ # No-op for supervised providers - supervisor handles this
135
+ end
136
+
137
+ def record_activity(message = nil)
138
+ # No-op for supervised providers - supervisor handles this
139
+ end
140
+
141
+ def mark_completed
142
+ # No-op for supervised providers - supervisor handles this
143
+ end
144
+
145
+ def mark_failed(message = nil)
146
+ # No-op for supervised providers - supervisor handles this
147
+ end
148
+
149
+ private
150
+
151
+ def calculate_timeout
152
+ # Priority order for timeout calculation:
153
+ # 1. Quick mode (for testing)
154
+ # 2. Environment variable override
155
+ # 3. Adaptive timeout based on step type
156
+ # 4. Default timeout
157
+
158
+ if ENV["AIDP_QUICK_MODE"]
159
+ log_info("Quick mode enabled - #{QUICK_MODE_TIMEOUT / 60} minute timeout")
160
+ return QUICK_MODE_TIMEOUT
161
+ end
162
+
163
+ provider_timeout_var = "AIDP_#{provider_name.upcase}_TIMEOUT"
164
+ if ENV[provider_timeout_var]
165
+ return ENV[provider_timeout_var].to_i
166
+ end
167
+
168
+ # Adaptive timeout based on step type
169
+ step_timeout = get_adaptive_timeout
170
+ if step_timeout
171
+ log_info("Using adaptive timeout: #{step_timeout} seconds")
172
+ return step_timeout
173
+ end
174
+
175
+ # Default timeout for interactive use
176
+ log_info("Using default timeout: #{DEFAULT_TIMEOUT / 60} minutes")
177
+ DEFAULT_TIMEOUT
178
+ end
179
+
180
+ def get_adaptive_timeout
181
+ # Try to get timeout recommendations from metrics storage
182
+ begin
183
+ require_relative "../analyze/metrics_storage"
184
+ storage = Aidp::Analyze::MetricsStorage.new(Dir.pwd)
185
+ recommendations = storage.calculate_timeout_recommendations
186
+
187
+ # Get current step name from environment or context
188
+ step_name = ENV["AIDP_CURRENT_STEP"] || "unknown"
189
+
190
+ if recommendations[step_name]
191
+ recommended = recommendations[step_name][:recommended_timeout]
192
+ # Add buffer for safety
193
+ return (recommended * ADAPTIVE_TIMEOUT_BUFFER).ceil
194
+ end
195
+ rescue => e
196
+ log_warning("Could not get adaptive timeout: #{e.message}") if ENV["AIDP_DEBUG"]
197
+ end
198
+
199
+ # Fallback timeouts based on step type patterns
200
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
201
+
202
+ case step_name
203
+ when /REPOSITORY_ANALYSIS/
204
+ REPOSITORY_ANALYSIS_TIMEOUT
205
+ when /ARCHITECTURE_ANALYSIS/
206
+ ARCHITECTURE_ANALYSIS_TIMEOUT
207
+ when /TEST_ANALYSIS/
208
+ TEST_ANALYSIS_TIMEOUT
209
+ when /FUNCTIONALITY_ANALYSIS/
210
+ FUNCTIONALITY_ANALYSIS_TIMEOUT
211
+ when /DOCUMENTATION_ANALYSIS/
212
+ DOCUMENTATION_ANALYSIS_TIMEOUT
213
+ when /STATIC_ANALYSIS/
214
+ STATIC_ANALYSIS_TIMEOUT
215
+ when /REFACTORING_RECOMMENDATIONS/
216
+ REFACTORING_RECOMMENDATIONS_TIMEOUT
217
+ else
218
+ nil # Use default
219
+ end
220
+ end
221
+
222
+ def update_metrics(supervisor, result)
223
+ @metrics[:total_executions] += 1
224
+ @metrics[:total_duration] += supervisor.duration
225
+ @metrics[:average_duration] = @metrics[:total_duration] / @metrics[:total_executions]
226
+
227
+ case result[:state]
228
+ when :completed
229
+ @metrics[:successful_executions] += 1
230
+ when :timeout
231
+ @metrics[:timeout_count] += 1
232
+ when :failed, :killed
233
+ @metrics[:failure_count] += 1
234
+ end
235
+
236
+ # Log metrics update if in job context
237
+ if @job_context
238
+ @job_context[:job_manager].log_message(
239
+ @job_context[:job_id],
240
+ @job_context[:execution_id],
241
+ "Updated execution metrics",
242
+ "debug",
243
+ @metrics
244
+ )
245
+ end
246
+ end
247
+
248
+ def handle_execution_failure(result, supervisor)
249
+ case result[:reason]
250
+ when "user_aborted"
251
+ message = "#{provider_name} was aborted by user after #{format_duration(result[:duration])}"
252
+ log_error(message)
253
+ raise Interrupt, message
254
+ when "non_zero_exit"
255
+ error_msg = result[:error_output].empty? ? "Unknown error" : result[:error_output].strip
256
+ message = "#{provider_name} failed with exit code #{result[:exit_code]}: #{error_msg}"
257
+ log_error(message)
258
+ raise message
259
+ else
260
+ message = "#{provider_name} failed: #{result[:reason] || "Unknown error"}"
261
+ log_error(message)
262
+ raise message
263
+ end
264
+ end
265
+
266
+ def format_duration(seconds)
267
+ minutes = (seconds / 60).to_i
268
+ secs = (seconds % 60).to_i
269
+
270
+ if minutes > 0
271
+ "#{minutes}m #{secs}s"
272
+ else
273
+ "#{secs}s"
274
+ end
275
+ end
276
+
277
+ def log_info(message)
278
+ if @job_context
279
+ @job_context[:job_manager].log_message(
280
+ @job_context[:job_id],
281
+ @job_context[:execution_id],
282
+ message,
283
+ "info"
284
+ )
285
+ else
286
+ puts message
287
+ end
288
+ end
289
+
290
+ def log_warning(message)
291
+ if @job_context
292
+ @job_context[:job_manager].log_message(
293
+ @job_context[:job_id],
294
+ @job_context[:execution_id],
295
+ message,
296
+ "warning"
297
+ )
298
+ else
299
+ puts "⚠️ #{message}"
300
+ end
301
+ end
302
+
303
+ def log_error(message)
304
+ if @job_context
305
+ @job_context[:job_manager].log_message(
306
+ @job_context[:job_id],
307
+ @job_context[:execution_id],
308
+ message,
309
+ "error"
310
+ )
311
+ else
312
+ puts "❌ #{message}"
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "supervised_base"
4
+ require_relative "../util"
5
+
6
+ module Aidp
7
+ module Providers
8
+ class SupervisedCursor < SupervisedBase
9
+ def self.available?
10
+ !!Aidp::Util.which("cursor-agent")
11
+ end
12
+
13
+ def provider_name
14
+ "cursor"
15
+ end
16
+
17
+ def command
18
+ ["cursor-agent", "-p"]
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/aidp/sync.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Aidp
6
+ # Synchronization utilities
7
+ class Sync
8
+ def self.ensure_workspace_sync
9
+ # Placeholder for workspace synchronization logic
10
+ true
11
+ end
12
+ end
13
+ end
data/lib/aidp/util.rb ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Aidp
6
+ # Utility functions shared between execute and analyze modes
7
+ class Util
8
+ def self.which(cmd)
9
+ exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
10
+ ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
11
+ exts.each do |ext|
12
+ exe = File.join(path, "#{cmd}#{ext}")
13
+ return exe if File.executable?(exe) && !File.directory?(exe)
14
+ end
15
+ end
16
+ nil
17
+ end
18
+
19
+ def self.ensure_dirs(output_files, project_dir)
20
+ output_files.each do |file|
21
+ dir = File.dirname(File.join(project_dir, file))
22
+ FileUtils.mkdir_p(dir) unless dir == "."
23
+ end
24
+ end
25
+
26
+ def self.safe_file_write(path, content)
27
+ FileUtils.mkdir_p(File.dirname(path))
28
+ File.write(path, content)
29
+ end
30
+
31
+ def self.project_root?(dir = Dir.pwd)
32
+ File.exist?(File.join(dir, ".git")) ||
33
+ File.exist?(File.join(dir, "package.json")) ||
34
+ File.exist?(File.join(dir, "Gemfile")) ||
35
+ File.exist?(File.join(dir, "pom.xml")) ||
36
+ File.exist?(File.join(dir, "build.gradle"))
37
+ end
38
+ end
39
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- module Shared
5
- VERSION = "0.1.0"
6
- end
4
+ VERSION = "0.5.0"
7
5
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "digest"
5
+
6
+ module Aidp
7
+ # Workspace management utilities
8
+ class Workspace
9
+ def self.current
10
+ Dir.pwd
11
+ end
12
+
13
+ def self.ensure_project_root
14
+ unless Aidp::Util.project_root?
15
+ raise "Not in a project root directory. Please run from a directory with .git, package.json, Gemfile, etc."
16
+ end
17
+ end
18
+ end
19
+ end