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
data/lib/aidp/skills/registry.rb
CHANGED
|
@@ -8,10 +8,10 @@ module Aidp
|
|
|
8
8
|
# lookup, filtering, and management capabilities.
|
|
9
9
|
#
|
|
10
10
|
# Skills are loaded from:
|
|
11
|
-
# 1.
|
|
12
|
-
# 2.
|
|
11
|
+
# 1. Template skills directory (gem templates/skills/) - built-in templates
|
|
12
|
+
# 2. Project skills directory (.aidp/skills/) - project-specific skills
|
|
13
13
|
#
|
|
14
|
-
#
|
|
14
|
+
# Project skills with matching IDs override template skills.
|
|
15
15
|
#
|
|
16
16
|
# @example Basic usage
|
|
17
17
|
# registry = Registry.new(project_dir: "/path/to/project")
|
|
@@ -39,8 +39,8 @@ module Aidp
|
|
|
39
39
|
# Load skills from all search paths
|
|
40
40
|
#
|
|
41
41
|
# Skills are loaded in order:
|
|
42
|
-
# 1.
|
|
43
|
-
# 2.
|
|
42
|
+
# 1. Template skills (gem templates/skills/) - built-in templates
|
|
43
|
+
# 2. Project skills (.aidp/skills/) - override templates
|
|
44
44
|
#
|
|
45
45
|
# @return [Integer] Number of skills loaded
|
|
46
46
|
def load_skills
|
|
@@ -48,13 +48,13 @@ module Aidp
|
|
|
48
48
|
|
|
49
49
|
@skills = {}
|
|
50
50
|
|
|
51
|
-
# Load
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
# Load template skills first
|
|
52
|
+
template_skills = load_from_path(template_skills_path)
|
|
53
|
+
template_skills.each { |skill| register_skill(skill, source: :template) }
|
|
54
54
|
|
|
55
|
-
# Load
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
# Load project skills (override templates if IDs match)
|
|
56
|
+
project_skills = load_from_path(project_skills_path)
|
|
57
|
+
project_skills.each { |skill| register_skill(skill, source: :project) }
|
|
58
58
|
|
|
59
59
|
@loaded = true
|
|
60
60
|
|
|
@@ -139,13 +139,13 @@ module Aidp
|
|
|
139
139
|
|
|
140
140
|
# Get skill IDs grouped by source
|
|
141
141
|
#
|
|
142
|
-
# @return [Hash] Hash with :
|
|
142
|
+
# @return [Hash] Hash with :template and :project arrays
|
|
143
143
|
def by_source
|
|
144
144
|
load_skills unless loaded?
|
|
145
145
|
|
|
146
146
|
{
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
template: @skills.values.select { |s| template_skill?(s) }.map(&:id),
|
|
148
|
+
project: @skills.values.select { |s| project_skill?(s) }.map(&:id)
|
|
149
149
|
}
|
|
150
150
|
end
|
|
151
151
|
|
|
@@ -186,34 +186,36 @@ module Aidp
|
|
|
186
186
|
Loader.load_from_directory(path, provider: provider)
|
|
187
187
|
end
|
|
188
188
|
|
|
189
|
-
# Get
|
|
189
|
+
# Get template skills path (from gem)
|
|
190
190
|
#
|
|
191
|
-
# @return [String] Path to
|
|
192
|
-
def
|
|
193
|
-
|
|
191
|
+
# @return [String] Path to template skills directory
|
|
192
|
+
def template_skills_path
|
|
193
|
+
# Get the gem root directory (go up from lib/aidp/skills/registry.rb)
|
|
194
|
+
gem_root = File.expand_path("../../../..", __dir__)
|
|
195
|
+
File.join(gem_root, "templates", "skills")
|
|
194
196
|
end
|
|
195
197
|
|
|
196
|
-
# Get
|
|
198
|
+
# Get project skills path
|
|
197
199
|
#
|
|
198
|
-
# @return [String] Path to
|
|
199
|
-
def
|
|
200
|
+
# @return [String] Path to project-specific skills directory
|
|
201
|
+
def project_skills_path
|
|
200
202
|
File.join(project_dir, ".aidp", "skills")
|
|
201
203
|
end
|
|
202
204
|
|
|
203
|
-
# Check if skill is from
|
|
205
|
+
# Check if skill is from template directory
|
|
204
206
|
#
|
|
205
207
|
# @param skill [Skill] Skill to check
|
|
206
|
-
# @return [Boolean] True if
|
|
207
|
-
def
|
|
208
|
-
skill.source_path.start_with?(
|
|
208
|
+
# @return [Boolean] True if template
|
|
209
|
+
def template_skill?(skill)
|
|
210
|
+
skill.source_path.start_with?(template_skills_path)
|
|
209
211
|
end
|
|
210
212
|
|
|
211
|
-
# Check if skill is from
|
|
213
|
+
# Check if skill is from project directory
|
|
212
214
|
#
|
|
213
215
|
# @param skill [Skill] Skill to check
|
|
214
|
-
# @return [Boolean] True if
|
|
215
|
-
def
|
|
216
|
-
skill.source_path.start_with?(
|
|
216
|
+
# @return [Boolean] True if project
|
|
217
|
+
def project_skill?(skill)
|
|
218
|
+
skill.source_path.start_with?(project_skills_path)
|
|
217
219
|
end
|
|
218
220
|
end
|
|
219
221
|
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Skills
|
|
7
|
+
# Routes file paths and tasks to appropriate skills based on routing rules
|
|
8
|
+
#
|
|
9
|
+
# The Router reads routing configuration from aidp.yml and matches:
|
|
10
|
+
# - File paths against glob patterns (path_rules)
|
|
11
|
+
# - Task descriptions against keywords (task_rules)
|
|
12
|
+
# - Combined path + task rules (combined_rules)
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# router = Router.new(project_dir: Dir.pwd)
|
|
16
|
+
# skill_id = router.route_by_path("app/controllers/users_controller.rb")
|
|
17
|
+
# # => "rails_expert"
|
|
18
|
+
#
|
|
19
|
+
# @example Task-based routing
|
|
20
|
+
# router = Router.new(project_dir: Dir.pwd)
|
|
21
|
+
# skill_id = router.route_by_task("Add API endpoint")
|
|
22
|
+
# # => "backend_developer"
|
|
23
|
+
#
|
|
24
|
+
# @example Combined routing
|
|
25
|
+
# router = Router.new(project_dir: Dir.pwd)
|
|
26
|
+
# skill_id = router.route(path: "lib/cli.rb", task: "Add command")
|
|
27
|
+
# # => "cli_expert"
|
|
28
|
+
class Router
|
|
29
|
+
attr_reader :config, :project_dir
|
|
30
|
+
|
|
31
|
+
# Initialize router with project directory
|
|
32
|
+
#
|
|
33
|
+
# @param project_dir [String] Path to project directory
|
|
34
|
+
def initialize(project_dir:)
|
|
35
|
+
@project_dir = project_dir
|
|
36
|
+
@config = load_config
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Route based on both path and task (highest priority)
|
|
40
|
+
#
|
|
41
|
+
# @param path [String, nil] File path to route
|
|
42
|
+
# @param task [String, nil] Task description to route
|
|
43
|
+
# @return [String, nil] Matched skill ID or nil
|
|
44
|
+
def route(path: nil, task: nil)
|
|
45
|
+
# Priority 1: Combined rules (path AND task)
|
|
46
|
+
if path && task
|
|
47
|
+
combined_match = match_combined_rules(path, task)
|
|
48
|
+
return combined_match if combined_match
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Priority 2: Path rules
|
|
52
|
+
path_match = route_by_path(path) if path
|
|
53
|
+
return path_match if path_match
|
|
54
|
+
|
|
55
|
+
# Priority 3: Task rules
|
|
56
|
+
task_match = route_by_task(task) if task
|
|
57
|
+
return task_match if task_match
|
|
58
|
+
|
|
59
|
+
# Priority 4: Default skill
|
|
60
|
+
config.dig("skills", "routing", "default")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Route based on file path
|
|
64
|
+
#
|
|
65
|
+
# @param path [String] File path to match against patterns
|
|
66
|
+
# @return [String, nil] Matched skill ID or nil
|
|
67
|
+
def route_by_path(path)
|
|
68
|
+
return nil unless path
|
|
69
|
+
|
|
70
|
+
path_rules = config.dig("skills", "routing", "path_rules") || {}
|
|
71
|
+
|
|
72
|
+
path_rules.each do |skill_id, patterns|
|
|
73
|
+
patterns = [patterns] unless patterns.is_a?(Array)
|
|
74
|
+
patterns.each do |pattern|
|
|
75
|
+
return skill_id if File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Route based on task description
|
|
83
|
+
#
|
|
84
|
+
# @param task [String] Task description to match against keywords
|
|
85
|
+
# @return [String, nil] Matched skill ID or nil
|
|
86
|
+
def route_by_task(task)
|
|
87
|
+
return nil unless task
|
|
88
|
+
|
|
89
|
+
task_rules = config.dig("skills", "routing", "task_rules") || {}
|
|
90
|
+
task_lower = task.downcase
|
|
91
|
+
|
|
92
|
+
task_rules.each do |skill_id, keywords|
|
|
93
|
+
keywords = [keywords] unless keywords.is_a?(Array)
|
|
94
|
+
keywords.each do |keyword|
|
|
95
|
+
return skill_id if task_lower.include?(keyword.downcase)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check if routing is enabled
|
|
103
|
+
#
|
|
104
|
+
# @return [Boolean] true if routing is configured
|
|
105
|
+
def routing_enabled?
|
|
106
|
+
config.dig("skills", "routing", "enabled") == true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Get default skill
|
|
110
|
+
#
|
|
111
|
+
# @return [String, nil] Default skill ID or nil
|
|
112
|
+
def default_skill
|
|
113
|
+
config.dig("skills", "routing", "default")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get all routing rules
|
|
117
|
+
#
|
|
118
|
+
# @return [Hash] Hash containing path_rules, task_rules, and combined_rules
|
|
119
|
+
def rules
|
|
120
|
+
{
|
|
121
|
+
path_rules: config.dig("skills", "routing", "path_rules") || {},
|
|
122
|
+
task_rules: config.dig("skills", "routing", "task_rules") || {},
|
|
123
|
+
combined_rules: config.dig("skills", "routing", "combined_rules") || {}
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
# Match combined rules (path AND task must both match)
|
|
130
|
+
#
|
|
131
|
+
# @param path [String] File path
|
|
132
|
+
# @param task [String] Task description
|
|
133
|
+
# @return [String, nil] Matched skill ID or nil
|
|
134
|
+
def match_combined_rules(path, task)
|
|
135
|
+
combined_rules = config.dig("skills", "routing", "combined_rules") || {}
|
|
136
|
+
task_lower = task.downcase
|
|
137
|
+
|
|
138
|
+
combined_rules.each do |skill_id, rule|
|
|
139
|
+
path_patterns = rule["paths"] || []
|
|
140
|
+
task_keywords = rule["tasks"] || []
|
|
141
|
+
|
|
142
|
+
path_patterns = [path_patterns] unless path_patterns.is_a?(Array)
|
|
143
|
+
task_keywords = [task_keywords] unless task_keywords.is_a?(Array)
|
|
144
|
+
|
|
145
|
+
# Check if path matches any pattern
|
|
146
|
+
path_match = path_patterns.any? do |pattern|
|
|
147
|
+
File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Check if task matches any keyword
|
|
151
|
+
task_match = task_keywords.any? do |keyword|
|
|
152
|
+
task_lower.include?(keyword.downcase)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
return skill_id if path_match && task_match
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Load routing configuration from aidp.yml
|
|
162
|
+
#
|
|
163
|
+
# @return [Hash] Configuration hash
|
|
164
|
+
def load_config
|
|
165
|
+
config_path = File.join(project_dir, ".aidp", "aidp.yml")
|
|
166
|
+
|
|
167
|
+
if File.exist?(config_path)
|
|
168
|
+
YAML.safe_load_file(config_path, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
|
|
169
|
+
else
|
|
170
|
+
{}
|
|
171
|
+
end
|
|
172
|
+
rescue => e
|
|
173
|
+
Aidp.log_error("skills", "Failed to load routing config", error: e.message)
|
|
174
|
+
{}
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../skill"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Skills
|
|
7
|
+
module Wizard
|
|
8
|
+
# Builds Skill objects from wizard responses
|
|
9
|
+
#
|
|
10
|
+
# Takes user input from the wizard and constructs a valid Skill object.
|
|
11
|
+
# Handles template inheritance by merging metadata and content.
|
|
12
|
+
#
|
|
13
|
+
# @example Building a new skill
|
|
14
|
+
# builder = Builder.new
|
|
15
|
+
# responses = { id: "my_skill", name: "My Skill", ... }
|
|
16
|
+
# skill = builder.build(responses)
|
|
17
|
+
#
|
|
18
|
+
# @example Building from a template
|
|
19
|
+
# builder = Builder.new(base_skill: template)
|
|
20
|
+
# responses = { id: "my_skill", ... }
|
|
21
|
+
# skill = builder.build(responses) # Inherits from template
|
|
22
|
+
class Builder
|
|
23
|
+
attr_reader :base_skill
|
|
24
|
+
|
|
25
|
+
# Initialize builder
|
|
26
|
+
#
|
|
27
|
+
# @param base_skill [Skill, nil] Optional base skill for inheritance
|
|
28
|
+
def initialize(base_skill: nil)
|
|
29
|
+
@base_skill = base_skill
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Build a Skill from wizard responses
|
|
33
|
+
#
|
|
34
|
+
# @param responses [Hash] Wizard responses
|
|
35
|
+
# @option responses [String] :id Skill ID
|
|
36
|
+
# @option responses [String] :name Skill name
|
|
37
|
+
# @option responses [String] :description Description
|
|
38
|
+
# @option responses [String] :version Version (default: "1.0.0")
|
|
39
|
+
# @option responses [Array<String>] :expertise Expertise areas
|
|
40
|
+
# @option responses [Array<String>] :keywords Keywords
|
|
41
|
+
# @option responses [Array<String>] :when_to_use When to use
|
|
42
|
+
# @option responses [Array<String>] :when_not_to_use When not to use
|
|
43
|
+
# @option responses [Array<String>] :compatible_providers Compatible providers
|
|
44
|
+
# @option responses [String] :content Markdown content
|
|
45
|
+
# @return [Skill] Built skill
|
|
46
|
+
def build(responses)
|
|
47
|
+
# Merge with base skill if provided
|
|
48
|
+
merged = if base_skill
|
|
49
|
+
merge_with_base(responses)
|
|
50
|
+
else
|
|
51
|
+
responses
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Create skill with merged attributes
|
|
55
|
+
# Note: source_path will be set by Writer when saved
|
|
56
|
+
Skill.new(
|
|
57
|
+
id: merged[:id],
|
|
58
|
+
name: merged[:name],
|
|
59
|
+
description: merged[:description],
|
|
60
|
+
version: merged[:version] || "1.0.0",
|
|
61
|
+
expertise: Array(merged[:expertise]),
|
|
62
|
+
keywords: Array(merged[:keywords]),
|
|
63
|
+
when_to_use: Array(merged[:when_to_use]),
|
|
64
|
+
when_not_to_use: Array(merged[:when_not_to_use]),
|
|
65
|
+
compatible_providers: Array(merged[:compatible_providers]),
|
|
66
|
+
content: merged[:content],
|
|
67
|
+
source_path: merged[:source_path] || "<pending>"
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Generate YAML frontmatter + content for writing
|
|
72
|
+
#
|
|
73
|
+
# @param skill [Skill] Skill to serialize
|
|
74
|
+
# @return [String] Complete SKILL.md content
|
|
75
|
+
def to_skill_md(skill)
|
|
76
|
+
frontmatter = build_frontmatter(skill)
|
|
77
|
+
"#{frontmatter}\n#{skill.content}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Merge responses with base skill
|
|
83
|
+
#
|
|
84
|
+
# @param responses [Hash] User responses
|
|
85
|
+
# @return [Hash] Merged attributes
|
|
86
|
+
def merge_with_base(responses)
|
|
87
|
+
{
|
|
88
|
+
id: responses[:id],
|
|
89
|
+
name: responses[:name] || base_skill.name,
|
|
90
|
+
description: responses[:description] || base_skill.description,
|
|
91
|
+
version: responses[:version] || "1.0.0",
|
|
92
|
+
expertise: merge_arrays(base_skill.expertise, responses[:expertise]),
|
|
93
|
+
keywords: merge_arrays(base_skill.keywords, responses[:keywords]),
|
|
94
|
+
when_to_use: merge_arrays(base_skill.when_to_use, responses[:when_to_use]),
|
|
95
|
+
when_not_to_use: merge_arrays(base_skill.when_not_to_use, responses[:when_not_to_use]),
|
|
96
|
+
compatible_providers: responses[:compatible_providers] || base_skill.compatible_providers,
|
|
97
|
+
content: responses[:content] || base_skill.content,
|
|
98
|
+
source_path: responses[:source_path]
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Merge arrays (base + new, deduplicated)
|
|
103
|
+
#
|
|
104
|
+
# @param base [Array] Base array
|
|
105
|
+
# @param new_items [Array, nil] New items
|
|
106
|
+
# @return [Array] Merged and deduplicated array
|
|
107
|
+
def merge_arrays(base, new_items)
|
|
108
|
+
return base if new_items.nil? || new_items.empty?
|
|
109
|
+
(base + Array(new_items)).uniq
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Build YAML frontmatter
|
|
113
|
+
#
|
|
114
|
+
# @param skill [Skill] Skill object
|
|
115
|
+
# @return [String] YAML frontmatter block
|
|
116
|
+
def build_frontmatter(skill)
|
|
117
|
+
data = {
|
|
118
|
+
"id" => skill.id,
|
|
119
|
+
"name" => skill.name,
|
|
120
|
+
"description" => skill.description,
|
|
121
|
+
"version" => skill.version
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Add optional arrays if not empty
|
|
125
|
+
data["expertise"] = skill.expertise unless skill.expertise.empty?
|
|
126
|
+
data["keywords"] = skill.keywords unless skill.keywords.empty?
|
|
127
|
+
data["when_to_use"] = skill.when_to_use unless skill.when_to_use.empty?
|
|
128
|
+
data["when_not_to_use"] = skill.when_not_to_use unless skill.when_not_to_use.empty?
|
|
129
|
+
data["compatible_providers"] = skill.compatible_providers unless skill.compatible_providers.empty?
|
|
130
|
+
|
|
131
|
+
# Generate YAML
|
|
132
|
+
yaml_content = data.to_yaml
|
|
133
|
+
# Remove the leading "---\n" that to_yaml adds, we'll add our own delimiters
|
|
134
|
+
yaml_content = yaml_content.sub(/\A---\n/, "")
|
|
135
|
+
|
|
136
|
+
"---\n#{yaml_content}---"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "template_library"
|
|
4
|
+
require_relative "prompter"
|
|
5
|
+
require_relative "builder"
|
|
6
|
+
require_relative "writer"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
module Skills
|
|
10
|
+
module Wizard
|
|
11
|
+
# Controller orchestrating the skill creation wizard
|
|
12
|
+
#
|
|
13
|
+
# Coordinates the wizard flow: template selection, prompting, building,
|
|
14
|
+
# preview, and writing the skill.
|
|
15
|
+
#
|
|
16
|
+
# @example Creating a new skill
|
|
17
|
+
# controller = Controller.new(project_dir: "/path/to/project")
|
|
18
|
+
# controller.run
|
|
19
|
+
#
|
|
20
|
+
# @example With options
|
|
21
|
+
# controller = Controller.new(
|
|
22
|
+
# project_dir: "/path/to/project",
|
|
23
|
+
# options: { id: "my_skill", dry_run: true }
|
|
24
|
+
# )
|
|
25
|
+
# controller.run
|
|
26
|
+
class Controller
|
|
27
|
+
attr_reader :project_dir, :options, :template_library, :prompter, :writer
|
|
28
|
+
|
|
29
|
+
# Initialize controller
|
|
30
|
+
#
|
|
31
|
+
# @param project_dir [String] Root directory of the project
|
|
32
|
+
# @param options [Hash] Wizard options
|
|
33
|
+
# @option options [String] :id Pre-filled skill ID
|
|
34
|
+
# @option options [String] :name Pre-filled skill name
|
|
35
|
+
# @option options [Boolean] :dry_run Don't write files
|
|
36
|
+
# @option options [Boolean] :minimal Skip optional sections
|
|
37
|
+
# @option options [Boolean] :open_editor Open in $EDITOR (future)
|
|
38
|
+
def initialize(project_dir:, options: {})
|
|
39
|
+
@project_dir = project_dir
|
|
40
|
+
@options = options
|
|
41
|
+
@template_library = TemplateLibrary.new(project_dir: project_dir)
|
|
42
|
+
@prompter = Prompter.new
|
|
43
|
+
@writer = Writer.new(project_dir: project_dir)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Run the wizard
|
|
47
|
+
#
|
|
48
|
+
# @return [String, nil] Path to created skill file, or nil if cancelled/dry-run
|
|
49
|
+
def run
|
|
50
|
+
Aidp.log_info("wizard", "Starting skill creation wizard", options: options)
|
|
51
|
+
|
|
52
|
+
# Gather user responses
|
|
53
|
+
responses = prompter.gather_responses(template_library, options: options)
|
|
54
|
+
|
|
55
|
+
# Build skill from responses
|
|
56
|
+
base_skill = responses.delete(:base_skill)
|
|
57
|
+
builder = Builder.new(base_skill: base_skill)
|
|
58
|
+
skill = builder.build(responses)
|
|
59
|
+
|
|
60
|
+
# Generate SKILL.md content
|
|
61
|
+
skill_md_content = builder.to_skill_md(skill)
|
|
62
|
+
|
|
63
|
+
# Preview
|
|
64
|
+
show_preview(skill, skill_md_content) unless options[:minimal]
|
|
65
|
+
|
|
66
|
+
# Confirm
|
|
67
|
+
unless options[:dry_run] || options[:yes] || confirm_save(skill)
|
|
68
|
+
prompter.prompt.warn("Cancelled")
|
|
69
|
+
return nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Write to disk
|
|
73
|
+
path = writer.write(
|
|
74
|
+
skill,
|
|
75
|
+
content: skill_md_content,
|
|
76
|
+
dry_run: options[:dry_run],
|
|
77
|
+
backup: true
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Show success message
|
|
81
|
+
show_success(skill, path) unless options[:dry_run]
|
|
82
|
+
|
|
83
|
+
path
|
|
84
|
+
rescue Interrupt
|
|
85
|
+
prompter.prompt.warn("\nWizard cancelled")
|
|
86
|
+
nil
|
|
87
|
+
rescue => e
|
|
88
|
+
Aidp.log_error("wizard", "Wizard failed", error: e.message, backtrace: e.backtrace.first(5))
|
|
89
|
+
prompter.prompt.error("Error: #{e.message}")
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Show preview of the skill
|
|
96
|
+
#
|
|
97
|
+
# @param skill [Skill] Built skill
|
|
98
|
+
# @param content [String] SKILL.md content
|
|
99
|
+
def show_preview(skill, content)
|
|
100
|
+
prompter.prompt.say("\n" + "=" * 60)
|
|
101
|
+
prompter.prompt.say("Skill Preview: #{skill.id} v#{skill.version}")
|
|
102
|
+
prompter.prompt.say("=" * 60 + "\n")
|
|
103
|
+
|
|
104
|
+
# Show first 20 lines of content
|
|
105
|
+
lines = content.lines
|
|
106
|
+
preview_lines = lines.first(20)
|
|
107
|
+
prompter.prompt.say(preview_lines.join)
|
|
108
|
+
|
|
109
|
+
if lines.size > 20
|
|
110
|
+
prompter.prompt.say("\n... (#{lines.size - 20} more lines)")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
prompter.prompt.say("\n" + "=" * 60 + "\n")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Confirm before saving
|
|
117
|
+
#
|
|
118
|
+
# @param skill [Skill] Skill to save
|
|
119
|
+
# @return [Boolean] True if user confirms
|
|
120
|
+
def confirm_save(skill)
|
|
121
|
+
if writer.exists?(skill.id)
|
|
122
|
+
prompter.prompt.warn("Warning: Skill '#{skill.id}' already exists and will be overwritten (backup will be created)")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
prompter.prompt.yes?("Save this skill?")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Show success message
|
|
129
|
+
#
|
|
130
|
+
# @param skill [Skill] Created skill
|
|
131
|
+
# @param path [String] Path to file
|
|
132
|
+
def show_success(skill, path)
|
|
133
|
+
prompter.prompt.say("\n✅ Skill created successfully!\n")
|
|
134
|
+
prompter.prompt.say(" ID: #{skill.id}")
|
|
135
|
+
prompter.prompt.say(" Version: #{skill.version}")
|
|
136
|
+
prompter.prompt.say(" File: #{path}")
|
|
137
|
+
prompter.prompt.say("\nNext steps:")
|
|
138
|
+
prompter.prompt.say(" • Review: aidp skill preview #{skill.id}")
|
|
139
|
+
prompter.prompt.say(" • Edit: aidp skill edit #{skill.id}")
|
|
140
|
+
prompter.prompt.say(" • Use: Reference in your workflow steps\n")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|