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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -0
  3. data/lib/aidp/analyze/json_file_storage.rb +21 -21
  4. data/lib/aidp/cli/enhanced_input.rb +114 -0
  5. data/lib/aidp/cli/first_run_wizard.rb +28 -309
  6. data/lib/aidp/cli/issue_importer.rb +359 -0
  7. data/lib/aidp/cli/mcp_dashboard.rb +3 -3
  8. data/lib/aidp/cli/terminal_io.rb +26 -0
  9. data/lib/aidp/cli.rb +155 -7
  10. data/lib/aidp/daemon/process_manager.rb +146 -0
  11. data/lib/aidp/daemon/runner.rb +232 -0
  12. data/lib/aidp/execute/async_work_loop_runner.rb +216 -0
  13. data/lib/aidp/execute/future_work_backlog.rb +411 -0
  14. data/lib/aidp/execute/guard_policy.rb +246 -0
  15. data/lib/aidp/execute/instruction_queue.rb +131 -0
  16. data/lib/aidp/execute/interactive_repl.rb +335 -0
  17. data/lib/aidp/execute/repl_macros.rb +651 -0
  18. data/lib/aidp/execute/steps.rb +8 -0
  19. data/lib/aidp/execute/work_loop_runner.rb +322 -36
  20. data/lib/aidp/execute/work_loop_state.rb +162 -0
  21. data/lib/aidp/harness/condition_detector.rb +6 -6
  22. data/lib/aidp/harness/config_loader.rb +23 -23
  23. data/lib/aidp/harness/config_manager.rb +61 -61
  24. data/lib/aidp/harness/config_schema.rb +88 -0
  25. data/lib/aidp/harness/config_validator.rb +9 -9
  26. data/lib/aidp/harness/configuration.rb +76 -29
  27. data/lib/aidp/harness/error_handler.rb +13 -13
  28. data/lib/aidp/harness/provider_config.rb +79 -79
  29. data/lib/aidp/harness/provider_factory.rb +40 -40
  30. data/lib/aidp/harness/provider_info.rb +37 -20
  31. data/lib/aidp/harness/provider_manager.rb +58 -53
  32. data/lib/aidp/harness/provider_type_checker.rb +6 -6
  33. data/lib/aidp/harness/runner.rb +7 -7
  34. data/lib/aidp/harness/status_display.rb +33 -46
  35. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -1
  36. data/lib/aidp/harness/ui/job_monitor.rb +7 -7
  37. data/lib/aidp/harness/user_interface.rb +43 -43
  38. data/lib/aidp/init/doc_generator.rb +256 -0
  39. data/lib/aidp/init/project_analyzer.rb +343 -0
  40. data/lib/aidp/init/runner.rb +83 -0
  41. data/lib/aidp/init.rb +5 -0
  42. data/lib/aidp/logger.rb +279 -0
  43. data/lib/aidp/providers/anthropic.rb +100 -26
  44. data/lib/aidp/providers/base.rb +13 -0
  45. data/lib/aidp/providers/codex.rb +28 -27
  46. data/lib/aidp/providers/cursor.rb +141 -34
  47. data/lib/aidp/providers/github_copilot.rb +26 -26
  48. data/lib/aidp/providers/macos_ui.rb +2 -18
  49. data/lib/aidp/providers/opencode.rb +26 -26
  50. data/lib/aidp/setup/wizard.rb +777 -0
  51. data/lib/aidp/tooling_detector.rb +115 -0
  52. data/lib/aidp/version.rb +1 -1
  53. data/lib/aidp/watch/build_processor.rb +282 -0
  54. data/lib/aidp/watch/plan_generator.rb +166 -0
  55. data/lib/aidp/watch/plan_processor.rb +83 -0
  56. data/lib/aidp/watch/repository_client.rb +243 -0
  57. data/lib/aidp/watch/runner.rb +93 -0
  58. data/lib/aidp/watch/state_store.rb +105 -0
  59. data/lib/aidp/watch.rb +9 -0
  60. data/lib/aidp/workflows/guided_agent.rb +344 -23
  61. data/lib/aidp.rb +14 -0
  62. data/templates/implementation/simple_task.md +36 -0
  63. metadata +27 -1
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "time"
5
+
6
+ module Aidp
7
+ module Init
8
+ # Creates project documentation artefacts based on the analyzer output. All
9
+ # documents are deterministic and tailored with repository insights.
10
+ class DocGenerator
11
+ OUTPUT_DIR = "docs"
12
+ STYLE_GUIDE_PATH = File.join(OUTPUT_DIR, "LLM_STYLE_GUIDE.md")
13
+ ANALYSIS_PATH = File.join(OUTPUT_DIR, "PROJECT_ANALYSIS.md")
14
+ QUALITY_PLAN_PATH = File.join(OUTPUT_DIR, "CODE_QUALITY_PLAN.md")
15
+
16
+ def initialize(project_dir = Dir.pwd)
17
+ @project_dir = project_dir
18
+ end
19
+
20
+ def generate(analysis:, preferences: {})
21
+ ensure_output_directory
22
+ write_style_guide(analysis, preferences)
23
+ write_project_analysis(analysis)
24
+ write_quality_plan(analysis, preferences)
25
+ end
26
+
27
+ private
28
+
29
+ def ensure_output_directory
30
+ FileUtils.mkdir_p(File.join(@project_dir, OUTPUT_DIR))
31
+ end
32
+
33
+ def write_style_guide(analysis, preferences)
34
+ languages = format_list(analysis[:languages].keys)
35
+ frameworks = format_list(analysis[:frameworks])
36
+ test_frameworks = format_list(analysis[:test_frameworks])
37
+ key_dirs = format_list(analysis[:key_directories])
38
+ tooling = analysis[:tooling].keys.map { |tool| format_tool(tool) }.sort
39
+
40
+ adoption_note = if truthy?(preferences[:adopt_new_conventions])
41
+ "This project has opted to adopt new conventions recommended by aidp init. When in doubt, prefer the rules below over legacy patterns."
42
+ else
43
+ "Retain existing conventions when they do not conflict with the guidance below."
44
+ end
45
+
46
+ content = <<~GUIDE
47
+ # Project LLM Style Guide
48
+
49
+ > Generated automatically by `aidp init` on #{Time.now.utc.iso8601}.
50
+ >
51
+ > Detected languages: #{languages}
52
+ > Framework hints: #{frameworks.empty? ? "None detected" : frameworks}
53
+ > Primary test frameworks: #{test_frameworks.empty? ? "Unknown" : test_frameworks}
54
+ > Key directories: #{key_dirs.empty? ? "Standard structure" : key_dirs}
55
+
56
+ #{adoption_note}
57
+
58
+ ## 1. Core Engineering Rules
59
+ - Prioritise readability and maintainability; extract objects or modules once business logic exceeds a few branches.
60
+ - Co-locate domain objects with their tests under the matching directory (e.g., `lib/` ↔ `spec/`).
61
+ - Remove dead code and feature flags that are no longer exercised; keep git history as the source of truth.
62
+ - Use small, composable services rather than bloated classes.
63
+
64
+ ## 2. Naming & Structure
65
+ - Follow idiomatic naming for the detected languages (#{languages}); align files under #{key_dirs.empty? ? "the project root" : key_dirs}.
66
+ - Ensure top-level namespaces mirror the directory structure (e.g., `Aidp::Init` lives in `lib/aidp/init/`).
67
+ - Keep public APIs explicit with keyword arguments and descriptive method names.
68
+
69
+ ## 3. Parameters & Data
70
+ - Limit positional arguments to three; prefer keyword arguments or value objects beyond that.
71
+ - Reuse shared data structures to capture configuration (YAML/JSON) instead of scattered constants.
72
+ - Validate incoming data at boundaries; rely on plain objects internally.
73
+
74
+ ## 4. Error Handling
75
+ - Raise domain-specific errors; avoid using plain `StandardError` without context.
76
+ - Wrap external calls with rescuable adapters and surface actionable error messages.
77
+ - Log failures with relevant identifiers only—never entire payloads.
78
+
79
+ ## 5. Testing Contracts
80
+ - Mirror production directory structure inside `#{preferred_test_dirs(analysis)}`.
81
+ - Keep tests independent; mock external services only at the boundary layers.
82
+ - Use the project's native assertions (#{test_frameworks.empty? ? "choose an appropriate framework" : test_frameworks}) and ensure every bug fix comes with a regression test.
83
+
84
+ ## 6. Framework-Specific Guidelines
85
+ - Adopt the idioms of detected frameworks#{frameworks.empty? ? " once adopted." : " (#{frameworks})."}
86
+ - Keep controllers/handlers thin; delegate logic to service objects or interactors.
87
+ - Store shared UI or component primitives in a central folder to make reuse easier.
88
+
89
+ ## 7. Dependencies & External Services
90
+ - Document every external integration inside `docs/` and keep credentials outside the repo.
91
+ - Use dependency injection for clients; avoid global state or singletons.
92
+ - When adding new gems or packages, document the rationale in `PROJECT_ANALYSIS.md`.
93
+
94
+ ## 8. Build & Development
95
+ - Run linters before committing: #{tooling.empty? ? "add rubocop/eslint/flake8 as appropriate." : tooling.join(", ")}.
96
+ - Keep build scripts in `bin/` or `scripts/` and ensure they are idempotent.
97
+ - Prefer `mise` or language-specific version managers to keep toolchains aligned.
98
+
99
+ ## 9. Performance
100
+ - Measure before optimising; add benchmarks for hotspots.
101
+ - Cache expensive computations when they are pure and repeatable.
102
+ - Review dependency load time; lazy-load optional components where possible.
103
+
104
+ ## 10. Project-Specific Anti-Patterns
105
+ - Avoid sprawling God objects that mix persistence, business logic, and presentation.
106
+ - Resist ad-hoc shelling out; prefer library APIs with proper error handling.
107
+ - Do not bypass the agreed testing workflow—even for small fixes.
108
+
109
+ ---
110
+ Generated from template `planning/generate_llm_style_guide.md` with repository-aware adjustments.
111
+ GUIDE
112
+
113
+ File.write(File.join(@project_dir, STYLE_GUIDE_PATH), content)
114
+ end
115
+
116
+ def write_project_analysis(analysis)
117
+ languages = format_language_breakdown(analysis[:languages])
118
+ frameworks = bullet_list(analysis[:frameworks], default: "_None detected_")
119
+ config_files = bullet_list(analysis[:config_files], default: "_No dedicated configuration files discovered_")
120
+ tooling = format_tooling_section(analysis[:tooling])
121
+
122
+ stats = analysis[:repo_stats]
123
+ stats_lines = [
124
+ "- Total files scanned: #{stats[:total_files]}",
125
+ "- Unique directories: #{stats[:total_directories]}",
126
+ "- Documentation folder present: #{stats[:docs_present] ? "Yes" : "No"}",
127
+ "- CI configuration present: #{stats[:has_ci_config] ? "Yes" : "No"}",
128
+ "- Containerisation assets: #{stats[:has_containerization] ? "Yes" : "No"}"
129
+ ]
130
+
131
+ content = <<~ANALYSIS
132
+ # Project Analysis
133
+
134
+ Generated automatically by `aidp init` on #{Time.now.utc.iso8601}. This document summarises the repository structure to guide future autonomous work loops.
135
+
136
+ ## Language & Framework Footprint
137
+ #{languages}
138
+
139
+ ### Framework Signals
140
+ #{frameworks}
141
+
142
+ ## Key Directories
143
+ #{bullet_list(analysis[:key_directories], default: "_No conventional application directories detected_")}
144
+
145
+ ## Configuration & Tooling Files
146
+ #{config_files}
147
+
148
+ ## Test & Quality Signals
149
+ #{bullet_list(analysis[:test_frameworks], prefix: "- Detected test suite: ", default: "_Unable to infer test suite_")}
150
+
151
+ ## Local Quality Toolchain
152
+ #{tooling}
153
+
154
+ ## Repository Stats
155
+ #{stats_lines.join("\n")}
156
+
157
+ ---
158
+ Template inspiration: `analysis/analyze_repository.md`, `analysis/analyze_tests.md`.
159
+ ANALYSIS
160
+
161
+ File.write(File.join(@project_dir, ANALYSIS_PATH), content)
162
+ end
163
+
164
+ def write_quality_plan(analysis, preferences)
165
+ tooling = analysis[:tooling]
166
+ proactive = if truthy?(preferences[:stricter_linters])
167
+ "- Enable stricter linting rules and fail CI on offences.\n- Enforce formatting checks (`mise exec --` for consistent environments).\n"
168
+ else
169
+ "- Maintain current linting thresholds while documenting exceptions.\n"
170
+ end
171
+
172
+ migration = if truthy?(preferences[:migrate_styles])
173
+ "- Plan refactors to align legacy files with the new style guide.\n- Schedule incremental clean-up tasks to avoid large-batch rewrites.\n"
174
+ else
175
+ "- Keep legacy style deviations documented until dedicated refactors are scheduled.\n"
176
+ end
177
+
178
+ content = <<~PLAN
179
+ # Code Quality Plan
180
+
181
+ This plan captures the current tooling landscape and proposes next steps for keeping the codebase healthy. Generated by `aidp init` on #{Time.now.utc.iso8601}.
182
+
183
+ ## Local Quality Toolchain
184
+ #{tooling.empty? ? "_No linting/formatting tools detected. Consider adding RuboCop, ESLint, or Prettier based on the primary language._" : format_tooling_table(tooling)}
185
+
186
+ ## Immediate Actions
187
+ #{proactive}#{migration}- Document onboarding steps in `docs/` to ensure future contributors follow the agreed workflow.
188
+
189
+ ## Long-Term Improvements
190
+ - Keep the style guide in sync with real-world code changes; regenerate with `aidp init` after major rewrites.
191
+ - Automate test and lint runs via CI (detected: #{analysis.dig(:repo_stats, :has_ci_config) ? "yes" : "no"}).
192
+ - Track flaky tests or unstable tooling in `PROJECT_ANALYSIS.md` under a ā€œHealth Logā€ section.
193
+
194
+ ---
195
+ Based on templates: `analysis/analyze_static_code.md`, `analysis/analyze_tests.md`.
196
+ PLAN
197
+
198
+ File.write(File.join(@project_dir, QUALITY_PLAN_PATH), content)
199
+ end
200
+
201
+ def preferred_test_dirs(analysis)
202
+ detected = Array(analysis[:key_directories]).select { |dir| dir =~ /\b(spec|test|tests)\b/ }
203
+ detected.empty? ? "the chosen test directory" : detected.join(", ")
204
+ end
205
+
206
+ def format_list(values)
207
+ Array(values).join(", ")
208
+ end
209
+
210
+ def bullet_list(values, prefix: "- ", default: "_None_")
211
+ items = Array(values)
212
+ return default if items.empty?
213
+
214
+ items.map { |value| "#{prefix}#{value}" }.join("\n")
215
+ end
216
+
217
+ def format_language_breakdown(languages)
218
+ return "_No source files detected._" if languages.nil? || languages.empty?
219
+
220
+ total = languages.values.sum
221
+ languages.map do |language, weight|
222
+ percentage = total.zero? ? 0 : ((weight.to_f / total) * 100).round(2)
223
+ "- #{language}: #{percentage}% of codebase"
224
+ end.join("\n")
225
+ end
226
+
227
+ def format_tooling_table(tooling)
228
+ rows = tooling.map do |tool, evidence|
229
+ "| #{format_tool(tool)} | #{evidence.uniq.join(", ")} |"
230
+ end
231
+ header = "| Tool | Evidence |\n|------|----------|"
232
+ ([header] + rows).join("\n")
233
+ end
234
+
235
+ def format_tooling_section(tooling)
236
+ return "_No tooling detected._" if tooling.nil? || tooling.empty?
237
+
238
+ header = "| Tool | Evidence |\n|------|----------|"
239
+ rows = tooling.map do |tool, evidence|
240
+ "| #{format_tool(tool)} | #{Array(evidence).uniq.join(", ")} |"
241
+ end
242
+ ([header] + rows).join("\n")
243
+ end
244
+
245
+ def format_tool(tool)
246
+ tool.to_s.split("_").map(&:capitalize).join(" ")
247
+ end
248
+
249
+ def truthy?(value)
250
+ value == true || value.to_s.strip.casecmp("yes").zero?
251
+ rescue
252
+ false
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,343 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "find"
4
+ require "json"
5
+
6
+ module Aidp
7
+ module Init
8
+ # Performs lightweight static analysis of the repository to detect languages,
9
+ # frameworks, config files, and quality tooling. Designed to run quickly and
10
+ # deterministically without external services.
11
+ class ProjectAnalyzer
12
+ IGNORED_DIRECTORIES = %w[
13
+ .git
14
+ .svn
15
+ .hg
16
+ node_modules
17
+ vendor
18
+ tmp
19
+ log
20
+ build
21
+ dist
22
+ coverage
23
+ .yardoc
24
+ ].freeze
25
+
26
+ LANGUAGE_EXTENSIONS = {
27
+ ".rb" => "Ruby",
28
+ ".rake" => "Ruby",
29
+ ".gemspec" => "Ruby",
30
+ ".js" => "JavaScript",
31
+ ".jsx" => "JavaScript",
32
+ ".ts" => "TypeScript",
33
+ ".tsx" => "TypeScript",
34
+ ".py" => "Python",
35
+ ".go" => "Go",
36
+ ".java" => "Java",
37
+ ".kt" => "Kotlin",
38
+ ".cs" => "C#",
39
+ ".php" => "PHP",
40
+ ".rs" => "Rust",
41
+ ".swift" => "Swift",
42
+ ".scala" => "Scala",
43
+ ".c" => "C",
44
+ ".cpp" => "C++",
45
+ ".hpp" => "C++",
46
+ ".m" => "Objective-C",
47
+ ".mm" => "Objective-C++",
48
+ ".hs" => "Haskell",
49
+ ".erl" => "Erlang",
50
+ ".ex" => "Elixir",
51
+ ".exs" => "Elixir",
52
+ ".clj" => "Clojure",
53
+ ".coffee" => "CoffeeScript"
54
+ }.freeze
55
+
56
+ CONFIG_FILES = %w[
57
+ .editorconfig
58
+ .rubocop.yml
59
+ .rubocop_todo.yml
60
+ .standardrb
61
+ .eslintrc
62
+ .eslintrc.js
63
+ .eslintrc.cjs
64
+ .eslintrc.json
65
+ .prettierrc
66
+ .prettierrc.js
67
+ .prettierrc.cjs
68
+ .prettierrc.json
69
+ .stylelintrc
70
+ .flake8
71
+ pyproject.toml
72
+ tox.ini
73
+ setup.cfg
74
+ package.json
75
+ Gemfile
76
+ Gemfile.lock
77
+ mix.exs
78
+ go.mod
79
+ go.sum
80
+ composer.json
81
+ Cargo.toml
82
+ Cargo.lock
83
+ pom.xml
84
+ build.gradle
85
+ build.gradle.kts
86
+ ].freeze
87
+
88
+ KEY_DIRECTORIES = %w[
89
+ app
90
+ src
91
+ lib
92
+ spec
93
+ test
94
+ tests
95
+ scripts
96
+ bin
97
+ config
98
+ docs
99
+ examples
100
+ packages
101
+ modules
102
+ ].freeze
103
+
104
+ TOOLING_HINTS = {
105
+ rubocop: [".rubocop.yml", ".rubocop_todo.yml"],
106
+ standardrb: [".standardrb"],
107
+ eslint: [".eslintrc", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json"],
108
+ prettier: [".prettierrc", ".prettierrc.js", ".prettierrc.cjs", ".prettierrc.json"],
109
+ stylelint: [".stylelintrc"],
110
+ flake8: [".flake8"],
111
+ black: ["pyproject.toml"],
112
+ pytest: ["pytest.ini", "pyproject.toml"],
113
+ jest: ["package.json"],
114
+ rspec: ["spec", ".rspec"],
115
+ minitest: ["test"],
116
+ gofmt: ["go.mod"],
117
+ cargo_fmt: ["Cargo.toml"]
118
+ }.freeze
119
+
120
+ FRAMEWORK_HINTS = {
121
+ "Rails" => {files: ["config/application.rb"], contents: [/rails/i]},
122
+ "Hanami" => {files: ["config/app.rb"], contents: [/hanami/i]},
123
+ "Sinatra" => {files: ["config.ru"], contents: [/sinatra/i]},
124
+ "React" => {files: ["package.json"], contents: [/react/]},
125
+ "Next.js" => {files: ["package.json"], contents: [/next/]},
126
+ "Express" => {files: ["package.json"], contents: [/express/]},
127
+ "Angular" => {files: ["angular.json"]},
128
+ "Vue" => {files: ["package.json"], contents: [/vue/]},
129
+ "Django" => {files: ["manage.py"], contents: [/django/]},
130
+ "Flask" => {files: ["requirements.txt"], contents: [/flask/]},
131
+ "FastAPI" => {files: ["pyproject.toml", "requirements.txt"], contents: [/fastapi/]},
132
+ "Phoenix" => {files: ["mix.exs"], contents: [/phoenix/]},
133
+ "Spring" => {files: ["pom.xml", "build.gradle", "build.gradle.kts"], contents: [/spring/]},
134
+ "Laravel" => {files: ["composer.json"], contents: [/laravel/]},
135
+ "Go Gin" => {files: ["go.mod"], contents: [/gin-gonic/]},
136
+ "Go Fiber" => {files: ["go.mod"], contents: [/github.com\/gofiber/]}
137
+ }.freeze
138
+
139
+ TEST_FRAMEWORK_HINTS = {
140
+ "RSpec" => {directories: ["spec"], dependencies: [/rspec/]},
141
+ "Minitest" => {directories: ["test"], dependencies: [/minitest/]},
142
+ "Cucumber" => {directories: ["features"]},
143
+ "Jest" => {dependencies: [/jest/]},
144
+ "Mocha" => {dependencies: [/mocha/]},
145
+ "Jasmine" => {dependencies: [/jasmine/]},
146
+ "Pytest" => {directories: ["tests", "test"], dependencies: [/pytest/]},
147
+ "NUnit" => {files: ["*.csproj"], contents: [/nunit/]},
148
+ "JUnit" => {dependencies: [/junit/]},
149
+ "Go test" => {files: ["*_test.go"]}
150
+ }.freeze
151
+
152
+ attr_reader :project_dir
153
+
154
+ def initialize(project_dir = Dir.pwd)
155
+ @project_dir = project_dir
156
+ end
157
+
158
+ def analyze
159
+ {
160
+ languages: detect_languages,
161
+ frameworks: detect_frameworks,
162
+ key_directories: detect_key_directories,
163
+ config_files: detect_config_files,
164
+ test_frameworks: detect_test_frameworks,
165
+ tooling: detect_tooling,
166
+ repo_stats: collect_repo_stats
167
+ }
168
+ end
169
+
170
+ private
171
+
172
+ def detect_languages
173
+ counts = Hash.new(0)
174
+
175
+ traverse_files do |relative_path|
176
+ ext = File.extname(relative_path)
177
+ language = LANGUAGE_EXTENSIONS[ext]
178
+ next unless language
179
+
180
+ full_path = File.join(project_dir, relative_path)
181
+ counts[language] += File.size(full_path)
182
+ end
183
+
184
+ counts.sort_by { |_lang, weight| -weight }.to_h
185
+ end
186
+
187
+ def detect_frameworks
188
+ frameworks = Set.new
189
+
190
+ FRAMEWORK_HINTS.each do |framework, rules|
191
+ if rules[:files]&.any? { |file| project_glob?(file) }
192
+ if rules[:contents]
193
+ frameworks << framework if rules[:contents].any? { |pattern| search_files_for_pattern(rules[:files], pattern) }
194
+ else
195
+ frameworks << framework
196
+ end
197
+ elsif rules[:contents]&.any? { |pattern| search_project_for_pattern(pattern) }
198
+ frameworks << framework
199
+ end
200
+ end
201
+
202
+ frameworks.to_a.sort
203
+ end
204
+
205
+ def detect_key_directories
206
+ KEY_DIRECTORIES.select { |dir| Dir.exist?(File.join(project_dir, dir)) }
207
+ end
208
+
209
+ def detect_config_files
210
+ CONFIG_FILES.select { |file| project_glob?(file) }
211
+ end
212
+
213
+ def detect_test_frameworks
214
+ results = []
215
+
216
+ TEST_FRAMEWORK_HINTS.each do |framework, hints|
217
+ has_directory = hints[:directories]&.any? { |dir| Dir.exist?(File.join(project_dir, dir)) }
218
+ has_dependency = hints[:dependencies]&.any? { |pattern| search_project_for_pattern(pattern, limit_files: ["Gemfile", "Gemfile.lock", "package.json", "pyproject.toml", "requirements.txt", "go.mod", "mix.exs", "composer.json", "Cargo.toml"]) }
219
+ has_files = hints[:files]&.any? { |glob| project_glob?(glob) }
220
+
221
+ results << framework if has_directory || has_dependency || has_files
222
+ end
223
+
224
+ results.uniq.sort
225
+ end
226
+
227
+ def detect_tooling
228
+ tooling = Hash.new { |hash, key| hash[key] = [] }
229
+
230
+ TOOLING_HINTS.each do |tool, indicators|
231
+ hit = indicators.any? do |indicator|
232
+ if indicator.include?("*")
233
+ end
234
+ project_glob?(indicator)
235
+ end
236
+ tooling[tool] << "config" if hit
237
+ end
238
+
239
+ # Post-process for package.json to extract scripts referencing linters
240
+ package_json_path = File.join(project_dir, "package.json")
241
+ if File.exist?(package_json_path)
242
+ begin
243
+ json = JSON.parse(File.read(package_json_path))
244
+ scripts = json.fetch("scripts", {})
245
+ tooling[:eslint] << "package.json scripts" if scripts.values.any? { |cmd| cmd.include?("eslint") }
246
+ tooling[:prettier] << "package.json scripts" if scripts.values.any? { |cmd| cmd.include?("prettier") }
247
+ tooling[:jest] << "package.json scripts" if scripts.values.any? { |cmd| cmd.include?("jest") }
248
+ rescue JSON::ParserError
249
+ # ignore malformed package.json
250
+ end
251
+ end
252
+
253
+ tooling.delete_if { |_tool, evidence| evidence.empty? }
254
+ end
255
+
256
+ def collect_repo_stats
257
+ {
258
+ total_files: counted_files.size,
259
+ total_directories: counted_directories.size,
260
+ docs_present: Dir.exist?(File.join(project_dir, "docs")),
261
+ has_ci_config: project_glob?(".github/workflows/*.yml") || project_glob?(".gitlab-ci.yml"),
262
+ has_containerization: project_glob?("Dockerfile") || project_glob?("docker-compose.yml")
263
+ }
264
+ end
265
+
266
+ def counted_files
267
+ @counted_files ||= begin
268
+ files = []
269
+ traverse_files { |path| files << path }
270
+ files
271
+ end
272
+ end
273
+
274
+ def counted_directories
275
+ @counted_directories ||= begin
276
+ dirs = Set.new
277
+ traverse_files do |path|
278
+ dirs << File.dirname(path)
279
+ end
280
+ dirs.to_a
281
+ end
282
+ end
283
+
284
+ def traverse_files
285
+ Find.find(project_dir) do |path|
286
+ next if path == project_dir
287
+
288
+ relative = path.sub("#{project_dir}/", "")
289
+ if File.directory?(path)
290
+ dirname = File.basename(path)
291
+ if IGNORED_DIRECTORIES.include?(dirname)
292
+ Find.prune
293
+ else
294
+ next
295
+ end
296
+ else
297
+ yield relative
298
+ end
299
+ end
300
+ end
301
+
302
+ def project_glob?(pattern)
303
+ Dir.glob(File.join(project_dir, pattern)).any?
304
+ end
305
+
306
+ def search_files_for_pattern(files, pattern)
307
+ files.any? do |file|
308
+ Dir.glob(File.join(project_dir, file)).any? do |path|
309
+ File.read(path).match?(pattern)
310
+ rescue Errno::ENOENT, Errno::EISDIR
311
+ false
312
+ end
313
+ end
314
+ end
315
+
316
+ def search_project_for_pattern(pattern, limit_files: nil)
317
+ if limit_files
318
+ limit_files.any? do |file|
319
+ path = File.join(project_dir, file)
320
+ next false unless File.exist?(path)
321
+
322
+ File.read(path).match?(pattern)
323
+ rescue Errno::ENOENT
324
+ false
325
+ end
326
+ else
327
+ traverse_files do |relative_path|
328
+ path = File.join(project_dir, relative_path)
329
+ begin
330
+ return true if File.read(path).match?(pattern)
331
+ rescue Errno::ENOENT
332
+ next
333
+ rescue ArgumentError, Encoding::InvalidByteSequenceError => e
334
+ warn "[AIDP] Skipping file with invalid encoding: #{path} (#{e.class})"
335
+ next
336
+ end
337
+ end
338
+ false
339
+ end
340
+ end
341
+ end
342
+ end
343
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require_relative "../message_display"
5
+ require_relative "project_analyzer"
6
+ require_relative "doc_generator"
7
+
8
+ module Aidp
9
+ module Init
10
+ # High-level coordinator for `aidp init`. Handles analysis, optional user
11
+ # preferences, and documentation generation.
12
+ class Runner
13
+ include Aidp::MessageDisplay
14
+
15
+ def initialize(project_dir = Dir.pwd, prompt: TTY::Prompt.new, analyzer: nil, doc_generator: nil)
16
+ @project_dir = project_dir
17
+ @prompt = prompt
18
+ @analyzer = analyzer || ProjectAnalyzer.new(project_dir)
19
+ @doc_generator = doc_generator || DocGenerator.new(project_dir)
20
+ end
21
+
22
+ def run
23
+ display_message("šŸ” Running aidp init project analysis...", type: :info)
24
+ analysis = @analyzer.analyze
25
+ display_summary(analysis)
26
+
27
+ preferences = gather_preferences
28
+
29
+ @doc_generator.generate(analysis: analysis, preferences: preferences)
30
+
31
+ display_message("\nšŸ“„ Generated documentation:", type: :info)
32
+ display_message(" - docs/LLM_STYLE_GUIDE.md", type: :success)
33
+ display_message(" - docs/PROJECT_ANALYSIS.md", type: :success)
34
+ display_message(" - docs/CODE_QUALITY_PLAN.md", type: :success)
35
+ display_message("\nāœ… aidp init complete.", type: :success)
36
+
37
+ {
38
+ analysis: analysis,
39
+ preferences: preferences,
40
+ generated_files: [
41
+ "docs/LLM_STYLE_GUIDE.md",
42
+ "docs/PROJECT_ANALYSIS.md",
43
+ "docs/CODE_QUALITY_PLAN.md"
44
+ ]
45
+ }
46
+ end
47
+
48
+ private
49
+
50
+ def display_summary(analysis)
51
+ languages = analysis[:languages].keys
52
+ frameworks = analysis[:frameworks]
53
+ tests = analysis[:test_frameworks]
54
+ config_files = analysis[:config_files]
55
+
56
+ display_message("\nšŸ“Š Repository Snapshot", type: :highlight)
57
+ display_message(" Languages: #{languages.empty? ? "Unknown" : languages.join(", ")}", type: :info)
58
+ display_message(" Frameworks: #{frameworks.empty? ? "None detected" : frameworks.join(", ")}", type: :info)
59
+ display_message(" Test suites: #{tests.empty? ? "Not found" : tests.join(", ")}", type: :info)
60
+ display_message(" Config files: #{config_files.empty? ? "None detected" : config_files.join(", ")}", type: :info)
61
+ end
62
+
63
+ def gather_preferences
64
+ display_message("\nāš™ļø Customise bootstrap plans (press Enter to accept defaults):", type: :info)
65
+
66
+ {
67
+ adopt_new_conventions: ask_yes_no("Adopt the newly generated conventions as canonical defaults?", default: true),
68
+ stricter_linters: ask_yes_no("Enforce stricter linting based on detected tools?", default: false),
69
+ migrate_styles: ask_yes_no("Plan migrations to align legacy files with the new style guide?", default: false)
70
+ }
71
+ end
72
+
73
+ def ask_yes_no(question, default:)
74
+ @prompt.yes?(question) do |q|
75
+ q.default default ? "yes" : "no"
76
+ end
77
+ rescue NoMethodError
78
+ # Compatibility with simplified prompts in tests (e.g. TestPrompt)
79
+ default
80
+ end
81
+ end
82
+ end
83
+ end
data/lib/aidp/init.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "init/project_analyzer"
4
+ require_relative "init/doc_generator"
5
+ require_relative "init/runner"