Module: Rails::Hyperdrive::BundlerArtifactDiscovery

Defined in:
lib/rails/hyperdrive/bundler_artifact_discovery.rb

Defined Under Namespace

Classes: Artifact, Report

Constant Summary collapse

SKILL_FILE_NAMES =
["SKILL.md", "SKILL.md.erb"].freeze

Class Method Summary collapse

Class Method Details

.discover(specs: nil, enabled_gems: [], report: Report.new) ⇒ Object

Non-fatal problems are reported and the artifact is dropped; discovery never raises. A gem that has not opted in as a companion is never scanned — its skills.sh content is only reported through notices.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/rails/hyperdrive/bundler_artifact_discovery.rb', line 78

def discover(specs: nil, enabled_gems: [], report: Report.new)
  specs ||= safe_bundler_specs
  enabled = Array(enabled_gems).map(&:to_s)
  resolved = specs.each_with_object({}) { |s, h| h[s.name.to_s] = s.version }

  candidates = []
  specs.each do |spec|
    report.bundled_gems << spec.name.to_s
    unless opted_in?(spec, enabled_gems: enabled)
      notice_skills_sh_content(spec, report: report)
      next
    end
    manifest = GemManifest.load(spec, warnings: report.warnings)
    seen = each_artifact_path(spec, manifest: manifest, report: report) do |path, kind, support_root, key|
      gate = manifest.gate(kind.type, key)
      artifact = parse(path, source_spec: spec, kind: kind, resolved: resolved,
        report: report, support_root: support_root, gate: gate, key: key, manifest: manifest)
      if artifact.is_a?(Artifact)
        candidates << artifact
      elsif artifact != :gate_miss
        report.mark_skipped_gem(spec.name.to_s)
      end
    end
    warn_unknown_manifest_keys(manifest, spec, seen, report)
  end

  # Collapse same-name variants within one source gem (highest
  # spec_version, path as tiebreak); never across sources — composite
  # identity is (name, source_gem).
  candidates.group_by { |a| [a.name, a.source_gem, a.artifact_type] }.map do |_key, group|
    group.max_by { |a| [Gem::Version.new(a.spec_version), a.path] }
  end
end

.flat_keys(spec, kind, manifest:) ⇒ Object

Every filename a manifest may key one of the kind's artifacts by: the name as shipped, plus each template's rendered face. Templates the tie rule drops are included — the manifest names shipped files, not surviving ones.



230
231
232
233
234
# File 'lib/rails/hyperdrive/bundler_artifact_discovery.rb', line 230

def flat_keys(spec, kind, manifest:)
  flat_candidates(spec, kind, manifest: manifest)
    .flat_map { |path| [File.basename(path), File.basename(target_path(path))] }
    .uniq
end

.install_ready_body(artifact) ⇒ Object

Guideline frontmatter is stripped because the installed file is @-included eagerly into agent context, where it would be inert noise.



595
596
597
598
599
600
601
# File 'lib/rails/hyperdrive/bundler_artifact_discovery.rb', line 595

def install_ready_body(artifact)
  kind = InstallLayout.kind(artifact.artifact_type)
  return artifact.body unless kind&.install_body == :strip_frontmatter

  _frontmatter, rest = split_frontmatter(artifact.body)
  (rest || artifact.body).sub(/\A\n+/, "")
end

.skill_paths(spec, manifest:, report: Report.new) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/rails/hyperdrive/bundler_artifact_discovery.rb', line 145

def skill_paths(spec, manifest:, report: Report.new)
  roots = artifact_roots(spec, InstallLayout.kind(:skill), manifest)

  candidates = []
  seen = {}
  roots.each do |root|
    Dir.glob(File.join(root, "**", "{SKILL.md,SKILL.md.erb}"))
       .group_by { |p| File.dirname(p) }.each do |dir, group|
      next if seen[File.expand_path(dir)]
      seen[File.expand_path(dir)] = true
      candidates << {
        dir: dir,
        rel: dir.delete_prefix(root).delete_prefix("/"),
        path: resolve_same_dir_tie(dir, group, report)
      }
    end
  end

  pair_with_templates(candidates, spec, manifest: manifest, report: report)
end

.split_frontmatter(body) ⇒ Object



603
604
605
606
607
608
609
610
611
612
# File 'lib/rails/hyperdrive/bundler_artifact_discovery.rb', line 603

def split_frontmatter(body)
  lines = body.lines
  return [nil, body] unless lines.first&.strip == "---"

  closing_index = lines[1..].index { |l| l.strip == "---" }
  return [nil, body] unless closing_index

  absolute_closing = closing_index + 1
  [lines[1...absolute_closing].join, lines[(absolute_closing + 1)..].join]
end

.support_relpaths(root, glob: "**/*") ⇒ Object

Dir-relative paths of everything under root besides SKILL.md(.erb). ".." segments are rejected to keep the tree inside that directory.



414
415
416
417
418
419
420
421
422
423
424
# File 'lib/rails/hyperdrive/bundler_artifact_discovery.rb', line 414

def support_relpaths(root, glob: "**/*")
  return [] if root.nil?

  Dir.glob(File.join(root, glob)).filter_map do |file|
    next unless File.file?(file)
    rel = file.delete_prefix("#{root}/")
    next if SKILL_FILE_NAMES.include?(File.basename(rel))
    next if rel.split(%r{[/\\]}).include?("..")
    rel
  end
end

.target_path(path) ⇒ Object



589
590
591
# File 'lib/rails/hyperdrive/bundler_artifact_discovery.rb', line 589

def target_path(path)
  erb_template?(path) ? path.delete_suffix(".erb") : path
end

.template_dir_for(path, support_root) ⇒ Object

nil when the definition file sits in the support root, i.e. the skill is not template-paired.



428
429
430
431
# File 'lib/rails/hyperdrive/bundler_artifact_discovery.rb', line 428

def template_dir_for(path, support_root)
  dir = File.dirname(path)
  dir unless File.expand_path(dir) == File.expand_path(support_root)
end