rails-hyperdrive 0.3.0 → 0.5.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 +137 -1
- data/README.md +96 -102
- data/lib/generators/hyperdrive/install/USAGE +3 -2
- data/lib/generators/hyperdrive/sync/USAGE +17 -3
- data/lib/generators/hyperdrive/sync/sync_generator.rb +16 -1
- data/lib/generators/hyperdrive/sync_runner.rb +17 -2
- data/lib/rails/hyperdrive/ancestor_locator.rb +76 -0
- data/lib/rails/hyperdrive/artifact_status.rb +1 -1
- data/lib/rails/hyperdrive/auto_install.rb +3 -1
- data/lib/rails/hyperdrive/bundler_artifact_discovery.rb +172 -55
- data/lib/rails/hyperdrive/canonical_skill_render.rb +105 -0
- data/lib/rails/hyperdrive/drift_verdict.rb +9 -14
- data/lib/rails/hyperdrive/gem_manifest.rb +175 -0
- data/lib/rails/hyperdrive/install_layout.rb +4 -0
- data/lib/rails/hyperdrive/install_pipeline.rb +212 -31
- data/lib/rails/hyperdrive/lock_file.rb +15 -0
- data/lib/rails/hyperdrive/resources/stack_profile.rb +1 -1
- 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 +31 -40
- data/lib/rails/hyperdrive/three_way_merge.rb +47 -0
- data/lib/rails/hyperdrive/tools/describe_app.rb +1 -1
- data/lib/rails/hyperdrive/version.rb +1 -1
- data/lib/tasks/hyperdrive.rake +1 -1
- metadata +6 -3
- data/lib/rails/hyperdrive/audit_header.rb +0 -83
- data/lib/rails/hyperdrive/data/gem_categories.yml +0 -52
|
@@ -5,15 +5,7 @@ require "digest"
|
|
|
5
5
|
module Rails
|
|
6
6
|
module Hyperdrive
|
|
7
7
|
class StackProfile
|
|
8
|
-
CATEGORIES_PATH = File.expand_path("data/gem_categories.yml", __dir__)
|
|
9
|
-
|
|
10
8
|
class << self
|
|
11
|
-
def categories
|
|
12
|
-
@categories ||= YAML.load_file(CATEGORIES_PATH).each_with_object({}) do |(cat, gems), acc|
|
|
13
|
-
gems.each { |g| acc[g] = cat.to_sym }
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
9
|
def current
|
|
18
10
|
@current ||= from_lockfile(default_lockfile_path)
|
|
19
11
|
end
|
|
@@ -54,16 +46,11 @@ module Rails
|
|
|
54
46
|
specs = parser.specs.each_with_object({}) { |s, h| h[s.name] = s.version.to_s }
|
|
55
47
|
|
|
56
48
|
@data = {
|
|
57
|
-
rails:
|
|
58
|
-
ruby:
|
|
59
|
-
database:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
frontend: frontend_info(specs),
|
|
63
|
-
auth: bucket(specs, :auth),
|
|
64
|
-
authz: bucket(specs, :authz),
|
|
65
|
-
db_gems: bucket(specs, :db),
|
|
66
|
-
gem_skills: gem_skills_info
|
|
49
|
+
rails: rails_info(specs),
|
|
50
|
+
ruby: ruby_info(parser),
|
|
51
|
+
database: database_info(specs),
|
|
52
|
+
direct_dependencies: direct_dependencies(parser, specs),
|
|
53
|
+
gem_skills: gem_skills_info
|
|
67
54
|
}
|
|
68
55
|
self
|
|
69
56
|
end
|
|
@@ -77,8 +64,7 @@ module Rails
|
|
|
77
64
|
def empty_profile
|
|
78
65
|
{
|
|
79
66
|
rails: {}, ruby: {}, database: {},
|
|
80
|
-
|
|
81
|
-
auth: [], authz: [], db_gems: [], gem_skills: []
|
|
67
|
+
direct_dependencies: [], gem_skills: []
|
|
82
68
|
}
|
|
83
69
|
end
|
|
84
70
|
|
|
@@ -132,33 +118,20 @@ module Rails
|
|
|
132
118
|
::Rails.root.join("config", "database.yml").to_s
|
|
133
119
|
end
|
|
134
120
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
{ key: name.to_sym, version: version }
|
|
121
|
+
# Presence in the resolved specs does not mean the app chose a gem —
|
|
122
|
+
# minitest resolves under every Rails app. Transitive gems stay out.
|
|
123
|
+
def direct_dependencies(parser, specs)
|
|
124
|
+
parser.dependencies.keys.sort.map do |name|
|
|
125
|
+
{ name: name, version: specs[name] }
|
|
141
126
|
end
|
|
142
127
|
end
|
|
143
128
|
|
|
144
|
-
# Frontend gets a special shape because Hotwire is actually two gems.
|
|
145
|
-
def frontend_info(specs)
|
|
146
|
-
bucket(specs, :frontend).map do |entry|
|
|
147
|
-
if entry[:key] == :"turbo-rails" || entry[:key] == :"stimulus-rails"
|
|
148
|
-
turbo = specs["turbo-rails"]
|
|
149
|
-
stim = specs["stimulus-rails"]
|
|
150
|
-
{ key: :hotwire, versions: { turbo: turbo, stimulus: stim }.compact }
|
|
151
|
-
else
|
|
152
|
-
entry
|
|
153
|
-
end
|
|
154
|
-
end.uniq
|
|
155
|
-
end
|
|
156
|
-
|
|
157
129
|
# Loaded lazily and rescued broadly so StackProfile stays usable where
|
|
158
130
|
# Bundler is absent or refuses to resolve.
|
|
159
131
|
def gem_skills_info
|
|
160
132
|
require "rails/hyperdrive/bundler_artifact_discovery"
|
|
161
|
-
::Rails::Hyperdrive::BundlerArtifactDiscovery.discover
|
|
133
|
+
discovered = ::Rails::Hyperdrive::BundlerArtifactDiscovery.discover(enabled_gems: enabled_gems)
|
|
134
|
+
discovered.select(&:skill?).map do |skill|
|
|
162
135
|
{
|
|
163
136
|
name: skill.name,
|
|
164
137
|
gem: skill.target_gem,
|
|
@@ -171,6 +144,24 @@ module Rails
|
|
|
171
144
|
rescue StandardError
|
|
172
145
|
[]
|
|
173
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
|
|
174
165
|
end
|
|
175
166
|
end
|
|
176
167
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "tmpdir"
|
|
3
|
+
|
|
4
|
+
module Rails
|
|
5
|
+
module Hyperdrive
|
|
6
|
+
# Tempfiles live in the system tmpdir, never in the app, so a dry run
|
|
7
|
+
# stays write-free. Fail-open: a missing or erroring git reads as
|
|
8
|
+
# :unavailable, never an exception.
|
|
9
|
+
module ThreeWayMerge
|
|
10
|
+
Outcome = Struct.new(:status, :body, keyword_init: true) do
|
|
11
|
+
def clean?
|
|
12
|
+
status == :clean
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def merge(ours:, base:, theirs:)
|
|
19
|
+
return Outcome.new(status: :unavailable) if [ours, base, theirs].any? { |b| binary?(b) }
|
|
20
|
+
|
|
21
|
+
Dir.mktmpdir("hyperdrive-merge") do |dir|
|
|
22
|
+
paths = { "ours" => ours, "base" => base, "theirs" => theirs }.map do |name, content|
|
|
23
|
+
File.join(dir, name).tap { |p| File.binwrite(p, content) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
out, _err, status = Open3.capture3("git", "merge-file", "-p", *paths)
|
|
27
|
+
if status.success?
|
|
28
|
+
Outcome.new(status: :clean, body: out)
|
|
29
|
+
elsif status.exited? && status.exitstatus.between?(1, 127)
|
|
30
|
+
# git merge-file exits with the conflict count.
|
|
31
|
+
Outcome.new(status: :conflicted)
|
|
32
|
+
else
|
|
33
|
+
Outcome.new(status: :unavailable)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
rescue SystemCallError, IOError
|
|
37
|
+
Outcome.new(status: :unavailable)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def binary?(content)
|
|
41
|
+
s = content.to_s
|
|
42
|
+
return true if s.include?("\0".b)
|
|
43
|
+
!s.dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -6,7 +6,7 @@ module Rails
|
|
|
6
6
|
module Tools
|
|
7
7
|
class DescribeApp < Base
|
|
8
8
|
tool_name "describe_app"
|
|
9
|
-
description "Snapshot of Rails/Ruby/DB versions plus the
|
|
9
|
+
description "Snapshot of Rails/Ruby/DB versions plus the app's direct gem dependencies and installed gem skills."
|
|
10
10
|
|
|
11
11
|
input_schema(properties: {})
|
|
12
12
|
|
data/lib/tasks/hyperdrive.rake
CHANGED
|
@@ -6,7 +6,7 @@ namespace :hyperdrive do
|
|
|
6
6
|
Rails::Generators::Hyperdrive::InstallGenerator.start(ARGV.drop(1))
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
desc "Sync Rails Hyperdrive content (skills, guidelines, index.md, lockfile); locally-edited files are preserved — pass --
|
|
9
|
+
desc "Sync Rails Hyperdrive content (skills, guidelines, index.md, lockfile); locally-edited files are preserved — pass --merge (three-way merge), --sidecar (deliver upstream to <file>.new), or --overwrite (restore gem-shipped content)"
|
|
10
10
|
task :sync do
|
|
11
11
|
require "rails/generators"
|
|
12
12
|
require "generators/hyperdrive/sync/sync_generator"
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bakaface
|
|
@@ -96,17 +96,18 @@ files:
|
|
|
96
96
|
- lib/generators/hyperdrive/sync_runner.rb
|
|
97
97
|
- lib/rails-hyperdrive.rb
|
|
98
98
|
- lib/rails/hyperdrive.rb
|
|
99
|
+
- lib/rails/hyperdrive/ancestor_locator.rb
|
|
99
100
|
- lib/rails/hyperdrive/artifact_status.rb
|
|
100
|
-
- lib/rails/hyperdrive/audit_header.rb
|
|
101
101
|
- lib/rails/hyperdrive/auto_install.rb
|
|
102
102
|
- lib/rails/hyperdrive/bundler_artifact_discovery.rb
|
|
103
|
+
- lib/rails/hyperdrive/canonical_skill_render.rb
|
|
103
104
|
- lib/rails/hyperdrive/claude_md_import.rb
|
|
104
105
|
- lib/rails/hyperdrive/companion_discovery.rb
|
|
105
106
|
- lib/rails/hyperdrive/console_executor.rb
|
|
106
|
-
- lib/rails/hyperdrive/data/gem_categories.yml
|
|
107
107
|
- lib/rails/hyperdrive/drift_verdict.rb
|
|
108
108
|
- lib/rails/hyperdrive/eager_footprint.rb
|
|
109
109
|
- lib/rails/hyperdrive/engine.rb
|
|
110
|
+
- lib/rails/hyperdrive/gem_manifest.rb
|
|
110
111
|
- lib/rails/hyperdrive/index_document.rb
|
|
111
112
|
- lib/rails/hyperdrive/install_layout.rb
|
|
112
113
|
- lib/rails/hyperdrive/install_pipeline.rb
|
|
@@ -117,9 +118,11 @@ files:
|
|
|
117
118
|
- lib/rails/hyperdrive/resources/skill.rb
|
|
118
119
|
- lib/rails/hyperdrive/resources/stack_profile.rb
|
|
119
120
|
- lib/rails/hyperdrive/safety/rack_middleware.rb
|
|
121
|
+
- lib/rails/hyperdrive/skill_tasks.rb
|
|
120
122
|
- lib/rails/hyperdrive/skill_template.rb
|
|
121
123
|
- lib/rails/hyperdrive/sql_safety.rb
|
|
122
124
|
- lib/rails/hyperdrive/stack_profile.rb
|
|
125
|
+
- lib/rails/hyperdrive/three_way_merge.rb
|
|
123
126
|
- lib/rails/hyperdrive/tools/base.rb
|
|
124
127
|
- lib/rails/hyperdrive/tools/describe_app.rb
|
|
125
128
|
- lib/rails/hyperdrive/tools/list_models.rb
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
require "time"
|
|
2
|
-
|
|
3
|
-
module Rails
|
|
4
|
-
module Hyperdrive
|
|
5
|
-
module AuditHeader
|
|
6
|
-
YAML_LINE = /\A#\s*hyperdrive:/.freeze
|
|
7
|
-
HTML_LINE = /\A<!--\s*hyperdrive:.*-->\s*\z/.freeze
|
|
8
|
-
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
def fields(source_gem:, version:, sha256:, installed_at: Time.now.utc)
|
|
12
|
-
[
|
|
13
|
-
"source=#{source_gem}@#{version}",
|
|
14
|
-
"sha256=#{sha256}",
|
|
15
|
-
"installed_at=#{installed_at.iso8601}"
|
|
16
|
-
]
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def build(source_gem:, version:, sha256:, installed_at: Time.now.utc)
|
|
20
|
-
fields(source_gem: source_gem, version: version, sha256: sha256, installed_at: installed_at)
|
|
21
|
-
.map { |f| "# hyperdrive: #{f}" }
|
|
22
|
-
.join("\n")
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def build_html(source_gem:, version:, sha256:, installed_at: Time.now.utc)
|
|
26
|
-
fields(source_gem: source_gem, version: version, sha256: sha256, installed_at: installed_at)
|
|
27
|
-
.map { |f| "<!-- hyperdrive: #{f} -->" }
|
|
28
|
-
.join("\n")
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def inject_into_frontmatter(body, header_block)
|
|
32
|
-
lines = body.lines
|
|
33
|
-
unless lines.first&.strip == "---"
|
|
34
|
-
return "---\n#{header_block}\n---\n\n#{body}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
closing_index = lines[1..].index { |l| l.strip == "---" }
|
|
38
|
-
return body unless closing_index
|
|
39
|
-
|
|
40
|
-
absolute_closing = closing_index + 1
|
|
41
|
-
lines.insert(absolute_closing, header_block + "\n")
|
|
42
|
-
lines.join
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def prepend_html(body, header_block)
|
|
46
|
-
"#{header_block}\n\n#{body}"
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Strips only the contiguous injected block, never stray "# hyperdrive:"
|
|
50
|
-
# or "<!-- -->" lines elsewhere in the body.
|
|
51
|
-
def strip(body)
|
|
52
|
-
lines = body.lines
|
|
53
|
-
if lines.first&.strip == "---"
|
|
54
|
-
strip_yaml(lines)
|
|
55
|
-
else
|
|
56
|
-
strip_html(lines)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def strip_yaml(lines)
|
|
61
|
-
closing_index = lines[1..].index { |l| l.strip == "---" }
|
|
62
|
-
return lines.join unless closing_index
|
|
63
|
-
|
|
64
|
-
absolute_closing = closing_index + 1
|
|
65
|
-
first = (1...absolute_closing).find { |i| lines[i] =~ YAML_LINE }
|
|
66
|
-
return lines.join unless first
|
|
67
|
-
|
|
68
|
-
last = first
|
|
69
|
-
last += 1 while last < absolute_closing && lines[last] =~ YAML_LINE
|
|
70
|
-
(lines[0...first] + lines[last..]).join
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def strip_html(lines)
|
|
74
|
-
return lines.join unless lines.first =~ HTML_LINE
|
|
75
|
-
|
|
76
|
-
i = 0
|
|
77
|
-
i += 1 while lines[i] =~ HTML_LINE
|
|
78
|
-
i += 1 if lines[i] && lines[i].strip.empty?
|
|
79
|
-
lines[i..].join
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# Category assignment is reporting metadata only; nothing here is executed.
|
|
2
|
-
|
|
3
|
-
test:
|
|
4
|
-
- rspec
|
|
5
|
-
- rspec-rails
|
|
6
|
-
- minitest
|
|
7
|
-
- minitest-rails
|
|
8
|
-
- test-unit
|
|
9
|
-
- capybara
|
|
10
|
-
- cucumber
|
|
11
|
-
- cucumber-rails
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
- sidekiq
|
|
15
|
-
- resque
|
|
16
|
-
- delayed_job
|
|
17
|
-
- good_job
|
|
18
|
-
- solid_queue
|
|
19
|
-
- sucker_punch
|
|
20
|
-
- shoryuken
|
|
21
|
-
- que
|
|
22
|
-
|
|
23
|
-
auth:
|
|
24
|
-
- devise
|
|
25
|
-
- clearance
|
|
26
|
-
- sorcery
|
|
27
|
-
- authlogic
|
|
28
|
-
- rodauth
|
|
29
|
-
- rodauth-rails
|
|
30
|
-
|
|
31
|
-
authz:
|
|
32
|
-
- pundit
|
|
33
|
-
- cancancan
|
|
34
|
-
- action_policy
|
|
35
|
-
|
|
36
|
-
frontend:
|
|
37
|
-
- turbo-rails
|
|
38
|
-
- stimulus-rails
|
|
39
|
-
- importmap-rails
|
|
40
|
-
- jsbundling-rails
|
|
41
|
-
- cssbundling-rails
|
|
42
|
-
- view_component
|
|
43
|
-
- phlex
|
|
44
|
-
- phlex-rails
|
|
45
|
-
- hotwire-rails
|
|
46
|
-
|
|
47
|
-
db:
|
|
48
|
-
- pg
|
|
49
|
-
- mysql2
|
|
50
|
-
- sqlite3
|
|
51
|
-
- trilogy
|
|
52
|
-
- sequel
|