aidp 0.21.1 โ 0.22.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/lib/aidp/cli/devcontainer_commands.rb +501 -0
- data/lib/aidp/cli/issue_importer.rb +15 -3
- data/lib/aidp/cli.rb +91 -0
- data/lib/aidp/execute/prompt_manager.rb +16 -2
- data/lib/aidp/execute/runner.rb +10 -5
- data/lib/aidp/execute/work_loop_runner.rb +3 -3
- data/lib/aidp/harness/state/persistence.rb +12 -1
- data/lib/aidp/harness/state_manager.rb +13 -1
- data/lib/aidp/jobs/background_runner.rb +3 -1
- data/lib/aidp/logger.rb +41 -5
- data/lib/aidp/safe_directory.rb +87 -0
- data/lib/aidp/setup/devcontainer/backup_manager.rb +175 -0
- data/lib/aidp/setup/devcontainer/generator.rb +409 -0
- data/lib/aidp/setup/devcontainer/parser.rb +249 -0
- data/lib/aidp/setup/devcontainer/port_manager.rb +286 -0
- data/lib/aidp/setup/wizard.rb +145 -0
- data/lib/aidp/storage/csv_storage.rb +39 -2
- data/lib/aidp/storage/file_manager.rb +28 -3
- data/lib/aidp/storage/json_storage.rb +41 -2
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/guided_agent.rb +8 -42
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e07daf05fddb6301340e9a01b1c6d8eea60ddcf53b2ec2d56e914be610cd67a7
|
|
4
|
+
data.tar.gz: 21f26ed81af8f996cbb50c4a4dfe7603230f29499399c134c7a83492042a39e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 45a1c8c35dbf2f3f672f1990d652c238dd050b3dae3a96edf4d69960475a9e9ee27f1f4ced5ed4a6b0639de6256f56c78d25931e08adacf05ed2aa444e23c9fe
|
|
7
|
+
data.tar.gz: 85560929fa2c4a1b51a862e8c541ed92d85e51a3c025eede695a5211c7b30b0a2115c098969fbfbb3a7e4237d5d0c074d194f0df0c50b7cdb025a75e7fc9249a
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../setup/devcontainer/parser"
|
|
4
|
+
require_relative "../setup/devcontainer/generator"
|
|
5
|
+
require_relative "../setup/devcontainer/port_manager"
|
|
6
|
+
require_relative "../setup/devcontainer/backup_manager"
|
|
7
|
+
require_relative "../message_display"
|
|
8
|
+
require "json"
|
|
9
|
+
require "yaml"
|
|
10
|
+
|
|
11
|
+
module Aidp
|
|
12
|
+
class CLI
|
|
13
|
+
# Commands for managing devcontainer configuration
|
|
14
|
+
class DevcontainerCommands
|
|
15
|
+
COMPONENT = "devcontainer_commands"
|
|
16
|
+
include Aidp::MessageDisplay
|
|
17
|
+
|
|
18
|
+
def initialize(project_dir: Dir.pwd, prompt: TTY::Prompt.new)
|
|
19
|
+
@project_dir = project_dir
|
|
20
|
+
@prompt = prompt
|
|
21
|
+
Aidp.log_debug(COMPONENT, "initialized", project_dir: project_dir)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Show diff between current and proposed devcontainer configuration
|
|
25
|
+
def diff(options = {})
|
|
26
|
+
Aidp.log_debug(COMPONENT, "diff.start", options: options)
|
|
27
|
+
parser = Aidp::Setup::Devcontainer::Parser.new(@project_dir)
|
|
28
|
+
|
|
29
|
+
unless parser.devcontainer_exists?
|
|
30
|
+
Aidp.log_debug(COMPONENT, "diff.no_devcontainer")
|
|
31
|
+
display_message("No existing devcontainer.json found", type: :warning)
|
|
32
|
+
display_message("Run 'aidp config --interactive' to create one", type: :muted)
|
|
33
|
+
return false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
devcontainer_path = parser.detect
|
|
37
|
+
current_config = parser.parse
|
|
38
|
+
|
|
39
|
+
# Load proposed config from aidp.yml or generate from config
|
|
40
|
+
proposed_config = load_proposed_config(options)
|
|
41
|
+
|
|
42
|
+
unless proposed_config
|
|
43
|
+
Aidp.log_debug(COMPONENT, "diff.no_proposed_config")
|
|
44
|
+
display_message("No proposed configuration found", type: :warning)
|
|
45
|
+
display_message("Update your aidp.yml or use --generate", type: :muted)
|
|
46
|
+
return false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
display_diff(current_config, proposed_config, devcontainer_path)
|
|
50
|
+
Aidp.log_debug(COMPONENT, "diff.complete", devcontainer_path: devcontainer_path)
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Apply devcontainer configuration from aidp.yml
|
|
55
|
+
def apply(options = {})
|
|
56
|
+
dry_run = options[:dry_run] || false
|
|
57
|
+
force = options[:force] || false
|
|
58
|
+
create_backup = options[:backup] != false # Default true
|
|
59
|
+
Aidp.log_debug(COMPONENT, "apply.start",
|
|
60
|
+
dry_run: dry_run,
|
|
61
|
+
force: force,
|
|
62
|
+
create_backup: create_backup)
|
|
63
|
+
|
|
64
|
+
parser = Aidp::Setup::Devcontainer::Parser.new(@project_dir)
|
|
65
|
+
existing_config = parser.devcontainer_exists? ? parser.parse : nil
|
|
66
|
+
devcontainer_path = parser.detect || default_devcontainer_path
|
|
67
|
+
|
|
68
|
+
# Load configuration
|
|
69
|
+
proposed_config = load_proposed_config(options)
|
|
70
|
+
|
|
71
|
+
unless proposed_config
|
|
72
|
+
Aidp.log_debug(COMPONENT, "apply.no_configuration")
|
|
73
|
+
display_message("โ No configuration found in aidp.yml", type: :error)
|
|
74
|
+
display_message("Run 'aidp config --interactive' first", type: :muted)
|
|
75
|
+
return false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Merge with existing if present
|
|
79
|
+
generator = Aidp::Setup::Devcontainer::Generator.new(@project_dir)
|
|
80
|
+
final_config = if existing_config
|
|
81
|
+
generator.merge_with_existing(proposed_config, existing_config)
|
|
82
|
+
else
|
|
83
|
+
proposed_config
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Show preview
|
|
87
|
+
if dry_run
|
|
88
|
+
display_message("๐ Dry Run - Changes Preview", type: :highlight)
|
|
89
|
+
display_diff(existing_config || {}, final_config, devcontainer_path)
|
|
90
|
+
display_message("\nNo changes made (dry run)", type: :muted)
|
|
91
|
+
Aidp.log_debug(COMPONENT, "apply.dry_run_preview",
|
|
92
|
+
has_existing: !existing_config.nil?,
|
|
93
|
+
devcontainer_path: devcontainer_path)
|
|
94
|
+
return true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Confirm unless forced
|
|
98
|
+
unless force
|
|
99
|
+
display_diff(existing_config || {}, final_config, devcontainer_path)
|
|
100
|
+
display_message("")
|
|
101
|
+
|
|
102
|
+
unless @prompt.yes?("Apply these changes?")
|
|
103
|
+
display_message("Cancelled", type: :warning)
|
|
104
|
+
return false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Create backup if existing file
|
|
109
|
+
if create_backup && File.exist?(devcontainer_path)
|
|
110
|
+
backup_manager = Aidp::Setup::Devcontainer::BackupManager.new(@project_dir)
|
|
111
|
+
backup_path = backup_manager.create_backup(devcontainer_path, {
|
|
112
|
+
reason: "cli_apply",
|
|
113
|
+
timestamp: Time.now.utc.iso8601
|
|
114
|
+
})
|
|
115
|
+
display_message("โ
Backup created: #{File.basename(backup_path)}", type: :success)
|
|
116
|
+
Aidp.log_debug(COMPONENT, "apply.backup_created", backup_path: backup_path)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Write devcontainer.json
|
|
120
|
+
write_devcontainer(devcontainer_path, final_config)
|
|
121
|
+
|
|
122
|
+
display_message("โ
Devcontainer configuration applied", type: :success)
|
|
123
|
+
display_message(" #{devcontainer_path}", type: :muted)
|
|
124
|
+
Aidp.log_debug(COMPONENT, "apply.completed",
|
|
125
|
+
devcontainer_path: devcontainer_path,
|
|
126
|
+
forward_ports: final_config["forwardPorts"]&.length)
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# List available backups
|
|
131
|
+
def list_backups
|
|
132
|
+
Aidp.log_debug(COMPONENT, "list_backups.start")
|
|
133
|
+
backup_manager = Aidp::Setup::Devcontainer::BackupManager.new(@project_dir)
|
|
134
|
+
backups = backup_manager.list_backups
|
|
135
|
+
|
|
136
|
+
if backups.empty?
|
|
137
|
+
display_message("No backups found", type: :muted)
|
|
138
|
+
Aidp.log_debug(COMPONENT, "list_backups.none_found")
|
|
139
|
+
return true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
display_message("๐ฆ Available Backups", type: :highlight)
|
|
143
|
+
display_message("")
|
|
144
|
+
|
|
145
|
+
backups.each_with_index do |backup, index|
|
|
146
|
+
display_message("#{index + 1}. #{backup[:filename]}", type: :info)
|
|
147
|
+
display_message(" Created: #{backup[:created_at].strftime("%Y-%m-%d %H:%M:%S")}", type: :muted)
|
|
148
|
+
display_message(" Size: #{format_size(backup[:size])}", type: :muted)
|
|
149
|
+
if backup[:metadata]
|
|
150
|
+
display_message(" Reason: #{backup[:metadata]["reason"]}", type: :muted)
|
|
151
|
+
end
|
|
152
|
+
display_message("")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
total_size = backup_manager.total_backup_size
|
|
156
|
+
display_message("Total: #{backups.size} backups (#{format_size(total_size)})", type: :muted)
|
|
157
|
+
Aidp.log_debug(COMPONENT, "list_backups.complete",
|
|
158
|
+
count: backups.size,
|
|
159
|
+
total_size: total_size)
|
|
160
|
+
true
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Restore from a backup
|
|
164
|
+
def restore(backup_index_or_path, options = {})
|
|
165
|
+
Aidp.log_debug(COMPONENT, "restore.start",
|
|
166
|
+
selector: backup_index_or_path,
|
|
167
|
+
options: options)
|
|
168
|
+
backup_manager = Aidp::Setup::Devcontainer::BackupManager.new(@project_dir)
|
|
169
|
+
|
|
170
|
+
backup_path = if backup_index_or_path.to_i.positive?
|
|
171
|
+
# Index-based selection
|
|
172
|
+
backups = backup_manager.list_backups
|
|
173
|
+
index = backup_index_or_path.to_i - 1
|
|
174
|
+
|
|
175
|
+
unless backups[index]
|
|
176
|
+
Aidp.log_debug(COMPONENT, "restore.invalid_index",
|
|
177
|
+
selector: backup_index_or_path)
|
|
178
|
+
display_message("โ Invalid backup index: #{backup_index_or_path}", type: :error)
|
|
179
|
+
return false
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
backups[index][:path]
|
|
183
|
+
else
|
|
184
|
+
# Direct path
|
|
185
|
+
backup_index_or_path
|
|
186
|
+
end
|
|
187
|
+
Aidp.log_debug(COMPONENT, "restore.resolved_backup", backup_path: backup_path)
|
|
188
|
+
|
|
189
|
+
unless File.exist?(backup_path)
|
|
190
|
+
Aidp.log_debug(COMPONENT, "restore.missing_backup", backup_path: backup_path)
|
|
191
|
+
display_message("โ Backup not found: #{backup_path}", type: :error)
|
|
192
|
+
return false
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
parser = Aidp::Setup::Devcontainer::Parser.new(@project_dir)
|
|
196
|
+
target_path = parser.detect || default_devcontainer_path
|
|
197
|
+
|
|
198
|
+
# Show what will be restored
|
|
199
|
+
JSON.parse(File.read(backup_path))
|
|
200
|
+
display_message("๐ฆ Restoring Backup", type: :highlight)
|
|
201
|
+
display_message(" From: #{File.basename(backup_path)}", type: :muted)
|
|
202
|
+
display_message(" To: #{target_path}", type: :muted)
|
|
203
|
+
display_message("")
|
|
204
|
+
|
|
205
|
+
unless options[:force] || @prompt.yes?("Restore this backup?")
|
|
206
|
+
Aidp.log_debug(COMPONENT, "restore.cancelled_by_user", target_path: target_path)
|
|
207
|
+
display_message("Cancelled", type: :warning)
|
|
208
|
+
return false
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Restore
|
|
212
|
+
backup_manager.restore_backup(backup_path, target_path, create_backup: !options[:no_backup])
|
|
213
|
+
Aidp.log_debug(COMPONENT, "restore.completed", target_path: target_path)
|
|
214
|
+
|
|
215
|
+
display_message("โ
Backup restored successfully", type: :success)
|
|
216
|
+
true
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
def load_proposed_config(options)
|
|
222
|
+
if options[:config_file]
|
|
223
|
+
# Load from specific file
|
|
224
|
+
config_file = options[:config_file]
|
|
225
|
+
unless File.exist?(config_file)
|
|
226
|
+
Aidp.log_debug(COMPONENT, "load_proposed_config.file_missing", path: config_file)
|
|
227
|
+
return nil
|
|
228
|
+
end
|
|
229
|
+
Aidp.log_debug(COMPONENT, "load_proposed_config.from_file", path: config_file)
|
|
230
|
+
JSON.parse(File.read(config_file))
|
|
231
|
+
elsif options[:generate]
|
|
232
|
+
# Generate from wizard config in aidp.yml
|
|
233
|
+
aidp_config = load_aidp_config
|
|
234
|
+
return nil unless aidp_config
|
|
235
|
+
|
|
236
|
+
generator = Aidp::Setup::Devcontainer::Generator.new(@project_dir, aidp_config)
|
|
237
|
+
wizard_config = extract_wizard_config(aidp_config)
|
|
238
|
+
Aidp.log_debug(COMPONENT, "load_proposed_config.generate_from_wizard",
|
|
239
|
+
wizard_keys: wizard_config.keys)
|
|
240
|
+
generator.generate(wizard_config)
|
|
241
|
+
else
|
|
242
|
+
# Load from aidp.yml and generate using wizard config
|
|
243
|
+
aidp_config = load_aidp_config
|
|
244
|
+
return nil unless aidp_config&.dig("devcontainer", "manage")
|
|
245
|
+
|
|
246
|
+
# Use Generator to create proper devcontainer config
|
|
247
|
+
generator = Aidp::Setup::Devcontainer::Generator.new(@project_dir, aidp_config)
|
|
248
|
+
wizard_config = extract_wizard_config_for_generation(aidp_config)
|
|
249
|
+
Aidp.log_debug(COMPONENT, "load_proposed_config.from_managed_config",
|
|
250
|
+
wizard_keys: wizard_config.keys)
|
|
251
|
+
generator.generate(wizard_config)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def load_aidp_config
|
|
256
|
+
config_path = File.join(@project_dir, ".aidp", "aidp.yml")
|
|
257
|
+
return nil unless File.exist?(config_path)
|
|
258
|
+
|
|
259
|
+
config = YAML.load_file(config_path)
|
|
260
|
+
Aidp.log_debug(COMPONENT, "load_aidp_config.loaded",
|
|
261
|
+
config_path: config_path,
|
|
262
|
+
manages_devcontainer: config.dig("devcontainer", "manage"))
|
|
263
|
+
config
|
|
264
|
+
rescue => e
|
|
265
|
+
Aidp.log_error("devcontainer_commands", "failed to load aidp.yml", error: e.message)
|
|
266
|
+
nil
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def extract_wizard_config(aidp_config)
|
|
270
|
+
# Extract wizard-compatible config from aidp.yml
|
|
271
|
+
{
|
|
272
|
+
project_name: aidp_config.dig("project", "name"),
|
|
273
|
+
language: aidp_config.dig("project", "language"),
|
|
274
|
+
test_framework: aidp_config.dig("testing", "framework"),
|
|
275
|
+
linters: aidp_config.dig("linting", "tools"),
|
|
276
|
+
providers: aidp_config.dig("providers")&.keys,
|
|
277
|
+
watch_mode: aidp_config.dig("watch", "enabled"),
|
|
278
|
+
app_type: aidp_config.dig("project", "type"),
|
|
279
|
+
app_port: aidp_config.dig("devcontainer", "ports", 0, "number")
|
|
280
|
+
}.compact
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def extract_wizard_config_for_generation(aidp_config)
|
|
284
|
+
# Extract wizard-compatible config including custom_ports
|
|
285
|
+
{
|
|
286
|
+
providers: aidp_config.dig("providers")&.keys,
|
|
287
|
+
test_framework: aidp_config.dig("work_loop", "test_commands", 0, "framework"),
|
|
288
|
+
linters: aidp_config.dig("work_loop", "linting", "tools"),
|
|
289
|
+
watch_mode: aidp_config.dig("work_loop", "watch", "enabled"),
|
|
290
|
+
custom_ports: aidp_config.dig("devcontainer", "custom_ports")
|
|
291
|
+
}.compact
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def build_config_from_aidp_yml(devcontainer_config)
|
|
295
|
+
config = {}
|
|
296
|
+
|
|
297
|
+
# Basic info
|
|
298
|
+
config["name"] = devcontainer_config["name"] if devcontainer_config["name"]
|
|
299
|
+
|
|
300
|
+
# Features
|
|
301
|
+
if devcontainer_config["features"]
|
|
302
|
+
config["features"] = devcontainer_config["features"].each_with_object({}) do |feature, h|
|
|
303
|
+
h[feature] = {}
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Ports
|
|
308
|
+
if devcontainer_config["ports"]
|
|
309
|
+
config["forwardPorts"] = devcontainer_config["ports"].map { |p| p["number"] }
|
|
310
|
+
config["portsAttributes"] = devcontainer_config["ports"].each_with_object({}) do |port, attrs|
|
|
311
|
+
attrs[port["number"].to_s] = {
|
|
312
|
+
"label" => port["label"],
|
|
313
|
+
"protocol" => port["protocol"] || "http"
|
|
314
|
+
}
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Environment
|
|
319
|
+
config["containerEnv"] = devcontainer_config["env"] if devcontainer_config["env"]
|
|
320
|
+
|
|
321
|
+
# Custom settings
|
|
322
|
+
config.merge!(devcontainer_config["custom_settings"] || {})
|
|
323
|
+
|
|
324
|
+
# AIDP metadata
|
|
325
|
+
config["_aidp"] = {
|
|
326
|
+
"managed" => true,
|
|
327
|
+
"version" => Aidp::VERSION,
|
|
328
|
+
"generated_at" => Time.now.utc.iso8601
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
config
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def display_diff(current, proposed, path)
|
|
335
|
+
display_message("๐ Devcontainer Changes Preview", type: :highlight)
|
|
336
|
+
display_message("โ" * 60, type: :muted)
|
|
337
|
+
display_message("File: #{path}", type: :muted)
|
|
338
|
+
display_message("")
|
|
339
|
+
|
|
340
|
+
# Features diff
|
|
341
|
+
display_features_diff(current["features"], proposed["features"])
|
|
342
|
+
|
|
343
|
+
# Ports diff
|
|
344
|
+
display_ports_diff(current["forwardPorts"], proposed["forwardPorts"])
|
|
345
|
+
|
|
346
|
+
# Port attributes diff
|
|
347
|
+
display_port_attributes_diff(
|
|
348
|
+
current["portsAttributes"],
|
|
349
|
+
proposed["portsAttributes"]
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Environment diff
|
|
353
|
+
display_env_diff(current["containerEnv"], proposed["containerEnv"])
|
|
354
|
+
|
|
355
|
+
# Other changes
|
|
356
|
+
display_other_changes(current, proposed)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def display_features_diff(current, proposed)
|
|
360
|
+
current_features = normalize_features(current)
|
|
361
|
+
proposed_features = normalize_features(proposed)
|
|
362
|
+
|
|
363
|
+
added = proposed_features.keys - current_features.keys
|
|
364
|
+
removed = current_features.keys - proposed_features.keys
|
|
365
|
+
|
|
366
|
+
if added.any? || removed.any?
|
|
367
|
+
display_message("Features:", type: :info)
|
|
368
|
+
added.each do |feature|
|
|
369
|
+
display_message(" + #{feature}", type: :success)
|
|
370
|
+
end
|
|
371
|
+
removed.each do |feature|
|
|
372
|
+
display_message(" - #{feature}", type: :error)
|
|
373
|
+
end
|
|
374
|
+
display_message("")
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def display_ports_diff(current, proposed)
|
|
379
|
+
current_ports = Array(current).sort
|
|
380
|
+
proposed_ports = Array(proposed).sort
|
|
381
|
+
|
|
382
|
+
added = proposed_ports - current_ports
|
|
383
|
+
removed = current_ports - proposed_ports
|
|
384
|
+
|
|
385
|
+
if added.any? || removed.any?
|
|
386
|
+
display_message("Ports:", type: :info)
|
|
387
|
+
added.each do |port|
|
|
388
|
+
display_message(" + #{port}", type: :success)
|
|
389
|
+
end
|
|
390
|
+
removed.each do |port|
|
|
391
|
+
display_message(" - #{port}", type: :error)
|
|
392
|
+
end
|
|
393
|
+
display_message("")
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def display_port_attributes_diff(current, proposed)
|
|
398
|
+
current_attrs = current || {}
|
|
399
|
+
proposed_attrs = proposed || {}
|
|
400
|
+
|
|
401
|
+
all_ports = (current_attrs.keys + proposed_attrs.keys).uniq
|
|
402
|
+
|
|
403
|
+
changes = all_ports.select do |port|
|
|
404
|
+
current_attrs[port] != proposed_attrs[port]
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
if changes.any?
|
|
408
|
+
display_message("Port Attributes:", type: :info)
|
|
409
|
+
changes.each do |port|
|
|
410
|
+
if current_attrs[port] && proposed_attrs[port]
|
|
411
|
+
display_message(" ~ #{port}: #{proposed_attrs[port]["label"]}", type: :warning)
|
|
412
|
+
elsif proposed_attrs[port]
|
|
413
|
+
display_message(" + #{port}: #{proposed_attrs[port]["label"]}", type: :success)
|
|
414
|
+
else
|
|
415
|
+
display_message(" - #{port}", type: :error)
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
display_message("")
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def display_env_diff(current, proposed)
|
|
423
|
+
current_env = current || {}
|
|
424
|
+
proposed_env = proposed || {}
|
|
425
|
+
|
|
426
|
+
added = proposed_env.keys - current_env.keys
|
|
427
|
+
removed = current_env.keys - proposed_env.keys
|
|
428
|
+
modified = (current_env.keys & proposed_env.keys).select do |key|
|
|
429
|
+
current_env[key] != proposed_env[key]
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
if added.any? || removed.any? || modified.any?
|
|
433
|
+
display_message("Environment:", type: :info)
|
|
434
|
+
added.each do |key|
|
|
435
|
+
display_message(" + #{key}=#{proposed_env[key]}", type: :success)
|
|
436
|
+
end
|
|
437
|
+
modified.each do |key|
|
|
438
|
+
display_message(" ~ #{key}: #{current_env[key]} โ #{proposed_env[key]}", type: :warning)
|
|
439
|
+
end
|
|
440
|
+
removed.each do |key|
|
|
441
|
+
display_message(" - #{key}", type: :error)
|
|
442
|
+
end
|
|
443
|
+
display_message("")
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def display_other_changes(current, proposed)
|
|
448
|
+
# Check for other significant changes
|
|
449
|
+
changes = []
|
|
450
|
+
|
|
451
|
+
if current["name"] != proposed["name"] && proposed["name"]
|
|
452
|
+
changes << " ~ name: #{current["name"]} โ #{proposed["name"]}"
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
if current["image"] != proposed["image"] && proposed["image"]
|
|
456
|
+
changes << " ~ image: #{current["image"]} โ #{proposed["image"]}"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
if current["postCreateCommand"] != proposed["postCreateCommand"] && proposed["postCreateCommand"]
|
|
460
|
+
changes << " ~ postCreateCommand"
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
if changes.any?
|
|
464
|
+
display_message("Other Changes:", type: :info)
|
|
465
|
+
changes.each { |change| display_message(change, type: :warning) }
|
|
466
|
+
display_message("")
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def normalize_features(features)
|
|
471
|
+
case features
|
|
472
|
+
when Hash
|
|
473
|
+
features
|
|
474
|
+
when Array
|
|
475
|
+
features.each_with_object({}) { |f, h| h[f] = {} }
|
|
476
|
+
else
|
|
477
|
+
{}
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def write_devcontainer(path, config)
|
|
482
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
483
|
+
File.write(path, JSON.pretty_generate(config))
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def default_devcontainer_path
|
|
487
|
+
File.join(@project_dir, ".devcontainer", "devcontainer.json")
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def format_size(bytes)
|
|
491
|
+
return "0 B" if bytes.zero?
|
|
492
|
+
|
|
493
|
+
units = %w[B KB MB GB]
|
|
494
|
+
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
495
|
+
exp = [exp, units.length - 1].min
|
|
496
|
+
|
|
497
|
+
"%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
end
|
|
@@ -5,6 +5,7 @@ require "net/http"
|
|
|
5
5
|
require "uri"
|
|
6
6
|
require "open3"
|
|
7
7
|
require "timeout"
|
|
8
|
+
require_relative "../execute/prompt_manager"
|
|
8
9
|
|
|
9
10
|
module Aidp
|
|
10
11
|
# Handles importing GitHub issues into AIDP work loops
|
|
@@ -239,12 +240,18 @@ module Aidp
|
|
|
239
240
|
end
|
|
240
241
|
|
|
241
242
|
def create_work_loop_prompt(issue_data)
|
|
242
|
-
# Create PROMPT.md for work loop
|
|
243
|
+
# Create PROMPT.md for work loop using PromptManager (issue #226)
|
|
243
244
|
prompt_content = generate_prompt_content(issue_data)
|
|
244
245
|
|
|
245
|
-
|
|
246
|
+
# Use PromptManager to write to .aidp/PROMPT.md and archive immediately
|
|
247
|
+
prompt_manager = Aidp::Execute::PromptManager.new(Dir.pwd)
|
|
248
|
+
step_name = "github_issue_#{issue_data[:number]}"
|
|
249
|
+
prompt_manager.write(prompt_content, step_name: step_name)
|
|
250
|
+
|
|
246
251
|
display_message("", type: :info)
|
|
247
252
|
display_message("๐ Created PROMPT.md for work loop", type: :success)
|
|
253
|
+
display_message(" Location: .aidp/PROMPT.md", type: :info)
|
|
254
|
+
display_message(" Archived to: .aidp/prompt_archive/", type: :info)
|
|
248
255
|
display_message(" You can now run 'aidp execute' to start working on this issue", type: :info)
|
|
249
256
|
end
|
|
250
257
|
|
|
@@ -479,7 +486,12 @@ module Aidp
|
|
|
479
486
|
+ (result.test_commands.empty? ? "" : "Test Commands:\n#{result.test_commands.map { |c| "- #{c}" }.join("\n")}\n\n") \
|
|
480
487
|
+ (result.lint_commands.empty? ? "" : "Lint Commands:\n#{result.lint_commands.map { |c| "- #{c}" }.join("\n")}\n")
|
|
481
488
|
|
|
482
|
-
|
|
489
|
+
# Use PromptManager to append to .aidp/PROMPT.md (issue #226)
|
|
490
|
+
prompt_manager = Aidp::Execute::PromptManager.new(Dir.pwd)
|
|
491
|
+
current_prompt = prompt_manager.read
|
|
492
|
+
updated_prompt = current_prompt + "\n---\n\n#{tooling_info}"
|
|
493
|
+
prompt_manager.write(updated_prompt, step_name: "github_issue_tooling")
|
|
494
|
+
|
|
483
495
|
display_message("๐งช Detected tooling and appended to PROMPT.md", type: :info)
|
|
484
496
|
end
|
|
485
497
|
end
|
data/lib/aidp/cli.rb
CHANGED
|
@@ -391,6 +391,7 @@ module Aidp
|
|
|
391
391
|
when "mcp" then run_mcp_command(args)
|
|
392
392
|
when "issue" then run_issue_command(args)
|
|
393
393
|
when "config" then run_config_command(args)
|
|
394
|
+
when "devcontainer" then run_devcontainer_command(args)
|
|
394
395
|
when "init" then run_init_command(args)
|
|
395
396
|
when "watch" then run_watch_command(args)
|
|
396
397
|
when "ws" then run_ws_command(args)
|
|
@@ -1095,6 +1096,96 @@ module Aidp
|
|
|
1095
1096
|
wizard.run
|
|
1096
1097
|
end
|
|
1097
1098
|
|
|
1099
|
+
def run_devcontainer_command(args)
|
|
1100
|
+
require_relative "cli/devcontainer_commands"
|
|
1101
|
+
|
|
1102
|
+
subcommand = args.shift
|
|
1103
|
+
|
|
1104
|
+
case subcommand
|
|
1105
|
+
when "diff"
|
|
1106
|
+
commands = CLI::DevcontainerCommands.new(project_dir: Dir.pwd, prompt: create_prompt)
|
|
1107
|
+
commands.diff
|
|
1108
|
+
when "apply"
|
|
1109
|
+
options = parse_devcontainer_apply_options(args)
|
|
1110
|
+
commands = CLI::DevcontainerCommands.new(project_dir: Dir.pwd, prompt: create_prompt)
|
|
1111
|
+
commands.apply(options)
|
|
1112
|
+
when "list-backups", "backups"
|
|
1113
|
+
commands = CLI::DevcontainerCommands.new(project_dir: Dir.pwd, prompt: create_prompt)
|
|
1114
|
+
commands.list_backups
|
|
1115
|
+
when "restore"
|
|
1116
|
+
backup = args.shift
|
|
1117
|
+
unless backup
|
|
1118
|
+
display_message("Error: backup index or path required", type: :error)
|
|
1119
|
+
display_devcontainer_usage
|
|
1120
|
+
return
|
|
1121
|
+
end
|
|
1122
|
+
options = parse_devcontainer_restore_options(args)
|
|
1123
|
+
commands = CLI::DevcontainerCommands.new(project_dir: Dir.pwd, prompt: create_prompt)
|
|
1124
|
+
commands.restore(backup, options)
|
|
1125
|
+
when "-h", "--help", nil
|
|
1126
|
+
display_devcontainer_usage
|
|
1127
|
+
else
|
|
1128
|
+
display_message("Unknown devcontainer subcommand: #{subcommand}", type: :error)
|
|
1129
|
+
display_devcontainer_usage
|
|
1130
|
+
end
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
def parse_devcontainer_apply_options(args)
|
|
1134
|
+
options = {}
|
|
1135
|
+
until args.empty?
|
|
1136
|
+
token = args.shift
|
|
1137
|
+
case token
|
|
1138
|
+
when "--dry-run"
|
|
1139
|
+
options[:dry_run] = true
|
|
1140
|
+
when "--force"
|
|
1141
|
+
options[:force] = true
|
|
1142
|
+
when "--no-backup"
|
|
1143
|
+
options[:backup] = false
|
|
1144
|
+
else
|
|
1145
|
+
display_message("Unknown apply option: #{token}", type: :error)
|
|
1146
|
+
end
|
|
1147
|
+
end
|
|
1148
|
+
options
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
def parse_devcontainer_restore_options(args)
|
|
1152
|
+
options = {}
|
|
1153
|
+
until args.empty?
|
|
1154
|
+
token = args.shift
|
|
1155
|
+
case token
|
|
1156
|
+
when "--force"
|
|
1157
|
+
options[:force] = true
|
|
1158
|
+
when "--no-backup"
|
|
1159
|
+
options[:no_backup] = true
|
|
1160
|
+
else
|
|
1161
|
+
display_message("Unknown restore option: #{token}", type: :error)
|
|
1162
|
+
end
|
|
1163
|
+
end
|
|
1164
|
+
options
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
def display_devcontainer_usage
|
|
1168
|
+
display_message("\nUsage: aidp devcontainer <subcommand> [options]", type: :info)
|
|
1169
|
+
display_message("\nSubcommands:", type: :info)
|
|
1170
|
+
display_message(" diff Show changes between current and proposed config", type: :muted)
|
|
1171
|
+
display_message(" apply Apply configuration from aidp.yml", type: :muted)
|
|
1172
|
+
display_message(" list-backups List available backups", type: :muted)
|
|
1173
|
+
display_message(" restore <index|path> Restore from backup", type: :muted)
|
|
1174
|
+
display_message("\nApply Options:", type: :info)
|
|
1175
|
+
display_message(" --dry-run Preview changes without applying", type: :muted)
|
|
1176
|
+
display_message(" --force Skip confirmation prompts", type: :muted)
|
|
1177
|
+
display_message(" --no-backup Don't create backup before applying", type: :muted)
|
|
1178
|
+
display_message("\nRestore Options:", type: :info)
|
|
1179
|
+
display_message(" --force Skip confirmation prompt", type: :muted)
|
|
1180
|
+
display_message(" --no-backup Don't create backup before restoring", type: :muted)
|
|
1181
|
+
display_message("\nExamples:", type: :info)
|
|
1182
|
+
display_message(" aidp devcontainer diff", type: :muted)
|
|
1183
|
+
display_message(" aidp devcontainer apply --dry-run", type: :muted)
|
|
1184
|
+
display_message(" aidp devcontainer apply --force", type: :muted)
|
|
1185
|
+
display_message(" aidp devcontainer list-backups", type: :muted)
|
|
1186
|
+
display_message(" aidp devcontainer restore 1", type: :muted)
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1098
1189
|
def run_init_command(args = [])
|
|
1099
1190
|
options = {}
|
|
1100
1191
|
|