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.
@@ -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
@@ -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
- FileUtils.mkdir_p(@base_dir) unless Dir.exist?(@base_dir)
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
- FileUtils.mkdir_p(@base_dir) unless Dir.exist?(@base_dir)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.21.1"
4
+ VERSION = "0.22.0"
5
5
  end