aidp 0.21.1 → 0.23.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 +145 -31
- data/lib/aidp/cli/devcontainer_commands.rb +501 -0
- data/lib/aidp/cli/issue_importer.rb +15 -3
- data/lib/aidp/cli.rb +107 -2
- 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/runner.rb +20 -6
- 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 +182 -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 +219 -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/watch/build_processor.rb +94 -4
- data/lib/aidp/watch/plan_generator.rb +16 -1
- data/lib/aidp/watch/plan_processor.rb +54 -3
- data/lib/aidp/watch/repository_client.rb +74 -0
- data/lib/aidp/watch/repository_safety_checker.rb +12 -3
- data/lib/aidp/watch/runner.rb +17 -7
- data/lib/aidp/workflows/guided_agent.rb +8 -42
- metadata +7 -1
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module Setup
|
|
5
|
+
module Devcontainer
|
|
6
|
+
# Manages port configuration for devcontainers and generates documentation
|
|
7
|
+
class PortManager
|
|
8
|
+
# Standard port assignments for common services
|
|
9
|
+
STANDARD_PORTS = {
|
|
10
|
+
web_app: {number: 3000, label: "Application", protocol: "http"},
|
|
11
|
+
remote_terminal: {number: 7681, label: "Remote Terminal (ttyd)", protocol: "http"},
|
|
12
|
+
playwright_debug: {number: 9222, label: "Playwright Debug", protocol: "http"},
|
|
13
|
+
mcp_server: {number: 8080, label: "MCP Server", protocol: "http"},
|
|
14
|
+
postgres: {number: 5432, label: "PostgreSQL", protocol: "tcp"},
|
|
15
|
+
redis: {number: 6379, label: "Redis", protocol: "tcp"},
|
|
16
|
+
mysql: {number: 3306, label: "MySQL", protocol: "tcp"}
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(wizard_config)
|
|
20
|
+
@wizard_config = wizard_config
|
|
21
|
+
@detected_ports = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Detect all required ports based on wizard configuration
|
|
25
|
+
# @return [Array<Hash>] Array of port configurations
|
|
26
|
+
def detect_required_ports
|
|
27
|
+
return @detected_ports if @detected_ports.any?
|
|
28
|
+
|
|
29
|
+
@detected_ports = []
|
|
30
|
+
|
|
31
|
+
detect_application_ports
|
|
32
|
+
detect_tool_ports
|
|
33
|
+
detect_service_ports
|
|
34
|
+
add_custom_ports
|
|
35
|
+
|
|
36
|
+
Aidp.log_debug("port_manager", "detected ports",
|
|
37
|
+
count: @detected_ports.size,
|
|
38
|
+
ports: @detected_ports.map { |p| p[:number] })
|
|
39
|
+
|
|
40
|
+
@detected_ports
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Generate forwardPorts array for devcontainer.json
|
|
44
|
+
# @return [Array<Integer>] Port numbers
|
|
45
|
+
def generate_forward_ports
|
|
46
|
+
detect_required_ports.map { |p| p[:number] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Generate portsAttributes hash for devcontainer.json
|
|
50
|
+
# @return [Hash] Port attributes with labels and settings
|
|
51
|
+
def generate_port_attributes
|
|
52
|
+
detect_required_ports.each_with_object({}) do |port, attrs|
|
|
53
|
+
attrs[port[:number].to_s] = {
|
|
54
|
+
"label" => port[:label],
|
|
55
|
+
"protocol" => port[:protocol] || "http",
|
|
56
|
+
"onAutoForward" => port[:auto_open] ? "notify" : "silent"
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Generate PORTS.md documentation
|
|
62
|
+
# @param output_path [String] Path to write PORTS.md
|
|
63
|
+
# @return [String] Generated markdown content
|
|
64
|
+
def generate_ports_documentation(output_path = nil)
|
|
65
|
+
detect_required_ports
|
|
66
|
+
|
|
67
|
+
content = build_ports_markdown
|
|
68
|
+
File.write(output_path, content) if output_path
|
|
69
|
+
|
|
70
|
+
content
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def detect_application_ports
|
|
76
|
+
# Web application port
|
|
77
|
+
if web_application?
|
|
78
|
+
@detected_ports << {
|
|
79
|
+
number: @wizard_config[:app_port] || 3000,
|
|
80
|
+
label: @wizard_config[:app_label] || "Application",
|
|
81
|
+
protocol: "http",
|
|
82
|
+
auto_open: true,
|
|
83
|
+
description: "Main application web server"
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# API server (if separate from main app)
|
|
88
|
+
if @wizard_config[:api_port]
|
|
89
|
+
@detected_ports << {
|
|
90
|
+
number: @wizard_config[:api_port],
|
|
91
|
+
label: "API Server",
|
|
92
|
+
protocol: "http",
|
|
93
|
+
auto_open: false,
|
|
94
|
+
description: "REST/GraphQL API endpoint"
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def detect_tool_ports
|
|
100
|
+
# Watch mode remote terminal
|
|
101
|
+
if @wizard_config[:watch_mode] || @wizard_config[:enable_watch]
|
|
102
|
+
@detected_ports << STANDARD_PORTS[:remote_terminal].merge(
|
|
103
|
+
auto_open: false,
|
|
104
|
+
description: "Terminal access via ttyd for watch mode operations"
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Playwright debug port
|
|
109
|
+
if playwright_enabled?
|
|
110
|
+
@detected_ports << STANDARD_PORTS[:playwright_debug].merge(
|
|
111
|
+
auto_open: false,
|
|
112
|
+
description: "Chrome DevTools Protocol for Playwright debugging"
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# MCP server
|
|
117
|
+
if mcp_enabled?
|
|
118
|
+
port_config = STANDARD_PORTS[:mcp_server].dup
|
|
119
|
+
port_config[:number] = @wizard_config[:mcp_port] if @wizard_config[:mcp_port]
|
|
120
|
+
@detected_ports << port_config.merge(
|
|
121
|
+
auto_open: false,
|
|
122
|
+
description: "Model Context Protocol server endpoint"
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def detect_service_ports
|
|
128
|
+
return unless @wizard_config[:services]
|
|
129
|
+
|
|
130
|
+
@wizard_config[:services].each do |service|
|
|
131
|
+
service_key = service.to_sym
|
|
132
|
+
next unless STANDARD_PORTS.key?(service_key)
|
|
133
|
+
|
|
134
|
+
@detected_ports << STANDARD_PORTS[service_key].merge(
|
|
135
|
+
auto_open: false,
|
|
136
|
+
description: service_description(service_key)
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def add_custom_ports
|
|
142
|
+
return unless @wizard_config[:custom_ports]
|
|
143
|
+
|
|
144
|
+
@wizard_config[:custom_ports].each do |port|
|
|
145
|
+
port_config = if port.is_a?(Hash)
|
|
146
|
+
{
|
|
147
|
+
number: port[:number],
|
|
148
|
+
label: port[:label] || "Custom Port",
|
|
149
|
+
protocol: port[:protocol] || "http",
|
|
150
|
+
auto_open: port[:auto_open] || false,
|
|
151
|
+
description: port[:description] || "User-defined port"
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
{
|
|
155
|
+
number: port.to_i,
|
|
156
|
+
label: "Custom Port",
|
|
157
|
+
protocol: "http",
|
|
158
|
+
auto_open: false,
|
|
159
|
+
description: "User-defined port"
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
@detected_ports << port_config if port_config[:number]&.positive?
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def build_ports_markdown
|
|
168
|
+
<<~MARKDOWN
|
|
169
|
+
# Port Configuration
|
|
170
|
+
|
|
171
|
+
This document lists all ports configured for this development environment.
|
|
172
|
+
|
|
173
|
+
## Overview
|
|
174
|
+
|
|
175
|
+
Total ports configured: **#{@detected_ports.size}**
|
|
176
|
+
|
|
177
|
+
## Port Details
|
|
178
|
+
|
|
179
|
+
#{build_port_table}
|
|
180
|
+
|
|
181
|
+
## Security Considerations
|
|
182
|
+
|
|
183
|
+
- All ports are forwarded from the devcontainer to your local machine
|
|
184
|
+
- Ports marked as "Auto-open" will trigger a notification when the container starts
|
|
185
|
+
- Ensure sensitive services (databases, etc.) are not exposed publicly
|
|
186
|
+
- Use firewall rules to restrict access if deploying to a remote environment
|
|
187
|
+
|
|
188
|
+
## Adding Custom Ports
|
|
189
|
+
|
|
190
|
+
To add custom ports, update your `aidp.yml`:
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
devcontainer:
|
|
194
|
+
custom_ports:
|
|
195
|
+
- number: 8000
|
|
196
|
+
label: "Custom Service"
|
|
197
|
+
protocol: "http"
|
|
198
|
+
auto_open: false
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Then re-run `aidp config --interactive` to update your devcontainer.
|
|
202
|
+
|
|
203
|
+
## Firewall Configuration
|
|
204
|
+
|
|
205
|
+
#{build_firewall_section}
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
*Generated by AIDP #{Aidp::VERSION} on #{Time.now.utc.strftime("%Y-%m-%d")}*
|
|
210
|
+
MARKDOWN
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def build_port_table
|
|
214
|
+
return "*No ports configured*" if @detected_ports.empty?
|
|
215
|
+
|
|
216
|
+
table = "| Port | Label | Protocol | Auto-open | Description |\n"
|
|
217
|
+
table += "|------|-------|----------|-----------|-------------|\n"
|
|
218
|
+
|
|
219
|
+
@detected_ports.sort_by { |p| p[:number] }.each do |port|
|
|
220
|
+
auto_open = port[:auto_open] ? "Yes" : "No"
|
|
221
|
+
table += "| #{port[:number]} | #{port[:label]} | #{port[:protocol]} | #{auto_open} | #{port[:description] || "-"} |\n"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
table
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def build_firewall_section
|
|
228
|
+
if @detected_ports.empty?
|
|
229
|
+
return "No ports require firewall configuration."
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
ports_list = @detected_ports.map { |p| p[:number] }.sort.join(", ")
|
|
233
|
+
|
|
234
|
+
<<~FIREWALL
|
|
235
|
+
If running in a cloud environment or VM, ensure these ports are allowed through your firewall:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# Example: UFW (Ubuntu)
|
|
239
|
+
#{@detected_ports.sort_by { |p| p[:number] }.map { |p| "sudo ufw allow #{p[:number]}/tcp # #{p[:label]}" }.join("\n")}
|
|
240
|
+
|
|
241
|
+
# Example: firewalld (RHEL/CentOS)
|
|
242
|
+
#{@detected_ports.sort_by { |p| p[:number] }.map { |p| "sudo firewall-cmd --permanent --add-port=#{p[:number]}/tcp # #{p[:label]}" }.join("\n")}
|
|
243
|
+
sudo firewall-cmd --reload
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Ports to allow:** #{ports_list}
|
|
247
|
+
FIREWALL
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def web_application?
|
|
251
|
+
@wizard_config[:app_type]&.match?(/web|rails|sinatra|express|django|flask/) ||
|
|
252
|
+
@wizard_config[:has_web_interface] == true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def playwright_enabled?
|
|
256
|
+
@wizard_config[:interactive_tools]&.include?("playwright") ||
|
|
257
|
+
@wizard_config[:test_framework]&.include?("playwright")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def mcp_enabled?
|
|
261
|
+
@wizard_config[:interactive_tools]&.include?("mcp") ||
|
|
262
|
+
@wizard_config[:enable_mcp] == true
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def service_description(service_key)
|
|
266
|
+
case service_key
|
|
267
|
+
when :postgres
|
|
268
|
+
"PostgreSQL database server"
|
|
269
|
+
when :redis
|
|
270
|
+
"Redis in-memory data store"
|
|
271
|
+
when :mysql
|
|
272
|
+
"MySQL database server"
|
|
273
|
+
when :remote_terminal
|
|
274
|
+
"Remote terminal access"
|
|
275
|
+
when :playwright_debug
|
|
276
|
+
"Browser automation debugging"
|
|
277
|
+
when :mcp_server
|
|
278
|
+
"Model Context Protocol server"
|
|
279
|
+
else
|
|
280
|
+
"Service port"
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -4,9 +4,14 @@ require "tty-prompt"
|
|
|
4
4
|
require "yaml"
|
|
5
5
|
require "time"
|
|
6
6
|
require "fileutils"
|
|
7
|
+
require "json"
|
|
7
8
|
|
|
8
9
|
require_relative "../util"
|
|
9
10
|
require_relative "../config/paths"
|
|
11
|
+
require_relative "devcontainer/parser"
|
|
12
|
+
require_relative "devcontainer/generator"
|
|
13
|
+
require_relative "devcontainer/port_manager"
|
|
14
|
+
require_relative "devcontainer/backup_manager"
|
|
10
15
|
|
|
11
16
|
module Aidp
|
|
12
17
|
module Setup
|
|
@@ -15,6 +20,7 @@ module Aidp
|
|
|
15
20
|
# while remaining idempotent and safe to re-run.
|
|
16
21
|
class Wizard
|
|
17
22
|
SCHEMA_VERSION = 1
|
|
23
|
+
DEVCONTAINER_COMPONENT = "setup_wizard.devcontainer"
|
|
18
24
|
|
|
19
25
|
attr_reader :project_dir, :prompt, :dry_run
|
|
20
26
|
|
|
@@ -41,6 +47,7 @@ module Aidp
|
|
|
41
47
|
configure_nfrs
|
|
42
48
|
configure_logging
|
|
43
49
|
configure_modes
|
|
50
|
+
configure_devcontainer
|
|
44
51
|
|
|
45
52
|
yaml_content = generate_yaml
|
|
46
53
|
display_preview(yaml_content)
|
|
@@ -779,6 +786,79 @@ module Aidp
|
|
|
779
786
|
watch_enabled: watch,
|
|
780
787
|
quick_mode_default: quick_mode
|
|
781
788
|
})
|
|
789
|
+
|
|
790
|
+
# Configure watch mode settings if enabled
|
|
791
|
+
configure_watch_mode if watch
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def configure_watch_mode
|
|
795
|
+
prompt.say("\n👀 Watch Mode Configuration")
|
|
796
|
+
prompt.say("-" * 40)
|
|
797
|
+
|
|
798
|
+
configure_watch_safety
|
|
799
|
+
configure_watch_labels
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def configure_watch_safety
|
|
803
|
+
prompt.say("\n🔒 Watch mode safety settings")
|
|
804
|
+
existing = get([:watch, :safety]) || {}
|
|
805
|
+
|
|
806
|
+
allow_public_repos = prompt.yes?(
|
|
807
|
+
"Allow watch mode on public repositories?",
|
|
808
|
+
default: existing.fetch(:allow_public_repos, false)
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
prompt.say("\n📝 Author allowlist (GitHub usernames allowed to trigger watch mode)")
|
|
812
|
+
prompt.say(" Leave empty to allow all authors (not recommended for public repos)")
|
|
813
|
+
author_allowlist = ask_list(
|
|
814
|
+
"Author allowlist (comma-separated GitHub usernames)",
|
|
815
|
+
existing[:author_allowlist] || [],
|
|
816
|
+
allow_empty: true
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
require_container = prompt.yes?(
|
|
820
|
+
"Require watch mode to run in a container?",
|
|
821
|
+
default: existing.fetch(:require_container, true)
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
set([:watch, :safety], {
|
|
825
|
+
allow_public_repos: allow_public_repos,
|
|
826
|
+
author_allowlist: author_allowlist,
|
|
827
|
+
require_container: require_container
|
|
828
|
+
})
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def configure_watch_labels
|
|
832
|
+
prompt.say("\n🏷️ Watch mode label configuration")
|
|
833
|
+
prompt.say(" Configure GitHub issue labels that trigger watch mode actions")
|
|
834
|
+
existing = get([:watch, :labels]) || {}
|
|
835
|
+
|
|
836
|
+
plan_trigger = ask_with_default(
|
|
837
|
+
"Label to trigger plan generation",
|
|
838
|
+
existing[:plan_trigger] || "aidp-plan"
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
needs_input = ask_with_default(
|
|
842
|
+
"Label for plans needing user input",
|
|
843
|
+
existing[:needs_input] || "aidp-needs-input"
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
ready_to_build = ask_with_default(
|
|
847
|
+
"Label for plans ready to build",
|
|
848
|
+
existing[:ready_to_build] || "aidp-ready"
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
build_trigger = ask_with_default(
|
|
852
|
+
"Label to trigger implementation",
|
|
853
|
+
existing[:build_trigger] || "aidp-build"
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
set([:watch, :labels], {
|
|
857
|
+
plan_trigger: plan_trigger,
|
|
858
|
+
needs_input: needs_input,
|
|
859
|
+
ready_to_build: ready_to_build,
|
|
860
|
+
build_trigger: build_trigger
|
|
861
|
+
})
|
|
782
862
|
end
|
|
783
863
|
|
|
784
864
|
# -------------------------------------------
|
|
@@ -809,6 +889,7 @@ module Aidp
|
|
|
809
889
|
.sub(/^nfrs:/, "# Non-functional requirements to reference during planning\nnfrs:")
|
|
810
890
|
.sub(/^logging:/, "# Logging configuration\nlogging:")
|
|
811
891
|
.sub(/^modes:/, "# Defaults for background/watch/quick modes\nmodes:")
|
|
892
|
+
.sub(/^watch:/, "# Watch mode safety and label configuration\nwatch:")
|
|
812
893
|
end
|
|
813
894
|
|
|
814
895
|
def display_preview(yaml_content)
|
|
@@ -852,6 +933,9 @@ module Aidp
|
|
|
852
933
|
def save_config(yaml_content)
|
|
853
934
|
Aidp::ConfigPaths.ensure_config_dir(project_dir)
|
|
854
935
|
File.write(config_path, yaml_content)
|
|
936
|
+
|
|
937
|
+
# Generate devcontainer if managed
|
|
938
|
+
generate_devcontainer_file
|
|
855
939
|
end
|
|
856
940
|
|
|
857
941
|
def display_warnings
|
|
@@ -1291,6 +1375,141 @@ module Aidp
|
|
|
1291
1375
|
def project_file?(relative_path)
|
|
1292
1376
|
File.exist?(File.join(project_dir, relative_path))
|
|
1293
1377
|
end
|
|
1378
|
+
|
|
1379
|
+
def configure_devcontainer
|
|
1380
|
+
prompt.say("\n🐳 Devcontainer Configuration")
|
|
1381
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.start")
|
|
1382
|
+
|
|
1383
|
+
# Detect existing devcontainer
|
|
1384
|
+
parser = Devcontainer::Parser.new(project_dir)
|
|
1385
|
+
existing_devcontainer = parser.devcontainer_exists? ? parser.parse : nil
|
|
1386
|
+
|
|
1387
|
+
if existing_devcontainer
|
|
1388
|
+
prompt.say("✓ Found existing devcontainer.json")
|
|
1389
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.detected_existing",
|
|
1390
|
+
path: parser.detect)
|
|
1391
|
+
end
|
|
1392
|
+
|
|
1393
|
+
# Ask if user wants AIDP to manage devcontainer
|
|
1394
|
+
manage = prompt.yes?(
|
|
1395
|
+
"Would you like AIDP to manage your devcontainer configuration?",
|
|
1396
|
+
default: (@config.dig(:devcontainer, :manage) || existing_devcontainer) ? true : false
|
|
1397
|
+
)
|
|
1398
|
+
|
|
1399
|
+
unless manage
|
|
1400
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.opt_out")
|
|
1401
|
+
return set([:devcontainer, :manage], false)
|
|
1402
|
+
end
|
|
1403
|
+
|
|
1404
|
+
# Build wizard config and detect ports
|
|
1405
|
+
wizard_config = build_wizard_config_for_devcontainer
|
|
1406
|
+
port_manager = Devcontainer::PortManager.new(wizard_config)
|
|
1407
|
+
detected_ports = port_manager.detect_required_ports
|
|
1408
|
+
|
|
1409
|
+
# Show detected ports
|
|
1410
|
+
if detected_ports.any?
|
|
1411
|
+
prompt.say("\nDetected ports:")
|
|
1412
|
+
detected_ports.each do |port|
|
|
1413
|
+
prompt.say(" • #{port[:number]} - #{port[:label]}")
|
|
1414
|
+
end
|
|
1415
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.detected_ports",
|
|
1416
|
+
ports: detected_ports.map { |port| port[:number] })
|
|
1417
|
+
end
|
|
1418
|
+
|
|
1419
|
+
# Ask about custom ports
|
|
1420
|
+
custom_ports = []
|
|
1421
|
+
if prompt.yes?("Add custom ports?", default: false)
|
|
1422
|
+
loop do
|
|
1423
|
+
port_num = prompt.ask("Port number (or press Enter to finish):")
|
|
1424
|
+
break if port_num.nil? || port_num.to_s.strip.empty?
|
|
1425
|
+
|
|
1426
|
+
unless port_num.to_s.match?(/^\d+$/)
|
|
1427
|
+
prompt.error("Port must be a number")
|
|
1428
|
+
next
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
port_label = prompt.ask("Port label:", default: "Custom")
|
|
1432
|
+
custom_ports << {number: port_num.to_i, label: port_label}
|
|
1433
|
+
end
|
|
1434
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.custom_ports_selected",
|
|
1435
|
+
ports: custom_ports.map { |port| port[:number] })
|
|
1436
|
+
end
|
|
1437
|
+
|
|
1438
|
+
# Save configuration
|
|
1439
|
+
set([:devcontainer, :manage], true)
|
|
1440
|
+
set([:devcontainer, :custom_ports], custom_ports) if custom_ports.any?
|
|
1441
|
+
set([:devcontainer, :last_generated], Time.now.utc.iso8601)
|
|
1442
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.enabled",
|
|
1443
|
+
custom_port_count: custom_ports.count,
|
|
1444
|
+
detected_port_count: detected_ports.count)
|
|
1445
|
+
end
|
|
1446
|
+
|
|
1447
|
+
def build_wizard_config_for_devcontainer
|
|
1448
|
+
{
|
|
1449
|
+
providers: @config[:providers]&.keys,
|
|
1450
|
+
test_framework: @config.dig(:work_loop, :test_commands)&.first&.dig(:framework),
|
|
1451
|
+
linters: @config.dig(:work_loop, :linting, :tools),
|
|
1452
|
+
watch_mode: @config.dig(:work_loop, :watch, :enabled),
|
|
1453
|
+
app_type: detect_app_type,
|
|
1454
|
+
services: detect_services,
|
|
1455
|
+
custom_ports: @config.dig(:devcontainer, :custom_ports)
|
|
1456
|
+
}.compact
|
|
1457
|
+
end
|
|
1458
|
+
|
|
1459
|
+
def detect_app_type
|
|
1460
|
+
return "rails_web" if project_file?("config/routes.rb")
|
|
1461
|
+
return "sinatra" if project_file?("config.ru")
|
|
1462
|
+
return "express" if project_file?("app.js") && project_file?("package.json")
|
|
1463
|
+
"cli"
|
|
1464
|
+
end
|
|
1465
|
+
|
|
1466
|
+
def detect_services
|
|
1467
|
+
services = []
|
|
1468
|
+
services << "postgres" if project_file?("config/database.yml")
|
|
1469
|
+
services << "redis" if project_file?("config/redis.yml")
|
|
1470
|
+
services
|
|
1471
|
+
end
|
|
1472
|
+
|
|
1473
|
+
def generate_devcontainer_file
|
|
1474
|
+
unless @config.dig(:devcontainer, :manage)
|
|
1475
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "generate.skip_unmanaged")
|
|
1476
|
+
return
|
|
1477
|
+
end
|
|
1478
|
+
|
|
1479
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "generate.start")
|
|
1480
|
+
|
|
1481
|
+
wizard_config = build_wizard_config_for_devcontainer
|
|
1482
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "generate.wizard_config",
|
|
1483
|
+
keys: wizard_config.keys)
|
|
1484
|
+
|
|
1485
|
+
parser = Devcontainer::Parser.new(project_dir)
|
|
1486
|
+
existing = parser.devcontainer_exists? ? parser.parse : nil
|
|
1487
|
+
|
|
1488
|
+
generator = Devcontainer::Generator.new(project_dir, @config)
|
|
1489
|
+
new_config = generator.generate(wizard_config, existing)
|
|
1490
|
+
|
|
1491
|
+
# Create backup if existing file
|
|
1492
|
+
if existing
|
|
1493
|
+
backup_manager = Devcontainer::BackupManager.new(project_dir)
|
|
1494
|
+
backup_manager.create_backup(
|
|
1495
|
+
parser.detect,
|
|
1496
|
+
{reason: "wizard_update", timestamp: Time.now.utc.iso8601}
|
|
1497
|
+
)
|
|
1498
|
+
prompt.say(" └─ Backup created")
|
|
1499
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "generate.backup_created",
|
|
1500
|
+
path: parser.detect)
|
|
1501
|
+
end
|
|
1502
|
+
|
|
1503
|
+
# Write devcontainer.json
|
|
1504
|
+
devcontainer_path = File.join(project_dir, ".devcontainer", "devcontainer.json")
|
|
1505
|
+
FileUtils.mkdir_p(File.dirname(devcontainer_path))
|
|
1506
|
+
File.write(devcontainer_path, JSON.pretty_generate(new_config))
|
|
1507
|
+
|
|
1508
|
+
prompt.ok("✅ Generated #{devcontainer_path}")
|
|
1509
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "generate.complete",
|
|
1510
|
+
devcontainer_path: devcontainer_path,
|
|
1511
|
+
forward_ports: new_config["forwardPorts"]&.length)
|
|
1512
|
+
end
|
|
1294
1513
|
end
|
|
1295
1514
|
end
|
|
1296
1515
|
end
|
|
@@ -11,7 +11,7 @@ module Aidp
|
|
|
11
11
|
include Aidp::RescueLogging
|
|
12
12
|
|
|
13
13
|
def initialize(base_dir = ".aidp")
|
|
14
|
-
@base_dir = base_dir
|
|
14
|
+
@base_dir = sanitize_base_dir(base_dir)
|
|
15
15
|
ensure_directory_exists
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -191,7 +191,44 @@ module Aidp
|
|
|
191
191
|
end
|
|
192
192
|
|
|
193
193
|
def ensure_directory_exists
|
|
194
|
-
|
|
194
|
+
return if Dir.exist?(@base_dir)
|
|
195
|
+
begin
|
|
196
|
+
FileUtils.mkdir_p(@base_dir)
|
|
197
|
+
rescue SystemCallError => e
|
|
198
|
+
fallback = begin
|
|
199
|
+
home = Dir.respond_to?(:home) ? Dir.home : nil
|
|
200
|
+
if home && !home.empty? && File.writable?(home)
|
|
201
|
+
File.join(home, ".aidp")
|
|
202
|
+
else
|
|
203
|
+
File.join(Dir.tmpdir, "aidp_storage")
|
|
204
|
+
end
|
|
205
|
+
rescue
|
|
206
|
+
File.join(Dir.tmpdir, "aidp_storage")
|
|
207
|
+
end
|
|
208
|
+
Kernel.warn "[AIDP Storage] Cannot create base directory #{@base_dir}: #{e.class}: #{e.message}. Using fallback #{fallback}"
|
|
209
|
+
@base_dir = fallback
|
|
210
|
+
begin
|
|
211
|
+
FileUtils.mkdir_p(@base_dir) unless Dir.exist?(@base_dir)
|
|
212
|
+
rescue SystemCallError => e2
|
|
213
|
+
Kernel.warn "[AIDP Storage] Fallback directory creation also failed: #{e2.class}: #{e2.message}. Continuing without persistent CSV storage."
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def sanitize_base_dir(dir)
|
|
219
|
+
return Dir.pwd if dir.nil? || dir.to_s.strip.empty?
|
|
220
|
+
str = dir.to_s
|
|
221
|
+
if str == File::SEPARATOR
|
|
222
|
+
fallback = begin
|
|
223
|
+
home = Dir.home
|
|
224
|
+
(home && !home.empty? && File.writable?(home)) ? File.join(home, ".aidp") : File.join(Dir.tmpdir, "aidp_storage")
|
|
225
|
+
rescue
|
|
226
|
+
File.join(Dir.tmpdir, "aidp_storage")
|
|
227
|
+
end
|
|
228
|
+
Kernel.warn "[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'"
|
|
229
|
+
return fallback
|
|
230
|
+
end
|
|
231
|
+
str
|
|
195
232
|
end
|
|
196
233
|
end
|
|
197
234
|
end
|
|
@@ -8,9 +8,16 @@ module Aidp
|
|
|
8
8
|
# Simple file manager that provides easy access to JSON and CSV storage
|
|
9
9
|
class FileManager
|
|
10
10
|
def initialize(base_dir = ".aidp")
|
|
11
|
-
@base_dir = base_dir
|
|
12
|
-
@json_storage = JsonStorage.new(base_dir)
|
|
13
|
-
@csv_storage = CsvStorage.new(base_dir)
|
|
11
|
+
@base_dir = sanitize_base_dir(base_dir)
|
|
12
|
+
@json_storage = JsonStorage.new(@base_dir)
|
|
13
|
+
@csv_storage = CsvStorage.new(@base_dir)
|
|
14
|
+
# Normalize base_dir if storages had to fallback
|
|
15
|
+
json_dir = @json_storage.instance_variable_get(:@base_dir)
|
|
16
|
+
csv_dir = @csv_storage.instance_variable_get(:@base_dir)
|
|
17
|
+
if json_dir != @base_dir || csv_dir != @base_dir
|
|
18
|
+
@base_dir = json_dir # Prefer JSON storage directory
|
|
19
|
+
Kernel.warn "[AIDP Storage] Base directory normalized to #{@base_dir} after fallback."
|
|
20
|
+
end
|
|
14
21
|
end
|
|
15
22
|
|
|
16
23
|
# JSON operations for structured data
|
|
@@ -209,6 +216,24 @@ module Aidp
|
|
|
209
216
|
rescue => error
|
|
210
217
|
{success: false, error: error.message}
|
|
211
218
|
end
|
|
219
|
+
|
|
220
|
+
private
|
|
221
|
+
|
|
222
|
+
def sanitize_base_dir(dir)
|
|
223
|
+
return Dir.pwd if dir.nil? || dir.to_s.strip.empty?
|
|
224
|
+
str = dir.to_s
|
|
225
|
+
if str == File::SEPARATOR
|
|
226
|
+
fallback = begin
|
|
227
|
+
home = Dir.home
|
|
228
|
+
(home && !home.empty? && File.writable?(home)) ? File.join(home, ".aidp") : File.join(Dir.tmpdir, "aidp_storage")
|
|
229
|
+
rescue
|
|
230
|
+
File.join(Dir.tmpdir, "aidp_storage")
|
|
231
|
+
end
|
|
232
|
+
Kernel.warn "[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'"
|
|
233
|
+
return fallback
|
|
234
|
+
end
|
|
235
|
+
str
|
|
236
|
+
end
|
|
212
237
|
end
|
|
213
238
|
end
|
|
214
239
|
end
|