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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e549225e7af0a6d96b302304225a274d5f5aa6074e8546c64034b3aba056d675
4
- data.tar.gz: f2791cd10d4e1483763a890bb864cb5169ab6fc1c5239ae697e104d70ec1d898
3
+ metadata.gz: e537a3925c494c903dc9dd105d6a975d55d5fe55f963847cca3c363ce26120d6
4
+ data.tar.gz: 7f4fd4cff99d062ab50bf73b733aeddca903f23ffb657ccc7cc7616dcd7e2fe6
5
5
  SHA512:
6
- metadata.gz: 7a75fec793180d52a07e7eb42c84d79eca1ba720aec44c33ec13f6783678ec96770f0bbe0ce4789ecdfdafc6c7df882c392fcea9e48f55fb99d47877dff0b7b6
7
- data.tar.gz: d605f135da72c0bcdf5cb5f85c42b1f40bc5d13f46330264fd420401faa43dc67503949592c3dd15cfba0b62115b8a14a97d1850072a32309ad10a1833cbfb9c
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.md → ~/.gem/skills/chunker-ruby/1.2.3/SKILL.md
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
- This gives you the `gem skill` subcommand.
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
- For `bundle skill` support (project-aware, reads `Gemfile.lock`), also run:
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 = opts[:force]
39
- model = opts[:model] || Generator::DEFAULT_MODEL
40
+ force = opts[:force]
41
+ model = opts[:model] || Generator::DEFAULT_MODEL
40
42
  errors = []
41
43
 
42
- puts "Installing skills for #{gems.size} gem(s) (model: #{model})..."
43
- puts ""
44
-
45
- gems.each do |gem_name, version|
46
- if Cache.cached?(gem_name, version) && !force
47
- print " skip #{gem_name} #{version}"
48
- else
49
- print " gen #{gem_name} #{version} "
50
- $stdout.flush
51
- Generator.new(gem_name, version, model: model).generate(force: force) do |chunk|
52
- print chunk
53
- $stdout.flush
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
- Linker.link(gem_name, version)
58
- puts " ✓"
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
- puts "Refreshing skills (model: #{model})..."
84
- puts ""
85
-
86
- gems.each do |gem_name, version|
87
- if !force && linked[gem_name] == version
88
- puts " ok #{gem_name} #{version}"
89
- next
90
- end
91
-
92
- action = linked.key?(gem_name) ? "update" : "new"
93
- print " #{action.ljust(6)}#{gem_name} #{version} "
94
- $stdout.flush
95
-
96
- Generator.new(gem_name, version, model: model).generate(force: force) do |chunk|
97
- print chunk
98
- $stdout.flush
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
- Linker.link(gem_name, version)
102
- puts " ✓"
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 = entries.count { |e| e[:valid] }
128
- broken = entries.size - ok
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' (after: bundle plugin install gem-skill)
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
- threads = gem_names.map do |gem_name|
80
- spinner = multi.register(" [:spinner] :title")
81
- spinner.update(title: gem_name)
82
- Thread.new(gem_name, spinner) { |name, sp| install_one(name, spinner: sp, force: force, model: model) }
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::Generator.new(gem_name, version, model: model).generate(force: force)
106
- spinner.success("done")
107
- rescue => e
108
- spinner.error("#{gem_name} failed")
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
@@ -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 immediately with the first section heading. Use exactly these sections:
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.
@@ -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
- target = Cache.skill_path(gem_name, version)
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?(target)
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, "#{gem_name}.md")
22
+ link_path = File.join(dir, gem_name)
21
23
  File.unlink(link_path) if File.symlink?(link_path)
22
- File.symlink(target, link_path)
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), "#{gem_name}.md")
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, "*.md")).filter_map do |path|
36
+ Dir.glob(File.join(dir, "*")).filter_map do |path|
35
37
  next unless File.symlink?(path)
36
38
 
37
- gem_name = File.basename(path, ".md")
38
- target = File.readlink(path)
39
- version = target.match(%r{/([^/]+)/SKILL\.md$})&.captures&.first
40
- { gem_name: gem_name, version: version, target: target, valid: File.exist?(target) }
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
 
@@ -11,9 +11,9 @@ module Gem::Skill
11
11
  end
12
12
 
13
13
  def self.parse(content)
14
- specs = parse_specs(content)
15
- direct = parse_direct_names(content)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gem::Skill
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/gem/skill.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "skill/fetcher"
6
6
  require_relative "skill/generator"
7
7
  require_relative "skill/linker"
8
8
  require_relative "skill/lockfile"
9
+ require_relative "skill/runner"
9
10
 
10
11
  module Gem::Skill
11
12
  class Error < StandardError; end
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: fetch docs for a gem and generate a SKILL.md
5
- # Usage: bundle exec ruby bin/e2e_test [GEM_NAME [VERSION]]
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 "ruby_llm"
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] || "gpt-4o-mini"
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
- if sources.empty?
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: Generate ---
43
-
44
- puts "[ GENERATE ] streaming output below"
45
- puts "-" * 60
46
-
47
- skill_content = ""
48
- generator = Gem::Skill::Generator.new(GEM_NAME, VERSION, model: MODEL)
49
- skill_content = generator.generate(force: true) do |chunk|
50
- print chunk
51
- $stdout.flush
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
- puts ""
55
- puts "-" * 60
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
- if File.exist?(cached_path)
63
- puts " Written: #{cached_path}"
64
- puts " Size: #{File.size(cached_path)} bytes"
65
- else
66
- puts " ERROR: cache file not found at #{cached_path}"
67
- exit 1
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 "Done."
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.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: ruby_llm
13
+ name: async
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '1.0'
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: '1.0'
25
+ version: '2.0'
26
26
  - !ruby/object:Gem::Dependency
27
- name: thor
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