aidp 0.15.2 → 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/README.md +47 -0
- data/lib/aidp/analyze/error_handler.rb +46 -28
- data/lib/aidp/analyze/progress.rb +1 -1
- data/lib/aidp/analyze/runner.rb +27 -5
- data/lib/aidp/analyze/steps.rb +4 -0
- data/lib/aidp/cli/jobs_command.rb +2 -1
- data/lib/aidp/cli.rb +1086 -4
- data/lib/aidp/concurrency/backoff.rb +148 -0
- data/lib/aidp/concurrency/exec.rb +192 -0
- data/lib/aidp/concurrency/wait.rb +148 -0
- data/lib/aidp/concurrency.rb +71 -0
- data/lib/aidp/config.rb +21 -1
- data/lib/aidp/daemon/runner.rb +9 -8
- data/lib/aidp/debug_mixin.rb +1 -0
- data/lib/aidp/errors.rb +12 -0
- 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/interactive_repl.rb +102 -11
- data/lib/aidp/execute/progress.rb +1 -1
- data/lib/aidp/execute/repl_macros.rb +845 -2
- data/lib/aidp/execute/runner.rb +27 -5
- data/lib/aidp/execute/steps.rb +2 -0
- data/lib/aidp/harness/config_loader.rb +24 -2
- data/lib/aidp/harness/config_validator.rb +1 -1
- data/lib/aidp/harness/enhanced_runner.rb +16 -2
- data/lib/aidp/harness/error_handler.rb +1 -1
- data/lib/aidp/harness/provider_info.rb +19 -15
- data/lib/aidp/harness/provider_manager.rb +47 -41
- data/lib/aidp/harness/runner.rb +3 -11
- data/lib/aidp/harness/state/persistence.rb +1 -6
- data/lib/aidp/harness/state_manager.rb +115 -7
- data/lib/aidp/harness/status_display.rb +11 -18
- data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +1 -1
- data/lib/aidp/harness/user_interface.rb +12 -15
- data/lib/aidp/jobs/background_runner.rb +16 -6
- data/lib/aidp/providers/codex.rb +0 -1
- data/lib/aidp/providers/cursor.rb +0 -1
- data/lib/aidp/providers/github_copilot.rb +0 -1
- data/lib/aidp/providers/opencode.rb +0 -1
- data/lib/aidp/skills/composer.rb +178 -0
- data/lib/aidp/skills/loader.rb +205 -0
- data/lib/aidp/skills/registry.rb +222 -0
- data/lib/aidp/skills/router.rb +178 -0
- data/lib/aidp/skills/skill.rb +174 -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/skills.rb +30 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +93 -28
- data/lib/aidp/watch/runner.rb +3 -2
- data/lib/aidp/workstream_executor.rb +244 -0
- data/lib/aidp/workstream_state.rb +212 -0
- data/lib/aidp/worktree.rb +208 -0
- data/lib/aidp.rb +6 -0
- 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 +29 -4
|
@@ -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
|
|
@@ -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
|