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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -1
- data/README.md +60 -30
- data/lib/generators/hyperdrive/install/install_generator.rb +9 -6
- data/lib/generators/hyperdrive/sync/sync_generator.rb +1 -1
- data/lib/generators/hyperdrive/sync_runner.rb +17 -2
- data/lib/rails/hyperdrive/auto_install.rb +3 -1
- data/lib/rails/hyperdrive/bundler_artifact_discovery.rb +172 -82
- data/lib/rails/hyperdrive/canonical_skill_render.rb +105 -0
- data/lib/rails/hyperdrive/gem_manifest.rb +175 -0
- data/lib/rails/hyperdrive/install_pipeline.rb +27 -2
- data/lib/rails/hyperdrive/lock_file.rb +15 -0
- data/lib/rails/hyperdrive/skill_tasks.rb +27 -0
- data/lib/rails/hyperdrive/skill_template.rb +25 -0
- data/lib/rails/hyperdrive/stack_profile.rb +20 -1
- data/lib/rails/hyperdrive/tools/run_sql.rb +1 -1
- data/lib/rails/hyperdrive/version.rb +1 -1
- data/lib/rails/hyperdrive.rb +0 -22
- metadata +4 -2
- data/lib/generators/hyperdrive/install/templates/initializer.rb.tt +0 -3
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
data/lib/rails/hyperdrive.rb
CHANGED
|
@@ -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
|
+
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
|