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, logger: nil) ⇒ SkillSourceResolver

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

:reek:TooManyStatements -- constructor wiring one attribute per collaborator

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.

  • logger (Logger, nil) (defaults to: nil)

    logger for structured git operation messages (defaults to default_logger)



196
197
198
199
200
201
202
203
# File 'lib/rails_ai_bridge/registry/skill_source_resolver.rb', line 196

def initialize(cache_dir, git_runner = DefaultGitRunner.new, pull_ttl: 86_400, logger: nil)
  @cache_dir = validate_cache_dir(cache_dir)
  @git_runner = git_runner
  @pull_ttl = pull_ttl
  @logger = logger || self.class.default_logger
  @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 the full 64-character SHA256 hex digest to ensure uniqueness, consistent with the fingerprinting used elsewhere in the gem.

NOTE: This is a change from the previous 16-character truncated digest. Cache directories created under the old key format are orphaned; clear ~/.rails-ai-bridge/cache to reclaim disk space.

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_<64-hex-sha256>')



244
245
246
247
248
249
# File 'lib/rails_ai_bridge/registry/skill_source_resolver.rb', line 244

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)
  "#{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



221
222
223
224
225
226
227
# File 'lib/rails_ai_bridge/registry/skill_source_resolver.rb', line 221

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

.default_loggerLogger

Resolves the default logger for git operation messages.

Uses Rails.logger in a Rails context and a stderr logger otherwise, so resolution diagnostics are never silently lost.

Returns:

  • (Logger)

    logger instance



211
212
213
214
215
# File 'lib/rails_ai_bridge/registry/skill_source_resolver.rb', line 211

def self.default_logger
  return Rails.logger if defined?(Rails) && !Rails.logger.nil?

  Logger.new($stderr)
end

Instance Method Details

#current_commit(path) ⇒ String

Returns the current HEAD commit SHA for the repository at path.

Parameters:

  • path (String)

    path to the local repository

Returns:

  • (String)

    40-character hex commit SHA

Raises:



296
297
298
299
300
# File 'lib/rails_ai_bridge/registry/skill_source_resolver.rb', line 296

def current_commit(path)
  @git_runner.current_commit(path)
rescue StandardError => error
  raise ResolutionError, "failed to read current commit for pack at #{path}: #{error.message}"
end

#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:



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/rails_ai_bridge/registry/skill_source_resolver.rb', line 271

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