gem-skill 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module Gem::Skill
8
+ # Fetches documentation for a gem from multiple sources, in priority order:
9
+ # 1. Local gem installation (Gem::Specification) — metadata + README + CHANGELOG + examples
10
+ # 2. RubyGems API — metadata only, when gem isn't installed locally
11
+ # 3. GitHub raw README — when gem isn't installed locally
12
+ class Fetcher
13
+ RUBYGEMS_API = "https://rubygems.org/api/v1/gems/%s.json"
14
+ GITHUB_RAW = "https://raw.githubusercontent.com/%s/%s/%s"
15
+ MAX_REDIRECTS = 5
16
+ OPEN_TIMEOUT = 5
17
+ READ_TIMEOUT = 10
18
+
19
+ README_CANDIDATES = %w[README.md README.rdoc README.txt README].freeze
20
+ CHANGELOG_CANDIDATES = %w[CHANGELOG.md CHANGELOG.rdoc HISTORY.md CHANGES.md].freeze
21
+
22
+ attr_reader :gem_name, :version
23
+
24
+ def initialize(gem_name, version)
25
+ @gem_name = gem_name
26
+ @version = version
27
+ end
28
+
29
+ # Returns a hash of source_name => content strings (only populated keys).
30
+ def fetch_all
31
+ {
32
+ metadata: metadata,
33
+ readme: readme,
34
+ changelog: changelog,
35
+ examples: examples
36
+ }.compact
37
+ end
38
+
39
+ def metadata
40
+ @metadata ||= local_metadata || rubygems_metadata
41
+ end
42
+
43
+ def readme
44
+ @readme ||= local_file(*README_CANDIDATES) || github_readme
45
+ end
46
+
47
+ def changelog
48
+ @changelog ||= local_file(*CHANGELOG_CANDIDATES)
49
+ end
50
+
51
+ def examples
52
+ @examples ||= local_examples
53
+ end
54
+
55
+ private
56
+
57
+ # --- local gem spec ---
58
+
59
+ def gem_spec
60
+ @gem_spec ||= Gem::Specification.find_by_name(gem_name, version)
61
+ rescue Gem::MissingSpecError, Gem::MissingSpecVersionError
62
+ nil
63
+ end
64
+
65
+ def gem_dir
66
+ @gem_dir ||= gem_spec&.gem_dir || locate_gem_dir
67
+ end
68
+
69
+ def locate_gem_dir
70
+ dir_name = "#{gem_name}-#{version}"
71
+ Gem.path.each do |base|
72
+ path = File.join(base, "gems", dir_name)
73
+ return path if File.directory?(path)
74
+ end
75
+ nil
76
+ end
77
+
78
+ def local_file(*candidates)
79
+ dir = gem_dir
80
+ return nil unless dir
81
+
82
+ candidates.each do |name|
83
+ path = File.join(dir, name)
84
+ return File.read(path, encoding: "utf-8") if File.exist?(path)
85
+ end
86
+ nil
87
+ end
88
+
89
+ def local_metadata
90
+ return nil unless gem_spec
91
+
92
+ spec = gem_spec
93
+ lines = []
94
+ lines << "**Gem:** #{spec.name} #{spec.version}"
95
+ lines << "**Summary:** #{spec.summary}" if spec.summary.to_s.strip.length > 0
96
+ lines << "**Description:** #{spec.description}" if spec.description.to_s.strip.length > 0
97
+ lines << "**Author(s):** #{spec.authors.join(', ')}" if spec.authors.any?
98
+ lines << "**Homepage:** #{spec.homepage}" if spec.homepage.to_s.strip.length > 0
99
+ lines << "**License(s):** #{spec.licenses.join(', ')}" if spec.licenses.any?
100
+ lines << "**Source:** #{spec.metadata['source_code_uri']}" if spec.metadata["source_code_uri"]
101
+ lines << "**Documentation:** #{spec.metadata['documentation_uri']}" if spec.metadata["documentation_uri"]
102
+
103
+ runtime_deps = spec.runtime_dependencies
104
+ if runtime_deps.any?
105
+ dep_list = runtime_deps.map { |d| "#{d.name} (#{d.requirement})" }.join(", ")
106
+ lines << "**Runtime dependencies:** #{dep_list}"
107
+ end
108
+
109
+ lines.join("\n")
110
+ end
111
+
112
+ def local_examples
113
+ dir = gem_dir
114
+ return nil unless dir
115
+
116
+ examples_dir = File.join(dir, "examples")
117
+ return nil unless File.directory?(examples_dir)
118
+
119
+ files = Dir.glob(File.join(examples_dir, "**", "*.{rb,md}")).sort
120
+ return nil if files.empty?
121
+
122
+ files.map do |path|
123
+ relative = path.delete_prefix("#{examples_dir}/")
124
+ content = File.read(path, encoding: "utf-8")
125
+ "### #{relative}\n\n```\n#{content.strip}\n```"
126
+ end.join("\n\n")
127
+ rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
128
+ nil
129
+ end
130
+
131
+ # --- RubyGems API ---
132
+
133
+ def rubygems_data
134
+ @rubygems_data ||= begin
135
+ body = fetch_url(format(RUBYGEMS_API, gem_name))
136
+ body ? JSON.parse(body) : nil
137
+ rescue JSON::ParserError
138
+ nil
139
+ end
140
+ end
141
+
142
+ def rubygems_metadata
143
+ data = rubygems_data
144
+ return nil unless data
145
+
146
+ lines = []
147
+ lines << "**Gem:** #{data['name']} #{data['version']}"
148
+ lines << "**Summary:** #{data['info']}" if data["info"].to_s.strip.length > 0
149
+ lines << "**Homepage:** #{data['homepage_uri']}" if data["homepage_uri"]
150
+ lines << "**Source:** #{data['source_code_uri']}" if data["source_code_uri"]
151
+ lines << "**Documentation:** #{data['documentation_uri']}" if data["documentation_uri"]
152
+
153
+ runtime_deps = data.dig("dependencies", "runtime") || []
154
+ if runtime_deps.any?
155
+ dep_list = runtime_deps.map { |d| "#{d['name']} (#{d['requirements']})" }.join(", ")
156
+ lines << "**Runtime dependencies:** #{dep_list}"
157
+ end
158
+
159
+ lines.join("\n")
160
+ end
161
+
162
+ # --- GitHub raw README ---
163
+
164
+ def github_readme
165
+ repo = github_repo
166
+ return nil unless repo
167
+
168
+ README_CANDIDATES.each do |filename|
169
+ %w[main master].each do |branch|
170
+ url = format(GITHUB_RAW, repo, branch, filename)
171
+ content = fetch_url(url)
172
+ return content if content
173
+ end
174
+ end
175
+ nil
176
+ end
177
+
178
+ def github_repo
179
+ data = rubygems_data
180
+ return nil unless data
181
+
182
+ candidate_uris = [data["source_code_uri"], data["homepage_uri"]].compact
183
+ candidate_uris.each do |uri|
184
+ match = uri.match(%r{github\.com[/:](?<owner>[^/]+)/(?<repo>[^/.\s]+?)(?:\.git)?(?:/|$)})
185
+ return "#{match[:owner]}/#{match[:repo]}" if match
186
+ end
187
+ nil
188
+ end
189
+
190
+ # --- HTTP ---
191
+
192
+ def fetch_url(url, redirects_left: MAX_REDIRECTS)
193
+ return nil if redirects_left.zero?
194
+
195
+ uri = URI(url)
196
+ uri.scheme = "https" if uri.scheme == "http"
197
+
198
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
199
+ open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT) do |http|
200
+ http.get(uri.request_uri, "User-Agent" => "gem-skill/#{Gem::Skill::VERSION}")
201
+ end
202
+
203
+ case response
204
+ when Net::HTTPSuccess
205
+ response.body
206
+ when Net::HTTPRedirection
207
+ fetch_url(response["location"], redirects_left: redirects_left - 1)
208
+ end
209
+ rescue StandardError
210
+ nil
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_llm"
4
+
5
+ module Gem::Skill
6
+ # Drives the LLM pipeline: fetches docs, generates a SKILL.md, caches it.
7
+ class Generator
8
+ DEFAULT_MODEL = ENV.fetch("GEMSKILL_MODEL", "gpt-5.5")
9
+ MAX_SOURCE_CHARS = 60_000 # guard against enormous READMEs blowing the context window
10
+
11
+ SYSTEM_INSTRUCTIONS = <<~SYSTEM
12
+ You are a Ruby gem documentation specialist who generates Claude Code skill files.
13
+ A skill file gives Claude Code deep, practical knowledge about a library so it can
14
+ use it correctly without re-reading source docs. Be accurate, concise, and complete
15
+ for the most common use cases. No filler, no marketing language.
16
+ SYSTEM
17
+
18
+ SKILL_PROMPT = <<~PROMPT
19
+ Generate a SKILL.md for the Ruby gem "%<gem_name>s" version %<version>s.
20
+
21
+ Output raw Markdown directly. Do NOT wrap the output in a code fence or any
22
+ other container — the file is Markdown, so no ```markdown wrapper.
23
+
24
+ Begin immediately with the first section heading. Use exactly these sections:
25
+
26
+ ## Overview
27
+ One paragraph: what the gem does and when to reach for it.
28
+
29
+ ## Installation
30
+ Exact Gemfile/gemspec lines and any required post-install steps.
31
+
32
+ ## Core API
33
+ The most important classes, methods, and options. Show real method signatures
34
+ and return values. Prefer code over prose.
35
+
36
+ ## Common Patterns
37
+ The 3-5 most frequent real-world usage patterns with working code examples.
38
+
39
+ ## Gotchas & Edge Cases
40
+ Things that surprise developers: unexpected defaults, version-specific behavior,
41
+ thread safety concerns, performance cliffs, encoding issues.
42
+
43
+ ## Configuration
44
+ Initializer patterns, environment variables, defaults worth knowing.
45
+
46
+ ## Testing
47
+ How to test code that uses this gem: mocks, fakes, fixtures, VCR patterns.
48
+
49
+ Synthesize the sources below — do not copy them verbatim.
50
+ Write as a knowledgeable colleague, not a marketing document.
51
+
52
+ ---
53
+
54
+ %<sources>s
55
+ PROMPT
56
+
57
+ attr_reader :gem_name, :version, :model
58
+
59
+ def initialize(gem_name, version, model: DEFAULT_MODEL)
60
+ @gem_name = gem_name
61
+ @version = version
62
+ @model = model
63
+ end
64
+
65
+ # Generate and cache a SKILL.md. Returns the skill content string.
66
+ # Pass a block to stream output chunks to the caller for live feedback.
67
+ def generate(force: false, &block)
68
+ return Cache.read(gem_name, version) if Cache.cached?(gem_name, version) && !force
69
+
70
+ sources = Fetcher.new(gem_name, version).fetch_all
71
+ raise Error, "No documentation found for #{gem_name} #{version}" if sources.empty?
72
+
73
+ skill_content = block ? call_llm_streaming(sources, &block) : call_llm(sources)
74
+ Cache.store(gem_name, version, skill_content, { sources: sources.keys.map(&:to_s), model: model })
75
+ skill_content
76
+ rescue RubyLLM::Error => e
77
+ raise Error, e.message
78
+ end
79
+
80
+ private
81
+
82
+ def call_llm(sources)
83
+ chat = build_chat
84
+ response = chat.ask(format_prompt(sources))
85
+ strip_wrapper_fence(response.content)
86
+ end
87
+
88
+ def call_llm_streaming(sources)
89
+ content = +""
90
+ chat = build_chat
91
+ chat.ask(format_prompt(sources)) do |chunk|
92
+ text = chunk.content.to_s
93
+ next if text.empty?
94
+
95
+ yield text
96
+ content << text
97
+ end
98
+ strip_wrapper_fence(content)
99
+ end
100
+
101
+ # Removes a leading ```markdown (or ```) fence and its closing ```.
102
+ # Belt-and-suspenders: the prompt instructs the model not to wrap,
103
+ # but some models do it anyway.
104
+ def strip_wrapper_fence(content)
105
+ content
106
+ .sub(/\A\s*```(?:markdown)?\s*\n/, "")
107
+ .sub(/\n```\s*\z/, "")
108
+ .strip
109
+ end
110
+
111
+ def build_chat
112
+ RubyLLM.chat(model: model).with_instructions(SYSTEM_INSTRUCTIONS)
113
+ end
114
+
115
+ def format_prompt(sources)
116
+ formatted = sources.map do |name, content|
117
+ body = content.length > MAX_SOURCE_CHARS ? "#{content[0, MAX_SOURCE_CHARS]}\n[... truncated ...]" : content
118
+ "### #{name.to_s.upcase}\n\n#{body}"
119
+ end.join("\n\n---\n\n")
120
+
121
+ format(SKILL_PROMPT, gem_name: gem_name, version: version, sources: formatted)
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Gem::Skill
6
+ # Manages .claude/skills/ symlinks in a project, pointing to ~/.gem/skills cache.
7
+ module Linker
8
+ def self.skills_dir(project_root = Dir.pwd)
9
+ File.join(project_root, ".claude", "skills")
10
+ end
11
+
12
+ def self.link(gem_name, version, project_root = Dir.pwd)
13
+ target = Cache.skill_path(gem_name, version)
14
+ raise Error, "No cached skill for #{gem_name} #{version}. Run: gem skill install #{gem_name}" \
15
+ unless File.exist?(target)
16
+
17
+ dir = skills_dir(project_root)
18
+ FileUtils.mkdir_p(dir)
19
+
20
+ link_path = File.join(dir, "#{gem_name}.md")
21
+ File.unlink(link_path) if File.symlink?(link_path)
22
+ File.symlink(target, link_path)
23
+ end
24
+
25
+ def self.unlink(gem_name, project_root = Dir.pwd)
26
+ link_path = File.join(skills_dir(project_root), "#{gem_name}.md")
27
+ File.unlink(link_path) if File.symlink?(link_path)
28
+ end
29
+
30
+ def self.linked_gems(project_root = Dir.pwd)
31
+ dir = skills_dir(project_root)
32
+ return [] unless Dir.exist?(dir)
33
+
34
+ Dir.glob(File.join(dir, "*.md")).filter_map do |path|
35
+ next unless File.symlink?(path)
36
+
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) }
41
+ end
42
+ end
43
+
44
+ def self.prune_dead_links(project_root = Dir.pwd)
45
+ linked_gems(project_root)
46
+ .reject { |entry| entry[:valid] }
47
+ .each { |entry| unlink(entry[:gem_name], project_root) }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gem::Skill
4
+ # Parses Gemfile.lock to extract direct dependency gem name/version pairs.
5
+ # Direct deps are listed in the DEPENDENCIES section; versions come from specs.
6
+ module Lockfile
7
+ def self.gems(lockfile_path = "Gemfile.lock")
8
+ raise Error, "Gemfile.lock not found at #{lockfile_path}" unless File.exist?(lockfile_path)
9
+
10
+ parse(File.read(lockfile_path))
11
+ end
12
+
13
+ def self.parse(content)
14
+ specs = parse_specs(content)
15
+ direct = parse_direct_names(content)
16
+ direct.each_with_object({}) do |name, hash|
17
+ hash[name] = specs[name] if specs.key?(name)
18
+ end
19
+ end
20
+
21
+ def self.parse_specs(content)
22
+ in_specs = false
23
+ specs = {}
24
+
25
+ content.each_line do |line|
26
+ if line.strip == "specs:"
27
+ in_specs = true
28
+ next
29
+ elsif in_specs && line =~ /\A {4}(\S+) \(([^)]+)\)/
30
+ specs[Regexp.last_match(1)] = Regexp.last_match(2)
31
+ elsif in_specs && line !~ /\A /
32
+ in_specs = false
33
+ end
34
+ end
35
+
36
+ specs
37
+ end
38
+
39
+ def self.parse_direct_names(content)
40
+ in_deps = false
41
+ names = []
42
+
43
+ content.each_line do |line|
44
+ if line.strip == "DEPENDENCIES"
45
+ in_deps = true
46
+ next
47
+ elsif in_deps && line =~ /\A (\S+)/
48
+ # Strip version constraints from dep names like "ruby_llm (>= 1.0)"
49
+ names << Regexp.last_match(1)
50
+ elsif in_deps && !line.strip.empty?
51
+ in_deps = false
52
+ end
53
+ end
54
+
55
+ names
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gem::Skill
4
+ VERSION = "0.1.0"
5
+ end
data/lib/gem/skill.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "skill/version"
4
+ require_relative "skill/cache"
5
+ require_relative "skill/fetcher"
6
+ require_relative "skill/generator"
7
+ require_relative "skill/linker"
8
+ require_relative "skill/lockfile"
9
+
10
+ module Gem::Skill
11
+ class Error < StandardError; end
12
+
13
+ ENV_KEY_MAP = {
14
+ anthropic_api_key: "ANTHROPIC_API_KEY",
15
+ openai_api_key: "OPENAI_API_KEY",
16
+ gemini_api_key: "GEMINI_API_KEY",
17
+ mistral_api_key: "MISTRAL_API_KEY",
18
+ deepseek_api_key: "DEEPSEEK_API_KEY",
19
+ openrouter_api_key: "OPENROUTER_API_KEY",
20
+ xai_api_key: "XAI_API_KEY"
21
+ }.freeze
22
+
23
+ # Configure RubyLLM from environment variables. Called automatically by the
24
+ # CLI commands so users don't need a separate initializer for standalone use.
25
+ # No-op if RubyLLM is already configured (e.g. in a Rails app).
26
+ def self.configure_llm!
27
+ RubyLLM.configure do |config|
28
+ ENV_KEY_MAP.each do |attr, env_var|
29
+ value = ENV[env_var]
30
+ config.public_send(:"#{attr}=", value) if value
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gem/skill/cli/gem_command"
4
+ Gem::CommandManager.instance.register_command :skill
5
+
6
+ require "rubygems/commands/install_command"
7
+ require "tty-spinner"
8
+
9
+ module Gem::Skill
10
+ module InstallSkillOption
11
+ def initialize
12
+ super
13
+ add_option("--with-skill", "Generate Claude Code SKILL.md files after installation") do |_, opts|
14
+ opts[:generate_skill] = true
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ Gem::Commands::InstallCommand.prepend(Gem::Skill::InstallSkillOption)
21
+
22
+ module Gem::Skill
23
+ @pending_skills = []
24
+ @pending_lock = Mutex.new
25
+
26
+ class << self
27
+ attr_reader :pending_skills, :pending_lock
28
+
29
+ def generate_pending_skills
30
+ return if @pending_skills.empty?
31
+
32
+ configure_llm!
33
+
34
+ multi = TTY::Spinner::Multi.new(
35
+ "[:spinner] Writing skills",
36
+ format: :dots,
37
+ output: $stderr
38
+ )
39
+
40
+ threads = @pending_skills.map do |gem_info|
41
+ name = gem_info[:name]
42
+ version = gem_info[:version]
43
+ sp = multi.register(" [:spinner] :title")
44
+ sp.update(title: "#{name} #{version}")
45
+ Thread.new(name, version, sp) { |n, v, spinner| generate_one_skill(n, v, spinner) }
46
+ end
47
+ threads.each(&:join)
48
+ rescue => e
49
+ warn "gem-skill: #{e.message}"
50
+ end
51
+
52
+ def generate_one_skill(name, version, spinner)
53
+ spinner.auto_spin
54
+ Generator.new(name, version).generate
55
+ spinner.success("done")
56
+ rescue => e
57
+ spinner.error("failed")
58
+ warn "gem-skill: #{name}: #{e.message}"
59
+ end
60
+ end
61
+ end
62
+
63
+ Gem.post_install do |installer|
64
+ next unless installer.options[:generate_skill]
65
+ Gem::Skill.pending_lock.synchronize do
66
+ Gem::Skill.pending_skills << { name: installer.spec.name, version: installer.spec.version.to_s }
67
+ end
68
+ end
69
+
70
+ at_exit { Gem::Skill.generate_pending_skills }
data/plugins.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bundler plugin entry point — loaded by `bundle plugin install gem-skill`
4
+ # or `plugin 'gem-skill'` in Gemfile. Registers the `bundle skill` command.
5
+
6
+ require_relative "lib/gem/skill/cli/bundle_command"
7
+
8
+ Bundler::Plugin::API.commands "skill" do |_command, args|
9
+ Gem::Skill::BundlerCommand.run(args)
10
+ end
data/scripts/e2e_test ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
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]]
6
+
7
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
8
+ require "gem/skill"
9
+ require "ruby_llm"
10
+
11
+ GEM_NAME = ARGV[0] || "zeitwerk"
12
+ VERSION = ARGV[1] || Gem::Specification.find_by_name(GEM_NAME)&.version&.to_s
13
+ MODEL = ARGV[2] || "gpt-4o-mini"
14
+
15
+ abort "Gem '#{GEM_NAME}' not installed. Run: gem install #{GEM_NAME}" unless VERSION
16
+
17
+ Gem::Skill.configure_llm!
18
+
19
+ puts "=" * 60
20
+ puts "gem-skill end-to-end test"
21
+ puts "Gem: #{GEM_NAME} #{VERSION}"
22
+ puts "Model: #{MODEL}"
23
+ puts "Cache: #{Gem::Skill::Cache.root}"
24
+ puts "=" * 60
25
+ puts ""
26
+
27
+ # --- Phase 1: Fetch ---
28
+
29
+ puts "[ FETCH ]"
30
+ fetcher = Gem::Skill::Fetcher.new(GEM_NAME, VERSION)
31
+ sources = fetcher.fetch_all
32
+
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
40
+ puts ""
41
+
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
52
+ end
53
+
54
+ puts ""
55
+ puts "-" * 60
56
+ puts ""
57
+
58
+ # --- Phase 3: Verify cache ---
59
+
60
+ puts "[ CACHE ]"
61
+ 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
68
+ end
69
+
70
+ puts ""
71
+ puts "Done."
data/sig/gem/skill.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Gem::Skill
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end