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.
- checksums.yaml +4 -4
- data/README.md +59 -4
- data/bin/aidp +2 -2
- data/lib/aidp/analyze/agent_personas.rb +1 -1
- data/lib/aidp/analyze/data_retention_manager.rb +2 -2
- data/lib/aidp/analyze/database.rb +99 -82
- data/lib/aidp/analyze/error_handler.rb +12 -76
- data/lib/aidp/analyze/focus_guidance.rb +2 -2
- data/lib/aidp/analyze/large_analysis_progress.rb +2 -2
- data/lib/aidp/analyze/metrics_storage.rb +336 -0
- data/lib/aidp/analyze/prioritizer.rb +4 -4
- data/lib/aidp/analyze/repository_chunker.rb +15 -13
- data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
- data/lib/aidp/analyze/runner.rb +107 -191
- data/lib/aidp/analyze/steps.rb +29 -30
- data/lib/aidp/analyze/storage.rb +234 -172
- data/lib/aidp/cli/jobs_command.rb +489 -0
- data/lib/aidp/cli/terminal_io.rb +52 -0
- data/lib/aidp/cli.rb +227 -0
- data/lib/aidp/config.rb +33 -0
- data/lib/aidp/core_ext/class_attribute.rb +36 -0
- data/lib/aidp/database/pg_adapter.rb +148 -0
- data/lib/aidp/database_config.rb +69 -0
- data/lib/aidp/database_connection.rb +72 -0
- data/lib/aidp/database_migration.rb +158 -0
- data/lib/aidp/execute/runner.rb +65 -92
- data/lib/aidp/execute/steps.rb +81 -82
- data/lib/aidp/job_manager.rb +41 -0
- data/lib/aidp/jobs/base_job.rb +47 -0
- data/lib/aidp/jobs/provider_execution_job.rb +96 -0
- data/lib/aidp/project_detector.rb +117 -0
- data/lib/aidp/provider_manager.rb +25 -0
- data/lib/aidp/providers/agent_supervisor.rb +348 -0
- data/lib/aidp/providers/anthropic.rb +187 -0
- data/lib/aidp/providers/base.rb +162 -0
- data/lib/aidp/providers/cursor.rb +304 -0
- data/lib/aidp/providers/gemini.rb +187 -0
- data/lib/aidp/providers/macos_ui.rb +24 -0
- data/lib/aidp/providers/supervised_base.rb +317 -0
- data/lib/aidp/providers/supervised_cursor.rb +22 -0
- data/lib/aidp/sync.rb +13 -0
- data/lib/aidp/util.rb +39 -0
- data/lib/aidp/{shared/version.rb → version.rb} +1 -3
- data/lib/aidp/workspace.rb +19 -0
- data/lib/aidp.rb +36 -45
- data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
- metadata +89 -45
- data/lib/aidp/shared/cli.rb +0 -117
- data/lib/aidp/shared/config.rb +0 -35
- data/lib/aidp/shared/project_detector.rb +0 -119
- data/lib/aidp/shared/providers/anthropic.rb +0 -26
- data/lib/aidp/shared/providers/base.rb +0 -17
- data/lib/aidp/shared/providers/cursor.rb +0 -102
- data/lib/aidp/shared/providers/gemini.rb +0 -26
- data/lib/aidp/shared/providers/macos_ui.rb +0 -26
- data/lib/aidp/shared/sync.rb +0 -15
- data/lib/aidp/shared/util.rb +0 -41
- data/lib/aidp/shared/workspace.rb +0 -21
data/lib/aidp/cli.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
# CLI interface for both execute and analyze modes
|
7
|
+
class CLI < Thor
|
8
|
+
desc "execute [STEP]", "Run execute mode step(s)"
|
9
|
+
option :force, type: :boolean, desc: "Force execution even if dependencies are not met"
|
10
|
+
option :rerun, type: :boolean, desc: "Re-run a completed step"
|
11
|
+
option :approve, type: :string, desc: "Approve a completed execute gate step"
|
12
|
+
option :reset, type: :boolean, desc: "Reset execute mode progress"
|
13
|
+
def execute(project_dir = Dir.pwd, step_name = nil, custom_options = {})
|
14
|
+
# Handle reset flag
|
15
|
+
if options[:reset] || options["reset"]
|
16
|
+
progress = Aidp::Execute::Progress.new(project_dir)
|
17
|
+
progress.reset
|
18
|
+
puts "🔄 Reset execute mode progress"
|
19
|
+
return {status: "success", message: "Progress reset"}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Handle approve flag
|
23
|
+
if options[:approve] || options["approve"]
|
24
|
+
step_name = options[:approve] || options["approve"]
|
25
|
+
progress = Aidp::Execute::Progress.new(project_dir)
|
26
|
+
progress.mark_step_completed(step_name)
|
27
|
+
puts "✅ Approved execute step: #{step_name}"
|
28
|
+
return {status: "success", step: step_name}
|
29
|
+
end
|
30
|
+
|
31
|
+
if step_name
|
32
|
+
runner = Aidp::Execute::Runner.new(project_dir)
|
33
|
+
# Merge Thor options with custom options
|
34
|
+
all_options = options.merge(custom_options)
|
35
|
+
runner.run_step(step_name, all_options)
|
36
|
+
else
|
37
|
+
puts "Available execute steps:"
|
38
|
+
Aidp::Execute::Steps::SPEC.keys.each { |step| puts " - #{step}" }
|
39
|
+
progress = Aidp::Execute::Progress.new(project_dir)
|
40
|
+
next_step = progress.next_step
|
41
|
+
{status: "success", message: "Available steps listed", next_step: next_step}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "analyze [STEP]", "Run analyze mode step(s)"
|
46
|
+
long_desc <<~DESC
|
47
|
+
Run analyze mode steps. STEP can be:
|
48
|
+
- A full step name (e.g., 01_REPOSITORY_ANALYSIS)
|
49
|
+
- A step number (e.g., 01, 02, 03)
|
50
|
+
- 'next' to run the next unfinished step
|
51
|
+
- 'current' to run the current step
|
52
|
+
- Empty to list available steps
|
53
|
+
DESC
|
54
|
+
option :force, type: :boolean, desc: "Force execution even if dependencies are not met"
|
55
|
+
option :rerun, type: :boolean, desc: "Re-run a completed step"
|
56
|
+
option :background, type: :boolean, desc: "Run analysis in background jobs (requires database setup)"
|
57
|
+
option :approve, type: :string, desc: "Approve a completed analyze gate step"
|
58
|
+
option :reset, type: :boolean, desc: "Reset analyze mode progress"
|
59
|
+
def analyze(*args)
|
60
|
+
# Handle reset flag
|
61
|
+
if options[:reset] || options["reset"]
|
62
|
+
project_dir = Dir.pwd
|
63
|
+
progress = Aidp::Analyze::Progress.new(project_dir)
|
64
|
+
progress.reset
|
65
|
+
puts "🔄 Reset analyze mode progress"
|
66
|
+
return {status: "success", message: "Progress reset"}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Handle approve flag
|
70
|
+
if options[:approve] || options["approve"]
|
71
|
+
project_dir = Dir.pwd
|
72
|
+
step_name = options[:approve] || options["approve"]
|
73
|
+
progress = Aidp::Analyze::Progress.new(project_dir)
|
74
|
+
progress.mark_step_completed(step_name)
|
75
|
+
puts "✅ Approved analyze step: #{step_name}"
|
76
|
+
return {status: "success", step: step_name}
|
77
|
+
end
|
78
|
+
|
79
|
+
# Handle both old and new calling patterns for backwards compatibility
|
80
|
+
case args.length
|
81
|
+
when 0
|
82
|
+
# analyze() - list steps
|
83
|
+
project_dir = Dir.pwd
|
84
|
+
step_name = nil
|
85
|
+
custom_options = {}
|
86
|
+
when 1
|
87
|
+
# analyze(step_name) - new CLI pattern
|
88
|
+
if args[0].is_a?(String) && Dir.exist?(args[0])
|
89
|
+
# analyze(project_dir) - old test pattern
|
90
|
+
project_dir = args[0]
|
91
|
+
step_name = nil
|
92
|
+
else
|
93
|
+
# analyze(step_name) - new CLI pattern
|
94
|
+
project_dir = Dir.pwd
|
95
|
+
step_name = args[0]
|
96
|
+
end
|
97
|
+
custom_options = {}
|
98
|
+
when 2
|
99
|
+
# analyze(project_dir, step_name) - old test pattern
|
100
|
+
# or analyze(step_name, options) - new CLI pattern
|
101
|
+
if Dir.exist?(args[0])
|
102
|
+
# analyze(project_dir, step_name)
|
103
|
+
project_dir = args[0]
|
104
|
+
step_name = args[1]
|
105
|
+
custom_options = {}
|
106
|
+
else
|
107
|
+
# analyze(step_name, options)
|
108
|
+
project_dir = Dir.pwd
|
109
|
+
step_name = args[0]
|
110
|
+
custom_options = args[1] || {}
|
111
|
+
end
|
112
|
+
when 3
|
113
|
+
# analyze(project_dir, step_name, options) - old test pattern
|
114
|
+
project_dir = args[0]
|
115
|
+
step_name = args[1]
|
116
|
+
custom_options = args[2] || {}
|
117
|
+
else
|
118
|
+
raise ArgumentError, "Wrong number of arguments (given #{args.length}, expected 0..3)"
|
119
|
+
end
|
120
|
+
|
121
|
+
progress = Aidp::Analyze::Progress.new(project_dir)
|
122
|
+
|
123
|
+
if step_name
|
124
|
+
# Resolve the step name
|
125
|
+
resolved_step = resolve_analyze_step(step_name, progress)
|
126
|
+
|
127
|
+
if resolved_step
|
128
|
+
runner = Aidp::Analyze::Runner.new(project_dir)
|
129
|
+
# Merge Thor options with custom options
|
130
|
+
all_options = options.merge(custom_options)
|
131
|
+
result = runner.run_step(resolved_step, all_options)
|
132
|
+
|
133
|
+
# Display the result
|
134
|
+
if result[:status] == "completed"
|
135
|
+
puts "✅ Step '#{resolved_step}' completed successfully"
|
136
|
+
puts " Provider: #{result[:provider]}"
|
137
|
+
puts " Message: #{result[:message]}" if result[:message]
|
138
|
+
elsif result[:status] == "error"
|
139
|
+
puts "❌ Step '#{resolved_step}' failed"
|
140
|
+
puts " Error: #{result[:error]}" if result[:error]
|
141
|
+
end
|
142
|
+
|
143
|
+
result
|
144
|
+
else
|
145
|
+
puts "❌ Step '#{step_name}' not found or not available"
|
146
|
+
puts "\nAvailable steps:"
|
147
|
+
Aidp::Analyze::Steps::SPEC.keys.each_with_index do |step, index|
|
148
|
+
status = progress.step_completed?(step) ? "✅" : "⏳"
|
149
|
+
puts " #{status} #{sprintf("%02d", index + 1)}: #{step}"
|
150
|
+
end
|
151
|
+
{status: "error", message: "Step not found"}
|
152
|
+
end
|
153
|
+
else
|
154
|
+
puts "Available analyze steps:"
|
155
|
+
Aidp::Analyze::Steps::SPEC.keys.each_with_index do |step, index|
|
156
|
+
status = progress.step_completed?(step) ? "✅" : "⏳"
|
157
|
+
puts " #{status} #{sprintf("%02d", index + 1)}: #{step}"
|
158
|
+
end
|
159
|
+
|
160
|
+
next_step = progress.next_step
|
161
|
+
if next_step
|
162
|
+
puts "\n💡 Run 'aidp analyze next' or 'aidp analyze #{next_step.match(/^(\d+)/)[1]}' to run the next step"
|
163
|
+
end
|
164
|
+
|
165
|
+
{status: "success", message: "Available steps listed", next_step: next_step,
|
166
|
+
completed_steps: progress.completed_steps}
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
desc "status", "Show current progress for both modes"
|
171
|
+
def status
|
172
|
+
puts "\n📊 AI Dev Pipeline Status"
|
173
|
+
puts "=" * 50
|
174
|
+
|
175
|
+
# Execute mode status
|
176
|
+
execute_progress = Aidp::Execute::Progress.new(Dir.pwd)
|
177
|
+
puts "\n🔧 Execute Mode:"
|
178
|
+
Aidp::Execute::Steps::SPEC.keys.each do |step|
|
179
|
+
status = execute_progress.step_completed?(step) ? "✅" : "⏳"
|
180
|
+
puts " #{status} #{step}"
|
181
|
+
end
|
182
|
+
|
183
|
+
# Analyze mode status
|
184
|
+
analyze_progress = Aidp::Analyze::Progress.new(Dir.pwd)
|
185
|
+
puts "\n🔍 Analyze Mode:"
|
186
|
+
Aidp::Analyze::Steps::SPEC.keys.each do |step|
|
187
|
+
status = analyze_progress.step_completed?(step) ? "✅" : "⏳"
|
188
|
+
puts " #{status} #{step}"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
desc "jobs", "Show and manage background jobs"
|
193
|
+
def jobs
|
194
|
+
require_relative "cli/jobs_command"
|
195
|
+
command = Aidp::CLI::JobsCommand.new
|
196
|
+
command.run
|
197
|
+
end
|
198
|
+
|
199
|
+
desc "version", "Show version information"
|
200
|
+
def version
|
201
|
+
puts "Aidp version #{Aidp::VERSION}"
|
202
|
+
end
|
203
|
+
|
204
|
+
private
|
205
|
+
|
206
|
+
def resolve_analyze_step(step_input, progress)
|
207
|
+
step_input = step_input.to_s.downcase.strip
|
208
|
+
|
209
|
+
case step_input
|
210
|
+
when "next"
|
211
|
+
progress.next_step
|
212
|
+
when "current"
|
213
|
+
progress.current_step || progress.next_step
|
214
|
+
else
|
215
|
+
# Check if it's a step number (e.g., "01", "02", "1", "2")
|
216
|
+
if step_input.match?(/^\d{1,2}$/)
|
217
|
+
step_number = sprintf("%02d", step_input.to_i)
|
218
|
+
# Find step that starts with this number
|
219
|
+
Aidp::Analyze::Steps::SPEC.keys.find { |step| step.start_with?(step_number) }
|
220
|
+
else
|
221
|
+
# Check if it's a full step name (case insensitive)
|
222
|
+
Aidp::Analyze::Steps::SPEC.keys.find { |step| step.downcase == step_input }
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
data/lib/aidp/config.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
# Configuration management for both execute and analyze modes
|
7
|
+
class Config
|
8
|
+
def self.load(project_dir = Dir.pwd)
|
9
|
+
config_file = File.join(project_dir, ".aidp.yml")
|
10
|
+
if File.exist?(config_file)
|
11
|
+
YAML.load_file(config_file) || {}
|
12
|
+
else
|
13
|
+
{}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.templates_root
|
18
|
+
File.join(Dir.pwd, "templates")
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.analyze_templates_root
|
22
|
+
File.join(Dir.pwd, "templates", "ANALYZE")
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.execute_templates_root
|
26
|
+
File.join(Dir.pwd, "templates", "EXECUTE")
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.common_templates_root
|
30
|
+
File.join(Dir.pwd, "templates", "COMMON")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aidp
|
4
|
+
module CoreExt
|
5
|
+
module ClassAttribute
|
6
|
+
def class_attribute(*attrs)
|
7
|
+
attrs.each do |name|
|
8
|
+
# Define class instance variable
|
9
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
10
|
+
@#{name} = nil
|
11
|
+
|
12
|
+
def self.#{name}
|
13
|
+
return @#{name} if defined?(@#{name})
|
14
|
+
return superclass.#{name} if superclass.respond_to?(:#{name})
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.#{name}=(val)
|
19
|
+
@#{name} = val
|
20
|
+
end
|
21
|
+
|
22
|
+
def #{name}
|
23
|
+
self.class.#{name}
|
24
|
+
end
|
25
|
+
|
26
|
+
def #{name}=(val)
|
27
|
+
raise "#{name} is a class attribute, cannot be set on instance"
|
28
|
+
end
|
29
|
+
RUBY
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
Class.include Aidp::CoreExt::ClassAttribute
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aidp
|
4
|
+
module Database
|
5
|
+
class PgAdapter
|
6
|
+
def initialize(connection)
|
7
|
+
@connection = connection
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute(sql, params = [])
|
11
|
+
result = @connection.exec_params(sql, params)
|
12
|
+
result.to_a.map { |row| row.transform_keys(&:to_sym) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def in_transaction?
|
16
|
+
@connection.transaction_status != PG::PQTRANS_IDLE
|
17
|
+
end
|
18
|
+
|
19
|
+
def checkout
|
20
|
+
yield self
|
21
|
+
end
|
22
|
+
|
23
|
+
def after_commit
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
|
27
|
+
def server_version
|
28
|
+
@connection.server_version
|
29
|
+
end
|
30
|
+
|
31
|
+
def transaction_status
|
32
|
+
@connection.transaction_status
|
33
|
+
end
|
34
|
+
|
35
|
+
def transaction
|
36
|
+
@connection.transaction do
|
37
|
+
yield
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def quote_table_name(name)
|
42
|
+
"\"#{name}\""
|
43
|
+
end
|
44
|
+
|
45
|
+
def quote_identifier(name)
|
46
|
+
"\"#{name}\""
|
47
|
+
end
|
48
|
+
|
49
|
+
def quote_string(string)
|
50
|
+
"'#{string.gsub("'", "''")}'"
|
51
|
+
end
|
52
|
+
|
53
|
+
def quote_date(date)
|
54
|
+
date.strftime("%Y-%m-%d")
|
55
|
+
end
|
56
|
+
|
57
|
+
def quote_time(time)
|
58
|
+
time.strftime("%Y-%m-%d %H:%M:%S.%6N %z")
|
59
|
+
end
|
60
|
+
|
61
|
+
# Additional methods required by Que
|
62
|
+
def async_connection
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def wait_for_notify(timeout = nil)
|
67
|
+
@connection.wait_for_notify(timeout)
|
68
|
+
end
|
69
|
+
|
70
|
+
def listen(channel)
|
71
|
+
@connection.exec("LISTEN #{quote_identifier(channel)}")
|
72
|
+
end
|
73
|
+
|
74
|
+
def unlisten(channel)
|
75
|
+
@connection.exec("UNLISTEN #{quote_identifier(channel)}")
|
76
|
+
end
|
77
|
+
|
78
|
+
def unlisten_all
|
79
|
+
@connection.exec("UNLISTEN *")
|
80
|
+
end
|
81
|
+
|
82
|
+
def notifications
|
83
|
+
@connection.notifications
|
84
|
+
end
|
85
|
+
|
86
|
+
def reset
|
87
|
+
@connection.reset
|
88
|
+
end
|
89
|
+
|
90
|
+
def type_map_for_queries
|
91
|
+
@connection.type_map_for_queries
|
92
|
+
end
|
93
|
+
|
94
|
+
def type_map_for_results
|
95
|
+
@connection.type_map_for_results
|
96
|
+
end
|
97
|
+
|
98
|
+
# Additional methods for Que compatibility
|
99
|
+
def adapter_name
|
100
|
+
"pg"
|
101
|
+
end
|
102
|
+
|
103
|
+
def active?
|
104
|
+
@connection.status == PG::CONNECTION_OK
|
105
|
+
end
|
106
|
+
|
107
|
+
def disconnect!
|
108
|
+
@connection.close
|
109
|
+
end
|
110
|
+
|
111
|
+
def reconnect!
|
112
|
+
@connection.reset
|
113
|
+
end
|
114
|
+
|
115
|
+
def raw_connection
|
116
|
+
@connection
|
117
|
+
end
|
118
|
+
|
119
|
+
def schema_search_path
|
120
|
+
execute("SHOW search_path")[0][:search_path]
|
121
|
+
end
|
122
|
+
|
123
|
+
def schema_search_path=(path)
|
124
|
+
execute("SET search_path TO #{path}")
|
125
|
+
end
|
126
|
+
|
127
|
+
def table_exists?(name)
|
128
|
+
execute(<<~SQL, [name]).any?
|
129
|
+
SELECT 1
|
130
|
+
FROM pg_tables
|
131
|
+
WHERE tablename = $1
|
132
|
+
SQL
|
133
|
+
end
|
134
|
+
|
135
|
+
def advisory_lock(id)
|
136
|
+
execute("SELECT pg_advisory_lock($1)", [id])
|
137
|
+
end
|
138
|
+
|
139
|
+
def advisory_unlock(id)
|
140
|
+
execute("SELECT pg_advisory_unlock($1)", [id])
|
141
|
+
end
|
142
|
+
|
143
|
+
def advisory_unlock_all
|
144
|
+
execute("SELECT pg_advisory_unlock_all()")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module Aidp
|
7
|
+
class DatabaseConfig
|
8
|
+
DEFAULT_CONFIG = {
|
9
|
+
"database" => {
|
10
|
+
"adapter" => "postgresql",
|
11
|
+
"host" => "localhost",
|
12
|
+
"port" => 5432,
|
13
|
+
"database" => "aidp",
|
14
|
+
"username" => ENV["USER"],
|
15
|
+
"password" => nil,
|
16
|
+
"pool" => 5,
|
17
|
+
"timeout" => 5000
|
18
|
+
}
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
def self.load(project_dir = Dir.pwd)
|
22
|
+
new(project_dir).load
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(project_dir)
|
26
|
+
@project_dir = project_dir
|
27
|
+
@config_file = File.join(project_dir, ".aidp-config.yml")
|
28
|
+
end
|
29
|
+
|
30
|
+
def load
|
31
|
+
ensure_config_exists
|
32
|
+
config = YAML.load_file(@config_file)
|
33
|
+
validate_config(config)
|
34
|
+
config["database"]
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def ensure_config_exists
|
40
|
+
return if File.exist?(@config_file)
|
41
|
+
|
42
|
+
# Create config directory if it doesn't exist
|
43
|
+
FileUtils.mkdir_p(File.dirname(@config_file))
|
44
|
+
|
45
|
+
# Write default config
|
46
|
+
File.write(@config_file, YAML.dump(DEFAULT_CONFIG))
|
47
|
+
|
48
|
+
puts "Created default database configuration at #{@config_file}"
|
49
|
+
puts "Please update the configuration with your database settings"
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_config(config)
|
53
|
+
unless config.is_a?(Hash) && config["database"].is_a?(Hash)
|
54
|
+
raise "Invalid configuration format in #{@config_file}"
|
55
|
+
end
|
56
|
+
|
57
|
+
required_keys = %w[adapter host port database username]
|
58
|
+
missing_keys = required_keys - config["database"].keys
|
59
|
+
|
60
|
+
unless missing_keys.empty?
|
61
|
+
raise "Missing required configuration keys: #{missing_keys.join(", ")}"
|
62
|
+
end
|
63
|
+
|
64
|
+
unless config["database"]["adapter"] == "postgresql"
|
65
|
+
raise "Only PostgreSQL is supported as a database adapter"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pg"
|
4
|
+
require "que"
|
5
|
+
require "sequel"
|
6
|
+
|
7
|
+
module Aidp
|
8
|
+
class DatabaseConnection
|
9
|
+
class << self
|
10
|
+
def initialize_mutex
|
11
|
+
@mutex ||= Mutex.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def establish_connection
|
15
|
+
initialize_mutex
|
16
|
+
@mutex.synchronize do
|
17
|
+
# Return existing connection if already established
|
18
|
+
return @connection if @connection && !@connection.finished?
|
19
|
+
@connection = PG.connect(connection_params)
|
20
|
+
@sequel_db = Sequel.connect(
|
21
|
+
adapter: "postgres",
|
22
|
+
host: ENV["AIDP_DB_HOST"] || "localhost",
|
23
|
+
port: (ENV["AIDP_DB_PORT"] || 5432).to_i,
|
24
|
+
database: ENV["AIDP_DB_NAME"] || "aidp",
|
25
|
+
user: ENV["AIDP_DB_USER"] || ENV["USER"],
|
26
|
+
password: ENV["AIDP_DB_PASSWORD"]
|
27
|
+
)
|
28
|
+
Que.connection = @sequel_db
|
29
|
+
Que.migrate!(version: Que::Migrations::CURRENT_VERSION)
|
30
|
+
@connection
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def connection
|
35
|
+
return @connection if @connection && !@connection.finished?
|
36
|
+
establish_connection
|
37
|
+
end
|
38
|
+
|
39
|
+
def disconnect
|
40
|
+
initialize_mutex
|
41
|
+
@mutex.synchronize do
|
42
|
+
return unless @connection
|
43
|
+
|
44
|
+
# Safely disconnect in reverse order
|
45
|
+
begin
|
46
|
+
Que.connection = nil
|
47
|
+
rescue => e
|
48
|
+
# Log but don't fail on Que disconnection issues
|
49
|
+
puts "Warning: Error setting Que.connection to nil: #{e.message}" if ENV["AIDP_DEBUG"]
|
50
|
+
end
|
51
|
+
|
52
|
+
@sequel_db&.disconnect
|
53
|
+
@connection&.close
|
54
|
+
@connection = nil
|
55
|
+
@sequel_db = nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def connection_params
|
62
|
+
{
|
63
|
+
host: ENV["AIDP_DB_HOST"] || "localhost",
|
64
|
+
port: (ENV["AIDP_DB_PORT"] || 5432).to_i,
|
65
|
+
dbname: ENV["AIDP_DB_NAME"] || "aidp",
|
66
|
+
user: ENV["AIDP_DB_USER"] || ENV["USER"],
|
67
|
+
password: ENV["AIDP_DB_PASSWORD"]
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|