rails-hyperdrive 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,7 @@ module Rails
26
26
  Result = Struct.new(:installed, :updated, :unchanged, :skipped, :orphaned, :removed, :merged, :sidecars,
27
27
  keyword_init: true)
28
28
 
29
- def initialize(root:, shell:, artifacts:, mode: :preserve, warnings: [])
29
+ def initialize(root:, shell:, artifacts:, mode: :preserve, warnings: [], notices: [])
30
30
  raise ArgumentError, "unknown mode #{mode.inspect}" unless MODES.include?(mode)
31
31
 
32
32
  @root = File.expand_path(root.to_s)
@@ -34,6 +34,7 @@ module Rails
34
34
  @artifacts = artifacts
35
35
  @mode = mode
36
36
  @warnings = warnings
37
+ @notices = notices
37
38
  @result = Result.new(
38
39
  installed: [], updated: [], unchanged: [], skipped: [], orphaned: [], removed: [], merged: [], sidecars: []
39
40
  )
@@ -54,6 +55,7 @@ module Rails
54
55
  write_claude_md(guidelines)
55
56
  write_lock
56
57
  print_warnings
58
+ print_notices
57
59
  print_footprint(guidelines: eager_guidelines)
58
60
  warn_if_destinations_gitignored
59
61
  @result
@@ -115,6 +117,7 @@ module Rails
115
117
  def install_skills
116
118
  @plan.select { |e| e.type == :skill }.each do |entry|
117
119
  skill_relpath = source_relpath_for(entry.artifact)
120
+ support_base = support_relpath_base(entry.artifact, skill_relpath)
118
121
  install_file(entry: entry, type: :skill, install_ready_body: entry.install_ready_body,
119
122
  source_relpath: skill_relpath, final_name: entry.final_name)
120
123
  entry.support_files.each do |file|
@@ -125,7 +128,7 @@ module Rails
125
128
  source_gem: entry.source_gem,
126
129
  version: entry.version,
127
130
  artifact_kind: "skill_support",
128
- source_relpath: skill_relpath && File.join(File.dirname(skill_relpath), file[:path])
131
+ source_relpath: support_base && File.join(support_base, file[:path])
129
132
  )
130
133
  end
131
134
  end
@@ -155,6 +158,22 @@ module Rails
155
158
  rel.end_with?(".md.erb") ? rel.delete_suffix(".erb") : rel
156
159
  end
157
160
 
161
+ # Supporting files live under the artifact's support root, which for a
162
+ # template-paired skill is not the definition file's directory.
163
+ def support_relpath_base(artifact, skill_relpath)
164
+ support_root = artifact.support_root
165
+ return skill_relpath && File.dirname(skill_relpath) if support_root.nil? || support_root.to_s.empty?
166
+
167
+ root = artifact.source_root
168
+ return nil if root.nil? || root.to_s.empty?
169
+
170
+ prefix = "#{File.expand_path(root.to_s)}/"
171
+ expanded = File.expand_path(support_root.to_s)
172
+ return nil unless expanded.start_with?(prefix)
173
+
174
+ expanded.delete_prefix(prefix)
175
+ end
176
+
158
177
  def install_file(dest: nil, type:, install_ready_body:, entry: nil, source_gem: nil, version: nil,
159
178
  artifact_kind: nil, source_relpath: nil, final_name: nil)
160
179
  write = {
@@ -524,6 +543,12 @@ module Rails
524
543
  @warnings.each { |w| @shell.say " - #{w}" }
525
544
  end
526
545
 
546
+ def print_notices
547
+ return if additive? || Array(@notices).empty?
548
+ @shell.say ""
549
+ @notices.each { |n| @shell.say_status :info, n, :blue }
550
+ end
551
+
527
552
  def print_footprint(guidelines:)
528
553
  return if additive?
529
554
 
@@ -33,6 +33,7 @@ module Rails
33
33
  @files = {} # path(String) => Entry
34
34
  @document = {} # raw parsed YAML, kept so unknown keys survive
35
35
  @disabled = empty_disabled
36
+ @enabled = []
36
37
  end
37
38
 
38
39
  def read
@@ -45,6 +46,7 @@ module Rails
45
46
  claude_md = data["claude_md"]
46
47
  @claude_md_state = claude_md["state"] if claude_md.is_a?(Hash)
47
48
  @disabled = parse_disabled(data["disabled"])
49
+ @enabled = parse_enabled(data["enabled"])
48
50
  Array(data["files"]).each do |raw|
49
51
  next unless raw.is_a?(Hash)
50
52
  entry = build_entry(raw)
@@ -71,11 +73,17 @@ module Rails
71
73
  Array(@disabled[type.to_sym]).include?(name.to_s)
72
74
  end
73
75
 
76
+ # Hand-editable list of gems the app opts into as companions.
77
+ def enabled_gems
78
+ @enabled
79
+ end
80
+
74
81
  # Adopt the state that is not derived from installed content, so
75
82
  # rewriting the file preserves it.
76
83
  def carry_settings(other)
77
84
  @document = other.document.dup
78
85
  @disabled = other.disabled_lists.dup
86
+ @enabled = other.enabled_gems.dup
79
87
  self
80
88
  end
81
89
 
@@ -103,6 +111,7 @@ module Rails
103
111
  "version" => SCHEMA_VERSION,
104
112
  "claude_md" => carried.merge("state" => @claude_md_state),
105
113
  "disabled" => DISABLED_KEYS.each_with_object({}) { |(type, key), h| h[key] = @disabled[type] },
114
+ "enabled" => @enabled,
106
115
  "files" => @files.values.sort_by(&:path).map { |e| serialize_entry(e) }
107
116
  )
108
117
  # A nil state means no import line is being managed. Recording one anyway
@@ -136,6 +145,12 @@ module Rails
136
145
  end
137
146
  end
138
147
 
148
+ def parse_enabled(raw)
149
+ return [] unless raw.is_a?(Array)
150
+
151
+ raw.map { |name| name.to_s.strip }.reject(&:empty?).uniq
152
+ end
153
+
139
154
  def build_entry(raw)
140
155
  source_gem, source_version = split_source(raw["source"])
141
156
  Entry.new(
@@ -0,0 +1,27 @@
1
+ # Rake tasks for companion gem repos: add `require "rails/hyperdrive/skill_tasks"`
2
+ # to the Rakefile. Both tasks take an optional gemspec-path argument, e.g.
3
+ # rake "hyperdrive:skills:render[path/to/name.gemspec]".
4
+ require "rake"
5
+ require "rails/hyperdrive/canonical_skill_render"
6
+
7
+ namespace :hyperdrive do
8
+ namespace :skills do
9
+ desc "Render each SKILL.md.erb template to its canonical static SKILL.md"
10
+ task :render, [:gemspec] do |_t, args|
11
+ written = Rails::Hyperdrive::CanonicalSkillRender.write(gemspec: args[:gemspec])
12
+ written.each { |r| puts "render #{r.dest}" }
13
+ puts "no skill templates found" if written.empty?
14
+ end
15
+
16
+ desc "Fail when a canonical SKILL.md is stale relative to its template"
17
+ task :check, [:gemspec] do |_t, args|
18
+ stale = Rails::Hyperdrive::CanonicalSkillRender.stale(gemspec: args[:gemspec])
19
+ if stale.empty?
20
+ puts "canonical skill files up to date"
21
+ else
22
+ stale.each { |r| warn "stale: #{r.dest}" }
23
+ abort "#{stale.size} canonical skill file(s) stale; run `rake hyperdrive:skills:render`"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -31,6 +31,27 @@ module Rails
31
31
  end
32
32
  end
33
33
 
34
+ # Fail-open binding for the canonical render: every gem reads as present
35
+ # at any requested version, so the output includes every conditional
36
+ # branch. There is no bundle to resolve against, so gem_version is nil.
37
+ class CanonicalContext
38
+ def gem?(_name, _requirement = nil)
39
+ true
40
+ end
41
+
42
+ def any_gem?(*)
43
+ true
44
+ end
45
+
46
+ def gem_version(_name)
47
+ nil
48
+ end
49
+
50
+ def template_binding
51
+ binding
52
+ end
53
+ end
54
+
34
55
  module_function
35
56
 
36
57
  # Errors (ERB SyntaxError included) propagate; the caller decides how to
@@ -39,6 +60,10 @@ module Rails
39
60
  ERB.new(source, trim_mode: "-").result(Context.new(resolved).template_binding)
40
61
  end
41
62
 
63
+ def render_canonical(source)
64
+ ERB.new(source, trim_mode: "-").result(CanonicalContext.new.template_binding)
65
+ end
66
+
42
67
  # Gem::Requirement.new does not parse a single comma-separated string, so
43
68
  # such strings are split into separate constraints first.
44
69
  def version_matches?(requirement_str, version)
@@ -130,7 +130,8 @@ module Rails
130
130
  # Bundler is absent or refuses to resolve.
131
131
  def gem_skills_info
132
132
  require "rails/hyperdrive/bundler_artifact_discovery"
133
- ::Rails::Hyperdrive::BundlerArtifactDiscovery.discover.select(&:skill?).map do |skill|
133
+ discovered = ::Rails::Hyperdrive::BundlerArtifactDiscovery.discover(enabled_gems: enabled_gems)
134
+ discovered.select(&:skill?).map do |skill|
134
135
  {
135
136
  name: skill.name,
136
137
  gem: skill.target_gem,
@@ -143,6 +144,24 @@ module Rails
143
144
  rescue StandardError
144
145
  []
145
146
  end
147
+
148
+ def enabled_gems
149
+ require "rails/hyperdrive/install_layout"
150
+ require "rails/hyperdrive/lock_file"
151
+ root =
152
+ if @app_root
153
+ @app_root.to_s
154
+ elsif defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
155
+ ::Rails.root.to_s
156
+ end
157
+ return [] unless root
158
+
159
+ ::Rails::Hyperdrive::LockFile.load(
160
+ File.join(root, ::Rails::Hyperdrive::InstallLayout::LOCK_PATH)
161
+ ).enabled_gems
162
+ rescue StandardError
163
+ []
164
+ end
146
165
  end
147
166
  end
148
167
  end
@@ -40,7 +40,7 @@ module Rails
40
40
  shown = rows.first(ROW_CAP)
41
41
  lines = [result.columns.join("\t")]
42
42
  shown.each { |row| lines << row.map { |v| v.to_s }.join("\t") }
43
- lines << "(#{rows.length} rows, truncated)" if rows.length > ROW_CAP
43
+ lines << "(showing first #{ROW_CAP} of #{rows.length} rows)" if rows.length > ROW_CAP
44
44
  lines.join("\n")
45
45
  end
46
46
  end
@@ -1,5 +1,5 @@
1
1
  module Rails
2
2
  module Hyperdrive
3
- VERSION = "0.4.0"
3
+ VERSION = "0.6.0"
4
4
  end
5
5
  end
@@ -2,16 +2,6 @@ require "rails/hyperdrive/version"
2
2
 
3
3
  module Rails
4
4
  module Hyperdrive
5
- class Configuration
6
- DEFAULT_MOUNT_AT = "/_hyperdrive".freeze
7
-
8
- attr_accessor :mount_at
9
-
10
- def initialize
11
- @mount_at = DEFAULT_MOUNT_AT
12
- end
13
- end
14
-
15
5
  def self.root
16
6
  @root ||= File.expand_path("..", __dir__)
17
7
  end
@@ -19,18 +9,6 @@ module Rails
19
9
  def self.dev_mode?
20
10
  defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.development?
21
11
  end
22
-
23
- def self.configuration
24
- @configuration ||= Configuration.new
25
- end
26
-
27
- def self.configure
28
- yield(configuration)
29
- end
30
-
31
- def self.reset_configuration!
32
- @configuration = nil
33
- end
34
12
  end
35
13
  end
36
14
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-hyperdrive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bakaface
@@ -89,7 +89,6 @@ files:
89
89
  - lib/generators/hyperdrive/gitignore_support.rb
90
90
  - lib/generators/hyperdrive/install/USAGE
91
91
  - lib/generators/hyperdrive/install/install_generator.rb
92
- - lib/generators/hyperdrive/install/templates/initializer.rb.tt
93
92
  - lib/generators/hyperdrive/install_summary.rb
94
93
  - lib/generators/hyperdrive/sync/USAGE
95
94
  - lib/generators/hyperdrive/sync/sync_generator.rb
@@ -100,12 +99,14 @@ files:
100
99
  - lib/rails/hyperdrive/artifact_status.rb
101
100
  - lib/rails/hyperdrive/auto_install.rb
102
101
  - lib/rails/hyperdrive/bundler_artifact_discovery.rb
102
+ - lib/rails/hyperdrive/canonical_skill_render.rb
103
103
  - lib/rails/hyperdrive/claude_md_import.rb
104
104
  - lib/rails/hyperdrive/companion_discovery.rb
105
105
  - lib/rails/hyperdrive/console_executor.rb
106
106
  - lib/rails/hyperdrive/drift_verdict.rb
107
107
  - lib/rails/hyperdrive/eager_footprint.rb
108
108
  - lib/rails/hyperdrive/engine.rb
109
+ - lib/rails/hyperdrive/gem_manifest.rb
109
110
  - lib/rails/hyperdrive/index_document.rb
110
111
  - lib/rails/hyperdrive/install_layout.rb
111
112
  - lib/rails/hyperdrive/install_pipeline.rb
@@ -116,6 +117,7 @@ files:
116
117
  - lib/rails/hyperdrive/resources/skill.rb
117
118
  - lib/rails/hyperdrive/resources/stack_profile.rb
118
119
  - lib/rails/hyperdrive/safety/rack_middleware.rb
120
+ - lib/rails/hyperdrive/skill_tasks.rb
119
121
  - lib/rails/hyperdrive/skill_template.rb
120
122
  - lib/rails/hyperdrive/sql_safety.rb
121
123
  - lib/rails/hyperdrive/stack_profile.rb
@@ -1,3 +0,0 @@
1
- Rails::Hyperdrive.configure do |c|
2
- c.mount_at = "<%= mount_path %>"
3
- end