Class: RailsAiBridge::Registry::SkillSourceResolver

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_ai_bridge/registry/skill_source_resolver.rb

Overview

Resolves remote git skill pack sources by cloning or pulling them into a local cache directory.

Manages a cache of git repositories based on source strings, computing a unique cache key for each source. If the repository is not cached, it clones it; if already cached and the pull TTL window has elapsed, it pulls updates. Otherwise the cached copy is used as-is.

Examples:

resolver = SkillSourceResolver.new('/tmp/cache', DefaultGitRunner.new)
local_path = resolver.resolve('igmarin/ruby-core-skills')

Defined Under Namespace

Classes: ResolutionError

Constant Summary collapse

PULL_TRACKER_MAX =

Maximum number of distinct cache paths tracked in @last_pulled. Oldest entries are evicted once this limit is exceeded, preventing unbounded growth in long-running MCP server processes.

500

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cache_dir, git_runner = DefaultGitRunner.new, pull_ttl: 86_400) ⇒ SkillSourceResolver

Creates a new SkillSourceResolver with the given cache directory and git runner.

Parameters:

  • cache_dir (String)

    path to the cache directory

  • git_runner (GitRunner) (defaults to: DefaultGitRunner.new)

    git runner implementation (defaults to DefaultGitRunner)

  • pull_ttl (Integer) (defaults to: 86_400)

    seconds between git pull refreshes per cached pack (default: 86400 = 24 h). Set to 0 to always pull on every resolve call.



153
154
155
156
157
158
159
# File 'lib/rails_ai_bridge/registry/skill_source_resolver.rb', line 153

def initialize(cache_dir, git_runner = DefaultGitRunner.new, pull_ttl: 86_400)
  @cache_dir = validate_cache_dir(cache_dir)
  @git_runner = git_runner
  @pull_ttl = pull_ttl
  @last_pulled = {} # cache_path => Float (monotonic seconds) — in-memory freshness tracking
  @pull_mutex = Mutex.new
end

Class Method Details

.compute_cache_key(source, ref = nil) ⇒ String

Computes a cache key for a given source string and optional ref.

When a ref is provided the key includes it so that different refs for the same source produce isolated cache directories, preventing cross-ref contamination. Sanitizes non-alphanumeric characters to underscores and appends a SHA256 hash suffix to ensure uniqueness.

Parameters:

  • source (String)

    source string (e.g., 'igmarin/ruby-core-skills')

  • ref (String, nil) (defaults to: nil)

    git ref or nil for the default branch

Returns:

  • (String)

    cache key (e.g., 'igmarin_ruby_core_skills_a1b2c3d4')



183
184
185
186
187
188
# File 'lib/rails_ai_bridge/registry/skill_source_resolver.rb', line 183

def self.compute_cache_key(source, ref = nil)
  identity  = ref ? "#{source}@#{ref}" : source
  sanitized = identity.gsub(/[^a-zA-Z0-9]/, '_')
  hash = Digest::SHA256.hexdigest(identity)[0..15]
  "#{sanitized}_#{hash}"
end

.default_cache_dirString

Resolves the default cache directory, checking RAILS_AI_BRIDGE_CACHE_DIR then HOME.

Returns:

  • (String)

    path to the default cache directory

Raises:

  • (RuntimeError)

    if HOME environment variable is not set or inaccessible



165
166
167
168
169
170
171
# File 'lib/rails_ai_bridge/registry/skill_source_resolver.rb', line 165

def self.default_cache_dir
  dir = ENV.fetch('RAILS_AI_BRIDGE_CACHE_DIR', nil)
  return dir if dir && !dir.strip.empty?

  home = Dir.home
  File.join(home, '.rails-ai-bridge', 'cache')
end

Instance Method Details

#resolve(source, ref: nil) ⇒ String

Resolves a source to a local path.

Delegates format detection to RailsAiBridge::Registry::SourceParser. Local paths are returned immediately without any git operations. For git sources:

  • When +ref+ is nil (floating branch), a pull is attempted if the pack's TTL window has elapsed, keeping the default branch up-to-date.
  • When +ref+ is set (pinned tag, SHA, or named branch), the pull is skipped entirely. A pinned ref is deterministic — there is no reason to pull, and doing so on a detached HEAD (after checkout) fails with "You are not currently on a branch".

Each (source, ref) pair gets its own cache directory so a repo checked out at two different refs coexists without interference.

Parameters:

  • source (String)

    source string — local path, git URL, or owner/repo shorthand

  • ref (String, nil) (defaults to: nil)

    optional git ref (branch, tag, or SHA) to check out after cloning; nil means the default branch

Returns:

  • (String)

    local path to the resolved directory

Raises:



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/rails_ai_bridge/registry/skill_source_resolver.rb', line 210

def resolve(source, ref: nil)
  parsed = SourceParser.parse(source)
  return parsed.resolved_url if parsed.type == :local_path

  cache_key  = self.class.compute_cache_key(source, ref)
  cache_path = File.join(@cache_dir, cache_key)

  if File.exist?(cache_path)
    # Only pull when no ref is pinned. A pinned ref is deterministic and
    # a previous checkout may have left the repo in detached HEAD, which
    # causes git pull to fail with "not currently on a branch".
    perform_pull(source, cache_path) if ref.nil? && pull_stale?(cache_path)
  else
    perform_clone(source, cache_path, parsed.resolved_url)
  end

  perform_checkout_ref(source, cache_path, ref) if ref
  cache_path
end