gem-skill 0.1.1 → 0.1.2
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/.envrc +1 -0
- data/README.md +16 -12
- data/lib/gem/skill/cli/bundle_command.rb +63 -61
- data/lib/gem/skill/cli/gem_command.rb +30 -21
- data/lib/gem/skill/generator.rb +3 -1
- data/lib/gem/skill/linker.rb +13 -10
- data/lib/gem/skill/lockfile.rb +30 -3
- data/lib/gem/skill/runner.rb +24 -0
- data/lib/gem/skill/version.rb +1 -1
- data/lib/gem/skill.rb +1 -0
- data/scripts/e2e_test +64 -30
- metadata +18 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e537a3925c494c903dc9dd105d6a975d55d5fe55f963847cca3c363ce26120d6
|
|
4
|
+
data.tar.gz: 7f4fd4cff99d062ab50bf73b733aeddca903f23ffb657ccc7cc7616dcd7e2fe6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d74ee4cdbf3b827984574580985426ec708e7dfd901f2ddb99f03268afdcc96a2ab9f8962781549c053e587f1ecbd4f0d5cab06189adb24603af475083ab54e
|
|
7
|
+
data.tar.gz: e012f26a513a5516d606c6664c1ccd470ed910bd57190850e96daf1bc88e686e00dfbfc2b6f9bd8a467bdde6d5ca938b3084697ca27d2a956043b05e9d8dac37
|
data/.envrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export RR=`pwd`
|
data/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Each project's `.claude/skills/` holds symlinks that point into this cache:
|
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
your-app/.claude/skills/
|
|
34
|
-
└── chunker-ruby
|
|
34
|
+
└── chunker-ruby/ → ~/.gem/skills/chunker-ruby/1.2.3/
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Two projects that pin different versions of the same gem each get the right
|
|
@@ -41,17 +41,14 @@ skill; the underlying content is generated once and shared.
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
gem install gem-skill
|
|
44
|
+
gem skill setup
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
`gem install` gives you the `gem skill` subcommand.
|
|
48
|
+
`gem skill setup` registers gem-skill as a Bundler plugin, enabling `bundle skill` in any project.
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
bundle plugin install gem-skill
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
or add it to your `Gemfile`:
|
|
50
|
+
You only need to run `gem skill setup` once per machine. Alternatively, you can
|
|
51
|
+
add the plugin directly to a project's `Gemfile`:
|
|
55
52
|
|
|
56
53
|
```ruby
|
|
57
54
|
plugin "gem-skill"
|
|
@@ -117,6 +114,16 @@ gem skill purge chunker-ruby --all
|
|
|
117
114
|
|
|
118
115
|
If a gem isn't installed locally, `gem skill install` will install it first.
|
|
119
116
|
|
|
117
|
+
### `gem skill setup`
|
|
118
|
+
|
|
119
|
+
Run once after `gem install gem-skill` to enable `bundle skill` globally:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
gem skill setup
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This registers gem-skill as a Bundler plugin so `bundle skill` works in every project.
|
|
126
|
+
|
|
120
127
|
### `gem install --with-skill`
|
|
121
128
|
|
|
122
129
|
Generate skills for gems as you install them:
|
|
@@ -146,9 +153,6 @@ bundle skill install --force
|
|
|
146
153
|
bundle skill install --model claude-haiku-4-5
|
|
147
154
|
```
|
|
148
155
|
|
|
149
|
-
The `install` and `refresh` commands stream LLM output as it is generated, so
|
|
150
|
-
you see progress rather than a silent wait.
|
|
151
|
-
|
|
152
156
|
## What gets generated
|
|
153
157
|
|
|
154
158
|
Each `SKILL.md` covers:
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "async"
|
|
3
4
|
require "fileutils"
|
|
4
5
|
require "json"
|
|
6
|
+
require "tty-spinner"
|
|
5
7
|
require "gem/skill"
|
|
6
8
|
|
|
7
9
|
module Gem::Skill
|
|
@@ -35,42 +37,33 @@ module Gem::Skill
|
|
|
35
37
|
return
|
|
36
38
|
end
|
|
37
39
|
|
|
38
|
-
force
|
|
39
|
-
model
|
|
40
|
+
force = opts[:force]
|
|
41
|
+
model = opts[:model] || Generator::DEFAULT_MODEL
|
|
40
42
|
errors = []
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
multi = TTY::Spinner::Multi.new(
|
|
45
|
+
"[:spinner] Installing skills (#{model})",
|
|
46
|
+
format: :dots,
|
|
47
|
+
output: $stderr
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
Async do
|
|
51
|
+
barrier = Async::Barrier.new
|
|
52
|
+
gems.each do |gem_name, version|
|
|
53
|
+
sp = multi.register(" [:spinner] :title")
|
|
54
|
+
sp.update(title: "#{gem_name} #{version}")
|
|
55
|
+
barrier.async do
|
|
56
|
+
err = install_one(gem_name, version, sp, force: force, model: model)
|
|
57
|
+
errors << "#{gem_name} #{version}: #{err}" if err
|
|
54
58
|
end
|
|
55
59
|
end
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
rescue Gem::Skill::Error => e
|
|
60
|
-
puts " ✗ #{e.message}"
|
|
61
|
-
errors << "#{gem_name} #{version}: #{e.message}"
|
|
60
|
+
barrier.wait
|
|
61
|
+
ensure
|
|
62
|
+
barrier.stop
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
Linker.prune_dead_links
|
|
65
|
-
|
|
66
|
-
puts ""
|
|
67
|
-
puts "Done. #{gems.size - errors.size}/#{gems.size} skill(s) linked into .claude/skills/"
|
|
68
|
-
|
|
69
|
-
if errors.any?
|
|
70
|
-
puts ""
|
|
71
|
-
puts "Errors:"
|
|
72
|
-
errors.each { |e| puts " #{e}" }
|
|
73
|
-
end
|
|
66
|
+
report_errors(errors)
|
|
74
67
|
end
|
|
75
68
|
|
|
76
69
|
def self.refresh(opts = {})
|
|
@@ -80,40 +73,35 @@ module Gem::Skill
|
|
|
80
73
|
model = opts[:model] || Generator::DEFAULT_MODEL
|
|
81
74
|
errors = []
|
|
82
75
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
76
|
+
multi = TTY::Spinner::Multi.new(
|
|
77
|
+
"[:spinner] Refreshing skills (#{model})",
|
|
78
|
+
format: :dots,
|
|
79
|
+
output: $stderr
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
Async do
|
|
83
|
+
barrier = Async::Barrier.new
|
|
84
|
+
gems.each do |gem_name, version|
|
|
85
|
+
sp = multi.register(" [:spinner] :title")
|
|
86
|
+
sp.update(title: "#{gem_name} #{version}")
|
|
87
|
+
barrier.async do
|
|
88
|
+
err = if !force && linked[gem_name] == version
|
|
89
|
+
sp.auto_spin
|
|
90
|
+
sp.success("up to date")
|
|
91
|
+
nil
|
|
92
|
+
else
|
|
93
|
+
install_one(gem_name, version, sp, force: force, model: model)
|
|
94
|
+
end
|
|
95
|
+
errors << "#{gem_name} #{version}: #{err}" if err
|
|
96
|
+
end
|
|
99
97
|
end
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
rescue Gem::Skill::Error => e
|
|
104
|
-
puts " ✗ #{e.message}"
|
|
105
|
-
errors << "#{gem_name} #{version}: #{e.message}"
|
|
98
|
+
barrier.wait
|
|
99
|
+
ensure
|
|
100
|
+
barrier.stop
|
|
106
101
|
end
|
|
107
102
|
|
|
108
103
|
Linker.prune_dead_links
|
|
109
|
-
|
|
110
|
-
puts ""
|
|
111
|
-
puts "Refreshed."
|
|
112
|
-
if errors.any?
|
|
113
|
-
puts ""
|
|
114
|
-
puts "Errors:"
|
|
115
|
-
errors.each { |e| puts " #{e}" }
|
|
116
|
-
end
|
|
104
|
+
report_errors(errors)
|
|
117
105
|
end
|
|
118
106
|
|
|
119
107
|
def self.list
|
|
@@ -124,8 +112,8 @@ module Gem::Skill
|
|
|
124
112
|
return
|
|
125
113
|
end
|
|
126
114
|
|
|
127
|
-
ok
|
|
128
|
-
broken
|
|
115
|
+
ok = entries.count { |e| e[:valid] }
|
|
116
|
+
broken = entries.size - ok
|
|
129
117
|
|
|
130
118
|
puts "Skills linked in .claude/skills/ (#{ok} ok#{broken > 0 ? ", #{broken} broken" : ""}):"
|
|
131
119
|
puts ""
|
|
@@ -137,6 +125,20 @@ module Gem::Skill
|
|
|
137
125
|
|
|
138
126
|
# --- private ---
|
|
139
127
|
|
|
128
|
+
def self.install_one(gem_name, version, spinner, force:, model:)
|
|
129
|
+
spinner.auto_spin
|
|
130
|
+
Runner.install_skill(gem_name, version, spinner, force: force, model: model)
|
|
131
|
+
end
|
|
132
|
+
private_class_method :install_one
|
|
133
|
+
|
|
134
|
+
def self.report_errors(errors)
|
|
135
|
+
return if errors.empty?
|
|
136
|
+
warn ""
|
|
137
|
+
warn "Errors (#{errors.size}):"
|
|
138
|
+
errors.each { |e| warn " #{e}" }
|
|
139
|
+
end
|
|
140
|
+
private_class_method :report_errors
|
|
141
|
+
|
|
140
142
|
def self.parse_options(args)
|
|
141
143
|
opts = {}
|
|
142
144
|
remaining = []
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rubygems/command"
|
|
4
|
+
require "async"
|
|
4
5
|
require "fileutils"
|
|
5
6
|
require "json"
|
|
6
7
|
require "tty-spinner"
|
|
@@ -20,14 +21,15 @@ class Gem::Commands::SkillCommand < Gem::Command
|
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def arguments
|
|
23
|
-
"SUBCOMMAND one of: install, list, purge"
|
|
24
|
+
"SUBCOMMAND one of: install, list, purge, setup"
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def usage
|
|
27
28
|
"#{program_name} install GEM_NAME [GEM_NAME ...]\n" \
|
|
28
29
|
" #{program_name} list\n" \
|
|
29
30
|
" #{program_name} purge GEM_NAME VERSION\n" \
|
|
30
|
-
" #{program_name} purge GEM_NAME --all"
|
|
31
|
+
" #{program_name} purge GEM_NAME --all\n" \
|
|
32
|
+
" #{program_name} setup"
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def description
|
|
@@ -35,9 +37,9 @@ class Gem::Commands::SkillCommand < Gem::Command
|
|
|
35
37
|
install Generate and cache a SKILL.md for a gem.
|
|
36
38
|
list Show all skills in the global cache (~/.gem/skills).
|
|
37
39
|
purge Remove a specific cached version.
|
|
40
|
+
setup Register gem-skill as a Bundler plugin (run once after install).
|
|
38
41
|
|
|
39
|
-
Use 'bundle skill install'
|
|
40
|
-
to generate and link skills for an entire project from Gemfile.lock.
|
|
42
|
+
Use 'bundle skill install' in any project after running 'gem skill setup'.
|
|
41
43
|
DESC
|
|
42
44
|
end
|
|
43
45
|
|
|
@@ -48,6 +50,7 @@ class Gem::Commands::SkillCommand < Gem::Command
|
|
|
48
50
|
when "install" then cmd_install
|
|
49
51
|
when "list" then cmd_list
|
|
50
52
|
when "purge" then cmd_purge
|
|
53
|
+
when "setup" then cmd_setup
|
|
51
54
|
when nil
|
|
52
55
|
say usage
|
|
53
56
|
else
|
|
@@ -76,36 +79,33 @@ class Gem::Commands::SkillCommand < Gem::Command
|
|
|
76
79
|
output: $stderr
|
|
77
80
|
)
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
Async do
|
|
83
|
+
barrier = Async::Barrier.new
|
|
84
|
+
gem_names.each do |gem_name|
|
|
85
|
+
spinner = multi.register(" [:spinner] :title")
|
|
86
|
+
spinner.update(title: gem_name)
|
|
87
|
+
barrier.async { install_one(gem_name, spinner: spinner, force: force, model: model) }
|
|
88
|
+
end
|
|
89
|
+
barrier.wait
|
|
90
|
+
ensure
|
|
91
|
+
barrier.stop
|
|
83
92
|
end
|
|
84
|
-
threads.each(&:join)
|
|
85
93
|
|
|
86
94
|
say "Tip: run 'bundle plugin install gem-skill' to enable 'bundle skill'."
|
|
87
95
|
end
|
|
88
96
|
|
|
89
97
|
def install_one(gem_name, spinner:, force:, model:)
|
|
90
98
|
spinner.auto_spin
|
|
91
|
-
|
|
92
99
|
version = resolve_installed_version(gem_name)
|
|
93
100
|
if version.nil?
|
|
94
101
|
spinner.update(title: "#{gem_name} (installing...)")
|
|
95
102
|
version = install_gem(gem_name)
|
|
96
103
|
end
|
|
97
|
-
|
|
98
|
-
if Gem::Skill::Cache.cached?(gem_name, version) && !force
|
|
99
|
-
spinner.update(title: "#{gem_name} #{version}")
|
|
100
|
-
spinner.success("already cached")
|
|
101
|
-
return
|
|
102
|
-
end
|
|
103
|
-
|
|
104
104
|
spinner.update(title: "#{gem_name} #{version}")
|
|
105
|
-
Gem::Skill::
|
|
106
|
-
|
|
107
|
-
rescue => e
|
|
108
|
-
spinner.error("
|
|
105
|
+
err = Gem::Skill::Runner.install_skill(gem_name, version, spinner, force: force, model: model)
|
|
106
|
+
alert_error "#{gem_name}: #{err}" if err
|
|
107
|
+
rescue Gem::Skill::Error => e
|
|
108
|
+
spinner.error("failed")
|
|
109
109
|
alert_error "#{gem_name}: #{e.message}"
|
|
110
110
|
end
|
|
111
111
|
|
|
@@ -127,6 +127,15 @@ class Gem::Commands::SkillCommand < Gem::Command
|
|
|
127
127
|
say "#{gems.size} gem(s), #{gems.sum { |n| Gem::Skill::Cache.versions(n).size }} version(s) total."
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
def cmd_setup
|
|
131
|
+
say "Registering gem-skill as a Bundler plugin..."
|
|
132
|
+
if system("bundle", "plugin", "install", "gem-skill")
|
|
133
|
+
say "Done. Use 'bundle skill install' in any project."
|
|
134
|
+
else
|
|
135
|
+
alert_error "Failed. Try running manually: bundle plugin install gem-skill"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
130
139
|
def cmd_purge
|
|
131
140
|
gem_name = options[:args].shift
|
|
132
141
|
unless gem_name
|
data/lib/gem/skill/generator.rb
CHANGED
|
@@ -21,7 +21,9 @@ module Gem::Skill
|
|
|
21
21
|
Output raw Markdown directly. Do NOT wrap the output in a code fence or any
|
|
22
22
|
other container — the file is Markdown, so no ```markdown wrapper.
|
|
23
23
|
|
|
24
|
-
Begin
|
|
24
|
+
Begin with a top-level heading identifying the gem and version, then use exactly these sections:
|
|
25
|
+
|
|
26
|
+
# %<gem_name>s v%<version>s
|
|
25
27
|
|
|
26
28
|
## Overview
|
|
27
29
|
One paragraph: what the gem does and when to reach for it.
|
data/lib/gem/skill/linker.rb
CHANGED
|
@@ -4,26 +4,28 @@ require "fileutils"
|
|
|
4
4
|
|
|
5
5
|
module Gem::Skill
|
|
6
6
|
# Manages .claude/skills/ symlinks in a project, pointing to ~/.gem/skills cache.
|
|
7
|
+
# Each symlink is a directory link: <gem_name> -> ~/.gem/skills/<gem>/<version>/
|
|
8
|
+
# Claude Code discovers skills by reading SKILL.md inside each linked directory.
|
|
7
9
|
module Linker
|
|
8
10
|
def self.skills_dir(project_root = Dir.pwd)
|
|
9
11
|
File.join(project_root, ".claude", "skills")
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def self.link(gem_name, version, project_root = Dir.pwd)
|
|
13
|
-
|
|
15
|
+
target_dir = File.dirname(Cache.skill_path(gem_name, version))
|
|
14
16
|
raise Error, "No cached skill for #{gem_name} #{version}. Run: gem skill install #{gem_name}" \
|
|
15
|
-
unless File.exist?(
|
|
17
|
+
unless File.exist?(Cache.skill_path(gem_name, version))
|
|
16
18
|
|
|
17
19
|
dir = skills_dir(project_root)
|
|
18
20
|
FileUtils.mkdir_p(dir)
|
|
19
21
|
|
|
20
|
-
link_path = File.join(dir,
|
|
22
|
+
link_path = File.join(dir, gem_name)
|
|
21
23
|
File.unlink(link_path) if File.symlink?(link_path)
|
|
22
|
-
File.symlink(
|
|
24
|
+
File.symlink(target_dir, link_path)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
def self.unlink(gem_name, project_root = Dir.pwd)
|
|
26
|
-
link_path = File.join(skills_dir(project_root),
|
|
28
|
+
link_path = File.join(skills_dir(project_root), gem_name)
|
|
27
29
|
File.unlink(link_path) if File.symlink?(link_path)
|
|
28
30
|
end
|
|
29
31
|
|
|
@@ -31,13 +33,14 @@ module Gem::Skill
|
|
|
31
33
|
dir = skills_dir(project_root)
|
|
32
34
|
return [] unless Dir.exist?(dir)
|
|
33
35
|
|
|
34
|
-
Dir.glob(File.join(dir, "
|
|
36
|
+
Dir.glob(File.join(dir, "*")).filter_map do |path|
|
|
35
37
|
next unless File.symlink?(path)
|
|
36
38
|
|
|
37
|
-
gem_name
|
|
38
|
-
|
|
39
|
-
version
|
|
40
|
-
|
|
39
|
+
gem_name = File.basename(path)
|
|
40
|
+
target_dir = File.readlink(path)
|
|
41
|
+
version = target_dir.match(%r{/([^/]+)$})&.captures&.first
|
|
42
|
+
skill_file = File.join(target_dir, "SKILL.md")
|
|
43
|
+
{ gem_name: gem_name, version: version, target: target_dir, valid: File.exist?(skill_file) }
|
|
41
44
|
end
|
|
42
45
|
end
|
|
43
46
|
|
data/lib/gem/skill/lockfile.rb
CHANGED
|
@@ -11,9 +11,9 @@ module Gem::Skill
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def self.parse(content)
|
|
14
|
-
specs
|
|
15
|
-
direct
|
|
16
|
-
direct.each_with_object({}) do |name, hash|
|
|
14
|
+
specs = parse_specs(content)
|
|
15
|
+
direct = parse_direct_names(content) + parse_path_dep_names(content)
|
|
16
|
+
direct.uniq.each_with_object({}) do |name, hash|
|
|
17
17
|
hash[name] = specs[name] if specs.key?(name)
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -36,6 +36,33 @@ module Gem::Skill
|
|
|
36
36
|
specs
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
def self.parse_path_dep_names(content)
|
|
40
|
+
in_path = false
|
|
41
|
+
in_specs = false
|
|
42
|
+
in_gem = false
|
|
43
|
+
names = []
|
|
44
|
+
|
|
45
|
+
content.each_line do |line|
|
|
46
|
+
if line.strip == "PATH"
|
|
47
|
+
in_path = true
|
|
48
|
+
in_specs = false
|
|
49
|
+
in_gem = false
|
|
50
|
+
elsif in_path && line.strip == "specs:"
|
|
51
|
+
in_specs = true
|
|
52
|
+
elsif in_path && in_specs && line =~ /\A {4}\S/
|
|
53
|
+
in_gem = true
|
|
54
|
+
elsif in_path && in_specs && in_gem && line =~ /\A {6}(\S+)/
|
|
55
|
+
names << Regexp.last_match(1)
|
|
56
|
+
elsif in_path && !line.start_with?(" ") && !line.strip.empty?
|
|
57
|
+
in_path = false
|
|
58
|
+
in_specs = false
|
|
59
|
+
in_gem = false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
names
|
|
64
|
+
end
|
|
65
|
+
|
|
39
66
|
def self.parse_direct_names(content)
|
|
40
67
|
in_deps = false
|
|
41
68
|
names = []
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gem::Skill
|
|
4
|
+
# Core install logic shared by gem_command and bundle_command.
|
|
5
|
+
# Callers are responsible for spinner.auto_spin and title setup before calling.
|
|
6
|
+
module Runner
|
|
7
|
+
# Generate + cache + link one skill.
|
|
8
|
+
# Returns nil on success, error message string on failure.
|
|
9
|
+
def self.install_skill(gem_name, version, spinner, force:, model:)
|
|
10
|
+
if Cache.cached?(gem_name, version) && !force
|
|
11
|
+
Linker.link(gem_name, version)
|
|
12
|
+
spinner.success("already cached")
|
|
13
|
+
return nil
|
|
14
|
+
end
|
|
15
|
+
Generator.new(gem_name, version, model: model).generate(force: force)
|
|
16
|
+
Linker.link(gem_name, version)
|
|
17
|
+
spinner.success("done")
|
|
18
|
+
nil
|
|
19
|
+
rescue => e
|
|
20
|
+
spinner.error("failed")
|
|
21
|
+
e.message
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/gem/skill/version.rb
CHANGED
data/lib/gem/skill.rb
CHANGED
data/scripts/e2e_test
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# End-to-end test:
|
|
5
|
-
# Usage: bundle exec ruby
|
|
4
|
+
# End-to-end test: full pipeline for one gem — fetch, generate, cache, link.
|
|
5
|
+
# Usage: bundle exec ruby scripts/e2e_test [GEM_NAME [VERSION [MODEL]]]
|
|
6
6
|
|
|
7
7
|
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
8
8
|
require "gem/skill"
|
|
9
|
-
require "
|
|
9
|
+
require "gem/skill/runner"
|
|
10
|
+
require "gem/skill/linker"
|
|
11
|
+
require "async"
|
|
12
|
+
require "tty-spinner"
|
|
13
|
+
require "tmpdir"
|
|
10
14
|
|
|
11
15
|
GEM_NAME = ARGV[0] || "zeitwerk"
|
|
12
16
|
VERSION = ARGV[1] || Gem::Specification.find_by_name(GEM_NAME)&.version&.to_s
|
|
13
|
-
MODEL = ARGV[2] ||
|
|
17
|
+
MODEL = ARGV[2] || Gem::Skill::Generator::DEFAULT_MODEL
|
|
14
18
|
|
|
15
19
|
abort "Gem '#{GEM_NAME}' not installed. Run: gem install #{GEM_NAME}" unless VERSION
|
|
16
20
|
|
|
@@ -29,43 +33,73 @@ puts ""
|
|
|
29
33
|
puts "[ FETCH ]"
|
|
30
34
|
fetcher = Gem::Skill::Fetcher.new(GEM_NAME, VERSION)
|
|
31
35
|
sources = fetcher.fetch_all
|
|
36
|
+
abort "No sources found — nothing to generate from." if sources.empty?
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
abort "No sources found — nothing to generate from."
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
sources.each do |name, content|
|
|
38
|
-
puts " %-12s %d chars" % [name, content.length]
|
|
39
|
-
end
|
|
38
|
+
sources.each { |name, content| puts " %-12s %d chars" % [name, content.length] }
|
|
40
39
|
puts ""
|
|
41
40
|
|
|
42
|
-
# --- Phase 2:
|
|
43
|
-
|
|
44
|
-
puts "[
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
# --- Phase 2: Runner.install_skill via async fiber ---
|
|
42
|
+
|
|
43
|
+
puts "[ RUNNER ] (via Async fiber + TTY spinner)"
|
|
44
|
+
multi = TTY::Spinner::Multi.new("[:spinner] Installing skill", format: :dots, output: $stderr)
|
|
45
|
+
sp = multi.register(" [:spinner] :title")
|
|
46
|
+
sp.update(title: "#{GEM_NAME} #{VERSION}")
|
|
47
|
+
|
|
48
|
+
err = nil
|
|
49
|
+
Async do
|
|
50
|
+
barrier = Async::Barrier.new
|
|
51
|
+
barrier.async do
|
|
52
|
+
sp.auto_spin
|
|
53
|
+
err = Gem::Skill::Runner.install_skill(GEM_NAME, VERSION, sp, force: true, model: MODEL)
|
|
54
|
+
end
|
|
55
|
+
barrier.wait
|
|
56
|
+
ensure
|
|
57
|
+
barrier.stop
|
|
52
58
|
end
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
if err
|
|
61
|
+
abort "\nRunner failed: #{err}"
|
|
62
|
+
end
|
|
56
63
|
puts ""
|
|
57
64
|
|
|
58
65
|
# --- Phase 3: Verify cache ---
|
|
59
66
|
|
|
60
67
|
puts "[ CACHE ]"
|
|
61
68
|
cached_path = Gem::Skill::Cache.skill_path(GEM_NAME, VERSION)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
abort " ERROR: cache file not found at #{cached_path}" unless File.exist?(cached_path)
|
|
70
|
+
|
|
71
|
+
metadata_path = Gem::Skill::Cache.metadata_path(GEM_NAME, VERSION)
|
|
72
|
+
puts " SKILL.md: #{cached_path} (#{File.size(cached_path)} bytes)"
|
|
73
|
+
puts " metadata: #{metadata_path} (#{File.exist?(metadata_path) ? "present" : "MISSING"})"
|
|
74
|
+
puts ""
|
|
75
|
+
|
|
76
|
+
# --- Phase 4: Linker ---
|
|
77
|
+
|
|
78
|
+
puts "[ LINKER ]"
|
|
79
|
+
project_dir = Dir.mktmpdir("gem-skill-e2e")
|
|
80
|
+
begin
|
|
81
|
+
Gem::Skill::Linker.link(GEM_NAME, VERSION, project_dir)
|
|
82
|
+
|
|
83
|
+
link_path = File.join(project_dir, ".claude", "skills", GEM_NAME)
|
|
84
|
+
skill_via_link = File.join(link_path, "SKILL.md")
|
|
85
|
+
|
|
86
|
+
abort " ERROR: symlink not created at #{link_path}" unless File.symlink?(link_path)
|
|
87
|
+
abort " ERROR: symlink does not resolve to a directory" unless File.directory?(link_path)
|
|
88
|
+
abort " ERROR: SKILL.md not accessible through symlink" unless File.exist?(skill_via_link)
|
|
89
|
+
|
|
90
|
+
puts " symlink: #{link_path}"
|
|
91
|
+
puts " -> target: #{File.readlink(link_path)}"
|
|
92
|
+
puts " SKILL.md accessible through symlink: yes"
|
|
93
|
+
|
|
94
|
+
entries = Gem::Skill::Linker.linked_gems(project_dir)
|
|
95
|
+
entry = entries.find { |e| e[:gem_name] == GEM_NAME }
|
|
96
|
+
abort " ERROR: linked_gems did not return #{GEM_NAME}" unless entry
|
|
97
|
+
puts " linked_gems reports: #{entry.inspect}"
|
|
98
|
+
ensure
|
|
99
|
+
FileUtils.rm_rf(project_dir)
|
|
68
100
|
end
|
|
69
101
|
|
|
70
102
|
puts ""
|
|
71
|
-
puts "
|
|
103
|
+
puts "=" * 60
|
|
104
|
+
puts "All phases passed."
|
|
105
|
+
puts "=" * 60
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gem-skill
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -10,21 +10,21 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: async
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '2.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '2.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: ruby_llm
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
@@ -62,6 +62,7 @@ executables: []
|
|
|
62
62
|
extensions: []
|
|
63
63
|
extra_rdoc_files: []
|
|
64
64
|
files:
|
|
65
|
+
- ".envrc"
|
|
65
66
|
- CHANGELOG.md
|
|
66
67
|
- LICENSE.txt
|
|
67
68
|
- README.md
|
|
@@ -74,6 +75,7 @@ files:
|
|
|
74
75
|
- lib/gem/skill/generator.rb
|
|
75
76
|
- lib/gem/skill/linker.rb
|
|
76
77
|
- lib/gem/skill/lockfile.rb
|
|
78
|
+
- lib/gem/skill/runner.rb
|
|
77
79
|
- lib/gem/skill/version.rb
|
|
78
80
|
- lib/rubygems_plugin.rb
|
|
79
81
|
- plugins.rb
|
|
@@ -86,6 +88,17 @@ metadata:
|
|
|
86
88
|
homepage_uri: https://github.com/madbomber/gem-skill
|
|
87
89
|
source_code_uri: https://github.com/madbomber/gem-skill
|
|
88
90
|
changelog_uri: https://github.com/madbomber/gem-skill/blob/master/CHANGELOG.md
|
|
91
|
+
post_install_message: |
|
|
92
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
93
|
+
gem-skill installed!
|
|
94
|
+
|
|
95
|
+
'gem skill install GEM_NAME' is ready to use.
|
|
96
|
+
|
|
97
|
+
To enable 'bundle skill' in your projects, run:
|
|
98
|
+
|
|
99
|
+
gem skill setup
|
|
100
|
+
|
|
101
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
89
102
|
rdoc_options: []
|
|
90
103
|
require_paths:
|
|
91
104
|
- lib
|