aidp 0.3.0 → 0.7.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 +191 -5
- data/lib/aidp/analysis/kb_inspector.rb +456 -0
- data/lib/aidp/analysis/seams.rb +188 -0
- data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +493 -0
- data/lib/aidp/analysis/tree_sitter_scan.rb +703 -0
- data/lib/aidp/analyze/agent_personas.rb +1 -1
- data/lib/aidp/analyze/agent_tool_executor.rb +5 -11
- data/lib/aidp/analyze/data_retention_manager.rb +0 -5
- data/lib/aidp/analyze/database.rb +99 -82
- data/lib/aidp/analyze/error_handler.rb +12 -79
- data/lib/aidp/analyze/export_manager.rb +0 -7
- data/lib/aidp/analyze/focus_guidance.rb +2 -2
- data/lib/aidp/analyze/incremental_analyzer.rb +1 -11
- data/lib/aidp/analyze/large_analysis_progress.rb +0 -5
- data/lib/aidp/analyze/memory_manager.rb +34 -60
- data/lib/aidp/analyze/metrics_storage.rb +336 -0
- data/lib/aidp/analyze/parallel_processor.rb +0 -6
- data/lib/aidp/analyze/performance_optimizer.rb +0 -3
- data/lib/aidp/analyze/prioritizer.rb +2 -2
- data/lib/aidp/analyze/repository_chunker.rb +14 -21
- data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
- data/lib/aidp/analyze/runner.rb +107 -191
- data/lib/aidp/analyze/steps.rb +35 -30
- data/lib/aidp/analyze/storage.rb +233 -178
- data/lib/aidp/analyze/tool_configuration.rb +21 -36
- data/lib/aidp/cli/jobs_command.rb +489 -0
- data/lib/aidp/cli/terminal_io.rb +52 -0
- data/lib/aidp/cli.rb +160 -45
- 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/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 +45 -0
- data/lib/aidp/jobs/provider_execution_job.rb +83 -0
- data/lib/aidp/provider_manager.rb +25 -0
- data/lib/aidp/providers/agent_supervisor.rb +348 -0
- data/lib/aidp/providers/anthropic.rb +160 -3
- data/lib/aidp/providers/base.rb +153 -6
- data/lib/aidp/providers/cursor.rb +245 -43
- data/lib/aidp/providers/gemini.rb +164 -3
- data/lib/aidp/providers/supervised_base.rb +317 -0
- data/lib/aidp/providers/supervised_cursor.rb +22 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +31 -34
- data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
- data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
- metadata +91 -36
@@ -251,32 +251,27 @@ module Aidp
|
|
251
251
|
def import_config(file_path, scope = :project)
|
252
252
|
return false unless File.exist?(file_path)
|
253
253
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
end
|
263
|
-
|
264
|
-
case scope
|
265
|
-
when :project
|
266
|
-
@project_config = config_data
|
267
|
-
save_project_config
|
268
|
-
when :user
|
269
|
-
@user_config = config_data
|
270
|
-
save_user_config
|
271
|
-
else
|
272
|
-
raise ArgumentError, "Invalid scope: #{scope}"
|
273
|
-
end
|
254
|
+
config_data = case File.extname(file_path)
|
255
|
+
when ".yml", ".yaml"
|
256
|
+
YAML.load_file(file_path)
|
257
|
+
when ".json"
|
258
|
+
JSON.parse(File.read(file_path))
|
259
|
+
else
|
260
|
+
raise ArgumentError, "Unsupported file format: #{File.extname(file_path)}"
|
261
|
+
end
|
274
262
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
263
|
+
case scope
|
264
|
+
when :project
|
265
|
+
@project_config = config_data
|
266
|
+
save_project_config
|
267
|
+
when :user
|
268
|
+
@user_config = config_data
|
269
|
+
save_user_config
|
270
|
+
else
|
271
|
+
raise ArgumentError, "Invalid scope: #{scope}"
|
279
272
|
end
|
273
|
+
|
274
|
+
true
|
280
275
|
end
|
281
276
|
|
282
277
|
# Validate configuration
|
@@ -297,24 +292,14 @@ module Aidp
|
|
297
292
|
def load_user_config
|
298
293
|
return {} unless File.exist?(USER_CONFIG_FILE)
|
299
294
|
|
300
|
-
|
301
|
-
YAML.load_file(USER_CONFIG_FILE) || {}
|
302
|
-
rescue => e
|
303
|
-
warn "Failed to load user config: #{e.message}"
|
304
|
-
{}
|
305
|
-
end
|
295
|
+
YAML.load_file(USER_CONFIG_FILE) || {}
|
306
296
|
end
|
307
297
|
|
308
298
|
def load_project_config
|
309
299
|
config_path = project_config_path
|
310
300
|
return {} unless File.exist?(config_path)
|
311
301
|
|
312
|
-
|
313
|
-
YAML.load_file(config_path) || {}
|
314
|
-
rescue => e
|
315
|
-
warn "Failed to load project config: #{e.message}"
|
316
|
-
{}
|
317
|
-
end
|
302
|
+
YAML.load_file(config_path) || {}
|
318
303
|
end
|
319
304
|
|
320
305
|
def project_config_path
|
@@ -0,0 +1,489 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-cursor"
|
4
|
+
require "tty-screen"
|
5
|
+
require "tty-table"
|
6
|
+
require "io/console"
|
7
|
+
require "que"
|
8
|
+
require "json"
|
9
|
+
require_relative "terminal_io"
|
10
|
+
|
11
|
+
module Aidp
|
12
|
+
class CLI
|
13
|
+
class JobsCommand
|
14
|
+
def initialize(input: $stdin, output: $stdout)
|
15
|
+
@io = TerminalIO.new(input, output)
|
16
|
+
@cursor = TTY::Cursor
|
17
|
+
@screen_width = TTY::Screen.width
|
18
|
+
@screen_height = TTY::Screen.height
|
19
|
+
@running = true
|
20
|
+
@view_mode = :list
|
21
|
+
@selected_job_id = nil
|
22
|
+
@jobs_displayed = false # Track if we've displayed jobs in interactive mode
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
# Initialize Que connection
|
27
|
+
setup_database_connection
|
28
|
+
|
29
|
+
# Start the UI loop with timeout
|
30
|
+
Timeout.timeout(60) do
|
31
|
+
while @running
|
32
|
+
case @view_mode
|
33
|
+
when :list
|
34
|
+
result = render_job_list
|
35
|
+
if result == :exit
|
36
|
+
# Exit immediately when no jobs are found
|
37
|
+
break
|
38
|
+
end
|
39
|
+
handle_input
|
40
|
+
sleep_for_refresh unless @running
|
41
|
+
when :details
|
42
|
+
render_job_details
|
43
|
+
handle_input
|
44
|
+
sleep_for_refresh unless @running
|
45
|
+
when :output
|
46
|
+
render_job_output
|
47
|
+
handle_input
|
48
|
+
sleep_for_refresh unless @running
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
rescue Timeout::Error
|
53
|
+
@io.puts "Command timed out"
|
54
|
+
@running = false
|
55
|
+
ensure
|
56
|
+
# Only clear screen and show cursor if we were in interactive mode
|
57
|
+
# (i.e., if we had jobs to display and were in a real terminal)
|
58
|
+
if @view_mode == :list && @jobs_displayed
|
59
|
+
@io.print @cursor.clear_screen
|
60
|
+
@io.print @cursor.show
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def setup_database_connection
|
67
|
+
# Skip database setup in test mode if we're mocking
|
68
|
+
return if ENV["RACK_ENV"] == "test" && ENV["MOCK_DATABASE"] == "true"
|
69
|
+
|
70
|
+
dbname = (ENV["RACK_ENV"] == "test") ? "aidp_test" : (ENV["AIDP_DB_NAME"] || "aidp")
|
71
|
+
|
72
|
+
# Use Sequel for connection pooling with timeout
|
73
|
+
Timeout.timeout(10) do
|
74
|
+
Que.connection = Sequel.connect(
|
75
|
+
adapter: "postgres",
|
76
|
+
host: ENV["AIDP_DB_HOST"] || "localhost",
|
77
|
+
port: ENV["AIDP_DB_PORT"] || 5432,
|
78
|
+
database: dbname,
|
79
|
+
user: ENV["AIDP_DB_USER"] || ENV["USER"],
|
80
|
+
password: ENV["AIDP_DB_PASSWORD"],
|
81
|
+
max_connections: 10,
|
82
|
+
pool_timeout: 30
|
83
|
+
)
|
84
|
+
|
85
|
+
Que.migrate!(version: Que::Migrations::CURRENT_VERSION)
|
86
|
+
end
|
87
|
+
rescue Timeout::Error
|
88
|
+
@io.puts "Database connection timed out"
|
89
|
+
raise
|
90
|
+
end
|
91
|
+
|
92
|
+
def render_job_list
|
93
|
+
jobs = fetch_jobs
|
94
|
+
|
95
|
+
if jobs.empty?
|
96
|
+
# Don't clear screen when no jobs - just show the message
|
97
|
+
@io.puts "Background Jobs"
|
98
|
+
@io.puts "-" * @screen_width
|
99
|
+
@io.puts
|
100
|
+
@io.puts "No jobs are currently running"
|
101
|
+
return :exit
|
102
|
+
else
|
103
|
+
# Clear screen and hide cursor only when we have jobs to display
|
104
|
+
@io.print(@cursor.hide)
|
105
|
+
@io.print(@cursor.clear_screen)
|
106
|
+
@io.print(@cursor.move_to(0, 0))
|
107
|
+
@jobs_displayed = true # Mark that we've displayed jobs
|
108
|
+
|
109
|
+
# Print header
|
110
|
+
@io.puts "Background Jobs"
|
111
|
+
@io.puts "-" * @screen_width
|
112
|
+
@io.puts
|
113
|
+
|
114
|
+
# Create table
|
115
|
+
table = TTY::Table.new(
|
116
|
+
header: ["ID", "Job", "Queue", "Status", "Runtime", "Error"],
|
117
|
+
rows: jobs.map do |job|
|
118
|
+
[
|
119
|
+
job[:id].to_s,
|
120
|
+
job[:job_class]&.split("::")&.last || "Unknown",
|
121
|
+
job[:queue] || "default",
|
122
|
+
job_status(job),
|
123
|
+
format_runtime(job),
|
124
|
+
truncate_error(job[:last_error_message])
|
125
|
+
]
|
126
|
+
end
|
127
|
+
)
|
128
|
+
|
129
|
+
# Render table
|
130
|
+
@io.puts table.render(:unicode, padding: [0, 1], width: @screen_width)
|
131
|
+
end
|
132
|
+
|
133
|
+
@io.puts
|
134
|
+
@io.puts "Commands: (d)etails, (o)utput, (r)etry, (k)ill, (q)uit"
|
135
|
+
:continue
|
136
|
+
end
|
137
|
+
|
138
|
+
def render_job_details
|
139
|
+
return switch_to_list unless @selected_job_id
|
140
|
+
|
141
|
+
job = fetch_job(@selected_job_id)
|
142
|
+
return switch_to_list unless job
|
143
|
+
|
144
|
+
# Clear screen and hide cursor
|
145
|
+
@io.print(@cursor.hide)
|
146
|
+
@io.print(@cursor.clear_screen)
|
147
|
+
@io.print(@cursor.move_to(0, 0))
|
148
|
+
|
149
|
+
# Print header
|
150
|
+
@io.puts "Job Details - ID: #{@selected_job_id}"
|
151
|
+
@io.puts "-" * @screen_width
|
152
|
+
@io.puts
|
153
|
+
|
154
|
+
# Print job details
|
155
|
+
@io.puts "Class: #{job[:job_class]}"
|
156
|
+
@io.puts "Queue: #{job[:queue]}"
|
157
|
+
@io.puts "Status: #{job_status(job)}"
|
158
|
+
@io.puts "Runtime: #{format_runtime(job)}"
|
159
|
+
@io.puts "Started: #{job[:run_at]}"
|
160
|
+
@io.puts "Finished: #{job[:finished_at]}"
|
161
|
+
@io.puts "Attempts: #{job[:error_count]}"
|
162
|
+
@io.puts
|
163
|
+
@io.puts "Error:" if job[:last_error_message]
|
164
|
+
@io.puts job[:last_error_message] if job[:last_error_message]
|
165
|
+
|
166
|
+
@io.puts
|
167
|
+
@io.puts "Commands: (b)ack, (o)utput, (r)etry, (k)ill, (q)uit"
|
168
|
+
end
|
169
|
+
|
170
|
+
def render_job_output
|
171
|
+
return switch_to_list unless @selected_job_id
|
172
|
+
|
173
|
+
job = fetch_job(@selected_job_id)
|
174
|
+
return switch_to_list unless job
|
175
|
+
|
176
|
+
# Clear screen and hide cursor
|
177
|
+
@io.print(@cursor.hide)
|
178
|
+
@io.print(@cursor.clear_screen)
|
179
|
+
@io.print(@cursor.move_to(0, 0))
|
180
|
+
|
181
|
+
# Print header
|
182
|
+
@io.puts "Job Output - ID: #{@selected_job_id}"
|
183
|
+
@io.puts "-" * @screen_width
|
184
|
+
@io.puts
|
185
|
+
|
186
|
+
# Get job output
|
187
|
+
output = get_job_output(@selected_job_id)
|
188
|
+
|
189
|
+
if output.empty?
|
190
|
+
@io.puts "No output available for this job."
|
191
|
+
@io.puts
|
192
|
+
@io.puts "This could mean:"
|
193
|
+
@io.puts "- The job hasn't started yet"
|
194
|
+
@io.puts "- The job is still running but hasn't produced output"
|
195
|
+
@io.puts "- The job completed without output"
|
196
|
+
else
|
197
|
+
@io.puts "Recent Output:"
|
198
|
+
@io.puts "-" * 20
|
199
|
+
@io.puts output
|
200
|
+
end
|
201
|
+
|
202
|
+
@io.puts
|
203
|
+
@io.puts "Commands: (b)ack, (r)efresh, (q)uit"
|
204
|
+
end
|
205
|
+
|
206
|
+
def handle_input
|
207
|
+
if @io.ready?
|
208
|
+
char = @io.getch
|
209
|
+
return if char.nil? || char.empty?
|
210
|
+
|
211
|
+
case char.downcase
|
212
|
+
when "q"
|
213
|
+
@running = false
|
214
|
+
when "d"
|
215
|
+
handle_details_command
|
216
|
+
when "o"
|
217
|
+
handle_output_command
|
218
|
+
when "r"
|
219
|
+
handle_retry_command
|
220
|
+
when "k"
|
221
|
+
handle_kill_command
|
222
|
+
when "b"
|
223
|
+
switch_to_list
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def handle_details_command
|
229
|
+
return unless @view_mode == :list
|
230
|
+
|
231
|
+
@io.print "Enter job ID: "
|
232
|
+
job_id = @io.gets.chomp
|
233
|
+
if job_exists?(job_id)
|
234
|
+
@selected_job_id = job_id
|
235
|
+
@view_mode = :details
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def handle_output_command
|
240
|
+
job_id = (@view_mode == :details) ? @selected_job_id : nil
|
241
|
+
|
242
|
+
unless job_id
|
243
|
+
@io.print "Enter job ID: "
|
244
|
+
job_id = @io.gets.chomp
|
245
|
+
end
|
246
|
+
|
247
|
+
if job_exists?(job_id)
|
248
|
+
@selected_job_id = job_id
|
249
|
+
@view_mode = :output
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def handle_retry_command
|
254
|
+
job_id = (@view_mode == :details) ? @selected_job_id : nil
|
255
|
+
|
256
|
+
unless job_id
|
257
|
+
@io.print "Enter job ID: "
|
258
|
+
job_id = @io.gets.chomp
|
259
|
+
end
|
260
|
+
|
261
|
+
if job_exists?(job_id)
|
262
|
+
job = fetch_job(job_id)
|
263
|
+
if job[:error_count].to_i > 0
|
264
|
+
Que.execute(
|
265
|
+
<<~SQL,
|
266
|
+
UPDATE que_jobs
|
267
|
+
SET error_count = 0,
|
268
|
+
last_error_message = NULL,
|
269
|
+
finished_at = NULL,
|
270
|
+
expired_at = NULL
|
271
|
+
WHERE id = $1
|
272
|
+
SQL
|
273
|
+
[job_id]
|
274
|
+
)
|
275
|
+
@io.puts "Job #{job_id} has been queued for retry"
|
276
|
+
else
|
277
|
+
@io.puts "Job #{job_id} has no errors to retry"
|
278
|
+
end
|
279
|
+
sleep 2
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def handle_kill_command
|
284
|
+
job_id = (@view_mode == :details) ? @selected_job_id : nil
|
285
|
+
|
286
|
+
unless job_id
|
287
|
+
@io.print "Enter job ID: "
|
288
|
+
job_id = @io.gets.chomp
|
289
|
+
end
|
290
|
+
|
291
|
+
if job_exists?(job_id)
|
292
|
+
job = fetch_job(job_id)
|
293
|
+
|
294
|
+
# Only allow killing running jobs
|
295
|
+
if job_status(job) == "running"
|
296
|
+
@io.print "Are you sure you want to kill job #{job_id}? (y/N): "
|
297
|
+
confirmation = @io.gets.chomp.downcase
|
298
|
+
|
299
|
+
if confirmation == "y" || confirmation == "yes"
|
300
|
+
kill_job(job_id)
|
301
|
+
@io.puts "Job #{job_id} has been killed"
|
302
|
+
sleep 2
|
303
|
+
else
|
304
|
+
@io.puts "Job kill cancelled"
|
305
|
+
sleep 1
|
306
|
+
end
|
307
|
+
else
|
308
|
+
@io.puts "Only running jobs can be killed. Job #{job_id} is #{job_status(job)}"
|
309
|
+
sleep 2
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
def switch_to_list
|
315
|
+
@view_mode = :list
|
316
|
+
@selected_job_id = nil
|
317
|
+
end
|
318
|
+
|
319
|
+
def fetch_jobs
|
320
|
+
# For testing, return empty array if no database connection
|
321
|
+
return [] if ENV["RACK_ENV"] == "test" && !Que.connection
|
322
|
+
return [] if ENV["RACK_ENV"] == "test" && ENV["MOCK_DATABASE"] == "true"
|
323
|
+
|
324
|
+
Timeout.timeout(10) do
|
325
|
+
Que.execute(
|
326
|
+
<<~SQL
|
327
|
+
SELECT *
|
328
|
+
FROM que_jobs
|
329
|
+
ORDER BY
|
330
|
+
CASE
|
331
|
+
WHEN finished_at IS NULL AND error_count = 0 THEN 1 -- Running
|
332
|
+
WHEN error_count > 0 THEN 2 -- Failed
|
333
|
+
ELSE 3 -- Completed
|
334
|
+
END,
|
335
|
+
id DESC
|
336
|
+
SQL
|
337
|
+
)
|
338
|
+
end
|
339
|
+
rescue Timeout::Error
|
340
|
+
@io.puts "Database query timed out"
|
341
|
+
[]
|
342
|
+
rescue Sequel::DatabaseError => e
|
343
|
+
@io.puts "Error fetching jobs: #{e.message}"
|
344
|
+
[]
|
345
|
+
end
|
346
|
+
|
347
|
+
def fetch_job(job_id)
|
348
|
+
Timeout.timeout(5) do
|
349
|
+
Que.execute("SELECT * FROM que_jobs WHERE id = $1", [job_id]).first
|
350
|
+
end
|
351
|
+
rescue Timeout::Error
|
352
|
+
@io.puts "Database query timed out"
|
353
|
+
nil
|
354
|
+
rescue Sequel::DatabaseError => e
|
355
|
+
@io.puts "Error fetching job #{job_id}: #{e.message}"
|
356
|
+
nil
|
357
|
+
end
|
358
|
+
|
359
|
+
def job_exists?(job_id)
|
360
|
+
fetch_job(job_id) != nil
|
361
|
+
end
|
362
|
+
|
363
|
+
def sleep_for_refresh
|
364
|
+
sleep 1
|
365
|
+
end
|
366
|
+
|
367
|
+
def job_status(job)
|
368
|
+
return "unknown" unless job
|
369
|
+
|
370
|
+
if job[:finished_at]
|
371
|
+
(job[:error_count].to_i > 0) ? "failed" : "completed"
|
372
|
+
else
|
373
|
+
"running"
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
def format_runtime(job)
|
378
|
+
return "unknown" unless job
|
379
|
+
|
380
|
+
if job[:finished_at] && job[:run_at]
|
381
|
+
finished_at = job[:finished_at].is_a?(Time) ? job[:finished_at] : Time.parse(job[:finished_at])
|
382
|
+
run_at = job[:run_at].is_a?(Time) ? job[:run_at] : Time.parse(job[:run_at])
|
383
|
+
duration = finished_at - run_at
|
384
|
+
minutes = (duration / 60).to_i
|
385
|
+
seconds = (duration % 60).to_i
|
386
|
+
(minutes > 0) ? "#{minutes}m #{seconds}s" : "#{seconds}s"
|
387
|
+
elsif job[:run_at]
|
388
|
+
run_at = job[:run_at].is_a?(Time) ? job[:run_at] : Time.parse(job[:run_at])
|
389
|
+
duration = Time.now - run_at
|
390
|
+
minutes = (duration / 60).to_i
|
391
|
+
seconds = (duration % 60).to_i
|
392
|
+
(minutes > 0) ? "#{minutes}m #{seconds}s" : "#{seconds}s"
|
393
|
+
else
|
394
|
+
"pending"
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
def truncate_error(error)
|
399
|
+
return nil unless error
|
400
|
+
|
401
|
+
max_length = @screen_width - 60 # Account for other columns
|
402
|
+
if error.length > max_length
|
403
|
+
"#{error[0..max_length - 4]}..."
|
404
|
+
else
|
405
|
+
error
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def get_job_output(job_id)
|
410
|
+
# Try to get output from various sources
|
411
|
+
output = []
|
412
|
+
|
413
|
+
# 1. Check if there's a result stored in analysis_results table
|
414
|
+
begin
|
415
|
+
result = Que.execute(
|
416
|
+
"SELECT data FROM analysis_results WHERE step_name = $1",
|
417
|
+
["job_#{job_id}"]
|
418
|
+
).first
|
419
|
+
|
420
|
+
if result && result["data"]
|
421
|
+
data = JSON.parse(result["data"])
|
422
|
+
output << "Result: #{data["output"]}" if data["output"]
|
423
|
+
end
|
424
|
+
rescue Sequel::DatabaseError, PG::Error => e
|
425
|
+
# Database error - table might not exist
|
426
|
+
@io.puts "Warning: Could not fetch job result: #{e.message}" if ENV["AIDP_DEBUG"]
|
427
|
+
rescue JSON::ParserError => e
|
428
|
+
# JSON parse error
|
429
|
+
@io.puts "Warning: Could not parse job result data: #{e.message}" if ENV["AIDP_DEBUG"]
|
430
|
+
end
|
431
|
+
|
432
|
+
# 2. Check for any recent log entries
|
433
|
+
begin
|
434
|
+
logs = Que.execute(
|
435
|
+
"SELECT message FROM que_jobs WHERE id = $1 AND last_error_message IS NOT NULL",
|
436
|
+
[job_id]
|
437
|
+
).first
|
438
|
+
|
439
|
+
if logs && logs["last_error_message"]
|
440
|
+
output << "Error: #{logs["last_error_message"]}"
|
441
|
+
end
|
442
|
+
rescue Sequel::DatabaseError, PG::Error => e
|
443
|
+
# Database error fetching logs - continue with diagnostic
|
444
|
+
@io.puts "Warning: Could not fetch job logs: #{e.message}" if ENV["AIDP_DEBUG"]
|
445
|
+
end
|
446
|
+
|
447
|
+
# 3. Check if job appears to be hung
|
448
|
+
job = fetch_job(job_id)
|
449
|
+
if job && job_status(job) == "running"
|
450
|
+
run_at = job[:run_at].is_a?(Time) ? job[:run_at] : Time.parse(job[:run_at])
|
451
|
+
duration = Time.now - run_at
|
452
|
+
|
453
|
+
if duration > 300 # 5 minutes
|
454
|
+
output << "⚠️ WARNING: Job has been running for #{format_duration(duration)}"
|
455
|
+
output << " This job may be hung or stuck."
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
output.join("\n")
|
460
|
+
end
|
461
|
+
|
462
|
+
def kill_job(job_id)
|
463
|
+
# Mark the job as finished with an error to stop it
|
464
|
+
Que.execute(
|
465
|
+
<<~SQL,
|
466
|
+
UPDATE que_jobs
|
467
|
+
SET finished_at = NOW(),
|
468
|
+
last_error_message = 'Job killed by user',
|
469
|
+
error_count = error_count + 1
|
470
|
+
WHERE id = $1
|
471
|
+
SQL
|
472
|
+
[job_id]
|
473
|
+
)
|
474
|
+
end
|
475
|
+
|
476
|
+
def format_duration(seconds)
|
477
|
+
minutes = (seconds / 60).to_i
|
478
|
+
hours = (minutes / 60).to_i
|
479
|
+
minutes %= 60
|
480
|
+
|
481
|
+
if hours > 0
|
482
|
+
"#{hours}h #{minutes}m"
|
483
|
+
else
|
484
|
+
"#{minutes}m"
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
class CLI
|
7
|
+
class TerminalIO
|
8
|
+
def initialize(input = $stdin, output = $stdout)
|
9
|
+
@input = input
|
10
|
+
@output = output
|
11
|
+
end
|
12
|
+
|
13
|
+
def ready?
|
14
|
+
return false if @input.closed?
|
15
|
+
return true if @input.is_a?(StringIO)
|
16
|
+
# For regular IO, we can't easily check if data is ready
|
17
|
+
# So we'll assume it's always ready for non-blocking operations
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def getch
|
22
|
+
return nil unless ready?
|
23
|
+
if @input.is_a?(StringIO)
|
24
|
+
char = @input.getc
|
25
|
+
char&.chr || ""
|
26
|
+
else
|
27
|
+
@input.getch
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def gets
|
32
|
+
@input.gets
|
33
|
+
end
|
34
|
+
|
35
|
+
def write(str)
|
36
|
+
@output.write(str)
|
37
|
+
end
|
38
|
+
|
39
|
+
def puts(str = "")
|
40
|
+
@output.puts(str)
|
41
|
+
end
|
42
|
+
|
43
|
+
def print(str)
|
44
|
+
@output.print(str)
|
45
|
+
end
|
46
|
+
|
47
|
+
def flush
|
48
|
+
@output.flush
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|