Module: Gem::Skill::Linker
- Defined in:
- lib/gem/skill/linker.rb
Overview
Manages per-project skill symlinks pointing into the ~/.gem/skills cache. Each symlink is a directory link: <gem_name> -> ~/.gem/skills/<gem>/<version>/ The assistant discovers skills by reading SKILL.md inside each linked directory.
The project-relative directory is configurable via GEMSKILL_PROJECT_DIR (default “.claude/skills” for Claude Code). Codex users might set it to “.agents” or “.codex”; see the configuration docs.
Constant Summary collapse
- DEFAULT_PROJECT_DIR =
".claude/skills"
Class Method Summary collapse
- .link(gem_name, version, project_root = Dir.pwd) ⇒ Object
- .linked_gems(project_root = Dir.pwd) ⇒ Object
-
.project_dir ⇒ Object
Project-relative directory where skill symlinks are written.
- .prune_dead_links(project_root = Dir.pwd) ⇒ Object
- .skills_dir(project_root = Dir.pwd) ⇒ Object
- .unlink(gem_name, project_root = Dir.pwd) ⇒ Object
Class Method Details
.link(gem_name, version, project_root = Dir.pwd) ⇒ Object
28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/gem/skill/linker.rb', line 28 def self.link(gem_name, version, project_root = Dir.pwd) target_dir = File.dirname(Cache.skill_path(gem_name, version)) raise Error, "No cached skill for #{gem_name} #{version}. Run: gem skill install #{gem_name}" \ unless File.exist?(Cache.skill_path(gem_name, version)) dir = skills_dir(project_root) FileUtils.mkdir_p(dir) link_path = File.join(dir, gem_name) File.unlink(link_path) if File.symlink?(link_path) File.symlink(target_dir, link_path) end |
.linked_gems(project_root = Dir.pwd) ⇒ Object
46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/gem/skill/linker.rb', line 46 def self.linked_gems(project_root = Dir.pwd) dir = skills_dir(project_root) return [] unless Dir.exist?(dir) Dir.glob(File.join(dir, "*")).filter_map do |path| next unless File.symlink?(path) gem_name = File.basename(path) target_dir = File.readlink(path) version = target_dir.match(%r{/([^/]+)$})&.captures&.first skill_file = File.join(target_dir, "SKILL.md") { gem_name: gem_name, version: version, target: target_dir, valid: File.exist?(skill_file) } end end |
.project_dir ⇒ Object
Project-relative directory where skill symlinks are written. Read from the environment each call so a changed GEMSKILL_PROJECT_DIR takes effect without reloading.
19 20 21 22 |
# File 'lib/gem/skill/linker.rb', line 19 def self.project_dir value = ENV.fetch("GEMSKILL_PROJECT_DIR", DEFAULT_PROJECT_DIR).to_s.strip value.empty? ? DEFAULT_PROJECT_DIR : value end |
.prune_dead_links(project_root = Dir.pwd) ⇒ Object
61 62 63 64 65 |
# File 'lib/gem/skill/linker.rb', line 61 def self.prune_dead_links(project_root = Dir.pwd) linked_gems(project_root) .reject { |entry| entry[:valid] } .each { |entry| unlink(entry[:gem_name], project_root) } end |
.skills_dir(project_root = Dir.pwd) ⇒ Object
24 25 26 |
# File 'lib/gem/skill/linker.rb', line 24 def self.skills_dir(project_root = Dir.pwd) File.join(project_root, project_dir) end |
.unlink(gem_name, project_root = Dir.pwd) ⇒ Object
41 42 43 44 |
# File 'lib/gem/skill/linker.rb', line 41 def self.unlink(gem_name, project_root = Dir.pwd) link_path = File.join(skills_dir(project_root), gem_name) File.unlink(link_path) if File.symlink?(link_path) end |