aidp 0.9.6 → 0.11.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 +194 -25
- data/lib/aidp/analyze/error_handler.rb +4 -2
- data/lib/aidp/{analysis → analyze}/kb_inspector.rb +93 -89
- data/lib/aidp/analyze/prioritizer.rb +3 -2
- data/lib/aidp/analyze/progress.rb +2 -1
- data/lib/aidp/analyze/ruby_maat_integration.rb +7 -3
- data/lib/aidp/analyze/runner.rb +73 -11
- data/lib/aidp/{analysis → analyze}/seams.rb +1 -1
- data/lib/aidp/analyze/steps.rb +10 -8
- data/lib/aidp/{analysis → analyze}/tree_sitter_grammar_loader.rb +11 -5
- data/lib/aidp/{analysis → analyze}/tree_sitter_scan.rb +21 -15
- data/lib/aidp/cli/checkpoint_command.rb +98 -0
- data/lib/aidp/cli/first_run_wizard.rb +83 -103
- data/lib/aidp/cli/jobs_command.rb +270 -36
- data/lib/aidp/cli/terminal_io.rb +3 -3
- data/lib/aidp/cli.rb +411 -69
- data/lib/aidp/config.rb +5 -8
- data/lib/aidp/debug_logger.rb +4 -4
- data/lib/aidp/debug_mixin.rb +11 -4
- data/lib/aidp/execute/checkpoint.rb +282 -0
- data/lib/aidp/execute/checkpoint_display.rb +221 -0
- data/lib/aidp/execute/progress.rb +2 -1
- data/lib/aidp/execute/prompt_manager.rb +62 -0
- data/lib/aidp/execute/runner.rb +67 -20
- data/lib/aidp/execute/steps.rb +36 -27
- data/lib/aidp/execute/work_loop_runner.rb +308 -0
- data/lib/aidp/execute/workflow_selector.rb +50 -26
- data/lib/aidp/harness/condition_detector.rb +4 -4
- data/lib/aidp/harness/config_schema.rb +40 -0
- data/lib/aidp/harness/config_validator.rb +3 -6
- data/lib/aidp/harness/configuration.rb +35 -1
- data/lib/aidp/harness/enhanced_runner.rb +25 -4
- data/lib/aidp/harness/error_handler.rb +103 -28
- data/lib/aidp/harness/provider_factory.rb +6 -1
- data/lib/aidp/harness/provider_manager.rb +273 -19
- data/lib/aidp/harness/runner.rb +14 -6
- data/lib/aidp/harness/simple_user_interface.rb +6 -4
- data/lib/aidp/harness/status_display.rb +118 -106
- data/lib/aidp/harness/test_runner.rb +83 -0
- data/lib/aidp/harness/ui/enhanced_tui.rb +7 -5
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +22 -4
- data/lib/aidp/harness/ui/error_handler.rb +7 -2
- data/lib/aidp/harness/ui/frame_manager.rb +61 -39
- data/lib/aidp/harness/ui/job_monitor.rb +2 -0
- data/lib/aidp/harness/ui/navigation/main_menu.rb +27 -16
- data/lib/aidp/harness/ui/navigation/menu_item.rb +1 -0
- data/lib/aidp/harness/ui/navigation/menu_state.rb +1 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +2 -0
- data/lib/aidp/harness/ui/progress_display.rb +26 -7
- data/lib/aidp/harness/ui/question_collector.rb +2 -0
- data/lib/aidp/harness/ui/spinner_group.rb +2 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +1 -1
- data/lib/aidp/harness/ui/status_manager.rb +4 -2
- data/lib/aidp/harness/ui/status_widget.rb +20 -9
- data/lib/aidp/harness/ui/workflow_controller.rb +27 -9
- data/lib/aidp/harness/user_interface.rb +338 -330
- data/lib/aidp/jobs/background_runner.rb +278 -0
- data/lib/aidp/message_display.rb +48 -0
- data/lib/aidp/provider_manager.rb +13 -7
- data/lib/aidp/providers/anthropic.rb +101 -18
- data/lib/aidp/providers/base.rb +51 -1
- data/lib/aidp/providers/codex.rb +248 -0
- data/lib/aidp/providers/cursor.rb +39 -48
- data/lib/aidp/providers/gemini.rb +26 -16
- data/lib/aidp/providers/github_copilot.rb +263 -0
- data/lib/aidp/providers/opencode.rb +38 -47
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/definitions.rb +357 -0
- data/lib/aidp/workflows/selector.rb +171 -0
- data/lib/aidp.rb +16 -4
- data/templates/planning/generate_llm_style_guide.md +119 -0
- metadata +43 -31
- data/lib/aidp/analyze/progress_visualizer.rb +0 -314
- /data/templates/{ANALYZE/02_ARCHITECTURE_ANALYSIS.md → analysis/analyze_architecture.md} +0 -0
- /data/templates/{ANALYZE/05_DOCUMENTATION_ANALYSIS.md → analysis/analyze_documentation.md} +0 -0
- /data/templates/{ANALYZE/04_FUNCTIONALITY_ANALYSIS.md → analysis/analyze_functionality.md} +0 -0
- /data/templates/{ANALYZE/01_REPOSITORY_ANALYSIS.md → analysis/analyze_repository.md} +0 -0
- /data/templates/{ANALYZE/06_STATIC_ANALYSIS.md → analysis/analyze_static_code.md} +0 -0
- /data/templates/{ANALYZE/03_TEST_ANALYSIS.md → analysis/analyze_tests.md} +0 -0
- /data/templates/{ANALYZE/07_REFACTORING_RECOMMENDATIONS.md → analysis/recommend_refactoring.md} +0 -0
- /data/templates/{ANALYZE/06a_tree_sitter_scan.md → analysis/scan_with_tree_sitter.md} +0 -0
- /data/templates/{EXECUTE/11_STATIC_ANALYSIS.md → implementation/configure_static_analysis.md} +0 -0
- /data/templates/{EXECUTE/14_DOCS_PORTAL.md → implementation/create_documentation_portal.md} +0 -0
- /data/templates/{EXECUTE/10_IMPLEMENTATION_AGENT.md → implementation/implement_features.md} +0 -0
- /data/templates/{EXECUTE/13_DELIVERY_ROLLOUT.md → implementation/plan_delivery.md} +0 -0
- /data/templates/{EXECUTE/15_POST_RELEASE.md → implementation/review_post_release.md} +0 -0
- /data/templates/{EXECUTE/09_SCAFFOLDING_DEVEX.md → implementation/setup_scaffolding.md} +0 -0
- /data/templates/{EXECUTE/02A_ARCH_GATE_QUESTIONS.md → planning/ask_architecture_questions.md} +0 -0
- /data/templates/{EXECUTE/00_PRD.md → planning/create_prd.md} +0 -0
- /data/templates/{EXECUTE/08_TASKS.md → planning/create_tasks.md} +0 -0
- /data/templates/{EXECUTE/04_DOMAIN_DECOMPOSITION.md → planning/decompose_domain.md} +0 -0
- /data/templates/{EXECUTE/01_NFRS.md → planning/define_nfrs.md} +0 -0
- /data/templates/{EXECUTE/05_CONTRACTS.md → planning/design_apis.md} +0 -0
- /data/templates/{EXECUTE/02_ARCHITECTURE.md → planning/design_architecture.md} +0 -0
- /data/templates/{EXECUTE/06_THREAT_MODEL.md → planning/design_data_model.md} +0 -0
- /data/templates/{EXECUTE/03_ADR_FACTORY.md → planning/generate_adrs.md} +0 -0
- /data/templates/{EXECUTE/12_OBSERVABILITY_SLOS.md → planning/plan_observability.md} +0 -0
- /data/templates/{EXECUTE/07_TEST_PLAN.md → planning/plan_testing.md} +0 -0
data/lib/aidp/analyze/steps.rb
CHANGED
|
@@ -3,51 +3,53 @@
|
|
|
3
3
|
module Aidp
|
|
4
4
|
module Analyze
|
|
5
5
|
module Steps
|
|
6
|
+
# Analysis step specifications
|
|
7
|
+
# Templates are organized by purpose and named with action verbs
|
|
6
8
|
SPEC = {
|
|
7
9
|
"01_REPOSITORY_ANALYSIS" => {
|
|
8
|
-
"templates" => ["
|
|
10
|
+
"templates" => ["analysis/analyze_repository.md"],
|
|
9
11
|
"description" => "Initial code-maat based repository mining",
|
|
10
12
|
"outs" => ["docs/analysis/repository_analysis.md"],
|
|
11
13
|
"gate" => false
|
|
12
14
|
},
|
|
13
15
|
"02_ARCHITECTURE_ANALYSIS" => {
|
|
14
|
-
"templates" => ["
|
|
16
|
+
"templates" => ["analysis/analyze_architecture.md"],
|
|
15
17
|
"description" => "Identify architectural patterns, dependencies, and violations",
|
|
16
18
|
"outs" => ["docs/analysis/architecture_analysis.md"],
|
|
17
19
|
"gate" => true
|
|
18
20
|
},
|
|
19
21
|
"03_TEST_ANALYSIS" => {
|
|
20
|
-
"templates" => ["
|
|
22
|
+
"templates" => ["analysis/analyze_tests.md"],
|
|
21
23
|
"description" => "Analyze existing test coverage and identify gaps",
|
|
22
24
|
"outs" => ["docs/analysis/test_analysis.md"],
|
|
23
25
|
"gate" => false
|
|
24
26
|
},
|
|
25
27
|
"04_FUNCTIONALITY_ANALYSIS" => {
|
|
26
|
-
"templates" => ["
|
|
28
|
+
"templates" => ["analysis/analyze_functionality.md"],
|
|
27
29
|
"description" => "Map features, identify dead code, analyze complexity",
|
|
28
30
|
"outs" => ["docs/analysis/functionality_analysis.md"],
|
|
29
31
|
"gate" => false
|
|
30
32
|
},
|
|
31
33
|
"05_DOCUMENTATION_ANALYSIS" => {
|
|
32
|
-
"templates" => ["
|
|
34
|
+
"templates" => ["analysis/analyze_documentation.md"],
|
|
33
35
|
"description" => "Identify missing documentation and generate what's needed",
|
|
34
36
|
"outs" => ["docs/analysis/documentation_analysis.md"],
|
|
35
37
|
"gate" => false
|
|
36
38
|
},
|
|
37
39
|
"06_STATIC_ANALYSIS" => {
|
|
38
|
-
"templates" => ["
|
|
40
|
+
"templates" => ["analysis/analyze_static_code.md"],
|
|
39
41
|
"description" => "Check for existing tools and recommend improvements",
|
|
40
42
|
"outs" => ["docs/analysis/static_analysis.md"],
|
|
41
43
|
"gate" => false
|
|
42
44
|
},
|
|
43
45
|
"06A_TREE_SITTER_SCAN" => {
|
|
44
|
-
"templates" => ["
|
|
46
|
+
"templates" => ["analysis/scan_with_tree_sitter.md"],
|
|
45
47
|
"description" => "Tree-sitter powered static analysis to build knowledge base",
|
|
46
48
|
"outs" => [".aidp/kb/symbols.json", ".aidp/kb/seams.json", ".aidp/kb/hotspots.json"],
|
|
47
49
|
"gate" => false
|
|
48
50
|
},
|
|
49
51
|
"07_REFACTORING_RECOMMENDATIONS" => {
|
|
50
|
-
"templates" => ["
|
|
52
|
+
"templates" => ["analysis/recommend_refactoring.md"],
|
|
51
53
|
"description" => "Provide actionable refactoring guidance",
|
|
52
54
|
"outs" => ["docs/analysis/refactoring_recommendations.md"],
|
|
53
55
|
"gate" => true
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require "pathname"
|
|
4
4
|
require "tree_sitter"
|
|
5
|
+
require "tty-prompt"
|
|
5
6
|
require "fileutils"
|
|
6
7
|
|
|
7
8
|
module Aidp
|
|
8
|
-
module
|
|
9
|
+
module Analyze
|
|
9
10
|
class TreeSitterGrammarLoader
|
|
11
|
+
include Aidp::MessageDisplay
|
|
12
|
+
|
|
10
13
|
# Default grammar configurations
|
|
11
14
|
GRAMMAR_CONFIGS = {
|
|
12
15
|
"ruby" => {
|
|
@@ -61,10 +64,11 @@ module Aidp
|
|
|
61
64
|
}
|
|
62
65
|
}.freeze
|
|
63
66
|
|
|
64
|
-
def initialize(project_dir = Dir.pwd)
|
|
67
|
+
def initialize(project_dir = Dir.pwd, prompt: TTY::Prompt.new)
|
|
65
68
|
@project_dir = project_dir
|
|
66
69
|
@grammars_dir = File.join(project_dir, ".aidp", "grammars")
|
|
67
70
|
@loaded_grammars = {}
|
|
71
|
+
@prompt = prompt
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
# Load grammar for a specific language
|
|
@@ -92,7 +96,7 @@ module Aidp
|
|
|
92
96
|
grammar_path = File.join(@grammars_dir, language)
|
|
93
97
|
|
|
94
98
|
unless File.exist?(grammar_path)
|
|
95
|
-
|
|
99
|
+
display_message("Installing Tree-sitter grammar for #{language}...", type: :info)
|
|
96
100
|
install_grammar(language, config)
|
|
97
101
|
end
|
|
98
102
|
end
|
|
@@ -110,7 +114,7 @@ module Aidp
|
|
|
110
114
|
require "json"
|
|
111
115
|
File.write(File.join(grammar_path, "grammar.json"), JSON.generate(config))
|
|
112
116
|
|
|
113
|
-
|
|
117
|
+
display_message("Grammar for #{language} marked as available", type: :success)
|
|
114
118
|
end
|
|
115
119
|
|
|
116
120
|
def create_parser(language, config)
|
|
@@ -140,7 +144,7 @@ module Aidp
|
|
|
140
144
|
real: true
|
|
141
145
|
}
|
|
142
146
|
rescue TreeSitter::ParserNotFoundError => e
|
|
143
|
-
|
|
147
|
+
display_message("Warning: Tree-sitter parser not found for #{language}: #{e.message}", type: :warn)
|
|
144
148
|
create_mock_parser(language)
|
|
145
149
|
end
|
|
146
150
|
|
|
@@ -476,6 +480,8 @@ module Aidp
|
|
|
476
480
|
|
|
477
481
|
nodes
|
|
478
482
|
end
|
|
483
|
+
|
|
484
|
+
private
|
|
479
485
|
end
|
|
480
486
|
end
|
|
481
487
|
end
|
|
@@ -4,19 +4,23 @@ require "json"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "digest"
|
|
6
6
|
require "etc"
|
|
7
|
+
require "tty-prompt"
|
|
7
8
|
|
|
8
9
|
require_relative "tree_sitter_grammar_loader"
|
|
9
10
|
require_relative "seams"
|
|
10
11
|
|
|
11
12
|
module Aidp
|
|
12
|
-
module
|
|
13
|
+
module Analyze
|
|
13
14
|
class TreeSitterScan
|
|
14
|
-
|
|
15
|
+
include Aidp::MessageDisplay
|
|
16
|
+
|
|
17
|
+
def initialize(root: Dir.pwd, kb_dir: ".aidp/kb", langs: %w[ruby], threads: Etc.nprocessors, prompt: TTY::Prompt.new)
|
|
15
18
|
@root = File.expand_path(root)
|
|
16
19
|
@kb_dir = File.expand_path(kb_dir, @root)
|
|
17
20
|
@langs = Array(langs)
|
|
18
21
|
@threads = threads
|
|
19
|
-
@
|
|
22
|
+
@prompt = prompt
|
|
23
|
+
@grammar_loader = TreeSitterGrammarLoader.new(@root, prompt: @prompt)
|
|
20
24
|
|
|
21
25
|
# Data structures to accumulate analysis results
|
|
22
26
|
@symbols = []
|
|
@@ -34,14 +38,14 @@ module Aidp
|
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
def run
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
display_message("🔍 Starting Tree-sitter static analysis...", type: :highlight)
|
|
42
|
+
display_message("📁 Root: #{@root}", type: :info)
|
|
43
|
+
display_message("🗂️ KB Directory: #{@kb_dir}", type: :info)
|
|
44
|
+
display_message("🌐 Languages: #{@langs.join(", ")}", type: :info)
|
|
45
|
+
display_message("🧵 Threads: #{@threads}", type: :info)
|
|
42
46
|
|
|
43
47
|
files = discover_files
|
|
44
|
-
|
|
48
|
+
display_message("📄 Found #{files.length} files to analyze", type: :info)
|
|
45
49
|
|
|
46
50
|
prepare_kb_dir
|
|
47
51
|
load_cache
|
|
@@ -49,8 +53,8 @@ module Aidp
|
|
|
49
53
|
parallel_parse(files)
|
|
50
54
|
write_kb_files
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
display_message("✅ Tree-sitter analysis complete!", type: :success)
|
|
57
|
+
display_message("📊 Generated KB files in #{@kb_dir}", type: :success)
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
private
|
|
@@ -144,14 +148,14 @@ module Aidp
|
|
|
144
148
|
end
|
|
145
149
|
|
|
146
150
|
def parallel_parse(files)
|
|
147
|
-
|
|
151
|
+
display_message("🔄 Parsing files in parallel...", type: :info)
|
|
148
152
|
|
|
149
153
|
# Group files by language for efficient processing
|
|
150
154
|
files_by_lang = files.group_by { |file| detect_language(file) }
|
|
151
155
|
|
|
152
156
|
# Process each language group
|
|
153
157
|
files_by_lang.each do |lang, lang_files|
|
|
154
|
-
|
|
158
|
+
display_message("📝 Processing #{lang_files.length} #{lang} files...", type: :info)
|
|
155
159
|
|
|
156
160
|
# Load grammar for this language
|
|
157
161
|
grammar = @grammar_loader.load_grammar(lang)
|
|
@@ -614,7 +618,7 @@ module Aidp
|
|
|
614
618
|
end
|
|
615
619
|
|
|
616
620
|
def write_kb_files
|
|
617
|
-
|
|
621
|
+
display_message("💾 Writing knowledge base files...", type: :info)
|
|
618
622
|
|
|
619
623
|
prepare_kb_dir
|
|
620
624
|
|
|
@@ -637,7 +641,7 @@ module Aidp
|
|
|
637
641
|
def write_json_file(filename, data)
|
|
638
642
|
file_path = File.join(@kb_dir, filename)
|
|
639
643
|
File.write(file_path, JSON.pretty_generate(data))
|
|
640
|
-
|
|
644
|
+
display_message("📄 Written #{filename} (#{data.length} entries)", type: :success)
|
|
641
645
|
end
|
|
642
646
|
|
|
643
647
|
def generate_hotspots
|
|
@@ -681,6 +685,8 @@ module Aidp
|
|
|
681
685
|
# This would analyze test file naming and content
|
|
682
686
|
[]
|
|
683
687
|
end
|
|
688
|
+
|
|
689
|
+
private
|
|
684
690
|
end
|
|
685
691
|
end
|
|
686
692
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "../execute/checkpoint"
|
|
5
|
+
require_relative "../execute/checkpoint_display"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module CLI
|
|
9
|
+
# CLI command for viewing checkpoint data and progress reports
|
|
10
|
+
class CheckpointCommand < Thor
|
|
11
|
+
desc "show", "Show the latest checkpoint data"
|
|
12
|
+
def show
|
|
13
|
+
checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
|
|
14
|
+
display = Aidp::Execute::CheckpointDisplay.new
|
|
15
|
+
|
|
16
|
+
latest = checkpoint.latest_checkpoint
|
|
17
|
+
if latest
|
|
18
|
+
display.display_checkpoint(latest, show_details: true)
|
|
19
|
+
else
|
|
20
|
+
puts "No checkpoint data found."
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "summary", "Show progress summary with trends"
|
|
25
|
+
def summary
|
|
26
|
+
checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
|
|
27
|
+
display = Aidp::Execute::CheckpointDisplay.new
|
|
28
|
+
|
|
29
|
+
summary = checkpoint.progress_summary
|
|
30
|
+
if summary
|
|
31
|
+
display.display_progress_summary(summary)
|
|
32
|
+
else
|
|
33
|
+
puts "No checkpoint data found."
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "history [LIMIT]", "Show checkpoint history (default: last 10)"
|
|
38
|
+
def history(limit = "10")
|
|
39
|
+
checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
|
|
40
|
+
display = Aidp::Execute::CheckpointDisplay.new
|
|
41
|
+
|
|
42
|
+
history = checkpoint.checkpoint_history(limit: limit.to_i)
|
|
43
|
+
if history.any?
|
|
44
|
+
display.display_checkpoint_history(history, limit: limit.to_i)
|
|
45
|
+
else
|
|
46
|
+
puts "No checkpoint history found."
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
desc "clear", "Clear all checkpoint data"
|
|
51
|
+
option :force, type: :boolean, default: false, desc: "Skip confirmation"
|
|
52
|
+
def clear
|
|
53
|
+
unless options[:force]
|
|
54
|
+
prompt = TTY::Prompt.new
|
|
55
|
+
confirm = prompt.yes?("Are you sure you want to clear all checkpoint data?")
|
|
56
|
+
return unless confirm
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
|
|
60
|
+
checkpoint.clear
|
|
61
|
+
puts "✓ Checkpoint data cleared."
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
desc "metrics", "Show detailed metrics for the latest checkpoint"
|
|
65
|
+
def metrics
|
|
66
|
+
checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
|
|
67
|
+
latest = checkpoint.latest_checkpoint
|
|
68
|
+
|
|
69
|
+
unless latest
|
|
70
|
+
puts "No checkpoint data found."
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
puts
|
|
75
|
+
puts "📊 Detailed Metrics"
|
|
76
|
+
puts "=" * 60
|
|
77
|
+
|
|
78
|
+
metrics = latest[:metrics]
|
|
79
|
+
puts "Lines of Code: #{metrics[:lines_of_code]}"
|
|
80
|
+
puts "File Count: #{metrics[:file_count]}"
|
|
81
|
+
puts "Test Coverage: #{metrics[:test_coverage]}%"
|
|
82
|
+
puts "Code Quality: #{metrics[:code_quality]}%"
|
|
83
|
+
puts "PRD Task Progress: #{metrics[:prd_task_progress]}%"
|
|
84
|
+
|
|
85
|
+
if metrics[:tests_passing]
|
|
86
|
+
puts "Tests: #{metrics[:tests_passing] ? "✓ Passing" : "✗ Failing"}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if metrics[:linters_passing]
|
|
90
|
+
puts "Linters: #{metrics[:linters_passing] ? "✓ Passing" : "✗ Failing"}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
puts "=" * 60
|
|
94
|
+
puts
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -3,60 +3,52 @@
|
|
|
3
3
|
|
|
4
4
|
require "yaml"
|
|
5
5
|
require "tty-prompt"
|
|
6
|
+
require_relative "../harness/provider_factory"
|
|
6
7
|
|
|
7
8
|
module Aidp
|
|
8
9
|
class CLI
|
|
9
10
|
# Handles interactive first-time project setup when no aidp.yml exists
|
|
10
11
|
class FirstRunWizard
|
|
12
|
+
include Aidp::MessageDisplay
|
|
13
|
+
|
|
11
14
|
TEMPLATES_DIR = File.expand_path(File.join(__dir__, "..", "..", "..", "templates"))
|
|
12
15
|
|
|
13
|
-
def self.ensure_config(project_dir,
|
|
16
|
+
def self.ensure_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
|
|
14
17
|
return true if Aidp::Config.config_exists?(project_dir)
|
|
15
18
|
|
|
16
|
-
wizard = new(project_dir,
|
|
19
|
+
wizard = new(project_dir, prompt: prompt)
|
|
17
20
|
|
|
18
|
-
if non_interactive
|
|
21
|
+
if non_interactive
|
|
19
22
|
# Non-interactive environment - create minimal config silently
|
|
20
23
|
path = wizard.send(:write_minimal_config, project_dir)
|
|
21
|
-
|
|
24
|
+
wizard.send(:display_message, "Created minimal configuration at #{wizard.send(:relative, path)} (non-interactive default)", type: :success)
|
|
22
25
|
return true
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
wizard.run
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
def self.setup_config(project_dir,
|
|
29
|
-
wizard = new(project_dir,
|
|
31
|
+
def self.setup_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
|
|
32
|
+
wizard = new(project_dir, prompt: prompt)
|
|
30
33
|
|
|
31
|
-
if non_interactive
|
|
34
|
+
if non_interactive
|
|
32
35
|
# Non-interactive environment - skip setup
|
|
33
|
-
|
|
36
|
+
wizard.send(:display_message, "Configuration setup skipped in non-interactive environment", type: :info)
|
|
34
37
|
return true
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
wizard.run_setup_config
|
|
38
41
|
end
|
|
39
42
|
|
|
40
|
-
def initialize(project_dir,
|
|
43
|
+
def initialize(project_dir, prompt: TTY::Prompt.new)
|
|
41
44
|
@project_dir = project_dir
|
|
42
|
-
@input = input
|
|
43
|
-
@output = output
|
|
44
45
|
@prompt = prompt
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
def run
|
|
48
49
|
banner
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
case choice
|
|
52
|
-
when "1" then return finish(write_quick_config(@project_dir))
|
|
53
|
-
when "2" then return finish(run_custom)
|
|
54
|
-
when "q", "Q" then @output.puts("Exiting without creating configuration.")
|
|
55
|
-
return false
|
|
56
|
-
else
|
|
57
|
-
@output.puts "Invalid selection. Please choose one of the listed options."
|
|
58
|
-
end
|
|
59
|
-
end
|
|
50
|
+
# Always run the full interactive custom configuration flow.
|
|
51
|
+
finish(run_custom)
|
|
60
52
|
end
|
|
61
53
|
|
|
62
54
|
def run_setup_config
|
|
@@ -81,31 +73,19 @@ module Aidp
|
|
|
81
73
|
private
|
|
82
74
|
|
|
83
75
|
def banner
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def ask_choice
|
|
91
|
-
@output.puts "Choose a configuration style:" unless @asking
|
|
92
|
-
|
|
93
|
-
options = {
|
|
94
|
-
"Quick setup (cursor + macos, no API keys needed)" => "1",
|
|
95
|
-
"Custom setup (choose your own providers and settings)" => "2",
|
|
96
|
-
"Quit" => "q"
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
@prompt.select("Select an option:", options, default: "Quick setup (cursor + macos, no API keys needed)")
|
|
76
|
+
display_message("\n🚀 First-time setup detected", type: :highlight)
|
|
77
|
+
display_message("No 'aidp.yml' configuration file found in #{relative(@project_dir)}.")
|
|
78
|
+
display_message("Creating a configuration so you can start using AI Dev Pipeline.")
|
|
79
|
+
display_message("")
|
|
100
80
|
end
|
|
101
81
|
|
|
102
82
|
def finish(path)
|
|
103
83
|
if path
|
|
104
|
-
|
|
105
|
-
|
|
84
|
+
display_message("\n✅ Configuration created at #{relative(path)}", type: :success)
|
|
85
|
+
display_message("You can edit this file anytime. Continuing startup...\n")
|
|
106
86
|
true
|
|
107
87
|
else
|
|
108
|
-
|
|
88
|
+
display_message("❌ Failed to create configuration file.", type: :error)
|
|
109
89
|
false
|
|
110
90
|
end
|
|
111
91
|
end
|
|
@@ -113,7 +93,7 @@ module Aidp
|
|
|
113
93
|
def copy_template(filename)
|
|
114
94
|
src = File.join(TEMPLATES_DIR, filename)
|
|
115
95
|
unless File.exist?(src)
|
|
116
|
-
|
|
96
|
+
display_message("Template not found: #{filename}", type: :error)
|
|
117
97
|
return nil
|
|
118
98
|
end
|
|
119
99
|
dest = File.join(@project_dir, "aidp.yml")
|
|
@@ -122,7 +102,7 @@ module Aidp
|
|
|
122
102
|
end
|
|
123
103
|
|
|
124
104
|
def write_minimal_config(project_dir)
|
|
125
|
-
dest = File.join(project_dir, "aidp.yml")
|
|
105
|
+
dest = File.join(project_dir, ".aidp", "aidp.yml")
|
|
126
106
|
return dest if File.exist?(dest)
|
|
127
107
|
data = {
|
|
128
108
|
"harness" => {
|
|
@@ -138,31 +118,7 @@ module Aidp
|
|
|
138
118
|
}
|
|
139
119
|
}
|
|
140
120
|
}
|
|
141
|
-
File.
|
|
142
|
-
dest
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def write_quick_config(project_dir)
|
|
146
|
-
dest = File.join(project_dir, "aidp.yml")
|
|
147
|
-
return dest if File.exist?(dest)
|
|
148
|
-
data = {
|
|
149
|
-
"harness" => {
|
|
150
|
-
"max_retries" => 2,
|
|
151
|
-
"default_provider" => "cursor",
|
|
152
|
-
"fallback_providers" => ["macos"],
|
|
153
|
-
"no_api_keys_required" => true
|
|
154
|
-
},
|
|
155
|
-
"providers" => {
|
|
156
|
-
"cursor" => {
|
|
157
|
-
"type" => "subscription",
|
|
158
|
-
"default_flags" => []
|
|
159
|
-
},
|
|
160
|
-
"macos" => {
|
|
161
|
-
"type" => "usage_based",
|
|
162
|
-
"default_flags" => []
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
121
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
166
122
|
File.write(dest, YAML.dump(data))
|
|
167
123
|
dest
|
|
168
124
|
end
|
|
@@ -191,15 +147,11 @@ module Aidp
|
|
|
191
147
|
provider_name = default_provider.split(" - ").first
|
|
192
148
|
|
|
193
149
|
# Validate fallback providers
|
|
194
|
-
|
|
195
|
-
q.validate(/^[a-zA-Z0-9_,\s]+$/, "Invalid characters. Use only letters, numbers, commas, and spaces.")
|
|
196
|
-
q.validate(->(input) { validate_provider_list(input, available_providers) }, "One or more providers are not supported.")
|
|
197
|
-
end
|
|
150
|
+
fallback_providers = select_fallback_providers(available_providers, provider_name)
|
|
198
151
|
|
|
199
152
|
restrict = @prompt.yes?("Only use providers that don't require API keys?", default: false)
|
|
200
153
|
|
|
201
|
-
# Process
|
|
202
|
-
fallback_providers = fallback_input.split(/\s*,\s*/).map(&:strip).reject(&:empty?)
|
|
154
|
+
# Process providers preserving order
|
|
203
155
|
providers = [provider_name] + fallback_providers
|
|
204
156
|
providers.uniq!
|
|
205
157
|
|
|
@@ -248,15 +200,11 @@ module Aidp
|
|
|
248
200
|
provider_name = default_provider.split(" - ").first
|
|
249
201
|
|
|
250
202
|
# Validate fallback providers
|
|
251
|
-
|
|
252
|
-
q.validate(/^[a-zA-Z0-9_,\s]+$/, "Invalid characters. Use only letters, numbers, commas, and spaces.")
|
|
253
|
-
q.validate(->(input) { validate_provider_list(input, available_providers) }, "One or more providers are not supported.")
|
|
254
|
-
end
|
|
203
|
+
fallback_providers = select_fallback_providers(available_providers, provider_name, preselected: current_fallbacks - [provider_name])
|
|
255
204
|
|
|
256
205
|
restrict_input = @prompt.yes?("Only use providers that don't require API keys?", default: current_restrict)
|
|
257
206
|
|
|
258
|
-
# Process
|
|
259
|
-
fallback_providers = fallback_input.split(/\s*,\s*/).map(&:strip).reject(&:empty?)
|
|
207
|
+
# Process providers preserving order
|
|
260
208
|
providers = [provider_name] + fallback_providers
|
|
261
209
|
providers.uniq!
|
|
262
210
|
|
|
@@ -310,14 +258,10 @@ module Aidp
|
|
|
310
258
|
|
|
311
259
|
def ask(prompt, default: nil)
|
|
312
260
|
if default
|
|
313
|
-
@
|
|
261
|
+
@prompt.ask("#{prompt}:", default: default)
|
|
314
262
|
else
|
|
315
|
-
@
|
|
263
|
+
@prompt.ask("#{prompt}:")
|
|
316
264
|
end
|
|
317
|
-
@output.flush
|
|
318
|
-
ans = @input.gets&.strip
|
|
319
|
-
return default if (ans.nil? || ans.empty?) && default
|
|
320
|
-
ans
|
|
321
265
|
end
|
|
322
266
|
|
|
323
267
|
def relative(path)
|
|
@@ -331,24 +275,25 @@ module Aidp
|
|
|
331
275
|
|
|
332
276
|
# Get available providers for validation
|
|
333
277
|
def get_available_providers
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
#
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
278
|
+
# Get all supported providers from the factory (single source of truth)
|
|
279
|
+
all_providers = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES.keys
|
|
280
|
+
|
|
281
|
+
# Filter out providers we don't want to show in the wizard
|
|
282
|
+
# - "anthropic" is an internal name, we show "claude" instead
|
|
283
|
+
# - "macos" is disabled (as per issue #73)
|
|
284
|
+
excluded = ["anthropic", "macos"]
|
|
285
|
+
available = all_providers - excluded
|
|
286
|
+
|
|
287
|
+
# Get display names from the providers themselves
|
|
288
|
+
available.map do |provider_name|
|
|
289
|
+
provider_class = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES[provider_name]
|
|
290
|
+
if provider_class
|
|
291
|
+
# Instantiate to get display name
|
|
292
|
+
instance = provider_class.new
|
|
293
|
+
display_name = instance.display_name
|
|
294
|
+
"#{provider_name} - #{display_name}"
|
|
350
295
|
else
|
|
351
|
-
|
|
296
|
+
provider_name
|
|
352
297
|
end
|
|
353
298
|
end
|
|
354
299
|
end
|
|
@@ -364,6 +309,41 @@ module Aidp
|
|
|
364
309
|
valid_providers = available_providers.map { |p| p.split(" - ").first }
|
|
365
310
|
providers.all? { |provider| valid_providers.include?(provider) }
|
|
366
311
|
end
|
|
312
|
+
|
|
313
|
+
# Interactive ordered multi-select for fallback providers
|
|
314
|
+
def select_fallback_providers(available_with_labels, default_provider, preselected: [])
|
|
315
|
+
# Extract provider names and exclude the already chosen default
|
|
316
|
+
options = available_with_labels.map { |o| o.split(" - ").first }
|
|
317
|
+
candidates = options.reject { |p| p == default_provider }
|
|
318
|
+
|
|
319
|
+
return [] if candidates.empty?
|
|
320
|
+
|
|
321
|
+
selected = preselected.select { |p| candidates.include?(p) }
|
|
322
|
+
|
|
323
|
+
loop do
|
|
324
|
+
display_message("\nSelect fallback providers in order of preference (first = highest priority).", type: :info)
|
|
325
|
+
display_message("Current order: #{selected.empty? ? "(none)" : selected.join(" > ")}", type: :muted)
|
|
326
|
+
choice = @prompt.select("Add provider, or choose an action:", cycle: true) do |menu|
|
|
327
|
+
(candidates - selected).each { |prov| menu.choice("Add #{prov}", prov) }
|
|
328
|
+
menu.choice("Done", :done)
|
|
329
|
+
menu.choice("Clear", :clear) unless selected.empty?
|
|
330
|
+
menu.choice("Remove last (#{selected.last})", :remove) unless selected.empty?
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
case choice
|
|
334
|
+
when :done
|
|
335
|
+
break
|
|
336
|
+
when :clear
|
|
337
|
+
selected.clear
|
|
338
|
+
when :remove
|
|
339
|
+
selected.pop
|
|
340
|
+
else
|
|
341
|
+
selected << choice unless selected.include?(choice)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
selected
|
|
346
|
+
end
|
|
367
347
|
end
|
|
368
348
|
end
|
|
369
349
|
end
|