gem-skill 0.1.1 → 0.1.3

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.
@@ -0,0 +1,160 @@
1
+ # How It Works
2
+
3
+ gem-skill is built as a pipeline of independent modules. Each has a single
4
+ responsibility and can be used or tested in isolation.
5
+
6
+ ## Pipeline overview
7
+
8
+ ```
9
+ Gemfile.lock / gem name
10
+ ↓
11
+ Lockfile parse direct deps + gemspec deps
12
+ ↓
13
+ Fetcher collect documentation from multiple sources
14
+ ↓
15
+ Generator call LLM, produce SKILL.md content
16
+ ↓
17
+ Cache write to ~/.gem/skills/<gem>/<version>/
18
+ ↓
19
+ Linker symlink .claude/skills/<gem> → cache dir
20
+ ```
21
+
22
+ `Runner.install_skill` is the glue that drives steps 2–5 for a single gem.
23
+ The CLI commands (`gem skill`, `bundle skill`) fan it out concurrently across
24
+ multiple gems using async fibers.
25
+
26
+ ---
27
+
28
+ ## Modules
29
+
30
+ ### Lockfile
31
+
32
+ `lib/gem/skill/lockfile.rb`
33
+
34
+ Parses `Gemfile.lock` to produce a `{ gem_name => version }` hash of the gems
35
+ to process. Reads from two sections:
36
+
37
+ - **`DEPENDENCIES`** — direct deps listed in `Gemfile`
38
+ - **`PATH` → `specs:`** — runtime deps from any `gemspec` referenced by `gemspec` in `Gemfile`
39
+
40
+ Versions are resolved from the `GEM → specs:` section, which contains the full
41
+ lockfile-resolved version for every gem.
42
+
43
+ ### Fetcher
44
+
45
+ `lib/gem/skill/fetcher.rb`
46
+
47
+ Collects documentation from up to three sources, tried in priority order:
48
+
49
+ 1. **Local gem install** — reads `README` and `CHANGELOG` from the gem's install directory via `Gem::Specification`
50
+ 2. **RubyGems API** — fetches summary, runtime dependencies, source URI
51
+ 3. **GitHub raw README** — fetched when the gem is not installed locally; tries `main` then `master` branches, and four common README filename variants
52
+
53
+ Content is truncated at 60,000 characters per source to avoid blowing the LLM
54
+ context window.
55
+
56
+ ### Generator
57
+
58
+ `lib/gem/skill/generator.rb`
59
+
60
+ Calls the LLM via `ruby_llm`. Constructs a detailed prompt instructing the
61
+ model to produce a structured `SKILL.md` covering:
62
+
63
+ - Overview, Installation, Core API, Common Patterns, Gotchas, Configuration, Testing
64
+
65
+ Supports both streaming (live output) and non-streaming modes. Strips any
66
+ markdown code fence wrapper the model adds despite being told not to.
67
+
68
+ The model is configurable via `GEMSKILL_MODEL` or `--model`.
69
+
70
+ ### Cache
71
+
72
+ `lib/gem/skill/cache.rb`
73
+
74
+ Manages the global skill cache. Structure:
75
+
76
+ ```
77
+ ~/.gem/skills/ (GEMSKILL_DIR)
78
+ └── <gem_name>/
79
+ └── <version>/
80
+ ├── SKILL.md
81
+ └── metadata.json (gem, version, model, generated_at, sources)
82
+ ```
83
+
84
+ `Cache::ROOT` is set once at load time from `GEMSKILL_DIR` (default: `~/.gem/skills`).
85
+
86
+ ### Linker
87
+
88
+ `lib/gem/skill/linker.rb`
89
+
90
+ Creates and manages directory symlinks in `.claude/skills/` inside a project:
91
+
92
+ ```
93
+ .claude/skills/<gem_name> → ~/.gem/skills/<gem_name>/<version>/
94
+ ```
95
+
96
+ Symlinks point to the **version directory**, not directly to `SKILL.md`.
97
+ Claude Code discovers `SKILL.md` by reading inside the linked directory.
98
+
99
+ `Linker.prune_dead_links` removes any symlink whose target no longer exists
100
+ in the cache (e.g. after `gem skill purge`).
101
+
102
+ ### Runner
103
+
104
+ `lib/gem/skill/runner.rb`
105
+
106
+ Shared core used by both CLI commands. Drives one gem through the
107
+ cache-check → generate → link sequence:
108
+
109
+ ```ruby
110
+ Runner.install_skill(gem_name, version, spinner, force:, model:)
111
+ # Returns nil on success, error message string on failure
112
+ ```
113
+
114
+ Returns the error message rather than raising, so the caller (the concurrent
115
+ fiber) can record it without killing other in-flight fibers.
116
+
117
+ ---
118
+
119
+ ## Concurrency
120
+
121
+ Both CLI commands use the `async` gem with `Async::Barrier`:
122
+
123
+ ```ruby
124
+ Async do
125
+ barrier = Async::Barrier.new
126
+ gems.each do |gem_name, version|
127
+ barrier.async { Runner.install_skill(...) }
128
+ end
129
+ barrier.wait
130
+ ensure
131
+ barrier.stop
132
+ end
133
+ ```
134
+
135
+ Each gem gets its own fiber. Fibers yield to the event loop during network I/O
136
+ (HTTP fetches, LLM API calls), so all gems make progress concurrently on a
137
+ single thread. This is more memory-efficient than one thread per gem.
138
+
139
+ ---
140
+
141
+ ## Plugin architecture
142
+
143
+ gem-skill registers itself in two ways:
144
+
145
+ ### RubyGems plugin (`lib/rubygems_plugin.rb`)
146
+
147
+ Auto-loaded by RubyGems on every `gem` command via the `rubygems_plugin`
148
+ naming convention. Prepends `Gem::Skill::InstallSkillOption` onto
149
+ `Gem::Commands::InstallCommand` to add the `--with-skill` flag.
150
+
151
+ After all gem installs complete, `Gem.post_install` collects gem names/versions
152
+ into a pending list, and `at_exit` fires `generate_pending_skills` to process
153
+ them concurrently.
154
+
155
+ ### Bundler plugin (`plugins.rb`)
156
+
157
+ Registered via `bundle plugin install gem-skill` (or `gem skill setup`).
158
+ Bundler's plugin API loads `plugins.rb` and discovers the
159
+ `Gem::Skill::BundlerPlugin` class, which routes `bundle skill SUBCOMMAND` to
160
+ `Gem::Skill::BundlerCommand`.
data/docs/index.md ADDED
@@ -0,0 +1,69 @@
1
+ <p align="center">
2
+ <table style="width:38%;margin:0 auto;" border="0" cellpadding="8">
3
+ <tr>
4
+ <td width="40%"><img src="assets/images/gem-skill.jpg" alt="gem-skill logo" style="width:100%;display:block;"></td>
5
+ <td width="60%">
6
+ Generate Claude Code skill files from Ruby gem
7
+ documentation and caches them globally so every project that uses a gem
8
+ can share the same pre-built knowledge.<br><br>
9
+ <strong><a href="https://madbomber.github.io/gem-skill">Full documentation →</a></strong>
10
+ </td>
11
+ </tr>
12
+ </table>
13
+ </p>
14
+
15
+ ## The problem it solves
16
+
17
+ Every time Claude Code encounters a gem it hasn't seen in the current context,
18
+ it re-reads the README, scans examples, and figures out the API. That costs
19
+ tokens and time — and the result evaporates when the conversation ends.
20
+
21
+ `gem-skill` runs that pipeline once, offline, and stores the output as a
22
+ `SKILL.md` in `~/.gem/skills`. Projects symlink to the cached version, so
23
+ Claude has accurate, version-specific knowledge about each gem without
24
+ repeating the ingestion work.
25
+
26
+ ## Quick start
27
+
28
+ ```bash
29
+ # 1. Install
30
+ gem install gem-skill
31
+
32
+ # 2. Register the Bundler plugin (once per machine)
33
+ gem skill setup
34
+
35
+ # 3. Generate a skill for any installed gem
36
+ gem skill install debug_me
37
+
38
+ # 4. In a project — generate skills for all direct dependencies
39
+ cd your-project
40
+ bundle skill install
41
+ ```
42
+
43
+ ## How it works
44
+
45
+ ```
46
+ gem README / changelog / RubyGems API
47
+ ↓
48
+ Fetcher collects docs
49
+ ↓
50
+ Generator calls LLM (ruby_llm)
51
+ ↓
52
+ SKILL.md cached at ~/.gem/skills/<gem>/<version>/
53
+ ↓
54
+ Linker creates .claude/skills/<gem> → cache dir
55
+ ↓
56
+ Claude Code reads SKILL.md automatically
57
+ ```
58
+
59
+ All concurrent work is handled by async fibers — multiple gems are processed
60
+ simultaneously with live TTY spinner progress.
61
+
62
+ ## Key features
63
+
64
+ - **Global cache** — generate once, use everywhere; skills are version-specific
65
+ - **Gemfile.lock awareness** — `bundle skill install` installs skills for every direct dependency including gemspec runtime deps
66
+ - **Concurrent** — all LLM calls run concurrently via async fibers
67
+ - **Two interfaces** — `gem skill` for global cache management, `bundle skill` for project-aware linking
68
+ - **Auto-install** — `gem install --with-skill` generates skills during normal gem installation
69
+ - **Configurable** — `GEMSKILL_DIR` and `GEMSKILL_MODEL` environment variables
@@ -0,0 +1,62 @@
1
+ # Installation
2
+
3
+ ## Requirements
4
+
5
+ - Ruby >= 3.2.0
6
+ - At least one LLM provider API key (see [Configuration](configuration.md))
7
+
8
+ ## Install the gem
9
+
10
+ ```bash
11
+ gem install gem-skill
12
+ ```
13
+
14
+ This makes the `gem skill` subcommand available immediately.
15
+
16
+ ## Register the Bundler plugin
17
+
18
+ To enable `bundle skill` in your projects, run once after installation:
19
+
20
+ ```bash
21
+ gem skill setup
22
+ ```
23
+
24
+ This registers gem-skill as a Bundler plugin globally. You only need to do this
25
+ once per machine.
26
+
27
+ !!! tip "Alternative: per-project plugin"
28
+ You can also add the plugin to a specific project's `Gemfile`:
29
+ ```ruby
30
+ plugin "gem-skill"
31
+ ```
32
+ This installs the plugin for that project only.
33
+
34
+ ## Verify installation
35
+
36
+ ```bash
37
+ # Check gem skill is available
38
+ gem skill
39
+
40
+ # Check bundle skill is available (after gem skill setup)
41
+ bundle skill
42
+ ```
43
+
44
+ ## Upgrading
45
+
46
+ ```bash
47
+ gem update gem-skill
48
+ gem skill setup # re-register the Bundler plugin with the new version
49
+ ```
50
+
51
+ ## Development installation
52
+
53
+ To run from source without building and releasing a gem:
54
+
55
+ ```bash
56
+ git clone https://github.com/madbomber/gem-skill
57
+ cd gem-skill
58
+ bundle install
59
+ bin/dev_install # points both Bundler plugin indexes at the source tree
60
+ ```
61
+
62
+ `bin/dev_install --reset` restores the indexes to the last released gem.
@@ -0,0 +1,78 @@
1
+ # Skill Files
2
+
3
+ A `SKILL.md` is a structured Markdown document that gives Claude Code deep,
4
+ practical knowledge about a Ruby gem. Claude reads it automatically when it is
5
+ present in `.claude/skills/`.
6
+
7
+ ## Format
8
+
9
+ Every generated skill starts with a top-level heading identifying the gem and
10
+ version, then covers seven sections:
11
+
12
+ ```markdown
13
+ # faraday v2.14.3
14
+
15
+ ## Overview
16
+ What the gem does and when to reach for it.
17
+
18
+ ## Installation
19
+ Exact Gemfile/gemspec lines and any required post-install steps.
20
+
21
+ ## Core API
22
+ Key classes, methods, and options with real method signatures and return values.
23
+
24
+ ## Common Patterns
25
+ The 3–5 most frequent real-world usage patterns with working code examples.
26
+
27
+ ## Gotchas & Edge Cases
28
+ Surprising defaults, version-specific behavior, thread safety, encoding issues.
29
+
30
+ ## Configuration
31
+ Initializer patterns, environment variables, defaults worth knowing.
32
+
33
+ ## Testing
34
+ How to test code that uses this gem: mocks, fakes, fixtures, VCR patterns.
35
+ ```
36
+
37
+ ## What Claude does with it
38
+
39
+ When Claude Code opens a project containing `.claude/skills/`, it reads every
40
+ `SKILL.md` it finds. This means:
41
+
42
+ - Claude knows the correct API for the exact version you're using
43
+ - No token cost re-deriving usage from READMEs mid-conversation
44
+ - The knowledge persists across conversation turns
45
+ - Multiple gems can be in scope simultaneously
46
+
47
+ ## Sources used to generate
48
+
49
+ The LLM is given up to three sources per gem (in priority order):
50
+
51
+ 1. **Local README + CHANGELOG** — from the gem's install directory
52
+ 2. **RubyGems API** — summary, dependencies, source URI
53
+ 3. **GitHub raw README** — fetched when not installed locally
54
+
55
+ Content is synthesized, not copied verbatim. The model is instructed to write
56
+ as a knowledgeable colleague, not a marketing document.
57
+
58
+ ## Quality and regeneration
59
+
60
+ Skill quality depends on the documentation available for the gem and the model
61
+ used. For gems with poor upstream documentation, results will reflect that.
62
+
63
+ To improve a skill:
64
+
65
+ ```bash
66
+ # Use a more capable model
67
+ gem skill install my_gem --force --model claude-opus-4-8
68
+
69
+ # Or set it as the default
70
+ export GEMSKILL_MODEL="claude-opus-4-8"
71
+ gem skill install my_gem --force
72
+ ```
73
+
74
+ ## Version specificity
75
+
76
+ Skills are cached per version. `faraday 2.12.0` and `faraday 2.14.3` each get
77
+ their own `SKILL.md`. Symlinks in `.claude/skills/` point to the version
78
+ matching your `Gemfile.lock`, so Claude always has the right version context.
@@ -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
@@ -15,6 +17,11 @@ module Gem::Skill
15
17
  opts, rest = parse_options(args)
16
18
  subcmd = rest.shift
17
19
 
20
+ if opts[:version]
21
+ puts Gem::Skill::VERSION
22
+ return
23
+ end
24
+
18
25
  case subcmd
19
26
  when "install" then install(opts)
20
27
  when "refresh" then refresh(opts)
@@ -35,42 +42,33 @@ module Gem::Skill
35
42
  return
36
43
  end
37
44
 
38
- force = opts[:force]
39
- model = opts[:model] || Generator::DEFAULT_MODEL
45
+ force = opts[:force]
46
+ model = opts[:model] || Generator::DEFAULT_MODEL
40
47
  errors = []
41
48
 
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
49
+ multi = TTY::Spinner::Multi.new(
50
+ "[:spinner] Installing skills (#{model})",
51
+ format: :dots,
52
+ output: $stderr
53
+ )
54
+
55
+ Async do
56
+ barrier = Async::Barrier.new
57
+ gems.each do |gem_name, version|
58
+ sp = multi.register(" [:spinner] :title")
59
+ sp.update(title: "#{gem_name} #{version}")
60
+ barrier.async do
61
+ err = install_one(gem_name, version, sp, force: force, model: model)
62
+ errors << "#{gem_name} #{version}: #{err}" if err
54
63
  end
55
64
  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}"
65
+ barrier.wait
66
+ ensure
67
+ barrier.stop
62
68
  end
63
69
 
64
70
  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
71
+ report_errors(errors)
74
72
  end
75
73
 
76
74
  def self.refresh(opts = {})
@@ -80,40 +78,35 @@ module Gem::Skill
80
78
  model = opts[:model] || Generator::DEFAULT_MODEL
81
79
  errors = []
82
80
 
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
81
+ multi = TTY::Spinner::Multi.new(
82
+ "[:spinner] Refreshing skills (#{model})",
83
+ format: :dots,
84
+ output: $stderr
85
+ )
86
+
87
+ Async do
88
+ barrier = Async::Barrier.new
89
+ gems.each do |gem_name, version|
90
+ sp = multi.register(" [:spinner] :title")
91
+ sp.update(title: "#{gem_name} #{version}")
92
+ barrier.async do
93
+ err = if !force && linked[gem_name] == version
94
+ sp.auto_spin
95
+ sp.success("up to date")
96
+ nil
97
+ else
98
+ install_one(gem_name, version, sp, force: force, model: model)
99
+ end
100
+ errors << "#{gem_name} #{version}: #{err}" if err
101
+ end
99
102
  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}"
103
+ barrier.wait
104
+ ensure
105
+ barrier.stop
106
106
  end
107
107
 
108
108
  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
109
+ report_errors(errors)
117
110
  end
118
111
 
119
112
  def self.list
@@ -124,8 +117,8 @@ module Gem::Skill
124
117
  return
125
118
  end
126
119
 
127
- ok = entries.count { |e| e[:valid] }
128
- broken = entries.size - ok
120
+ ok = entries.count { |e| e[:valid] }
121
+ broken = entries.size - ok
129
122
 
130
123
  puts "Skills linked in .claude/skills/ (#{ok} ok#{broken > 0 ? ", #{broken} broken" : ""}):"
131
124
  puts ""
@@ -137,6 +130,20 @@ module Gem::Skill
137
130
 
138
131
  # --- private ---
139
132
 
133
+ def self.install_one(gem_name, version, spinner, force:, model:)
134
+ spinner.auto_spin
135
+ Runner.install_skill(gem_name, version, spinner, force: force, model: model)
136
+ end
137
+ private_class_method :install_one
138
+
139
+ def self.report_errors(errors)
140
+ return if errors.empty?
141
+ warn ""
142
+ warn "Errors (#{errors.size}):"
143
+ errors.each { |e| warn " #{e}" }
144
+ end
145
+ private_class_method :report_errors
146
+
140
147
  def self.parse_options(args)
141
148
  opts = {}
142
149
  remaining = []
@@ -144,6 +151,7 @@ module Gem::Skill
144
151
  args.each do |arg|
145
152
  case arg
146
153
  when "--force" then opts[:force] = true
154
+ when "--version", "-v" then opts[:version] = true
147
155
  when /\A--model(?:=(.+))?\z/
148
156
  opts[:model] = $1 || args[args.index(arg) + 1]
149
157
  else
@@ -168,6 +176,7 @@ module Gem::Skill
168
176
  Options:
169
177
  --force Regenerate even if already cached
170
178
  --model MODEL LLM model to use (default: #{Generator::DEFAULT_MODEL})
179
+ --version, -v Print gem-skill version and exit
171
180
  USAGE
172
181
  end
173
182
  private_class_method :usage
@@ -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"
@@ -17,17 +18,19 @@ class Gem::Commands::SkillCommand < Gem::Command
17
18
  add_option("-m", "--model MODEL", "LLM model to use (default: #{Gem::Skill::Generator::DEFAULT_MODEL})") do |model, o|
18
19
  o[:model] = model
19
20
  end
21
+ add_option("-v", "--version", "Print gem-skill version and exit") { |_, o| o[:version] = true }
20
22
  end
21
23
 
22
24
  def arguments
23
- "SUBCOMMAND one of: install, list, purge"
25
+ "SUBCOMMAND one of: install, list, purge, setup"
24
26
  end
25
27
 
26
28
  def usage
27
29
  "#{program_name} install GEM_NAME [GEM_NAME ...]\n" \
28
30
  " #{program_name} list\n" \
29
31
  " #{program_name} purge GEM_NAME VERSION\n" \
30
- " #{program_name} purge GEM_NAME --all"
32
+ " #{program_name} purge GEM_NAME --all\n" \
33
+ " #{program_name} setup"
31
34
  end
32
35
 
33
36
  def description
@@ -35,19 +38,24 @@ class Gem::Commands::SkillCommand < Gem::Command
35
38
  install Generate and cache a SKILL.md for a gem.
36
39
  list Show all skills in the global cache (~/.gem/skills).
37
40
  purge Remove a specific cached version.
41
+ setup Register gem-skill as a Bundler plugin (run once after install).
38
42
 
39
- Use 'bundle skill install' (after: bundle plugin install gem-skill)
40
- to generate and link skills for an entire project from Gemfile.lock.
43
+ Use 'bundle skill install' in any project after running 'gem skill setup'.
41
44
  DESC
42
45
  end
43
46
 
44
47
  def execute
48
+ if options[:version]
49
+ say Gem::Skill::VERSION
50
+ return
51
+ end
45
52
  Gem::Skill.configure_llm!
46
53
  subcmd = options[:args].shift
47
54
  case subcmd
48
55
  when "install" then cmd_install
49
56
  when "list" then cmd_list
50
57
  when "purge" then cmd_purge
58
+ when "setup" then cmd_setup
51
59
  when nil
52
60
  say usage
53
61
  else
@@ -76,36 +84,33 @@ class Gem::Commands::SkillCommand < Gem::Command
76
84
  output: $stderr
77
85
  )
78
86
 
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) }
87
+ Async do
88
+ barrier = Async::Barrier.new
89
+ gem_names.each do |gem_name|
90
+ spinner = multi.register(" [:spinner] :title")
91
+ spinner.update(title: gem_name)
92
+ barrier.async { install_one(gem_name, spinner: spinner, force: force, model: model) }
93
+ end
94
+ barrier.wait
95
+ ensure
96
+ barrier.stop
83
97
  end
84
- threads.each(&:join)
85
98
 
86
99
  say "Tip: run 'bundle plugin install gem-skill' to enable 'bundle skill'."
87
100
  end
88
101
 
89
102
  def install_one(gem_name, spinner:, force:, model:)
90
103
  spinner.auto_spin
91
-
92
104
  version = resolve_installed_version(gem_name)
93
105
  if version.nil?
94
106
  spinner.update(title: "#{gem_name} (installing...)")
95
107
  version = install_gem(gem_name)
96
108
  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
109
  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")
110
+ err = Gem::Skill::Runner.install_skill(gem_name, version, spinner, force: force, model: model)
111
+ alert_error "#{gem_name}: #{err}" if err
112
+ rescue Gem::Skill::Error => e
113
+ spinner.error("failed")
109
114
  alert_error "#{gem_name}: #{e.message}"
110
115
  end
111
116
 
@@ -127,6 +132,15 @@ class Gem::Commands::SkillCommand < Gem::Command
127
132
  say "#{gems.size} gem(s), #{gems.sum { |n| Gem::Skill::Cache.versions(n).size }} version(s) total."
128
133
  end
129
134
 
135
+ def cmd_setup
136
+ say "Registering gem-skill as a Bundler plugin..."
137
+ if system("bundle", "plugin", "install", "gem-skill")
138
+ say "Done. Use 'bundle skill install' in any project."
139
+ else
140
+ alert_error "Failed. Try running manually: bundle plugin install gem-skill"
141
+ end
142
+ end
143
+
130
144
  def cmd_purge
131
145
  gem_name = options[:args].shift
132
146
  unless gem_name