Class: Rubino::Skills::Installer
- Inherits:
-
Object
- Object
- Rubino::Skills::Installer
- Defined in:
- lib/rubino/skills/installer.rb
Overview
Installs skills from git repositories into the user skills dir (#4) —the ‘rubino skills install/update/remove` backend. There is no marketplace and nothing is vendored in the gem: a source is just a repo (GitHub `owner/repo` shorthand or any git URL), shallow-cloned to a tmpdir, scanned for the registry’s own ‘<name>/SKILL.md` layout, and the selected skill dirs are copied into `~/.rubino/skills` where the existing Registry discovers them like any hand-written skill.
Provenance is recorded per installed skill in ‘<skills-dir>/.sources.json` (`name → path, commit`) so `update` can re-fetch from the recorded source and `remove` knows which dirs this mechanism owns. The dotfile name keeps it out of the registry’s ‘*.md` / `*/SKILL.md` globs.
Constant Summary collapse
- SOURCES_FILE =
".sources.json"- GITHUB_SHORTHAND =
GitHub shorthand: bare ‘owner/repo` (one slash, no scheme/host).
%r{\A[\w.-]+/[\w.-]+\z}
Instance Attribute Summary collapse
-
#skills_dir ⇒ Object
readonly
Returns the value of attribute skills_dir.
Class Method Summary collapse
-
.url_for(source) ⇒ Object
‘owner/repo` → the GitHub URL; anything else is passed to git verbatim.
Instance Method Summary collapse
-
#discover(checkout) ⇒ Object
Skills discoverable in a checkout, as ‘path:, description:` hashes (path = skill dir relative to the repo root).
-
#fetch(source) ⇒ Object
Shallow-clones
sourceand yields (checkout_dir, head_sha); the tmp checkout is deleted when the block returns. -
#initialize(skills_dir: nil) ⇒ Installer
constructor
A new instance of Installer.
-
#install(entries, checkout:, source:, commit:) ⇒ Object
Copies the discover-entries into the skills dir (replacing any prior copy of the same name) and records their provenance.
-
#remove(name) ⇒ Object
Deletes the skill dir + provenance entry.
-
#sources ⇒ Object
The provenance ledger (empty hash when absent or unparseable).
-
#update(names = []) ⇒ Object
Re-fetches
names(default: every recorded skill) from their recorded sources, one clone per distinct source.
Constructor Details
#initialize(skills_dir: nil) ⇒ Installer
Returns a new instance of Installer.
28 29 30 31 32 33 |
# File 'lib/rubino/skills/installer.rb', line 28 def initialize(skills_dir: nil) # The same resolved home the registry's "~/.rubino/skills" entry # expands to (RUBINO_HOME → else ~/.rubino), so an install is # discovered without any config change. @skills_dir = skills_dir || File.join(Config::Loader.default_home_path, "skills") end |
Instance Attribute Details
#skills_dir ⇒ Object (readonly)
Returns the value of attribute skills_dir.
26 27 28 |
# File 'lib/rubino/skills/installer.rb', line 26 def skills_dir @skills_dir end |
Class Method Details
.url_for(source) ⇒ Object
‘owner/repo` → the GitHub URL; anything else is passed to git verbatim.
36 37 38 |
# File 'lib/rubino/skills/installer.rb', line 36 def self.url_for(source) GITHUB_SHORTHAND.match?(source.to_s) ? "https://github.com/#{source}" : source.to_s end |
Instance Method Details
#discover(checkout) ⇒ Object
Skills discoverable in a checkout, as ‘path:, description:` hashes (path = skill dir relative to the repo root). Recursive (`**/` + the registry’s DIR_GLOB) so catalog repos that nest skills under a grouping dir are found too.
59 60 61 62 63 64 65 |
# File 'lib/rubino/skills/installer.rb', line 59 def discover(checkout) Dir.glob(File.join("**", Registry::DIR_GLOB), base: checkout).sort.map do |rel| dir = File.dirname(rel) skill = Skill.new(path: File.join(checkout, rel)) { name: skill.name, path: dir, description: skill.description.to_s } end end |
#fetch(source) ⇒ Object
Shallow-clones source and yields (checkout_dir, head_sha); the tmp checkout is deleted when the block returns. Returns the block’s value, or nil when the clone fails (unknown repo, no network — git’s own stderr is left visible as the diagnostic). The ONE network touchpoint, so specs stub this method and never shell out.
45 46 47 48 49 50 51 52 53 |
# File 'lib/rubino/skills/installer.rb', line 45 def fetch(source) Dir.mktmpdir("rubino-skills") do |dir| return nil unless system("git", "clone", "--depth", "1", "--quiet", self.class.url_for(source), dir, out: File::NULL) sha = IO.popen(["git", "-C", dir, "rev-parse", "HEAD"], &:read).strip yield dir, sha end end |
#install(entries, checkout:, source:, commit:) ⇒ Object
Copies the discover-entries into the skills dir (replacing any prior copy of the same name) and records their provenance.
69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/rubino/skills/installer.rb', line 69 def install(entries, checkout:, source:, commit:) FileUtils.mkdir_p(@skills_dir) data = sources entries.each do |entry| dest = File.join(@skills_dir, entry[:name]) FileUtils.rm_rf(dest) FileUtils.cp_r(File.join(checkout, entry[:path]), dest) data[entry[:name]] = { "source" => source, "path" => entry[:path], "commit" => commit } end write_sources(data) end |
#remove(name) ⇒ Object
Deletes the skill dir + provenance entry. Returns false (nothing touched) for a skill without a provenance entry — this mechanism only removes what it installed.
105 106 107 108 109 110 111 112 113 |
# File 'lib/rubino/skills/installer.rb', line 105 def remove(name) # rubocop:disable Naming/PredicateMethod -- "did I remove anything", a mutator reporting what it did data = sources return false unless data.key?(name) FileUtils.rm_rf(File.join(@skills_dir, name)) data.delete(name) write_sources(data) true end |
#sources ⇒ Object
The provenance ledger (empty hash when absent or unparseable).
116 117 118 119 120 121 |
# File 'lib/rubino/skills/installer.rb', line 116 def sources path = File.join(@skills_dir, SOURCES_FILE) File.file?(path) ? JSON.parse(File.read(path)) : {} rescue JSON::ParserError {} end |
#update(names = []) ⇒ Object
Re-fetches names (default: every recorded skill) from their recorded sources, one clone per distinct source. Returns name → :updated / :up_to_date / :failed (clone failed, or the skill’s recorded path no longer holds a SKILL.md) / :unknown (no provenance entry).
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/rubino/skills/installer.rb', line 85 def update(names = []) data = sources names = data.keys if names.empty? results = {} names.group_by { |name| data.dig(name, "source") }.each do |source, group| next group.each { |name| results[name] = :unknown } if source.nil? fetched = fetch(source) do |checkout, sha| group.each { |name| results[name] = update_one(name, data[name], checkout, sha) } write_sources(data) true end group.each { |name| results[name] = :failed } unless fetched end results end |