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
|
@@ -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
|