aidp 0.12.1 → 0.14.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 +7 -0
- data/lib/aidp/analyze/json_file_storage.rb +21 -21
- data/lib/aidp/cli/enhanced_input.rb +114 -0
- data/lib/aidp/cli/first_run_wizard.rb +28 -309
- data/lib/aidp/cli/issue_importer.rb +359 -0
- data/lib/aidp/cli/mcp_dashboard.rb +3 -3
- data/lib/aidp/cli/terminal_io.rb +26 -0
- data/lib/aidp/cli.rb +155 -7
- data/lib/aidp/daemon/process_manager.rb +146 -0
- data/lib/aidp/daemon/runner.rb +232 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +216 -0
- data/lib/aidp/execute/future_work_backlog.rb +411 -0
- data/lib/aidp/execute/guard_policy.rb +246 -0
- data/lib/aidp/execute/instruction_queue.rb +131 -0
- data/lib/aidp/execute/interactive_repl.rb +335 -0
- data/lib/aidp/execute/repl_macros.rb +651 -0
- data/lib/aidp/execute/steps.rb +8 -0
- data/lib/aidp/execute/work_loop_runner.rb +322 -36
- data/lib/aidp/execute/work_loop_state.rb +162 -0
- data/lib/aidp/harness/condition_detector.rb +6 -6
- data/lib/aidp/harness/config_loader.rb +23 -23
- data/lib/aidp/harness/config_manager.rb +61 -61
- data/lib/aidp/harness/config_schema.rb +88 -0
- data/lib/aidp/harness/config_validator.rb +9 -9
- data/lib/aidp/harness/configuration.rb +76 -29
- data/lib/aidp/harness/error_handler.rb +13 -13
- data/lib/aidp/harness/provider_config.rb +79 -79
- data/lib/aidp/harness/provider_factory.rb +40 -40
- data/lib/aidp/harness/provider_info.rb +37 -20
- data/lib/aidp/harness/provider_manager.rb +58 -53
- data/lib/aidp/harness/provider_type_checker.rb +6 -6
- data/lib/aidp/harness/runner.rb +7 -7
- data/lib/aidp/harness/status_display.rb +33 -46
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -1
- data/lib/aidp/harness/ui/job_monitor.rb +7 -7
- data/lib/aidp/harness/user_interface.rb +43 -43
- data/lib/aidp/init/doc_generator.rb +256 -0
- data/lib/aidp/init/project_analyzer.rb +343 -0
- data/lib/aidp/init/runner.rb +83 -0
- data/lib/aidp/init.rb +5 -0
- data/lib/aidp/logger.rb +279 -0
- data/lib/aidp/providers/anthropic.rb +100 -26
- data/lib/aidp/providers/base.rb +13 -0
- data/lib/aidp/providers/codex.rb +28 -27
- data/lib/aidp/providers/cursor.rb +141 -34
- data/lib/aidp/providers/github_copilot.rb +26 -26
- data/lib/aidp/providers/macos_ui.rb +2 -18
- data/lib/aidp/providers/opencode.rb +26 -26
- data/lib/aidp/setup/wizard.rb +777 -0
- data/lib/aidp/tooling_detector.rb +115 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +282 -0
- data/lib/aidp/watch/plan_generator.rb +166 -0
- data/lib/aidp/watch/plan_processor.rb +83 -0
- data/lib/aidp/watch/repository_client.rb +243 -0
- data/lib/aidp/watch/runner.rb +93 -0
- data/lib/aidp/watch/state_store.rb +105 -0
- data/lib/aidp/watch.rb +9 -0
- data/lib/aidp/workflows/guided_agent.rb +344 -23
- data/lib/aidp.rb +14 -0
- data/templates/implementation/simple_task.md +36 -0
- metadata +27 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75047af72249152053d5dfe5b5e7800c0c8c844c7a513cf227601b8adb006af2
|
|
4
|
+
data.tar.gz: 574d521b5065f17afa618cfd6b672ed834badca459631011dddde7888796d7c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 323d8e0cfe1f37934d703488c00cbb4d1769bc0dea9d6f2d826a1a995861e6a8c84d697963a5e9880e5555e1d3e3f32f551f5db8c7ca273de8d79514dbad0e70
|
|
7
|
+
data.tar.gz: 98c37900f0672cade4a7fa838ae94282355e8645fc0ea9a0e2a2e6b2c3d78d265f91e4f66c0f592cf1b8d3297984fdc6fdc26d8efd584df8a0b0c2b543c1f3b8
|
data/README.md
CHANGED
|
@@ -11,6 +11,13 @@ gem install aidp
|
|
|
11
11
|
# Navigate to your project
|
|
12
12
|
cd /your/project
|
|
13
13
|
|
|
14
|
+
# Launch the interactive configuration wizard
|
|
15
|
+
aidp config --interactive
|
|
16
|
+
|
|
17
|
+
# Analyze and bootstrap project docs
|
|
18
|
+
aidp init
|
|
19
|
+
# Creates LLM_STYLE_GUIDE.md, PROJECT_ANALYSIS.md, CODE_QUALITY_PLAN.md
|
|
20
|
+
|
|
14
21
|
# Start an interactive workflow
|
|
15
22
|
aidp execute
|
|
16
23
|
|
|
@@ -14,7 +14,7 @@ module Aidp
|
|
|
14
14
|
|
|
15
15
|
# Store data in a JSON file
|
|
16
16
|
def store_data(filename, data)
|
|
17
|
-
file_path =
|
|
17
|
+
file_path = file_path(filename)
|
|
18
18
|
|
|
19
19
|
# Ensure directory exists
|
|
20
20
|
FileUtils.mkdir_p(File.dirname(file_path))
|
|
@@ -31,8 +31,8 @@ module Aidp
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Retrieve data from a JSON file
|
|
34
|
-
def
|
|
35
|
-
file_path =
|
|
34
|
+
def data(filename)
|
|
35
|
+
file_path = file_path(filename)
|
|
36
36
|
|
|
37
37
|
return nil unless File.exist?(file_path)
|
|
38
38
|
|
|
@@ -45,12 +45,12 @@ module Aidp
|
|
|
45
45
|
|
|
46
46
|
# Check if a JSON file exists
|
|
47
47
|
def data_exists?(filename)
|
|
48
|
-
File.exist?(
|
|
48
|
+
File.exist?(file_path(filename))
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Delete a JSON file
|
|
52
52
|
def delete_data(filename)
|
|
53
|
-
file_path =
|
|
53
|
+
file_path = file_path(filename)
|
|
54
54
|
|
|
55
55
|
if File.exist?(file_path)
|
|
56
56
|
File.delete(file_path)
|
|
@@ -89,8 +89,8 @@ module Aidp
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
# Get project configuration
|
|
92
|
-
def
|
|
93
|
-
|
|
92
|
+
def project_config
|
|
93
|
+
data("project_config.json")
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
# Store runtime status
|
|
@@ -99,8 +99,8 @@ module Aidp
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# Get runtime status
|
|
102
|
-
def
|
|
103
|
-
|
|
102
|
+
def runtime_status
|
|
103
|
+
data("runtime_status.json")
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
# Store simple metrics
|
|
@@ -109,8 +109,8 @@ module Aidp
|
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
# Get simple metrics
|
|
112
|
-
def
|
|
113
|
-
|
|
112
|
+
def simple_metrics
|
|
113
|
+
data("simple_metrics.json")
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
# Store analysis session data
|
|
@@ -119,8 +119,8 @@ module Aidp
|
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
# Get analysis session data
|
|
122
|
-
def
|
|
123
|
-
|
|
122
|
+
def analysis_session(session_id)
|
|
123
|
+
data("sessions/#{session_id}.json")
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
# List analysis sessions
|
|
@@ -145,8 +145,8 @@ module Aidp
|
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# Get user preferences
|
|
148
|
-
def
|
|
149
|
-
|
|
148
|
+
def user_preferences
|
|
149
|
+
data("user_preferences.json")
|
|
150
150
|
end
|
|
151
151
|
|
|
152
152
|
# Store cache data
|
|
@@ -161,8 +161,8 @@ module Aidp
|
|
|
161
161
|
end
|
|
162
162
|
|
|
163
163
|
# Get cache data (respects TTL)
|
|
164
|
-
def
|
|
165
|
-
cache_file_data =
|
|
164
|
+
def cache(cache_key)
|
|
165
|
+
cache_file_data = data("cache/#{cache_key}.json")
|
|
166
166
|
return nil unless cache_file_data
|
|
167
167
|
|
|
168
168
|
# Check TTL if specified
|
|
@@ -203,7 +203,7 @@ module Aidp
|
|
|
203
203
|
end
|
|
204
204
|
|
|
205
205
|
# Get storage statistics
|
|
206
|
-
def
|
|
206
|
+
def storage_statistics
|
|
207
207
|
files = list_files
|
|
208
208
|
|
|
209
209
|
{
|
|
@@ -226,7 +226,7 @@ module Aidp
|
|
|
226
226
|
|
|
227
227
|
files = list_files
|
|
228
228
|
files.each do |file_info|
|
|
229
|
-
data =
|
|
229
|
+
data = data(file_info[:filename])
|
|
230
230
|
export_data["files"][file_info[:filename]] = {
|
|
231
231
|
"data" => data,
|
|
232
232
|
"metadata" => {
|
|
@@ -249,7 +249,7 @@ module Aidp
|
|
|
249
249
|
|
|
250
250
|
# Import data from an exported JSON file
|
|
251
251
|
def import_data(import_filename)
|
|
252
|
-
import_path =
|
|
252
|
+
import_path = file_path(import_filename)
|
|
253
253
|
|
|
254
254
|
unless File.exist?(import_path)
|
|
255
255
|
raise "Import file does not exist: #{import_filename}"
|
|
@@ -280,7 +280,7 @@ module Aidp
|
|
|
280
280
|
|
|
281
281
|
private
|
|
282
282
|
|
|
283
|
-
def
|
|
283
|
+
def file_path(filename)
|
|
284
284
|
File.join(@storage_dir, filename)
|
|
285
285
|
end
|
|
286
286
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-prompt"
|
|
4
|
+
require "reline"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
class CLI
|
|
8
|
+
# Enhanced input handler with full readline-style key bindings using Reline
|
|
9
|
+
class EnhancedInput
|
|
10
|
+
# Standard key bindings supported by Reline:
|
|
11
|
+
# - Ctrl-A: Move to beginning of line
|
|
12
|
+
# - Ctrl-E: Move to end of line
|
|
13
|
+
# - Ctrl-W: Delete word backward
|
|
14
|
+
# - Ctrl-K: Kill to end of line
|
|
15
|
+
# - Ctrl-U: Kill to beginning of line
|
|
16
|
+
# - Ctrl-D: Delete character forward
|
|
17
|
+
# - Ctrl-H/Backspace: Delete character backward
|
|
18
|
+
# - Left/Right arrows: Move cursor
|
|
19
|
+
# - Alt-F/Alt-B: Move forward/backward by word
|
|
20
|
+
# - Home/End: Jump to beginning/end
|
|
21
|
+
# - Ctrl-T: Transpose characters
|
|
22
|
+
# - And many more Emacs-style bindings
|
|
23
|
+
|
|
24
|
+
def initialize(prompt: nil, input: nil, output: nil, use_reline: true)
|
|
25
|
+
@use_reline = use_reline
|
|
26
|
+
@input = input || $stdin
|
|
27
|
+
@output = output || $stdout
|
|
28
|
+
@prompt = prompt || TTY::Prompt.new(
|
|
29
|
+
input: @input,
|
|
30
|
+
output: @output,
|
|
31
|
+
enable_color: true,
|
|
32
|
+
interrupt: :exit
|
|
33
|
+
)
|
|
34
|
+
@show_hints = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Ask a question with full readline support
|
|
38
|
+
# Uses Reline for readline-style editing when use_reline is true
|
|
39
|
+
def ask(question, **options)
|
|
40
|
+
# If reline is enabled and we're in a TTY, use reline for better editing
|
|
41
|
+
if @use_reline && @input.tty?
|
|
42
|
+
default = options[:default]
|
|
43
|
+
required = options[:required] || false
|
|
44
|
+
|
|
45
|
+
# Display helpful hint on first use
|
|
46
|
+
if @show_hints
|
|
47
|
+
@output.puts "💡 Hint: Use Ctrl-A (start), Ctrl-E (end), Ctrl-W (delete word), Ctrl-K (kill line)"
|
|
48
|
+
@show_hints = false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Use Reline for input with full key binding support
|
|
52
|
+
loop do
|
|
53
|
+
prompt_text = question.to_s
|
|
54
|
+
prompt_text += " (#{default})" if default
|
|
55
|
+
prompt_text += " "
|
|
56
|
+
|
|
57
|
+
# Reline provides full readline editing capabilities
|
|
58
|
+
Reline.output = @output
|
|
59
|
+
Reline.input = @input
|
|
60
|
+
Reline.completion_append_character = " "
|
|
61
|
+
|
|
62
|
+
answer = Reline.readline(prompt_text, false)
|
|
63
|
+
|
|
64
|
+
# Handle Ctrl-D (nil return)
|
|
65
|
+
if answer.nil?
|
|
66
|
+
@output.puts
|
|
67
|
+
raise Interrupt
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
answer = answer.strip
|
|
71
|
+
answer = default if answer.empty? && default
|
|
72
|
+
|
|
73
|
+
if required && (answer.nil? || answer.empty?)
|
|
74
|
+
@output.puts " Value required."
|
|
75
|
+
next
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return answer
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
# Fall back to TTY::Prompt's ask
|
|
82
|
+
@prompt.ask(question, **options)
|
|
83
|
+
end
|
|
84
|
+
rescue Interrupt
|
|
85
|
+
@output.puts
|
|
86
|
+
raise
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Enable hints for key bindings
|
|
90
|
+
def enable_hints!
|
|
91
|
+
@show_hints = true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Disable Reline (fall back to TTY::Prompt)
|
|
95
|
+
def disable_reline!
|
|
96
|
+
@use_reline = false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Enable Reline
|
|
100
|
+
def enable_reline!
|
|
101
|
+
@use_reline = true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Delegate other methods to underlying prompt
|
|
105
|
+
def method_missing(method, *args, **kwargs, &block)
|
|
106
|
+
@prompt.send(method, *args, **kwargs, &block)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def respond_to_missing?(method, include_private = false)
|
|
110
|
+
@prompt.respond_to?(method, include_private) || super
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -1,44 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "
|
|
4
|
+
require "time"
|
|
5
5
|
require "tty-prompt"
|
|
6
|
-
require_relative "../harness/provider_factory"
|
|
7
|
-
require_relative "../config/paths"
|
|
8
6
|
|
|
9
7
|
module Aidp
|
|
10
8
|
class CLI
|
|
11
|
-
#
|
|
9
|
+
# Wrapper around Aidp::Setup::Wizard to preserve existing CLI entry points.
|
|
12
10
|
class FirstRunWizard
|
|
13
11
|
include Aidp::MessageDisplay
|
|
14
12
|
|
|
15
|
-
TEMPLATES_DIR = File.expand_path(File.join(__dir__, "..", "..", "..", "templates"))
|
|
16
|
-
|
|
17
13
|
def self.ensure_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
|
|
18
14
|
return true if Aidp::Config.config_exists?(project_dir)
|
|
19
15
|
|
|
20
16
|
wizard = new(project_dir, prompt: prompt)
|
|
21
17
|
|
|
22
18
|
if non_interactive
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
wizard.create_minimal_config
|
|
20
|
+
wizard.send(:display_message, "Created minimal configuration (non-interactive default)", type: :success)
|
|
21
|
+
true
|
|
22
|
+
else
|
|
23
|
+
wizard.run
|
|
27
24
|
end
|
|
28
|
-
|
|
29
|
-
wizard.run
|
|
30
25
|
end
|
|
31
26
|
|
|
32
27
|
def self.setup_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
|
|
33
|
-
wizard = new(project_dir, prompt: prompt)
|
|
34
|
-
|
|
35
28
|
if non_interactive
|
|
36
|
-
|
|
37
|
-
wizard.send(:display_message, "Configuration setup skipped in non-interactive environment", type: :info)
|
|
29
|
+
new(project_dir, prompt: prompt).send(:display_message, "Configuration setup skipped in non-interactive environment", type: :info)
|
|
38
30
|
return true
|
|
39
31
|
end
|
|
40
32
|
|
|
41
|
-
|
|
33
|
+
new(project_dir, prompt: prompt).run
|
|
42
34
|
end
|
|
43
35
|
|
|
44
36
|
def initialize(project_dir, prompt: TTY::Prompt.new)
|
|
@@ -47,306 +39,33 @@ module Aidp
|
|
|
47
39
|
end
|
|
48
40
|
|
|
49
41
|
def run
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
finish(run_custom)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def run_setup_config
|
|
56
|
-
@prompt.say("🔧 Configuration Setup", color: :blue)
|
|
57
|
-
@prompt.say("Setting up your configuration file with current values as defaults.")
|
|
58
|
-
@prompt.say("")
|
|
59
|
-
|
|
60
|
-
# Load existing config to use as defaults (if it exists)
|
|
61
|
-
existing_config = load_existing_config
|
|
62
|
-
|
|
63
|
-
if existing_config
|
|
64
|
-
# Run custom configuration with existing values as defaults
|
|
65
|
-
finish(run_custom_with_defaults(existing_config))
|
|
66
|
-
else
|
|
67
|
-
# No existing config, run the normal setup flow
|
|
68
|
-
@prompt.say("No existing configuration found. Running first-time setup...")
|
|
69
|
-
@prompt.say("")
|
|
70
|
-
run
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
def banner
|
|
77
|
-
display_message("\n🚀 First-time setup detected", type: :highlight)
|
|
78
|
-
display_message("No 'aidp.yml' configuration file found in #{relative(@project_dir)}.")
|
|
79
|
-
display_message("Creating a configuration so you can start using AI Dev Pipeline.")
|
|
80
|
-
display_message("")
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def finish(path)
|
|
84
|
-
if path
|
|
85
|
-
display_message("\n✅ Configuration created at #{relative(path)}", type: :success)
|
|
86
|
-
display_message("You can edit this file anytime. Continuing startup...\n")
|
|
87
|
-
true
|
|
88
|
-
else
|
|
89
|
-
display_message("❌ Failed to create configuration file.", type: :error)
|
|
90
|
-
false
|
|
91
|
-
end
|
|
42
|
+
wizard = Aidp::Setup::Wizard.new(@project_dir, prompt: @prompt)
|
|
43
|
+
wizard.run
|
|
92
44
|
end
|
|
93
45
|
|
|
94
|
-
def
|
|
95
|
-
src = File.join(TEMPLATES_DIR, filename)
|
|
96
|
-
unless File.exist?(src)
|
|
97
|
-
display_message("Template not found: #{filename}", type: :error)
|
|
98
|
-
return nil
|
|
99
|
-
end
|
|
100
|
-
dest = Aidp::ConfigPaths.config_file(@project_dir)
|
|
46
|
+
def create_minimal_config
|
|
101
47
|
Aidp::ConfigPaths.ensure_config_dir(@project_dir)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def write_minimal_config(project_dir)
|
|
107
|
-
dest = Aidp::ConfigPaths.config_file(project_dir)
|
|
108
|
-
return dest if File.exist?(dest)
|
|
109
|
-
data = {
|
|
110
|
-
"harness" => {
|
|
111
|
-
"max_retries" => 2,
|
|
112
|
-
"default_provider" => "cursor",
|
|
113
|
-
"fallback_providers" => ["cursor"],
|
|
114
|
-
"no_api_keys_required" => false
|
|
115
|
-
},
|
|
48
|
+
minimal = {
|
|
49
|
+
"schema_version" => Aidp::Setup::Wizard::SCHEMA_VERSION,
|
|
50
|
+
"generated_by" => "aidp setup wizard minimal",
|
|
51
|
+
"generated_at" => Time.now.utc.iso8601,
|
|
116
52
|
"providers" => {
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"
|
|
53
|
+
"llm" => {
|
|
54
|
+
"name" => "cursor",
|
|
55
|
+
"model" => "cursor-agent",
|
|
56
|
+
"temperature" => 0.2,
|
|
57
|
+
"max_tokens" => 1024
|
|
120
58
|
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
Aidp::ConfigPaths.ensure_config_dir(project_dir)
|
|
124
|
-
File.write(dest, YAML.dump(data))
|
|
125
|
-
dest
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def write_example_config(project_dir)
|
|
129
|
-
Aidp::Config.create_example_config(project_dir)
|
|
130
|
-
Aidp::ConfigPaths.config_file(project_dir)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def run_custom
|
|
134
|
-
dest = Aidp::ConfigPaths.config_file(@project_dir)
|
|
135
|
-
return dest if File.exist?(dest)
|
|
136
|
-
|
|
137
|
-
@prompt.say("Interactive custom configuration: press Enter to accept defaults shown in [brackets].")
|
|
138
|
-
@prompt.say("")
|
|
139
|
-
|
|
140
|
-
# Get available providers for validation
|
|
141
|
-
available_providers = get_available_providers
|
|
142
|
-
|
|
143
|
-
# Use TTY::Prompt select for primary provider
|
|
144
|
-
# Find the formatted string that matches the default
|
|
145
|
-
default_option = available_providers.find { |option| option.start_with?("cursor -") } || available_providers.first
|
|
146
|
-
default_provider = @prompt.select("Default provider?", available_providers, default: default_option)
|
|
147
|
-
|
|
148
|
-
# Extract just the provider name from the formatted string
|
|
149
|
-
provider_name = default_provider.split(" - ").first
|
|
150
|
-
|
|
151
|
-
# Validate fallback providers
|
|
152
|
-
fallback_providers = select_fallback_providers(available_providers, provider_name)
|
|
153
|
-
|
|
154
|
-
restrict = @prompt.yes?("Only use providers that don't require API keys?", default: false)
|
|
155
|
-
|
|
156
|
-
# Process providers preserving order
|
|
157
|
-
providers = [provider_name] + fallback_providers
|
|
158
|
-
providers.uniq!
|
|
159
|
-
|
|
160
|
-
provider_section = {}
|
|
161
|
-
providers.each do |prov|
|
|
162
|
-
provider_section[prov] = {"type" => (prov == "cursor") ? "subscription" : "usage_based", "default_flags" => []}
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
data = {
|
|
166
|
-
"harness" => {
|
|
167
|
-
"max_retries" => 2,
|
|
168
|
-
"default_provider" => provider_name,
|
|
169
|
-
"fallback_providers" => fallback_providers,
|
|
170
|
-
"no_api_keys_required" => restrict
|
|
171
|
-
},
|
|
172
|
-
"providers" => provider_section
|
|
173
|
-
}
|
|
174
|
-
Aidp::ConfigPaths.ensure_config_dir(@project_dir)
|
|
175
|
-
File.write(dest, YAML.dump(data))
|
|
176
|
-
dest
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def run_custom_with_defaults(existing_config)
|
|
180
|
-
dest = Aidp::ConfigPaths.config_file(@project_dir)
|
|
181
|
-
|
|
182
|
-
# Extract current values from existing config
|
|
183
|
-
harness_config = existing_config[:harness] || existing_config["harness"] || {}
|
|
184
|
-
providers_config = existing_config[:providers] || existing_config["providers"] || {}
|
|
185
|
-
|
|
186
|
-
current_default = harness_config[:default_provider] || harness_config["default_provider"] || "cursor"
|
|
187
|
-
current_fallbacks = harness_config[:fallback_providers] || harness_config["fallback_providers"] || [current_default]
|
|
188
|
-
current_restrict = harness_config[:no_api_keys_required] || harness_config["no_api_keys_required"] || false
|
|
189
|
-
|
|
190
|
-
# Use TTY::Prompt for interactive configuration
|
|
191
|
-
@prompt.say("Interactive configuration update: press Enter to keep current values shown in [brackets].")
|
|
192
|
-
@prompt.say("")
|
|
193
|
-
|
|
194
|
-
# Get available providers for validation
|
|
195
|
-
available_providers = get_available_providers
|
|
196
|
-
|
|
197
|
-
# Use TTY::Prompt select for primary provider
|
|
198
|
-
# Find the formatted string that matches the current default
|
|
199
|
-
default_option = available_providers.find { |option| option.start_with?("#{current_default} -") } || available_providers.first
|
|
200
|
-
default_provider = @prompt.select("Default provider?", available_providers, default: default_option)
|
|
201
|
-
|
|
202
|
-
# Extract just the provider name from the formatted string
|
|
203
|
-
provider_name = default_provider.split(" - ").first
|
|
204
|
-
|
|
205
|
-
# Validate fallback providers
|
|
206
|
-
fallback_providers = select_fallback_providers(available_providers, provider_name, preselected: current_fallbacks - [provider_name])
|
|
207
|
-
|
|
208
|
-
restrict_input = @prompt.yes?("Only use providers that don't require API keys?", default: current_restrict)
|
|
209
|
-
|
|
210
|
-
# Process providers preserving order
|
|
211
|
-
providers = [provider_name] + fallback_providers
|
|
212
|
-
providers.uniq!
|
|
213
|
-
|
|
214
|
-
# Build provider section
|
|
215
|
-
provider_section = {}
|
|
216
|
-
providers.each do |prov|
|
|
217
|
-
# Try to preserve existing provider config if it exists
|
|
218
|
-
existing_provider = providers_config[prov.to_sym] || providers_config[prov.to_s]
|
|
219
|
-
if existing_provider
|
|
220
|
-
# Convert existing provider config to string keys
|
|
221
|
-
converted_provider = {}
|
|
222
|
-
existing_provider.each { |k, v| converted_provider[k.to_s] = v }
|
|
223
|
-
# Ensure the type is correct (fix old "package" and "api" types)
|
|
224
|
-
if converted_provider["type"] == "package"
|
|
225
|
-
converted_provider["type"] = "subscription"
|
|
226
|
-
elsif converted_provider["type"] == "api"
|
|
227
|
-
converted_provider["type"] = "usage_based"
|
|
228
|
-
end
|
|
229
|
-
provider_section[prov] = converted_provider
|
|
230
|
-
else
|
|
231
|
-
provider_section[prov] = {"type" => (prov == "cursor") ? "subscription" : "usage_based", "default_flags" => []}
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# Build the new config
|
|
236
|
-
data = {
|
|
237
|
-
"harness" => {
|
|
238
|
-
"max_retries" => harness_config[:max_retries] || harness_config["max_retries"] || 2,
|
|
239
|
-
"default_provider" => provider_name,
|
|
240
|
-
"fallback_providers" => fallback_providers,
|
|
241
|
-
"no_api_keys_required" => restrict_input
|
|
242
59
|
},
|
|
243
|
-
"
|
|
60
|
+
"work_loop" => {
|
|
61
|
+
"test" => {
|
|
62
|
+
"unit" => "bundle exec rspec",
|
|
63
|
+
"timeout_seconds" => 1800
|
|
64
|
+
}
|
|
65
|
+
}
|
|
244
66
|
}
|
|
245
67
|
|
|
246
|
-
Aidp::ConfigPaths.
|
|
247
|
-
File.write(dest, YAML.dump(data))
|
|
248
|
-
dest
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def load_existing_config
|
|
252
|
-
config_file = Aidp::ConfigPaths.config_file(@project_dir)
|
|
253
|
-
return nil unless File.exist?(config_file)
|
|
254
|
-
|
|
255
|
-
begin
|
|
256
|
-
YAML.load_file(config_file) || {}
|
|
257
|
-
rescue => e
|
|
258
|
-
@prompt.say("❌ Failed to load existing configuration: #{e.message}", color: :red)
|
|
259
|
-
nil
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def ask(prompt, default: nil)
|
|
264
|
-
if default
|
|
265
|
-
@prompt.ask("#{prompt}:", default: default)
|
|
266
|
-
else
|
|
267
|
-
@prompt.ask("#{prompt}:")
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def relative(path)
|
|
272
|
-
pn = Pathname.new(path)
|
|
273
|
-
wd = Pathname.new(@project_dir)
|
|
274
|
-
rel = pn.relative_path_from(wd).to_s
|
|
275
|
-
rel.start_with?("..") ? path : rel
|
|
276
|
-
rescue
|
|
277
|
-
path
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
# Get available providers for validation
|
|
281
|
-
def get_available_providers
|
|
282
|
-
# Get all supported providers from the factory (single source of truth)
|
|
283
|
-
all_providers = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES.keys
|
|
284
|
-
|
|
285
|
-
# Filter out providers we don't want to show in the wizard
|
|
286
|
-
# - "anthropic" is an internal name, we show "claude" instead
|
|
287
|
-
# - "macos" is disabled (as per issue #73)
|
|
288
|
-
excluded = ["anthropic", "macos"]
|
|
289
|
-
available = all_providers - excluded
|
|
290
|
-
|
|
291
|
-
# Get display names from the providers themselves
|
|
292
|
-
available.map do |provider_name|
|
|
293
|
-
provider_class = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES[provider_name]
|
|
294
|
-
if provider_class
|
|
295
|
-
# Instantiate to get display name
|
|
296
|
-
instance = provider_class.new
|
|
297
|
-
display_name = instance.display_name
|
|
298
|
-
"#{provider_name} - #{display_name}"
|
|
299
|
-
else
|
|
300
|
-
provider_name
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
# Validate provider list input
|
|
306
|
-
def validate_provider_list(input, available_providers)
|
|
307
|
-
return true if input.nil? || input.empty?
|
|
308
|
-
|
|
309
|
-
# Extract provider names from the input
|
|
310
|
-
providers = input.split(/\s*,\s*/).map(&:strip).reject(&:empty?)
|
|
311
|
-
|
|
312
|
-
# Check if all providers are valid
|
|
313
|
-
valid_providers = available_providers.map { |p| p.split(" - ").first }
|
|
314
|
-
providers.all? { |provider| valid_providers.include?(provider) }
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
# Interactive ordered multi-select for fallback providers
|
|
318
|
-
def select_fallback_providers(available_with_labels, default_provider, preselected: [])
|
|
319
|
-
# Extract provider names and exclude the already chosen default
|
|
320
|
-
options = available_with_labels.map { |o| o.split(" - ").first }
|
|
321
|
-
candidates = options.reject { |p| p == default_provider }
|
|
322
|
-
|
|
323
|
-
return [] if candidates.empty?
|
|
324
|
-
|
|
325
|
-
selected = preselected.select { |p| candidates.include?(p) }
|
|
326
|
-
|
|
327
|
-
loop do
|
|
328
|
-
display_message("\nSelect fallback providers in order of preference (first = highest priority).", type: :info)
|
|
329
|
-
display_message("Current order: #{selected.empty? ? "(none)" : selected.join(" > ")}", type: :muted)
|
|
330
|
-
choice = @prompt.select("Add provider, or choose an action:", cycle: true) do |menu|
|
|
331
|
-
(candidates - selected).each { |prov| menu.choice("Add #{prov}", prov) }
|
|
332
|
-
menu.choice("Done", :done)
|
|
333
|
-
menu.choice("Clear", :clear) unless selected.empty?
|
|
334
|
-
menu.choice("Remove last (#{selected.last})", :remove) unless selected.empty?
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
case choice
|
|
338
|
-
when :done
|
|
339
|
-
break
|
|
340
|
-
when :clear
|
|
341
|
-
selected.clear
|
|
342
|
-
when :remove
|
|
343
|
-
selected.pop
|
|
344
|
-
else
|
|
345
|
-
selected << choice unless selected.include?(choice)
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
selected
|
|
68
|
+
File.write(Aidp::ConfigPaths.config_file(@project_dir), minimal.to_yaml)
|
|
350
69
|
end
|
|
351
70
|
end
|
|
352
71
|
end
|