aidp 0.24.0 → 0.26.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 +72 -7
- data/lib/aidp/analyze/error_handler.rb +11 -0
- data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
- data/lib/aidp/auto_update/checkpoint.rb +178 -0
- data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
- data/lib/aidp/auto_update/coordinator.rb +204 -0
- data/lib/aidp/auto_update/errors.rb +17 -0
- data/lib/aidp/auto_update/failure_tracker.rb +162 -0
- data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
- data/lib/aidp/auto_update/update_check.rb +106 -0
- data/lib/aidp/auto_update/update_logger.rb +143 -0
- data/lib/aidp/auto_update/update_policy.rb +109 -0
- data/lib/aidp/auto_update/version_detector.rb +144 -0
- data/lib/aidp/auto_update.rb +52 -0
- data/lib/aidp/cli.rb +165 -1
- data/lib/aidp/execute/work_loop_runner.rb +225 -55
- data/lib/aidp/harness/config_loader.rb +20 -11
- data/lib/aidp/harness/config_schema.rb +80 -8
- data/lib/aidp/harness/configuration.rb +73 -2
- data/lib/aidp/harness/filter_strategy.rb +45 -0
- data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
- data/lib/aidp/harness/output_filter.rb +136 -0
- data/lib/aidp/harness/provider_factory.rb +2 -0
- data/lib/aidp/harness/provider_manager.rb +18 -3
- data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
- data/lib/aidp/harness/test_runner.rb +165 -27
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
- data/lib/aidp/logger.rb +35 -5
- data/lib/aidp/message_display.rb +56 -2
- data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
- data/lib/aidp/provider_manager.rb +2 -0
- data/lib/aidp/providers/kilocode.rb +202 -0
- data/lib/aidp/safe_directory.rb +10 -3
- data/lib/aidp/setup/provider_registry.rb +15 -0
- data/lib/aidp/setup/wizard.rb +12 -4
- data/lib/aidp/skills/composer.rb +4 -0
- data/lib/aidp/skills/loader.rb +3 -1
- data/lib/aidp/storage/csv_storage.rb +9 -3
- data/lib/aidp/storage/file_manager.rb +8 -2
- data/lib/aidp/storage/json_storage.rb +9 -3
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +106 -17
- data/lib/aidp/watch/change_request_processor.rb +659 -0
- data/lib/aidp/watch/ci_fix_processor.rb +448 -0
- data/lib/aidp/watch/plan_processor.rb +81 -8
- data/lib/aidp/watch/repository_client.rb +465 -20
- data/lib/aidp/watch/review_processor.rb +266 -0
- data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
- data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
- data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
- data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
- data/lib/aidp/watch/runner.rb +222 -0
- data/lib/aidp/watch/state_store.rb +99 -1
- data/lib/aidp/workstream_executor.rb +5 -2
- data/lib/aidp.rb +5 -0
- data/templates/aidp.yml.example +53 -0
- metadata +25 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "version_detector"
|
|
4
|
+
require_relative "checkpoint_store"
|
|
5
|
+
require_relative "update_logger"
|
|
6
|
+
require_relative "failure_tracker"
|
|
7
|
+
require_relative "update_policy"
|
|
8
|
+
require_relative "errors"
|
|
9
|
+
|
|
10
|
+
module Aidp
|
|
11
|
+
module AutoUpdate
|
|
12
|
+
# Facade for orchestrating the complete auto-update workflow
|
|
13
|
+
class Coordinator
|
|
14
|
+
attr_reader :policy, :version_detector, :checkpoint_store, :update_logger, :failure_tracker
|
|
15
|
+
|
|
16
|
+
def initialize(
|
|
17
|
+
policy:,
|
|
18
|
+
version_detector: nil,
|
|
19
|
+
checkpoint_store: nil,
|
|
20
|
+
update_logger: nil,
|
|
21
|
+
failure_tracker: nil,
|
|
22
|
+
project_dir: Dir.pwd
|
|
23
|
+
)
|
|
24
|
+
@policy = policy
|
|
25
|
+
@project_dir = project_dir
|
|
26
|
+
|
|
27
|
+
# Use provided instances or create defaults
|
|
28
|
+
@version_detector = version_detector || VersionDetector.new(policy: policy)
|
|
29
|
+
@checkpoint_store = checkpoint_store || CheckpointStore.new(project_dir: project_dir)
|
|
30
|
+
@update_logger = update_logger || UpdateLogger.new(project_dir: project_dir)
|
|
31
|
+
@failure_tracker = failure_tracker || FailureTracker.new(
|
|
32
|
+
project_dir: project_dir,
|
|
33
|
+
max_failures: policy.max_consecutive_failures
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Create coordinator from configuration
|
|
38
|
+
# @param config [Hash] Auto-update configuration from aidp.yml
|
|
39
|
+
# @param project_dir [String] Project root directory
|
|
40
|
+
# @return [Coordinator]
|
|
41
|
+
def self.from_config(config, project_dir: Dir.pwd)
|
|
42
|
+
policy = UpdatePolicy.from_config(config)
|
|
43
|
+
new(policy: policy, project_dir: project_dir)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check if update is available and allowed
|
|
47
|
+
# @return [UpdateCheck] Update check result
|
|
48
|
+
def check_for_update
|
|
49
|
+
return UpdateCheck.unavailable unless @policy.enabled
|
|
50
|
+
|
|
51
|
+
update_check = @version_detector.check_for_update
|
|
52
|
+
@update_logger.log_check(update_check)
|
|
53
|
+
update_check
|
|
54
|
+
rescue => e
|
|
55
|
+
Aidp.log_error("auto_update_coordinator", "check_failed",
|
|
56
|
+
error: e.message)
|
|
57
|
+
UpdateCheck.failed(e.message)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Initiate update process (checkpoint + exit with code 75)
|
|
61
|
+
# @param current_state [Hash] Current application state (from Watch::Runner)
|
|
62
|
+
# @return [void] (exits process with code 75)
|
|
63
|
+
# @raise [UpdateError] If updates are disabled or preconditions not met
|
|
64
|
+
# @raise [UpdateLoopError] If too many consecutive failures
|
|
65
|
+
def initiate_update(current_state)
|
|
66
|
+
raise UpdateError, "Updates disabled by configuration" unless @policy.enabled
|
|
67
|
+
|
|
68
|
+
# Check for restart loops
|
|
69
|
+
if @failure_tracker.too_many_failures?
|
|
70
|
+
@update_logger.log_restart_loop(@failure_tracker.failure_count)
|
|
71
|
+
raise UpdateLoopError, "Too many consecutive update failures (#{@failure_tracker.failure_count}/#{@policy.max_consecutive_failures})"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Verify supervisor is configured
|
|
75
|
+
unless @policy.supervised?
|
|
76
|
+
raise UpdateError, "No supervisor configured. Set auto_update.supervisor in aidp.yml"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get latest version to record in checkpoint
|
|
80
|
+
update_check = check_for_update
|
|
81
|
+
|
|
82
|
+
unless update_check.should_update?
|
|
83
|
+
Aidp.log_info("auto_update_coordinator", "no_update_needed",
|
|
84
|
+
reason: update_check.policy_reason)
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Create checkpoint from current state
|
|
89
|
+
checkpoint = build_checkpoint(current_state, update_check.available_version)
|
|
90
|
+
|
|
91
|
+
# Save checkpoint
|
|
92
|
+
unless @checkpoint_store.save_checkpoint(checkpoint)
|
|
93
|
+
raise UpdateError, "Failed to save checkpoint"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Log update initiation
|
|
97
|
+
@update_logger.log_update_initiated(checkpoint, target_version: update_check.available_version)
|
|
98
|
+
|
|
99
|
+
Aidp.log_info("auto_update_coordinator", "exiting_for_update",
|
|
100
|
+
from_version: update_check.current_version,
|
|
101
|
+
to_version: update_check.available_version,
|
|
102
|
+
checkpoint_id: checkpoint.checkpoint_id)
|
|
103
|
+
|
|
104
|
+
# Exit with special code 75 to signal supervisor to update
|
|
105
|
+
exit(75)
|
|
106
|
+
rescue UpdateError, UpdateLoopError
|
|
107
|
+
# Re-raise domain errors
|
|
108
|
+
raise
|
|
109
|
+
rescue => e
|
|
110
|
+
@failure_tracker.record_failure
|
|
111
|
+
@update_logger.log_failure(e.message)
|
|
112
|
+
Aidp.log_error("auto_update_coordinator", "initiate_failed",
|
|
113
|
+
error: e.message)
|
|
114
|
+
raise UpdateError, "Update initiation failed: #{e.message}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Restore from checkpoint after update
|
|
118
|
+
# @return [Checkpoint, nil] Restored checkpoint or nil
|
|
119
|
+
def restore_from_checkpoint
|
|
120
|
+
checkpoint = @checkpoint_store.latest_checkpoint
|
|
121
|
+
return nil unless checkpoint
|
|
122
|
+
|
|
123
|
+
Aidp.log_info("auto_update_coordinator", "restoring_checkpoint",
|
|
124
|
+
id: checkpoint.checkpoint_id,
|
|
125
|
+
created_at: checkpoint.created_at.iso8601)
|
|
126
|
+
|
|
127
|
+
# Validate checkpoint
|
|
128
|
+
unless checkpoint.valid?
|
|
129
|
+
Aidp.log_error("auto_update_coordinator", "invalid_checkpoint",
|
|
130
|
+
id: checkpoint.checkpoint_id,
|
|
131
|
+
reason: "Checksum validation failed")
|
|
132
|
+
@failure_tracker.record_failure
|
|
133
|
+
@update_logger.log_failure("Invalid checkpoint checksum", checkpoint_id: checkpoint.checkpoint_id)
|
|
134
|
+
return nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Check version compatibility
|
|
138
|
+
unless checkpoint.compatible_version?
|
|
139
|
+
Aidp.log_warn("auto_update_coordinator", "incompatible_version",
|
|
140
|
+
checkpoint_version: checkpoint.aidp_version,
|
|
141
|
+
current_version: Aidp::VERSION)
|
|
142
|
+
@failure_tracker.record_failure
|
|
143
|
+
@update_logger.log_failure(
|
|
144
|
+
"Incompatible version: checkpoint from #{checkpoint.aidp_version}, current #{Aidp::VERSION}",
|
|
145
|
+
checkpoint_id: checkpoint.checkpoint_id
|
|
146
|
+
)
|
|
147
|
+
return nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Log successful restoration
|
|
151
|
+
@update_logger.log_restore(checkpoint)
|
|
152
|
+
@update_logger.log_success(
|
|
153
|
+
from_version: checkpoint.aidp_version,
|
|
154
|
+
to_version: Aidp::VERSION
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Reset failure tracker on success
|
|
158
|
+
@failure_tracker.reset_on_success
|
|
159
|
+
|
|
160
|
+
# Delete checkpoint after successful restore
|
|
161
|
+
@checkpoint_store.delete_checkpoint(checkpoint.checkpoint_id)
|
|
162
|
+
|
|
163
|
+
checkpoint
|
|
164
|
+
rescue => e
|
|
165
|
+
@failure_tracker.record_failure
|
|
166
|
+
@update_logger.log_failure("Checkpoint restore failed: #{e.message}")
|
|
167
|
+
Aidp.log_error("auto_update_coordinator", "restore_failed",
|
|
168
|
+
error: e.message)
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Get status summary for CLI display
|
|
173
|
+
# @return [Hash] Status information
|
|
174
|
+
def status
|
|
175
|
+
update_check = check_for_update
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
enabled: @policy.enabled,
|
|
179
|
+
policy: @policy.policy,
|
|
180
|
+
supervisor: @policy.supervisor,
|
|
181
|
+
current_version: Aidp::VERSION,
|
|
182
|
+
available_version: update_check.available_version,
|
|
183
|
+
update_available: update_check.update_available,
|
|
184
|
+
update_allowed: update_check.update_allowed,
|
|
185
|
+
policy_reason: update_check.policy_reason,
|
|
186
|
+
failure_tracker: @failure_tracker.status,
|
|
187
|
+
recent_updates: @update_logger.recent_entries(limit: 5)
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
def build_checkpoint(current_state, target_version)
|
|
194
|
+
Checkpoint.new(
|
|
195
|
+
mode: current_state[:mode] || "watch",
|
|
196
|
+
watch_state: current_state[:watch_state]
|
|
197
|
+
).tap do |cp|
|
|
198
|
+
# Store target version in metadata for logging
|
|
199
|
+
cp.instance_variable_set(:@target_version, target_version)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module AutoUpdate
|
|
5
|
+
# Base error for all auto-update errors
|
|
6
|
+
class UpdateError < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Error raised when too many consecutive update failures detected
|
|
9
|
+
class UpdateLoopError < UpdateError; end
|
|
10
|
+
|
|
11
|
+
# Error raised when checkpoint is invalid or corrupted
|
|
12
|
+
class CheckpointError < UpdateError; end
|
|
13
|
+
|
|
14
|
+
# Error raised when version policy prevents update
|
|
15
|
+
class VersionPolicyError < UpdateError; end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
require_relative "../safe_directory"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
module AutoUpdate
|
|
10
|
+
# Service for tracking update failures to prevent restart loops
|
|
11
|
+
class FailureTracker
|
|
12
|
+
include Aidp::SafeDirectory
|
|
13
|
+
|
|
14
|
+
attr_reader :state_file, :max_failures
|
|
15
|
+
|
|
16
|
+
def initialize(project_dir: Dir.pwd, max_failures: 3)
|
|
17
|
+
@project_dir = project_dir
|
|
18
|
+
state_dir = File.join(project_dir, ".aidp")
|
|
19
|
+
actual_dir = safe_mkdir_p(state_dir, component_name: "FailureTracker")
|
|
20
|
+
@state_file = File.join(actual_dir, "auto_update_failures.json")
|
|
21
|
+
@max_failures = max_failures
|
|
22
|
+
@state = load_state
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Record a failure
|
|
26
|
+
def record_failure
|
|
27
|
+
@state[:failures] << {
|
|
28
|
+
timestamp: Time.now.utc.iso8601,
|
|
29
|
+
version: Aidp::VERSION
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Keep only recent failures (last hour)
|
|
33
|
+
@state[:failures].select! { |f|
|
|
34
|
+
Time.parse(f[:timestamp]) > Time.now - 3600
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
save_state
|
|
38
|
+
|
|
39
|
+
Aidp.log_warn("failure_tracker", "failure_recorded",
|
|
40
|
+
total_failures: @state[:failures].size,
|
|
41
|
+
max_failures: @max_failures)
|
|
42
|
+
rescue => e
|
|
43
|
+
Aidp.log_error("failure_tracker", "record_failure_failed",
|
|
44
|
+
error: e.message)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if too many consecutive failures have occurred
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def too_many_failures?
|
|
50
|
+
failure_count = @state[:failures].size
|
|
51
|
+
is_looping = failure_count >= @max_failures
|
|
52
|
+
|
|
53
|
+
if is_looping
|
|
54
|
+
Aidp.log_error("failure_tracker", "restart_loop_detected",
|
|
55
|
+
failure_count: failure_count,
|
|
56
|
+
max_failures: @max_failures)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
is_looping
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Reset failure count after successful operation
|
|
63
|
+
def reset_on_success
|
|
64
|
+
previous_failures = @state[:failures].size
|
|
65
|
+
|
|
66
|
+
@state[:failures] = []
|
|
67
|
+
@state[:last_success] = Time.now.utc.iso8601
|
|
68
|
+
@state[:last_success_version] = Aidp::VERSION
|
|
69
|
+
|
|
70
|
+
save_state
|
|
71
|
+
|
|
72
|
+
Aidp.log_info("failure_tracker", "reset_on_success",
|
|
73
|
+
previous_failures: previous_failures,
|
|
74
|
+
version: Aidp::VERSION)
|
|
75
|
+
rescue => e
|
|
76
|
+
Aidp.log_error("failure_tracker", "reset_failed",
|
|
77
|
+
error: e.message)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Get current failure count
|
|
81
|
+
# @return [Integer]
|
|
82
|
+
def failure_count
|
|
83
|
+
@state[:failures].size
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get time since last success
|
|
87
|
+
# @return [Integer, nil] Seconds since last success, or nil if never successful
|
|
88
|
+
def time_since_last_success
|
|
89
|
+
return nil unless @state[:last_success]
|
|
90
|
+
|
|
91
|
+
Time.now - Time.parse(@state[:last_success])
|
|
92
|
+
rescue => e
|
|
93
|
+
Aidp.log_error("failure_tracker", "time_calculation_failed",
|
|
94
|
+
error: e.message)
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get all failure timestamps
|
|
99
|
+
# @return [Array<Time>]
|
|
100
|
+
def failure_timestamps
|
|
101
|
+
@state[:failures].map { |f| Time.parse(f[:timestamp]) }
|
|
102
|
+
rescue => e
|
|
103
|
+
Aidp.log_error("failure_tracker", "timestamp_parsing_failed",
|
|
104
|
+
error: e.message)
|
|
105
|
+
[]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Manually reset failures (for CLI command or recovery)
|
|
109
|
+
def force_reset
|
|
110
|
+
Aidp.log_warn("failure_tracker", "manual_reset_triggered",
|
|
111
|
+
previous_failures: @state[:failures].size)
|
|
112
|
+
|
|
113
|
+
@state[:failures] = []
|
|
114
|
+
save_state
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get state summary for status display
|
|
118
|
+
# @return [Hash]
|
|
119
|
+
def status
|
|
120
|
+
{
|
|
121
|
+
failure_count: failure_count,
|
|
122
|
+
max_failures: @max_failures,
|
|
123
|
+
too_many_failures: too_many_failures?,
|
|
124
|
+
last_success: @state[:last_success],
|
|
125
|
+
last_success_version: @state[:last_success_version],
|
|
126
|
+
recent_failures: @state[:failures]
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def load_state
|
|
133
|
+
return default_state unless File.exist?(@state_file)
|
|
134
|
+
|
|
135
|
+
JSON.parse(File.read(@state_file), symbolize_names: true)
|
|
136
|
+
rescue JSON::ParserError => e
|
|
137
|
+
Aidp.log_warn("failure_tracker", "state_file_corrupted",
|
|
138
|
+
error: e.message)
|
|
139
|
+
default_state
|
|
140
|
+
rescue => e
|
|
141
|
+
Aidp.log_warn("failure_tracker", "load_state_failed",
|
|
142
|
+
error: e.message)
|
|
143
|
+
default_state
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def save_state
|
|
147
|
+
File.write(@state_file, JSON.pretty_generate(@state))
|
|
148
|
+
rescue => e
|
|
149
|
+
Aidp.log_error("failure_tracker", "save_state_failed",
|
|
150
|
+
error: e.message)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def default_state
|
|
154
|
+
{
|
|
155
|
+
failures: [],
|
|
156
|
+
last_success: nil,
|
|
157
|
+
last_success_version: nil
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module AutoUpdate
|
|
9
|
+
# Adapter for querying gem versions via RubyGems API (fallback)
|
|
10
|
+
class RubyGemsAPIAdapter
|
|
11
|
+
RUBYGEMS_API_BASE = "https://rubygems.org/api/v1"
|
|
12
|
+
TIMEOUT_SECONDS = 5
|
|
13
|
+
|
|
14
|
+
# Get the latest version of a gem from RubyGems API
|
|
15
|
+
# @param gem_name [String] Name of the gem
|
|
16
|
+
# @param allow_prerelease [Boolean] Whether to allow prerelease versions
|
|
17
|
+
# @return [Gem::Version, nil] Latest version or nil if unavailable
|
|
18
|
+
def latest_version_for(gem_name, allow_prerelease: false)
|
|
19
|
+
Aidp.log_debug("rubygems_api", "checking_gem_version",
|
|
20
|
+
gem: gem_name,
|
|
21
|
+
allow_prerelease: allow_prerelease)
|
|
22
|
+
|
|
23
|
+
uri = URI.parse("#{RUBYGEMS_API_BASE}/gems/#{gem_name}.json")
|
|
24
|
+
response = fetch_with_timeout(uri)
|
|
25
|
+
|
|
26
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
27
|
+
|
|
28
|
+
data = JSON.parse(response.body)
|
|
29
|
+
version_string = data["version"]
|
|
30
|
+
|
|
31
|
+
if version_string
|
|
32
|
+
version = Gem::Version.new(version_string)
|
|
33
|
+
|
|
34
|
+
# Filter out prerelease if not allowed
|
|
35
|
+
if !allow_prerelease && version.prerelease?
|
|
36
|
+
Aidp.log_debug("rubygems_api", "skipping_prerelease",
|
|
37
|
+
gem: gem_name,
|
|
38
|
+
version: version_string)
|
|
39
|
+
return nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Aidp.log_debug("rubygems_api", "found_version",
|
|
43
|
+
gem: gem_name,
|
|
44
|
+
version: version_string)
|
|
45
|
+
version
|
|
46
|
+
else
|
|
47
|
+
Aidp.log_debug("rubygems_api", "no_version_in_response",
|
|
48
|
+
gem: gem_name)
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
rescue JSON::ParserError => e
|
|
52
|
+
Aidp.log_error("rubygems_api", "json_parse_failed",
|
|
53
|
+
gem: gem_name,
|
|
54
|
+
error: e.message)
|
|
55
|
+
nil
|
|
56
|
+
rescue ArgumentError => e
|
|
57
|
+
Aidp.log_error("rubygems_api", "invalid_version",
|
|
58
|
+
gem: gem_name,
|
|
59
|
+
error: e.message)
|
|
60
|
+
nil
|
|
61
|
+
rescue => e
|
|
62
|
+
Aidp.log_error("rubygems_api", "api_request_failed",
|
|
63
|
+
gem: gem_name,
|
|
64
|
+
error: e.message)
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def fetch_with_timeout(uri)
|
|
71
|
+
Net::HTTP.start(
|
|
72
|
+
uri.host,
|
|
73
|
+
uri.port,
|
|
74
|
+
use_ssl: uri.scheme == "https",
|
|
75
|
+
open_timeout: TIMEOUT_SECONDS,
|
|
76
|
+
read_timeout: TIMEOUT_SECONDS
|
|
77
|
+
) do |http|
|
|
78
|
+
request = Net::HTTP::Get.new(uri)
|
|
79
|
+
request["User-Agent"] = "Aidp/#{Aidp::VERSION}"
|
|
80
|
+
http.request(request)
|
|
81
|
+
end
|
|
82
|
+
rescue Timeout::Error, Errno::ETIMEDOUT
|
|
83
|
+
Aidp.log_warn("rubygems_api", "request_timeout",
|
|
84
|
+
uri: uri.to_s,
|
|
85
|
+
timeout: TIMEOUT_SECONDS)
|
|
86
|
+
nil
|
|
87
|
+
rescue SocketError, Errno::ECONNREFUSED => e
|
|
88
|
+
Aidp.log_warn("rubygems_api", "connection_failed",
|
|
89
|
+
uri: uri.to_s,
|
|
90
|
+
error: e.message)
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module AutoUpdate
|
|
7
|
+
# Value object representing the result of an update check
|
|
8
|
+
class UpdateCheck
|
|
9
|
+
attr_reader :current_version, :available_version, :update_available,
|
|
10
|
+
:update_allowed, :policy_reason, :checked_at, :error
|
|
11
|
+
|
|
12
|
+
def initialize(
|
|
13
|
+
current_version:,
|
|
14
|
+
available_version:,
|
|
15
|
+
update_available:,
|
|
16
|
+
update_allowed:,
|
|
17
|
+
policy_reason: nil,
|
|
18
|
+
checked_at: Time.now,
|
|
19
|
+
error: nil
|
|
20
|
+
)
|
|
21
|
+
@current_version = current_version
|
|
22
|
+
@available_version = available_version
|
|
23
|
+
@update_available = update_available
|
|
24
|
+
@update_allowed = update_allowed
|
|
25
|
+
@policy_reason = policy_reason
|
|
26
|
+
@checked_at = checked_at
|
|
27
|
+
@error = error
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Create a failed update check
|
|
31
|
+
# @param error_message [String] Error message
|
|
32
|
+
# @param current_version [String] Current version
|
|
33
|
+
# @return [UpdateCheck]
|
|
34
|
+
def self.failed(error_message, current_version: Aidp::VERSION)
|
|
35
|
+
new(
|
|
36
|
+
current_version: current_version,
|
|
37
|
+
available_version: current_version,
|
|
38
|
+
update_available: false,
|
|
39
|
+
update_allowed: false,
|
|
40
|
+
policy_reason: "Update check failed",
|
|
41
|
+
error: error_message
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Create an unavailable update check (service temporarily unavailable)
|
|
46
|
+
# @param current_version [String] Current version
|
|
47
|
+
# @return [UpdateCheck]
|
|
48
|
+
def self.unavailable(current_version: Aidp::VERSION)
|
|
49
|
+
new(
|
|
50
|
+
current_version: current_version,
|
|
51
|
+
available_version: current_version,
|
|
52
|
+
update_available: false,
|
|
53
|
+
update_allowed: false,
|
|
54
|
+
policy_reason: "Update service temporarily unavailable"
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if update check was successful
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def success?
|
|
61
|
+
@error.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check if update check failed
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def failed?
|
|
67
|
+
!success?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Check if update should be performed
|
|
71
|
+
# @return [Boolean]
|
|
72
|
+
def should_update?
|
|
73
|
+
success? && @update_available && @update_allowed
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Convert to hash for serialization
|
|
77
|
+
# @return [Hash]
|
|
78
|
+
def to_h
|
|
79
|
+
{
|
|
80
|
+
current_version: @current_version,
|
|
81
|
+
available_version: @available_version,
|
|
82
|
+
update_available: @update_available,
|
|
83
|
+
update_allowed: @update_allowed,
|
|
84
|
+
policy_reason: @policy_reason,
|
|
85
|
+
checked_at: @checked_at.utc.iso8601,
|
|
86
|
+
error: @error
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Create from hash
|
|
91
|
+
# @param hash [Hash] Serialized update check
|
|
92
|
+
# @return [UpdateCheck]
|
|
93
|
+
def self.from_h(hash)
|
|
94
|
+
new(
|
|
95
|
+
current_version: hash[:current_version] || hash["current_version"],
|
|
96
|
+
available_version: hash[:available_version] || hash["available_version"],
|
|
97
|
+
update_available: hash[:update_available] || hash["update_available"],
|
|
98
|
+
update_allowed: hash[:update_allowed] || hash["update_allowed"],
|
|
99
|
+
policy_reason: hash[:policy_reason] || hash["policy_reason"],
|
|
100
|
+
checked_at: Time.parse(hash[:checked_at] || hash["checked_at"]),
|
|
101
|
+
error: hash[:error] || hash["error"]
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|