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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fd6b2db707c511e0bc99dedaf4c3f5ec8253eae8d7b93676c9b0dee8ef1f4ed
4
- data.tar.gz: 558e0bb69e104ed325b77333d401ebde141eb2b7f969ac8eb473ce253331b244
3
+ metadata.gz: e07daf05fddb6301340e9a01b1c6d8eea60ddcf53b2ec2d56e914be610cd67a7
4
+ data.tar.gz: 21f26ed81af8f996cbb50c4a4dfe7603230f29499399c134c7a83492042a39e2
5
5
  SHA512:
6
- metadata.gz: 4e1455bf6c12523a5598ec8da183c7bf720512a8edee62e9dd22d32f75774bbb4801c506fd9d45da29486fdd15b9a249a2846f092ce1100cd8b2df84f9ee3c39
7
- data.tar.gz: f100cbd42b108e823c0735d1c643cfc4cf3f9f9bc7ea92547466bc33a1001a36ceceedaee45f1e319214503edf804e982478047720be52fcafb23440728b8d55
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
- File.write("PROMPT.md", prompt_content)
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
- File.open("PROMPT.md", "a") { |f| f.puts("\n---\n\n#{tooling_info}") }
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