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.
- checksums.yaml +4 -4
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +28 -0
- data/README.md +35 -14
- data/docs/assets/images/gem-skill.jpg +0 -0
- data/docs/cache.md +108 -0
- data/docs/commands/bundle-skill.md +147 -0
- data/docs/commands/gem-skill.md +135 -0
- data/docs/configuration.md +75 -0
- data/docs/how-it-works.md +160 -0
- data/docs/index.md +69 -0
- data/docs/installation.md +62 -0
- data/docs/skill-files.md +78 -0
- data/lib/gem/skill/cli/bundle_command.rb +70 -61
- data/lib/gem/skill/cli/gem_command.rb +35 -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/mkdocs.yml +115 -0
- data/scripts/e2e_test +64 -30
- metadata +30 -6
|
@@ -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.
|
data/docs/skill-files.md
ADDED
|
@@ -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
|
|
39
|
-
model
|
|
45
|
+
force = opts[:force]
|
|
46
|
+
model = opts[:model] || Generator::DEFAULT_MODEL
|
|
40
47
|
errors = []
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
128
|
-
broken
|
|
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'
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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::
|
|
106
|
-
|
|
107
|
-
rescue => e
|
|
108
|
-
spinner.error("
|
|
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
|