aidp 0.16.0 → 0.17.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/lib/aidp/analyze/error_handler.rb +32 -13
- data/lib/aidp/analyze/progress.rb +1 -1
- data/lib/aidp/cli.rb +280 -7
- data/lib/aidp/config.rb +1 -1
- data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
- data/lib/aidp/execute/checkpoint.rb +1 -1
- data/lib/aidp/execute/future_work_backlog.rb +1 -1
- data/lib/aidp/execute/progress.rb +1 -1
- data/lib/aidp/execute/repl_macros.rb +79 -10
- data/lib/aidp/harness/config_validator.rb +1 -1
- data/lib/aidp/jobs/background_runner.rb +1 -1
- data/lib/aidp/skills/registry.rb +31 -29
- data/lib/aidp/skills/router.rb +178 -0
- data/lib/aidp/skills/wizard/builder.rb +141 -0
- data/lib/aidp/skills/wizard/controller.rb +145 -0
- data/lib/aidp/skills/wizard/differ.rb +232 -0
- data/lib/aidp/skills/wizard/prompter.rb +317 -0
- data/lib/aidp/skills/wizard/template_library.rb +164 -0
- data/lib/aidp/skills/wizard/writer.rb +105 -0
- data/lib/aidp/version.rb +1 -1
- data/templates/skills/README.md +334 -0
- data/templates/skills/architecture_analyst/SKILL.md +173 -0
- data/templates/skills/product_strategist/SKILL.md +141 -0
- data/templates/skills/repository_analyst/SKILL.md +117 -0
- data/templates/skills/test_analyzer/SKILL.md +213 -0
- metadata +13 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
require_relative "builder"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Skills
|
|
8
|
+
module Wizard
|
|
9
|
+
# Generates and displays diffs between skills
|
|
10
|
+
#
|
|
11
|
+
# Shows differences between an original skill and a modified version,
|
|
12
|
+
# or between a project skill and its template.
|
|
13
|
+
#
|
|
14
|
+
# @example Showing a diff
|
|
15
|
+
# differ = Differ.new
|
|
16
|
+
# diff = differ.diff(original_skill, modified_skill)
|
|
17
|
+
# differ.display(diff)
|
|
18
|
+
class Differ
|
|
19
|
+
attr_reader :pastel
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@pastel = Pastel.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Generate a diff between two skills
|
|
26
|
+
#
|
|
27
|
+
# @param original [Skill, String] Original skill or content
|
|
28
|
+
# @param modified [Skill, String] Modified skill or content
|
|
29
|
+
# @return [Hash] Diff information
|
|
30
|
+
def diff(original, modified)
|
|
31
|
+
original_content = skill_to_content(original)
|
|
32
|
+
modified_content = skill_to_content(modified)
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
original: original_content,
|
|
36
|
+
modified: modified_content,
|
|
37
|
+
lines: generate_line_diff(original_content, modified_content),
|
|
38
|
+
has_changes: original_content != modified_content
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Display a diff to the terminal
|
|
43
|
+
#
|
|
44
|
+
# @param diff [Hash] Diff information from #diff
|
|
45
|
+
# @param output [IO] Output stream (default: $stdout)
|
|
46
|
+
def display(diff, output: $stdout)
|
|
47
|
+
unless diff[:has_changes]
|
|
48
|
+
output.puts pastel.dim("No differences found")
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
output.puts pastel.bold("\n" + "=" * 60)
|
|
53
|
+
output.puts pastel.bold("Skill Diff")
|
|
54
|
+
output.puts pastel.bold("=" * 60)
|
|
55
|
+
|
|
56
|
+
diff[:lines].each do |line_info|
|
|
57
|
+
case line_info[:type]
|
|
58
|
+
when :context
|
|
59
|
+
output.puts pastel.dim(" #{line_info[:line]}")
|
|
60
|
+
when :add
|
|
61
|
+
output.puts pastel.green("+ #{line_info[:line]}")
|
|
62
|
+
when :remove
|
|
63
|
+
output.puts pastel.red("- #{line_info[:line]}")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
output.puts pastel.bold("=" * 60 + "\n")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Generate a unified diff string
|
|
71
|
+
#
|
|
72
|
+
# @param original [Skill, String] Original skill or content
|
|
73
|
+
# @param modified [Skill, String] Modified skill or content
|
|
74
|
+
# @return [String] Unified diff format
|
|
75
|
+
def unified_diff(original, modified)
|
|
76
|
+
original_content = skill_to_content(original)
|
|
77
|
+
modified_content = skill_to_content(modified)
|
|
78
|
+
|
|
79
|
+
original_lines = original_content.lines
|
|
80
|
+
modified_lines = modified_content.lines
|
|
81
|
+
|
|
82
|
+
diff_lines = []
|
|
83
|
+
diff_lines << "--- original"
|
|
84
|
+
diff_lines << "+++ modified"
|
|
85
|
+
|
|
86
|
+
# Simple line-by-line diff (not optimal but functional)
|
|
87
|
+
max_lines = [original_lines.size, modified_lines.size].max
|
|
88
|
+
|
|
89
|
+
(0...max_lines).each do |i|
|
|
90
|
+
orig_line = original_lines[i]
|
|
91
|
+
mod_line = modified_lines[i]
|
|
92
|
+
|
|
93
|
+
if orig_line && !mod_line
|
|
94
|
+
diff_lines << "-#{orig_line}"
|
|
95
|
+
elsif !orig_line && mod_line
|
|
96
|
+
diff_lines << "+#{mod_line}"
|
|
97
|
+
elsif orig_line != mod_line
|
|
98
|
+
diff_lines << "-#{orig_line}"
|
|
99
|
+
diff_lines << "+#{mod_line}"
|
|
100
|
+
else
|
|
101
|
+
diff_lines << " #{orig_line}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
diff_lines.join
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Compare a project skill with its template
|
|
109
|
+
#
|
|
110
|
+
# @param project_skill [Skill] Project skill
|
|
111
|
+
# @param template_skill [Skill] Template skill
|
|
112
|
+
# @return [Hash] Comparison information
|
|
113
|
+
def compare_with_template(project_skill, template_skill)
|
|
114
|
+
{
|
|
115
|
+
skill_id: project_skill.id,
|
|
116
|
+
overrides: detect_overrides(project_skill, template_skill),
|
|
117
|
+
additions: detect_additions(project_skill, template_skill),
|
|
118
|
+
diff: diff(template_skill, project_skill)
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
# Convert skill or string to content
|
|
125
|
+
#
|
|
126
|
+
# @param skill_or_content [Skill, String] Skill object or content string
|
|
127
|
+
# @return [String] Content string
|
|
128
|
+
def skill_to_content(skill_or_content)
|
|
129
|
+
if skill_or_content.is_a?(String)
|
|
130
|
+
skill_or_content
|
|
131
|
+
elsif skill_or_content.respond_to?(:content)
|
|
132
|
+
# Build full SKILL.md content
|
|
133
|
+
builder = Builder.new
|
|
134
|
+
builder.to_skill_md(skill_or_content)
|
|
135
|
+
else
|
|
136
|
+
skill_or_content.to_s
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Generate line-by-line diff
|
|
141
|
+
#
|
|
142
|
+
# @param original [String] Original content
|
|
143
|
+
# @param modified [String] Modified content
|
|
144
|
+
# @return [Array<Hash>] Array of line diff information
|
|
145
|
+
def generate_line_diff(original, modified)
|
|
146
|
+
original_lines = original.lines.map(&:chomp)
|
|
147
|
+
modified_lines = modified.lines.map(&:chomp)
|
|
148
|
+
|
|
149
|
+
lines = []
|
|
150
|
+
i = 0
|
|
151
|
+
j = 0
|
|
152
|
+
|
|
153
|
+
while i < original_lines.size || j < modified_lines.size
|
|
154
|
+
if i >= original_lines.size
|
|
155
|
+
# Only modified lines remain
|
|
156
|
+
lines << {type: :add, line: modified_lines[j]}
|
|
157
|
+
j += 1
|
|
158
|
+
elsif j >= modified_lines.size
|
|
159
|
+
# Only original lines remain
|
|
160
|
+
lines << {type: :remove, line: original_lines[i]}
|
|
161
|
+
i += 1
|
|
162
|
+
elsif original_lines[i] == modified_lines[j]
|
|
163
|
+
# Lines match
|
|
164
|
+
lines << {type: :context, line: original_lines[i]}
|
|
165
|
+
i += 1
|
|
166
|
+
j += 1
|
|
167
|
+
else
|
|
168
|
+
# Lines differ - show removal then addition
|
|
169
|
+
lines << {type: :remove, line: original_lines[i]}
|
|
170
|
+
lines << {type: :add, line: modified_lines[j]}
|
|
171
|
+
i += 1
|
|
172
|
+
j += 1
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
lines
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Detect field overrides between project and template
|
|
180
|
+
#
|
|
181
|
+
# @param project_skill [Skill] Project skill
|
|
182
|
+
# @param template_skill [Skill] Template skill
|
|
183
|
+
# @return [Hash] Hash of overridden fields
|
|
184
|
+
def detect_overrides(project_skill, template_skill)
|
|
185
|
+
overrides = {}
|
|
186
|
+
|
|
187
|
+
# Check metadata fields
|
|
188
|
+
if project_skill.name != template_skill.name
|
|
189
|
+
overrides[:name] = {template: template_skill.name, project: project_skill.name}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if project_skill.description != template_skill.description
|
|
193
|
+
overrides[:description] = {template: template_skill.description, project: project_skill.description}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if project_skill.version != template_skill.version
|
|
197
|
+
overrides[:version] = {template: template_skill.version, project: project_skill.version}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
if project_skill.content != template_skill.content
|
|
201
|
+
overrides[:content] = {template: "...", project: "..."}
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
overrides
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Detect additions in project skill compared to template
|
|
208
|
+
#
|
|
209
|
+
# @param project_skill [Skill] Project skill
|
|
210
|
+
# @param template_skill [Skill] Template skill
|
|
211
|
+
# @return [Hash] Hash of added items
|
|
212
|
+
def detect_additions(project_skill, template_skill)
|
|
213
|
+
additions = {}
|
|
214
|
+
|
|
215
|
+
# Check for added expertise
|
|
216
|
+
new_expertise = project_skill.expertise - template_skill.expertise
|
|
217
|
+
additions[:expertise] = new_expertise if new_expertise.any?
|
|
218
|
+
|
|
219
|
+
# Check for added keywords
|
|
220
|
+
new_keywords = project_skill.keywords - template_skill.keywords
|
|
221
|
+
additions[:keywords] = new_keywords if new_keywords.any?
|
|
222
|
+
|
|
223
|
+
# Check for added when_to_use
|
|
224
|
+
new_when_to_use = project_skill.when_to_use - template_skill.when_to_use
|
|
225
|
+
additions[:when_to_use] = new_when_to_use if new_when_to_use.any?
|
|
226
|
+
|
|
227
|
+
additions
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-prompt"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Skills
|
|
7
|
+
module Wizard
|
|
8
|
+
# Interactive prompter for the skill wizard
|
|
9
|
+
#
|
|
10
|
+
# Uses TTY::Prompt to gather user input through guided questions.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# prompter = Prompter.new
|
|
14
|
+
# responses = prompter.gather_responses(template_library)
|
|
15
|
+
class Prompter
|
|
16
|
+
attr_reader :prompt
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@prompt = TTY::Prompt.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Gather all responses for creating a skill
|
|
23
|
+
#
|
|
24
|
+
# @param template_library [TemplateLibrary] Library for template selection
|
|
25
|
+
# @param options [Hash] Options for the wizard
|
|
26
|
+
# @option options [String] :id Pre-filled skill ID
|
|
27
|
+
# @option options [String] :name Pre-filled skill name
|
|
28
|
+
# @option options [Boolean] :minimal Skip optional sections
|
|
29
|
+
# @option options [String] :from_template Template ID to inherit from
|
|
30
|
+
# @option options [String] :clone Skill ID to clone
|
|
31
|
+
# @return [Hash] Complete set of responses
|
|
32
|
+
def gather_responses(template_library, options: {})
|
|
33
|
+
responses = {}
|
|
34
|
+
|
|
35
|
+
# Step 1: Template selection
|
|
36
|
+
if options[:from_template]
|
|
37
|
+
responses[:base_skill] = template_library.find(options[:from_template])
|
|
38
|
+
unless responses[:base_skill]
|
|
39
|
+
raise Aidp::Errors::ValidationError, "Template not found: #{options[:from_template]}"
|
|
40
|
+
end
|
|
41
|
+
elsif options[:clone]
|
|
42
|
+
responses[:base_skill] = template_library.find(options[:clone])
|
|
43
|
+
unless responses[:base_skill]
|
|
44
|
+
raise Aidp::Errors::ValidationError, "Skill not found: #{options[:clone]}"
|
|
45
|
+
end
|
|
46
|
+
elsif !options[:minimal]
|
|
47
|
+
responses[:base_skill] = prompt_template_selection(template_library)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Step 2: Identity & Metadata
|
|
51
|
+
responses.merge!(prompt_identity(options))
|
|
52
|
+
|
|
53
|
+
# Step 3: Expertise & Keywords
|
|
54
|
+
responses.merge!(prompt_expertise) unless options[:minimal]
|
|
55
|
+
|
|
56
|
+
# Step 4: When to use
|
|
57
|
+
responses.merge!(prompt_when_to_use) unless options[:minimal]
|
|
58
|
+
|
|
59
|
+
# Step 5: Compatible providers
|
|
60
|
+
responses[:compatible_providers] = prompt_providers unless options[:minimal]
|
|
61
|
+
|
|
62
|
+
# Step 6: Content
|
|
63
|
+
responses[:content] = prompt_content(responses[:name], responses[:base_skill])
|
|
64
|
+
|
|
65
|
+
responses
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Prompt for template selection
|
|
71
|
+
#
|
|
72
|
+
# @param template_library [TemplateLibrary] Available templates
|
|
73
|
+
# @return [Skill, nil] Selected base skill or nil
|
|
74
|
+
def prompt_template_selection(template_library)
|
|
75
|
+
prompt.say("\n" + "=" * 60)
|
|
76
|
+
prompt.say("Create New Skill")
|
|
77
|
+
prompt.say("=" * 60 + "\n")
|
|
78
|
+
|
|
79
|
+
choice = prompt.select("How would you like to create your skill?") do |menu|
|
|
80
|
+
menu.choice "Start from scratch", :from_scratch
|
|
81
|
+
menu.choice "Inherit from a template", :inherit
|
|
82
|
+
menu.choice "Clone an existing skill", :clone
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
case choice
|
|
86
|
+
when :from_scratch
|
|
87
|
+
nil
|
|
88
|
+
when :inherit, :clone
|
|
89
|
+
select_base_skill(template_library)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Select a base skill from templates
|
|
94
|
+
#
|
|
95
|
+
# @param template_library [TemplateLibrary] Available templates
|
|
96
|
+
# @return [Skill, nil] Selected skill
|
|
97
|
+
def select_base_skill(template_library)
|
|
98
|
+
skills = template_library.skill_list
|
|
99
|
+
|
|
100
|
+
if skills.empty?
|
|
101
|
+
prompt.warn("No templates available")
|
|
102
|
+
return nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
choices = skills.map do |skill_info|
|
|
106
|
+
source_label = (skill_info[:source] == :template) ? "(template)" : "(project)"
|
|
107
|
+
{
|
|
108
|
+
name: "#{skill_info[:name]} #{source_label} - #{skill_info[:description]}",
|
|
109
|
+
value: skill_info[:id]
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
selected_id = prompt.select("Select a base skill:", choices, per_page: 10)
|
|
114
|
+
template_library.find(selected_id)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Prompt for identity and metadata
|
|
118
|
+
#
|
|
119
|
+
# @param options [Hash] Pre-filled values
|
|
120
|
+
# @return [Hash] Identity responses
|
|
121
|
+
def prompt_identity(options)
|
|
122
|
+
responses = {}
|
|
123
|
+
|
|
124
|
+
# Name
|
|
125
|
+
default_name = options[:name]
|
|
126
|
+
responses[:name] = prompt.ask("Skill Name:", default: default_name) do |q|
|
|
127
|
+
q.required true
|
|
128
|
+
q.modify :strip
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ID (auto-suggest from name)
|
|
132
|
+
suggested_id = options[:id] || slugify(responses[:name])
|
|
133
|
+
responses[:id] = prompt.ask("Skill ID:", default: suggested_id) do |q|
|
|
134
|
+
q.required true
|
|
135
|
+
q.modify :strip, :down
|
|
136
|
+
q.validate(/\A[a-z0-9_]+\z/, "ID must be lowercase alphanumeric with underscores only")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Description
|
|
140
|
+
responses[:description] = prompt.ask("Description (one-line summary):") do |q|
|
|
141
|
+
q.required true
|
|
142
|
+
q.modify :strip
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Version
|
|
146
|
+
responses[:version] = prompt.ask("Version:", default: "1.0.0") do |q|
|
|
147
|
+
q.validate(/\A\d+\.\d+\.\d+\z/, "Must be semantic version (X.Y.Z)")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
responses
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Prompt for expertise and keywords
|
|
154
|
+
#
|
|
155
|
+
# @return [Hash] Expertise responses
|
|
156
|
+
def prompt_expertise
|
|
157
|
+
responses = {}
|
|
158
|
+
|
|
159
|
+
# Expertise areas
|
|
160
|
+
prompt.say("\nExpertise Areas:")
|
|
161
|
+
prompt.say("(Enter expertise areas, one per line. Leave blank and press Enter when done)")
|
|
162
|
+
|
|
163
|
+
expertise = []
|
|
164
|
+
loop do
|
|
165
|
+
area = prompt.ask(" Area #{expertise.size + 1}:", required: false) do |q|
|
|
166
|
+
q.modify :strip
|
|
167
|
+
end
|
|
168
|
+
break if area.nil? || area.empty?
|
|
169
|
+
expertise << area
|
|
170
|
+
end
|
|
171
|
+
responses[:expertise] = expertise
|
|
172
|
+
|
|
173
|
+
# Keywords
|
|
174
|
+
keywords_input = prompt.ask("\nKeywords (comma-separated):", required: false) do |q|
|
|
175
|
+
q.modify :strip
|
|
176
|
+
end
|
|
177
|
+
responses[:keywords] = keywords_input ? keywords_input.split(",").map(&:strip).reject(&:empty?) : []
|
|
178
|
+
|
|
179
|
+
responses
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Prompt for when to use guidance
|
|
183
|
+
#
|
|
184
|
+
# @return [Hash] When to use responses
|
|
185
|
+
def prompt_when_to_use
|
|
186
|
+
responses = {}
|
|
187
|
+
|
|
188
|
+
# When to use
|
|
189
|
+
prompt.say("\nWhen to Use This Skill:")
|
|
190
|
+
prompt.say("(Enter use cases, one per line. Leave blank and press Enter when done)")
|
|
191
|
+
|
|
192
|
+
when_to_use = []
|
|
193
|
+
loop do
|
|
194
|
+
use_case = prompt.ask(" Use case #{when_to_use.size + 1}:", required: false) do |q|
|
|
195
|
+
q.modify :strip
|
|
196
|
+
end
|
|
197
|
+
break if use_case.nil? || use_case.empty?
|
|
198
|
+
when_to_use << use_case
|
|
199
|
+
end
|
|
200
|
+
responses[:when_to_use] = when_to_use
|
|
201
|
+
|
|
202
|
+
# When NOT to use
|
|
203
|
+
prompt.say("\nWhen NOT to Use This Skill:")
|
|
204
|
+
prompt.say("(Leave blank and press Enter when done)")
|
|
205
|
+
|
|
206
|
+
when_not_to_use = []
|
|
207
|
+
loop do
|
|
208
|
+
use_case = prompt.ask(" Avoid when #{when_not_to_use.size + 1}:", required: false) do |q|
|
|
209
|
+
q.modify :strip
|
|
210
|
+
end
|
|
211
|
+
break if use_case.nil? || use_case.empty?
|
|
212
|
+
when_not_to_use << use_case
|
|
213
|
+
end
|
|
214
|
+
responses[:when_not_to_use] = when_not_to_use
|
|
215
|
+
|
|
216
|
+
responses
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Prompt for compatible providers
|
|
220
|
+
#
|
|
221
|
+
# @return [Array<String>] Selected providers
|
|
222
|
+
def prompt_providers
|
|
223
|
+
choices = [
|
|
224
|
+
{name: "All providers (default)", value: []},
|
|
225
|
+
{name: "Anthropic only", value: ["anthropic"]},
|
|
226
|
+
{name: "OpenAI only", value: ["openai"]},
|
|
227
|
+
{name: "Codex only", value: ["codex"]},
|
|
228
|
+
{name: "Cursor only", value: ["cursor"]},
|
|
229
|
+
{name: "Custom selection", value: :custom}
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
selection = prompt.select("\nCompatible AI Providers:", choices)
|
|
233
|
+
|
|
234
|
+
if selection == :custom
|
|
235
|
+
prompt.multi_select("Select providers:") do |menu|
|
|
236
|
+
menu.choice "Anthropic", "anthropic"
|
|
237
|
+
menu.choice "OpenAI", "openai"
|
|
238
|
+
menu.choice "Codex", "codex"
|
|
239
|
+
menu.choice "Cursor", "cursor"
|
|
240
|
+
end
|
|
241
|
+
else
|
|
242
|
+
selection
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Prompt for skill content
|
|
247
|
+
#
|
|
248
|
+
# @param skill_name [String] Name of the skill
|
|
249
|
+
# @param base_skill [Skill, nil] Base skill if inheriting
|
|
250
|
+
# @return [String] Markdown content
|
|
251
|
+
def prompt_content(skill_name, base_skill)
|
|
252
|
+
if base_skill
|
|
253
|
+
prompt.say("\nContent will be inherited from: #{base_skill.name}")
|
|
254
|
+
if prompt.yes?("Would you like to customize the content?")
|
|
255
|
+
prompt_custom_content(skill_name)
|
|
256
|
+
else
|
|
257
|
+
base_skill.content
|
|
258
|
+
end
|
|
259
|
+
else
|
|
260
|
+
prompt_custom_content(skill_name)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Prompt for custom content
|
|
265
|
+
#
|
|
266
|
+
# @param skill_name [String] Name of the skill
|
|
267
|
+
# @return [String] Markdown content
|
|
268
|
+
def prompt_custom_content(skill_name)
|
|
269
|
+
prompt.say("\nContent:")
|
|
270
|
+
lines = prompt.multiline("Enter the skill content (markdown):")
|
|
271
|
+
|
|
272
|
+
if lines.empty?
|
|
273
|
+
# Provide a minimal template
|
|
274
|
+
generate_default_content(skill_name)
|
|
275
|
+
else
|
|
276
|
+
lines.join("\n")
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Generate default content template
|
|
281
|
+
#
|
|
282
|
+
# @param skill_name [String] Name of the skill
|
|
283
|
+
# @return [String] Default markdown content
|
|
284
|
+
def generate_default_content(skill_name)
|
|
285
|
+
<<~MARKDOWN
|
|
286
|
+
# #{skill_name}
|
|
287
|
+
|
|
288
|
+
You are a **#{skill_name}**, an expert in [describe expertise area].
|
|
289
|
+
|
|
290
|
+
## Your Core Capabilities
|
|
291
|
+
|
|
292
|
+
- [Capability 1]
|
|
293
|
+
- [Capability 2]
|
|
294
|
+
- [Capability 3]
|
|
295
|
+
|
|
296
|
+
## Your Approach
|
|
297
|
+
|
|
298
|
+
[Describe your philosophy and approach to tasks]
|
|
299
|
+
MARKDOWN
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Convert a name to a slug (for ID suggestion)
|
|
303
|
+
#
|
|
304
|
+
# @param name [String] Human-readable name
|
|
305
|
+
# @return [String] Slugified ID
|
|
306
|
+
def slugify(name)
|
|
307
|
+
name.to_s
|
|
308
|
+
.downcase
|
|
309
|
+
.gsub(/[^a-z0-9\s_-]/, "")
|
|
310
|
+
.gsub(/\s+/, "_")
|
|
311
|
+
.gsub(/-+/, "_").squeeze("_")
|
|
312
|
+
.gsub(/\A_|_\z/, "")
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../loader"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Skills
|
|
7
|
+
module Wizard
|
|
8
|
+
# Manages skill templates for the wizard
|
|
9
|
+
#
|
|
10
|
+
# Loads and provides access to template skills from the gem's templates/skills/ directory
|
|
11
|
+
# and existing project skills from .aidp/skills/ for cloning.
|
|
12
|
+
#
|
|
13
|
+
# @example Loading templates
|
|
14
|
+
# library = TemplateLibrary.new(project_dir: "/path/to/project")
|
|
15
|
+
# templates = library.templates
|
|
16
|
+
# template = library.find("base_developer")
|
|
17
|
+
class TemplateLibrary
|
|
18
|
+
attr_reader :project_dir
|
|
19
|
+
|
|
20
|
+
# Initialize template library
|
|
21
|
+
#
|
|
22
|
+
# @param project_dir [String] Root directory of the project
|
|
23
|
+
def initialize(project_dir:)
|
|
24
|
+
@project_dir = project_dir
|
|
25
|
+
@templates = nil
|
|
26
|
+
@project_skills = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get all available templates
|
|
30
|
+
#
|
|
31
|
+
# @return [Array<Skill>] Array of template skills
|
|
32
|
+
def templates
|
|
33
|
+
load_templates unless loaded?
|
|
34
|
+
@templates
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get all project skills (for cloning)
|
|
38
|
+
#
|
|
39
|
+
# @return [Array<Skill>] Array of project-specific skills
|
|
40
|
+
def project_skills
|
|
41
|
+
load_project_skills unless project_skills_loaded?
|
|
42
|
+
@project_skills
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get all skills (templates + project skills)
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<Skill>] Combined array of all skills
|
|
48
|
+
def all
|
|
49
|
+
templates + project_skills
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Find a template or project skill by ID
|
|
53
|
+
#
|
|
54
|
+
# @param skill_id [String] Skill identifier
|
|
55
|
+
# @return [Skill, nil] Found skill or nil
|
|
56
|
+
def find(skill_id)
|
|
57
|
+
all.find { |skill| skill.id == skill_id.to_s }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check if a skill ID exists
|
|
61
|
+
#
|
|
62
|
+
# @param skill_id [String] Skill identifier
|
|
63
|
+
# @return [Boolean] True if skill exists
|
|
64
|
+
def exists?(skill_id)
|
|
65
|
+
!find(skill_id).nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get template names for display
|
|
69
|
+
#
|
|
70
|
+
# @return [Array<Hash>] Array of {id, name, description} hashes
|
|
71
|
+
def template_list
|
|
72
|
+
templates.map do |skill|
|
|
73
|
+
{
|
|
74
|
+
id: skill.id,
|
|
75
|
+
name: skill.name,
|
|
76
|
+
description: skill.description,
|
|
77
|
+
source: :template
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get project skill names for display
|
|
83
|
+
#
|
|
84
|
+
# @return [Array<Hash>] Array of {id, name, description} hashes
|
|
85
|
+
def project_skill_list
|
|
86
|
+
project_skills.map do |skill|
|
|
87
|
+
{
|
|
88
|
+
id: skill.id,
|
|
89
|
+
name: skill.name,
|
|
90
|
+
description: skill.description,
|
|
91
|
+
source: :project
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Get combined list for selection
|
|
97
|
+
#
|
|
98
|
+
# @return [Array<Hash>] Array of {id, name, description, source} hashes
|
|
99
|
+
def skill_list
|
|
100
|
+
template_list + project_skill_list
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Load templates from gem directory
|
|
106
|
+
def load_templates
|
|
107
|
+
@templates = Loader.load_from_directory(templates_path)
|
|
108
|
+
Aidp.log_debug(
|
|
109
|
+
"wizard",
|
|
110
|
+
"Loaded templates",
|
|
111
|
+
count: @templates.size,
|
|
112
|
+
path: templates_path
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Load project skills
|
|
117
|
+
def load_project_skills
|
|
118
|
+
@project_skills = if Dir.exist?(project_skills_path)
|
|
119
|
+
Loader.load_from_directory(project_skills_path)
|
|
120
|
+
else
|
|
121
|
+
[]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
Aidp.log_debug(
|
|
125
|
+
"wizard",
|
|
126
|
+
"Loaded project skills",
|
|
127
|
+
count: @project_skills.size,
|
|
128
|
+
path: project_skills_path
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Check if templates are loaded
|
|
133
|
+
#
|
|
134
|
+
# @return [Boolean] True if loaded
|
|
135
|
+
def loaded?
|
|
136
|
+
!@templates.nil?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Check if project skills are loaded
|
|
140
|
+
#
|
|
141
|
+
# @return [Boolean] True if loaded
|
|
142
|
+
def project_skills_loaded?
|
|
143
|
+
!@project_skills.nil?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Get path to templates directory
|
|
147
|
+
#
|
|
148
|
+
# @return [String] Path to templates/skills directory
|
|
149
|
+
def templates_path
|
|
150
|
+
# Get the gem root directory (go up 4 levels from lib/aidp/skills/wizard/template_library.rb)
|
|
151
|
+
gem_root = File.expand_path("../../../..", __dir__)
|
|
152
|
+
File.join(gem_root, "templates", "skills")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Get path to project skills directory
|
|
156
|
+
#
|
|
157
|
+
# @return [String] Path to .aidp/skills directory
|
|
158
|
+
def project_skills_path
|
|
159
|
+
File.join(project_dir, ".aidp", "skills")
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|