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
|
@@ -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)
|
|
@@ -852,6 +859,9 @@ module Aidp
|
|
|
852
859
|
def save_config(yaml_content)
|
|
853
860
|
Aidp::ConfigPaths.ensure_config_dir(project_dir)
|
|
854
861
|
File.write(config_path, yaml_content)
|
|
862
|
+
|
|
863
|
+
# Generate devcontainer if managed
|
|
864
|
+
generate_devcontainer_file
|
|
855
865
|
end
|
|
856
866
|
|
|
857
867
|
def display_warnings
|
|
@@ -1291,6 +1301,141 @@ module Aidp
|
|
|
1291
1301
|
def project_file?(relative_path)
|
|
1292
1302
|
File.exist?(File.join(project_dir, relative_path))
|
|
1293
1303
|
end
|
|
1304
|
+
|
|
1305
|
+
def configure_devcontainer
|
|
1306
|
+
prompt.say("\n🐳 Devcontainer Configuration")
|
|
1307
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.start")
|
|
1308
|
+
|
|
1309
|
+
# Detect existing devcontainer
|
|
1310
|
+
parser = Devcontainer::Parser.new(project_dir)
|
|
1311
|
+
existing_devcontainer = parser.devcontainer_exists? ? parser.parse : nil
|
|
1312
|
+
|
|
1313
|
+
if existing_devcontainer
|
|
1314
|
+
prompt.say("✓ Found existing devcontainer.json")
|
|
1315
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.detected_existing",
|
|
1316
|
+
path: parser.detect)
|
|
1317
|
+
end
|
|
1318
|
+
|
|
1319
|
+
# Ask if user wants AIDP to manage devcontainer
|
|
1320
|
+
manage = prompt.yes?(
|
|
1321
|
+
"Would you like AIDP to manage your devcontainer configuration?",
|
|
1322
|
+
default: (@config.dig(:devcontainer, :manage) || existing_devcontainer) ? true : false
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
unless manage
|
|
1326
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.opt_out")
|
|
1327
|
+
return set([:devcontainer, :manage], false)
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
# Build wizard config and detect ports
|
|
1331
|
+
wizard_config = build_wizard_config_for_devcontainer
|
|
1332
|
+
port_manager = Devcontainer::PortManager.new(wizard_config)
|
|
1333
|
+
detected_ports = port_manager.detect_required_ports
|
|
1334
|
+
|
|
1335
|
+
# Show detected ports
|
|
1336
|
+
if detected_ports.any?
|
|
1337
|
+
prompt.say("\nDetected ports:")
|
|
1338
|
+
detected_ports.each do |port|
|
|
1339
|
+
prompt.say(" • #{port[:number]} - #{port[:label]}")
|
|
1340
|
+
end
|
|
1341
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.detected_ports",
|
|
1342
|
+
ports: detected_ports.map { |port| port[:number] })
|
|
1343
|
+
end
|
|
1344
|
+
|
|
1345
|
+
# Ask about custom ports
|
|
1346
|
+
custom_ports = []
|
|
1347
|
+
if prompt.yes?("Add custom ports?", default: false)
|
|
1348
|
+
loop do
|
|
1349
|
+
port_num = prompt.ask("Port number (or press Enter to finish):")
|
|
1350
|
+
break if port_num.nil? || port_num.to_s.strip.empty?
|
|
1351
|
+
|
|
1352
|
+
unless port_num.to_s.match?(/^\d+$/)
|
|
1353
|
+
prompt.error("Port must be a number")
|
|
1354
|
+
next
|
|
1355
|
+
end
|
|
1356
|
+
|
|
1357
|
+
port_label = prompt.ask("Port label:", default: "Custom")
|
|
1358
|
+
custom_ports << {number: port_num.to_i, label: port_label}
|
|
1359
|
+
end
|
|
1360
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.custom_ports_selected",
|
|
1361
|
+
ports: custom_ports.map { |port| port[:number] })
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
# Save configuration
|
|
1365
|
+
set([:devcontainer, :manage], true)
|
|
1366
|
+
set([:devcontainer, :custom_ports], custom_ports) if custom_ports.any?
|
|
1367
|
+
set([:devcontainer, :last_generated], Time.now.utc.iso8601)
|
|
1368
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.enabled",
|
|
1369
|
+
custom_port_count: custom_ports.count,
|
|
1370
|
+
detected_port_count: detected_ports.count)
|
|
1371
|
+
end
|
|
1372
|
+
|
|
1373
|
+
def build_wizard_config_for_devcontainer
|
|
1374
|
+
{
|
|
1375
|
+
providers: @config[:providers]&.keys,
|
|
1376
|
+
test_framework: @config.dig(:work_loop, :test_commands)&.first&.dig(:framework),
|
|
1377
|
+
linters: @config.dig(:work_loop, :linting, :tools),
|
|
1378
|
+
watch_mode: @config.dig(:work_loop, :watch, :enabled),
|
|
1379
|
+
app_type: detect_app_type,
|
|
1380
|
+
services: detect_services,
|
|
1381
|
+
custom_ports: @config.dig(:devcontainer, :custom_ports)
|
|
1382
|
+
}.compact
|
|
1383
|
+
end
|
|
1384
|
+
|
|
1385
|
+
def detect_app_type
|
|
1386
|
+
return "rails_web" if project_file?("config/routes.rb")
|
|
1387
|
+
return "sinatra" if project_file?("config.ru")
|
|
1388
|
+
return "express" if project_file?("app.js") && project_file?("package.json")
|
|
1389
|
+
"cli"
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1392
|
+
def detect_services
|
|
1393
|
+
services = []
|
|
1394
|
+
services << "postgres" if project_file?("config/database.yml")
|
|
1395
|
+
services << "redis" if project_file?("config/redis.yml")
|
|
1396
|
+
services
|
|
1397
|
+
end
|
|
1398
|
+
|
|
1399
|
+
def generate_devcontainer_file
|
|
1400
|
+
unless @config.dig(:devcontainer, :manage)
|
|
1401
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "generate.skip_unmanaged")
|
|
1402
|
+
return
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1405
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "generate.start")
|
|
1406
|
+
|
|
1407
|
+
wizard_config = build_wizard_config_for_devcontainer
|
|
1408
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "generate.wizard_config",
|
|
1409
|
+
keys: wizard_config.keys)
|
|
1410
|
+
|
|
1411
|
+
parser = Devcontainer::Parser.new(project_dir)
|
|
1412
|
+
existing = parser.devcontainer_exists? ? parser.parse : nil
|
|
1413
|
+
|
|
1414
|
+
generator = Devcontainer::Generator.new(project_dir, @config)
|
|
1415
|
+
new_config = generator.generate(wizard_config, existing)
|
|
1416
|
+
|
|
1417
|
+
# Create backup if existing file
|
|
1418
|
+
if existing
|
|
1419
|
+
backup_manager = Devcontainer::BackupManager.new(project_dir)
|
|
1420
|
+
backup_manager.create_backup(
|
|
1421
|
+
parser.detect,
|
|
1422
|
+
{reason: "wizard_update", timestamp: Time.now.utc.iso8601}
|
|
1423
|
+
)
|
|
1424
|
+
prompt.say(" └─ Backup created")
|
|
1425
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "generate.backup_created",
|
|
1426
|
+
path: parser.detect)
|
|
1427
|
+
end
|
|
1428
|
+
|
|
1429
|
+
# Write devcontainer.json
|
|
1430
|
+
devcontainer_path = File.join(project_dir, ".devcontainer", "devcontainer.json")
|
|
1431
|
+
FileUtils.mkdir_p(File.dirname(devcontainer_path))
|
|
1432
|
+
File.write(devcontainer_path, JSON.pretty_generate(new_config))
|
|
1433
|
+
|
|
1434
|
+
prompt.ok("✅ Generated #{devcontainer_path}")
|
|
1435
|
+
Aidp.log_debug(DEVCONTAINER_COMPONENT, "generate.complete",
|
|
1436
|
+
devcontainer_path: devcontainer_path,
|
|
1437
|
+
forward_ports: new_config["forwardPorts"]&.length)
|
|
1438
|
+
end
|
|
1294
1439
|
end
|
|
1295
1440
|
end
|
|
1296
1441
|
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
|
|
@@ -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
|
|
|
@@ -156,7 +156,46 @@ module Aidp
|
|
|
156
156
|
end
|
|
157
157
|
|
|
158
158
|
def ensure_directory_exists
|
|
159
|
-
|
|
159
|
+
return if Dir.exist?(@base_dir)
|
|
160
|
+
begin
|
|
161
|
+
FileUtils.mkdir_p(@base_dir)
|
|
162
|
+
rescue SystemCallError => e
|
|
163
|
+
# Fallback when directory creation fails (e.g., attempting to write to '/.aidp')
|
|
164
|
+
fallback = begin
|
|
165
|
+
home = Dir.respond_to?(:home) ? Dir.home : nil
|
|
166
|
+
if home && !home.empty? && File.writable?(home)
|
|
167
|
+
File.join(home, ".aidp")
|
|
168
|
+
else
|
|
169
|
+
File.join(Dir.tmpdir, "aidp_storage")
|
|
170
|
+
end
|
|
171
|
+
rescue
|
|
172
|
+
File.join(Dir.tmpdir, "aidp_storage")
|
|
173
|
+
end
|
|
174
|
+
Kernel.warn "[AIDP Storage] Cannot create base directory #{@base_dir}: #{e.class}: #{e.message}. Using fallback #{fallback}"
|
|
175
|
+
@base_dir = fallback
|
|
176
|
+
begin
|
|
177
|
+
FileUtils.mkdir_p(@base_dir) unless Dir.exist?(@base_dir)
|
|
178
|
+
rescue SystemCallError => e2
|
|
179
|
+
Kernel.warn "[AIDP Storage] Fallback directory creation also failed: #{e2.class}: #{e2.message}. Continuing without persistent JSON storage."
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def sanitize_base_dir(dir)
|
|
185
|
+
return Dir.pwd if dir.nil? || dir.to_s.strip.empty?
|
|
186
|
+
str = dir.to_s
|
|
187
|
+
# If given root '/', redirect to a writable location to avoid EACCES on CI
|
|
188
|
+
if str == File::SEPARATOR
|
|
189
|
+
fallback = begin
|
|
190
|
+
home = Dir.home
|
|
191
|
+
(home && !home.empty? && File.writable?(home)) ? File.join(home, ".aidp") : File.join(Dir.tmpdir, "aidp_storage")
|
|
192
|
+
rescue
|
|
193
|
+
File.join(Dir.tmpdir, "aidp_storage")
|
|
194
|
+
end
|
|
195
|
+
Kernel.warn "[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'"
|
|
196
|
+
return fallback
|
|
197
|
+
end
|
|
198
|
+
str
|
|
160
199
|
end
|
|
161
200
|
end
|
|
162
201
|
end
|
data/lib/aidp/version.rb
CHANGED