aidp 0.12.1 → 0.14.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 +7 -0
- data/lib/aidp/analyze/json_file_storage.rb +21 -21
- data/lib/aidp/cli/enhanced_input.rb +114 -0
- data/lib/aidp/cli/first_run_wizard.rb +28 -309
- data/lib/aidp/cli/issue_importer.rb +359 -0
- data/lib/aidp/cli/mcp_dashboard.rb +3 -3
- data/lib/aidp/cli/terminal_io.rb +26 -0
- data/lib/aidp/cli.rb +155 -7
- data/lib/aidp/daemon/process_manager.rb +146 -0
- data/lib/aidp/daemon/runner.rb +232 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +216 -0
- data/lib/aidp/execute/future_work_backlog.rb +411 -0
- data/lib/aidp/execute/guard_policy.rb +246 -0
- data/lib/aidp/execute/instruction_queue.rb +131 -0
- data/lib/aidp/execute/interactive_repl.rb +335 -0
- data/lib/aidp/execute/repl_macros.rb +651 -0
- data/lib/aidp/execute/steps.rb +8 -0
- data/lib/aidp/execute/work_loop_runner.rb +322 -36
- data/lib/aidp/execute/work_loop_state.rb +162 -0
- data/lib/aidp/harness/condition_detector.rb +6 -6
- data/lib/aidp/harness/config_loader.rb +23 -23
- data/lib/aidp/harness/config_manager.rb +61 -61
- data/lib/aidp/harness/config_schema.rb +88 -0
- data/lib/aidp/harness/config_validator.rb +9 -9
- data/lib/aidp/harness/configuration.rb +76 -29
- data/lib/aidp/harness/error_handler.rb +13 -13
- data/lib/aidp/harness/provider_config.rb +79 -79
- data/lib/aidp/harness/provider_factory.rb +40 -40
- data/lib/aidp/harness/provider_info.rb +37 -20
- data/lib/aidp/harness/provider_manager.rb +58 -53
- data/lib/aidp/harness/provider_type_checker.rb +6 -6
- data/lib/aidp/harness/runner.rb +7 -7
- data/lib/aidp/harness/status_display.rb +33 -46
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -1
- data/lib/aidp/harness/ui/job_monitor.rb +7 -7
- data/lib/aidp/harness/user_interface.rb +43 -43
- data/lib/aidp/init/doc_generator.rb +256 -0
- data/lib/aidp/init/project_analyzer.rb +343 -0
- data/lib/aidp/init/runner.rb +83 -0
- data/lib/aidp/init.rb +5 -0
- data/lib/aidp/logger.rb +279 -0
- data/lib/aidp/providers/anthropic.rb +100 -26
- data/lib/aidp/providers/base.rb +13 -0
- data/lib/aidp/providers/codex.rb +28 -27
- data/lib/aidp/providers/cursor.rb +141 -34
- data/lib/aidp/providers/github_copilot.rb +26 -26
- data/lib/aidp/providers/macos_ui.rb +2 -18
- data/lib/aidp/providers/opencode.rb +26 -26
- data/lib/aidp/setup/wizard.rb +777 -0
- data/lib/aidp/tooling_detector.rb +115 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +282 -0
- data/lib/aidp/watch/plan_generator.rb +166 -0
- data/lib/aidp/watch/plan_processor.rb +83 -0
- data/lib/aidp/watch/repository_client.rb +243 -0
- data/lib/aidp/watch/runner.rb +93 -0
- data/lib/aidp/watch/state_store.rb +105 -0
- data/lib/aidp/watch.rb +9 -0
- data/lib/aidp/workflows/guided_agent.rb +344 -23
- data/lib/aidp.rb +14 -0
- data/templates/implementation/simple_task.md +36 -0
- metadata +27 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
module Watch
|
|
10
|
+
# Lightweight adapter around GitHub for watch mode. Prefers the GitHub CLI
|
|
11
|
+
# (works for private repositories) and falls back to public REST endpoints
|
|
12
|
+
# when the CLI is unavailable.
|
|
13
|
+
class RepositoryClient
|
|
14
|
+
attr_reader :owner, :repo
|
|
15
|
+
|
|
16
|
+
def self.parse_issues_url(issues_url)
|
|
17
|
+
case issues_url
|
|
18
|
+
when %r{\Ahttps://github\.com/([^/]+)/([^/]+)(?:/issues)?/?\z}
|
|
19
|
+
[$1, $2]
|
|
20
|
+
when %r{\A([^/]+)/([^/]+)\z}
|
|
21
|
+
[$1, $2]
|
|
22
|
+
else
|
|
23
|
+
raise ArgumentError, "Unsupported issues URL: #{issues_url}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(owner:, repo:, gh_available: nil)
|
|
28
|
+
@owner = owner
|
|
29
|
+
@repo = repo
|
|
30
|
+
@gh_available = gh_available.nil? ? gh_cli_available? : gh_available
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def gh_available?
|
|
34
|
+
@gh_available
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def full_repo
|
|
38
|
+
"#{owner}/#{repo}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def list_issues(labels: [], state: "open")
|
|
42
|
+
gh_available? ? list_issues_via_gh(labels: labels, state: state) : list_issues_via_api(labels: labels, state: state)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def fetch_issue(number)
|
|
46
|
+
gh_available? ? fetch_issue_via_gh(number) : fetch_issue_via_api(number)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def post_comment(number, body)
|
|
50
|
+
gh_available? ? post_comment_via_gh(number, body) : post_comment_via_api(number, body)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_pull_request(title:, body:, head:, base:, issue_number:)
|
|
54
|
+
gh_available? ? create_pull_request_via_gh(title: title, body: body, head: head, base: base, issue_number: issue_number) : raise("GitHub CLI not available - cannot create PR")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def gh_cli_available?
|
|
60
|
+
_stdout, _stderr, status = Open3.capture3("gh", "--version")
|
|
61
|
+
status.success?
|
|
62
|
+
rescue Errno::ENOENT
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def list_issues_via_gh(labels:, state:)
|
|
67
|
+
json_fields = %w[number title labels updatedAt state url assignees]
|
|
68
|
+
cmd = ["gh", "issue", "list", "--repo", full_repo, "--state", state, "--json", json_fields.join(",")]
|
|
69
|
+
labels.each do |label|
|
|
70
|
+
cmd += ["--label", label]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
74
|
+
unless status.success?
|
|
75
|
+
warn("GitHub CLI list failed: #{stderr}")
|
|
76
|
+
return []
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
JSON.parse(stdout).map { |raw| normalize_issue(raw) }
|
|
80
|
+
rescue JSON::ParserError => e
|
|
81
|
+
warn("Failed to parse GH CLI response: #{e.message}")
|
|
82
|
+
[]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def list_issues_via_api(labels:, state:)
|
|
86
|
+
label_param = labels.join(",")
|
|
87
|
+
uri = URI("https://api.github.com/repos/#{full_repo}/issues?state=#{state}")
|
|
88
|
+
uri.query = [uri.query, "labels=#{URI.encode_www_form_component(label_param)}"].compact.join("&") unless label_param.empty?
|
|
89
|
+
|
|
90
|
+
response = Net::HTTP.get_response(uri)
|
|
91
|
+
return [] unless response.code == "200"
|
|
92
|
+
|
|
93
|
+
JSON.parse(response.body).reject { |item| item["pull_request"] }.map { |raw| normalize_issue_api(raw) }
|
|
94
|
+
rescue => e
|
|
95
|
+
warn("GitHub API list failed: #{e.message}")
|
|
96
|
+
[]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def fetch_issue_via_gh(number)
|
|
100
|
+
fields = %w[number title body comments labels state assignees url updatedAt author]
|
|
101
|
+
cmd = ["gh", "issue", "view", number.to_s, "--repo", full_repo, "--json", fields.join(",")]
|
|
102
|
+
|
|
103
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
104
|
+
raise "GitHub CLI error: #{stderr.strip}" unless status.success?
|
|
105
|
+
|
|
106
|
+
data = JSON.parse(stdout)
|
|
107
|
+
normalize_issue_detail(data)
|
|
108
|
+
rescue JSON::ParserError => e
|
|
109
|
+
raise "Failed to parse GitHub CLI issue response: #{e.message}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def fetch_issue_via_api(number)
|
|
113
|
+
uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}")
|
|
114
|
+
response = Net::HTTP.get_response(uri)
|
|
115
|
+
raise "GitHub API error (#{response.code})" unless response.code == "200"
|
|
116
|
+
|
|
117
|
+
data = JSON.parse(response.body)
|
|
118
|
+
comments = fetch_comments_via_api(number)
|
|
119
|
+
data["comments"] = comments
|
|
120
|
+
normalize_issue_detail_api(data)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def fetch_comments_via_api(number)
|
|
124
|
+
uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/comments")
|
|
125
|
+
response = Net::HTTP.get_response(uri)
|
|
126
|
+
return [] unless response.code == "200"
|
|
127
|
+
|
|
128
|
+
JSON.parse(response.body).map do |raw|
|
|
129
|
+
{
|
|
130
|
+
"body" => raw["body"],
|
|
131
|
+
"author" => raw.dig("user", "login"),
|
|
132
|
+
"createdAt" => raw["created_at"]
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
rescue
|
|
136
|
+
[]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def post_comment_via_gh(number, body)
|
|
140
|
+
cmd = ["gh", "issue", "comment", number.to_s, "--repo", full_repo, "--body", body]
|
|
141
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
142
|
+
raise "Failed to post comment via gh: #{stderr.strip}" unless status.success?
|
|
143
|
+
|
|
144
|
+
stdout.strip
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def post_comment_via_api(number, body)
|
|
148
|
+
uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/comments")
|
|
149
|
+
request = Net::HTTP::Post.new(uri)
|
|
150
|
+
request["Content-Type"] = "application/json"
|
|
151
|
+
request.body = JSON.dump({body: body})
|
|
152
|
+
|
|
153
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
154
|
+
http.request(request)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
raise "GitHub API comment failed (#{response.code})" unless response.code.start_with?("2")
|
|
158
|
+
response.body
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def create_pull_request_via_gh(title:, body:, head:, base:, issue_number:)
|
|
162
|
+
cmd = [
|
|
163
|
+
"gh", "pr", "create",
|
|
164
|
+
"--repo", full_repo,
|
|
165
|
+
"--title", title,
|
|
166
|
+
"--body", body,
|
|
167
|
+
"--head", head,
|
|
168
|
+
"--base", base
|
|
169
|
+
]
|
|
170
|
+
cmd += ["--issue", issue_number.to_s] if issue_number
|
|
171
|
+
|
|
172
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
173
|
+
raise "Failed to create PR via gh: #{stderr.strip}" unless status.success?
|
|
174
|
+
|
|
175
|
+
stdout.strip
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def normalize_issue(raw)
|
|
179
|
+
{
|
|
180
|
+
number: raw["number"],
|
|
181
|
+
title: raw["title"],
|
|
182
|
+
labels: Array(raw["labels"]).map { |label| label.is_a?(Hash) ? label["name"] : label },
|
|
183
|
+
updated_at: raw["updatedAt"],
|
|
184
|
+
state: raw["state"],
|
|
185
|
+
url: raw["url"],
|
|
186
|
+
assignees: Array(raw["assignees"]).map { |assignee| assignee.is_a?(Hash) ? assignee["login"] : assignee }
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def normalize_issue_api(raw)
|
|
191
|
+
{
|
|
192
|
+
number: raw["number"],
|
|
193
|
+
title: raw["title"],
|
|
194
|
+
labels: Array(raw["labels"]).map { |label| label["name"] },
|
|
195
|
+
updated_at: raw["updated_at"],
|
|
196
|
+
state: raw["state"],
|
|
197
|
+
url: raw["html_url"],
|
|
198
|
+
assignees: Array(raw["assignees"]).map { |assignee| assignee["login"] }
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def normalize_issue_detail(raw)
|
|
203
|
+
{
|
|
204
|
+
number: raw["number"],
|
|
205
|
+
title: raw["title"],
|
|
206
|
+
body: raw["body"] || "",
|
|
207
|
+
comments: Array(raw["comments"]).map { |comment| normalize_comment(comment) },
|
|
208
|
+
labels: Array(raw["labels"]).map { |label| label.is_a?(Hash) ? label["name"] : label },
|
|
209
|
+
state: raw["state"],
|
|
210
|
+
assignees: Array(raw["assignees"]).map { |assignee| assignee.is_a?(Hash) ? assignee["login"] : assignee },
|
|
211
|
+
url: raw["url"],
|
|
212
|
+
updated_at: raw["updatedAt"]
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def normalize_issue_detail_api(raw)
|
|
217
|
+
{
|
|
218
|
+
number: raw["number"],
|
|
219
|
+
title: raw["title"],
|
|
220
|
+
body: raw["body"] || "",
|
|
221
|
+
comments: Array(raw["comments"]).map { |comment| normalize_comment(comment) },
|
|
222
|
+
labels: Array(raw["labels"]).map { |label| label["name"] },
|
|
223
|
+
state: raw["state"],
|
|
224
|
+
assignees: Array(raw["assignees"]).map { |assignee| assignee["login"] },
|
|
225
|
+
url: raw["html_url"],
|
|
226
|
+
updated_at: raw["updated_at"]
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def normalize_comment(comment)
|
|
231
|
+
if comment.is_a?(Hash)
|
|
232
|
+
{
|
|
233
|
+
"body" => comment["body"],
|
|
234
|
+
"author" => comment["author"] || comment.dig("user", "login"),
|
|
235
|
+
"createdAt" => comment["createdAt"] || comment["created_at"]
|
|
236
|
+
}
|
|
237
|
+
else
|
|
238
|
+
{"body" => comment.to_s}
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-prompt"
|
|
4
|
+
|
|
5
|
+
require_relative "../message_display"
|
|
6
|
+
require_relative "repository_client"
|
|
7
|
+
require_relative "state_store"
|
|
8
|
+
require_relative "plan_generator"
|
|
9
|
+
require_relative "plan_processor"
|
|
10
|
+
require_relative "build_processor"
|
|
11
|
+
|
|
12
|
+
module Aidp
|
|
13
|
+
module Watch
|
|
14
|
+
# Coordinates the watch mode loop: monitors issues, handles plan/build
|
|
15
|
+
# triggers, and keeps running until interrupted.
|
|
16
|
+
class Runner
|
|
17
|
+
include Aidp::MessageDisplay
|
|
18
|
+
|
|
19
|
+
DEFAULT_INTERVAL = 30
|
|
20
|
+
|
|
21
|
+
def initialize(issues_url:, interval: DEFAULT_INTERVAL, provider_name: nil, gh_available: nil, project_dir: Dir.pwd, once: false, prompt: TTY::Prompt.new)
|
|
22
|
+
@prompt = prompt
|
|
23
|
+
@interval = interval
|
|
24
|
+
@once = once
|
|
25
|
+
@project_dir = project_dir
|
|
26
|
+
|
|
27
|
+
owner, repo = RepositoryClient.parse_issues_url(issues_url)
|
|
28
|
+
@repository_client = RepositoryClient.new(owner: owner, repo: repo, gh_available: gh_available)
|
|
29
|
+
@state_store = StateStore.new(project_dir: project_dir, repository: "#{owner}/#{repo}")
|
|
30
|
+
@plan_processor = PlanProcessor.new(
|
|
31
|
+
repository_client: @repository_client,
|
|
32
|
+
state_store: @state_store,
|
|
33
|
+
plan_generator: PlanGenerator.new(provider_name: provider_name)
|
|
34
|
+
)
|
|
35
|
+
@build_processor = BuildProcessor.new(
|
|
36
|
+
repository_client: @repository_client,
|
|
37
|
+
state_store: @state_store,
|
|
38
|
+
project_dir: project_dir
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def start
|
|
43
|
+
display_message("👀 Watch mode enabled for #{@repository_client.full_repo}", type: :highlight)
|
|
44
|
+
display_message("Polling every #{@interval} seconds. Press Ctrl+C to stop.", type: :muted)
|
|
45
|
+
|
|
46
|
+
loop do
|
|
47
|
+
process_cycle
|
|
48
|
+
break if @once
|
|
49
|
+
sleep @interval
|
|
50
|
+
end
|
|
51
|
+
rescue Interrupt
|
|
52
|
+
display_message("\n⏹️ Watch mode interrupted by user", type: :warning)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def process_cycle
|
|
58
|
+
process_plan_triggers
|
|
59
|
+
process_build_triggers
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def process_plan_triggers
|
|
63
|
+
issues = @repository_client.list_issues(labels: [PlanProcessor::PLAN_LABEL], state: "open")
|
|
64
|
+
issues.each do |issue|
|
|
65
|
+
next unless issue_has_label?(issue, PlanProcessor::PLAN_LABEL)
|
|
66
|
+
|
|
67
|
+
detailed = @repository_client.fetch_issue(issue[:number])
|
|
68
|
+
@plan_processor.process(detailed)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def process_build_triggers
|
|
73
|
+
issues = @repository_client.list_issues(labels: [BuildProcessor::BUILD_LABEL], state: "open")
|
|
74
|
+
issues.each do |issue|
|
|
75
|
+
next unless issue_has_label?(issue, BuildProcessor::BUILD_LABEL)
|
|
76
|
+
|
|
77
|
+
status = @state_store.build_status(issue[:number])
|
|
78
|
+
next if status["status"] == "completed"
|
|
79
|
+
|
|
80
|
+
detailed = @repository_client.fetch_issue(issue[:number])
|
|
81
|
+
@build_processor.process(detailed)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def issue_has_label?(issue, label)
|
|
86
|
+
Array(issue[:labels]).any? do |issue_label|
|
|
87
|
+
name = issue_label.is_a?(Hash) ? issue_label["name"] : issue_label.to_s
|
|
88
|
+
name.casecmp(label).zero?
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module Watch
|
|
9
|
+
# Persists watch mode progress for each repository/issue pair. Used to
|
|
10
|
+
# avoid re-processing plan/build triggers and to retain generated plan
|
|
11
|
+
# context between runs.
|
|
12
|
+
class StateStore
|
|
13
|
+
attr_reader :path
|
|
14
|
+
|
|
15
|
+
def initialize(project_dir:, repository:)
|
|
16
|
+
@project_dir = project_dir
|
|
17
|
+
@repository = repository
|
|
18
|
+
@path = File.join(project_dir, ".aidp", "watch", "#{sanitize_repository(repository)}.yml")
|
|
19
|
+
ensure_directory
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def plan_processed?(issue_number)
|
|
23
|
+
plans.key?(issue_number.to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def plan_data(issue_number)
|
|
27
|
+
plans[issue_number.to_s]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def record_plan(issue_number, data)
|
|
31
|
+
payload = {
|
|
32
|
+
"summary" => data[:summary],
|
|
33
|
+
"tasks" => data[:tasks],
|
|
34
|
+
"questions" => data[:questions],
|
|
35
|
+
"comment_body" => data[:comment_body],
|
|
36
|
+
"comment_hint" => data[:comment_hint],
|
|
37
|
+
"posted_at" => data[:posted_at] || Time.now.utc.iso8601
|
|
38
|
+
}.compact
|
|
39
|
+
|
|
40
|
+
plans[issue_number.to_s] = payload
|
|
41
|
+
save!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_status(issue_number)
|
|
45
|
+
builds[issue_number.to_s] || {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def record_build_status(issue_number, status:, details: {})
|
|
49
|
+
builds[issue_number.to_s] = {
|
|
50
|
+
"status" => status,
|
|
51
|
+
"updated_at" => Time.now.utc.iso8601
|
|
52
|
+
}.merge(stringify_keys(details))
|
|
53
|
+
save!
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def ensure_directory
|
|
59
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def sanitize_repository(repository)
|
|
63
|
+
repository.tr("/", "_")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load_state
|
|
67
|
+
@state ||= if File.exist?(@path)
|
|
68
|
+
YAML.safe_load_file(@path, permitted_classes: [Time]) || {}
|
|
69
|
+
else
|
|
70
|
+
{}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def save!
|
|
75
|
+
File.write(@path, YAML.dump(state))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def state
|
|
79
|
+
@state = nil if @state && !@state.is_a?(Hash)
|
|
80
|
+
@state ||= begin
|
|
81
|
+
base = load_state
|
|
82
|
+
base["plans"] ||= {}
|
|
83
|
+
base["builds"] ||= {}
|
|
84
|
+
base
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def plans
|
|
89
|
+
state["plans"]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def builds
|
|
93
|
+
state["builds"]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def stringify_keys(hash)
|
|
97
|
+
return {} unless hash
|
|
98
|
+
|
|
99
|
+
hash.each_with_object({}) do |(key, value), memo|
|
|
100
|
+
memo[key.to_s] = value
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/aidp/watch.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "message_display"
|
|
4
|
+
require_relative "watch/repository_client"
|
|
5
|
+
require_relative "watch/state_store"
|
|
6
|
+
require_relative "watch/plan_generator"
|
|
7
|
+
require_relative "watch/plan_processor"
|
|
8
|
+
require_relative "watch/build_processor"
|
|
9
|
+
require_relative "watch/runner"
|