rails-hyperdrive 0.5.0 → 0.7.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 +299 -4
- data/README.md +71 -36
- data/lib/generators/hyperdrive/install/USAGE +11 -3
- data/lib/generators/hyperdrive/install/install_generator.rb +25 -10
- data/lib/generators/hyperdrive/install_summary.rb +3 -3
- data/lib/generators/hyperdrive/sync/USAGE +7 -5
- data/lib/generators/hyperdrive/sync/sync_generator.rb +1 -1
- data/lib/generators/hyperdrive/sync_runner.rb +20 -10
- data/lib/hyperdrive/skill_tasks.rb +39 -0
- data/lib/rails/commands/hyperdrive/hyperdrive_command.rb +59 -0
- data/lib/rails/hyperdrive/ancestor_locator.rb +10 -13
- data/lib/rails/hyperdrive/artifact_status.rb +16 -7
- data/lib/rails/hyperdrive/auto_install.rb +25 -7
- data/lib/rails/hyperdrive/bundler_artifact_discovery.rb +360 -150
- data/lib/rails/hyperdrive/canonical_skill_render.rb +68 -39
- data/lib/rails/hyperdrive/companion_discovery.rb +4 -5
- data/lib/rails/hyperdrive/gem_manifest.rb +311 -80
- data/lib/rails/hyperdrive/gemspec_locator.rb +38 -0
- data/lib/rails/hyperdrive/install_layout.rb +148 -12
- data/lib/rails/hyperdrive/install_pipeline.rb +142 -78
- data/lib/rails/hyperdrive/install_plan.rb +21 -9
- data/lib/rails/hyperdrive/lock_file.rb +25 -3
- data/lib/rails/hyperdrive/manifest_lint.rb +230 -0
- data/lib/rails/hyperdrive/skill_template.rb +7 -5
- 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 +9 -8
- data/lib/generators/hyperdrive/install/templates/initializer.rb.tt +0 -3
- data/lib/rails/hyperdrive/skill_tasks.rb +0 -27
- data/lib/tasks/hyperdrive.rake +0 -22
|
@@ -20,12 +20,15 @@ module Rails
|
|
|
20
20
|
MCP_SERVER_KEY = "rails-hyperdrive".freeze
|
|
21
21
|
|
|
22
22
|
GEMFILE = "Gemfile".freeze
|
|
23
|
-
BUNDLER_PLUGIN = "bundler-
|
|
23
|
+
BUNDLER_PLUGIN = "bundler-hyperdrive".freeze
|
|
24
24
|
|
|
25
|
+
# No templates are rendered; source_root exists so Rails resolves the
|
|
26
|
+
# sibling USAGE file for `--help`.
|
|
25
27
|
source_root File.expand_path("templates", __dir__)
|
|
26
28
|
|
|
27
29
|
class_option :mount_at, type: :string, default: DEFAULT_MOUNT_AT, desc: "Engine mount path."
|
|
28
|
-
class_option :skip_content, type: :boolean, default: false, desc: "Skip all .claude content, CLAUDE.md, and the lockfile;
|
|
30
|
+
class_option :skip_content, type: :boolean, default: false, desc: "Skip all .claude content, CLAUDE.md, and the lockfile; leave the .mcp.json, .gitignore, Gemfile, and mount steps."
|
|
31
|
+
class_option :skip_mcp, type: :boolean, default: false, desc: "Skip MCP setup entirely; write no .mcp.json entry and no engine mount."
|
|
29
32
|
class_option :dry_run, type: :boolean, default: false, desc: "Show what would change; write nothing."
|
|
30
33
|
|
|
31
34
|
def verify_environment
|
|
@@ -39,6 +42,8 @@ module Rails
|
|
|
39
42
|
# The write is forced: Thor's conflict prompt would otherwise block the
|
|
40
43
|
# run waiting on stdin.
|
|
41
44
|
def write_mcp_json
|
|
45
|
+
return if options[:skip_mcp]
|
|
46
|
+
|
|
42
47
|
existing = mcp_json_on_disk
|
|
43
48
|
document = existing ? parse_mcp_json(existing) : {}
|
|
44
49
|
return if document.nil?
|
|
@@ -77,12 +82,9 @@ module Rails
|
|
|
77
82
|
append_to_file GEMFILE, "#{prefix}plugin #{BUNDLER_PLUGIN.inspect}\n"
|
|
78
83
|
end
|
|
79
84
|
|
|
80
|
-
def write_initializer
|
|
81
|
-
return if mount_path == DEFAULT_MOUNT_AT
|
|
82
|
-
template "initializer.rb.tt", "config/initializers/hyperdrive.rb"
|
|
83
|
-
end
|
|
84
|
-
|
|
85
85
|
def mount_engine
|
|
86
|
+
return if options[:skip_mcp]
|
|
87
|
+
|
|
86
88
|
routes_file = "config/routes.rb"
|
|
87
89
|
unless File.exist?(::Rails.root.join(routes_file))
|
|
88
90
|
say_status :skip, "no #{routes_file} found; skipping engine mount", :yellow
|
|
@@ -110,17 +112,30 @@ module Rails
|
|
|
110
112
|
def print_summary
|
|
111
113
|
say ""
|
|
112
114
|
say_status :done, "hyperdrive initialized", :green
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
if options[:skip_mcp]
|
|
116
|
+
say " MCP: skipped (--skip-mcp)"
|
|
117
|
+
else
|
|
118
|
+
say " Mount: #{mount_path} (in config/routes.rb)"
|
|
119
|
+
say " Server: #{::Rails::Hyperdrive::McpServer::TOOLS.size} MCP tools at http://localhost:3000#{mount_path}/mcp"
|
|
120
|
+
end
|
|
115
121
|
runner.summary_lines.each { |line| say line } unless options[:skip_content]
|
|
122
|
+
# Every next step is about reaching the MCP endpoint.
|
|
123
|
+
return if options[:skip_mcp]
|
|
124
|
+
|
|
116
125
|
say ""
|
|
117
126
|
say " Next steps:"
|
|
118
127
|
say " 1. bin/rails server"
|
|
119
128
|
say " 2. Open Claude Code in this directory; it will read .mcp.json"
|
|
120
|
-
say " 3. Verify the connection:
|
|
129
|
+
say " 3. Verify the connection: #{connection_check_command}"
|
|
121
130
|
end
|
|
122
131
|
|
|
123
132
|
no_tasks do
|
|
133
|
+
def connection_check_command
|
|
134
|
+
"curl -s http://localhost:3000#{mount_path}/mcp " \
|
|
135
|
+
"-H 'Content-Type: application/json' -H 'Accept: application/json' " \
|
|
136
|
+
"-d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'"
|
|
137
|
+
end
|
|
138
|
+
|
|
124
139
|
def mcp_json_on_disk
|
|
125
140
|
abs = ::Rails.root.join(MCP_JSON_PATH)
|
|
126
141
|
File.exist?(abs) ? File.read(abs) : nil
|
|
@@ -4,8 +4,8 @@ module Rails
|
|
|
4
4
|
module Generators
|
|
5
5
|
module Hyperdrive
|
|
6
6
|
module InstallSummary
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
KIND_ORDER = ::Rails::Hyperdrive::InstallLayout.content_kinds.map(&:lock_kind).freeze
|
|
8
|
+
KIND_WIDTH = KIND_ORDER.map(&:length).max
|
|
9
9
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
@@ -38,7 +38,7 @@ module Rails
|
|
|
38
38
|
|
|
39
39
|
def installed_counts(entries)
|
|
40
40
|
counts = entries.group_by { |e| e.kind.to_s }.transform_values(&:size)
|
|
41
|
-
"Installed #{quantify(counts[
|
|
41
|
+
"Installed #{KIND_ORDER.map { |kind| quantify(counts[kind].to_i, kind) }.join(", ")}"
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def group_by_source(entries)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Description:
|
|
2
2
|
Syncs Rails Hyperdrive content to match the current bundle (run after
|
|
3
3
|
`bundle update` or after adding a companion gem):
|
|
4
|
-
- discovers companion-gem skills
|
|
5
|
-
them (skills to .claude/skills/, guidelines to
|
|
6
|
-
.claude/hyperdrive/guidelines/
|
|
4
|
+
- discovers companion-gem skills, guidelines, agents, and commands in the
|
|
5
|
+
bundle and installs them (skills to .claude/skills/, guidelines to
|
|
6
|
+
.claude/hyperdrive/guidelines/, agents to .claude/agents/, commands to
|
|
7
|
+
.claude/commands/)
|
|
7
8
|
- maintains the .claude/hyperdrive/index.md aggregator and the single
|
|
8
9
|
@-include line in CLAUDE.md — both only while a companion gem ships a
|
|
9
10
|
guideline, and both removed when the last one goes
|
|
@@ -24,8 +25,9 @@ Description:
|
|
|
24
25
|
upstream wholesale. Either way the next sync is quiet: the lock already
|
|
25
26
|
records the delivered upstream, so the same version is never re-offered.
|
|
26
27
|
|
|
27
|
-
Touches no bootstrap artifact: .mcp.json, the engine mount, the
|
|
28
|
-
|
|
28
|
+
Touches no bootstrap artifact: .mcp.json, the engine mount, the Gemfile
|
|
29
|
+
plugin directive, and .gitignore belong to hyperdrive:init and are left
|
|
30
|
+
alone.
|
|
29
31
|
|
|
30
32
|
Examples:
|
|
31
33
|
bin/rails hyperdrive:sync
|
|
@@ -6,7 +6,7 @@ module Rails
|
|
|
6
6
|
module Generators
|
|
7
7
|
module Hyperdrive
|
|
8
8
|
# Content-only by contract: no step may write a bootstrap artifact
|
|
9
|
-
# (.mcp.json, the engine mount, the
|
|
9
|
+
# (.mcp.json, the engine mount, the .gitignore rule).
|
|
10
10
|
class SyncGenerator < ::Rails::Generators::Base
|
|
11
11
|
include ContentSyncSupport
|
|
12
12
|
|
|
@@ -32,18 +32,18 @@ module Rails
|
|
|
32
32
|
|
|
33
33
|
def discover_artifacts(skip: false)
|
|
34
34
|
@artifacts ||= skip ? [] : ::Rails::Hyperdrive::BundlerArtifactDiscovery.discover(
|
|
35
|
-
|
|
35
|
+
enabled_gems: enabled_gems, report: report
|
|
36
36
|
)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def install(mode:)
|
|
40
|
+
verify_lock_schema!
|
|
40
41
|
@pipeline = ::Rails::Hyperdrive::InstallPipeline.new(
|
|
41
42
|
root: root,
|
|
42
43
|
shell: @shell,
|
|
43
44
|
artifacts: discover_artifacts,
|
|
44
45
|
mode: mode,
|
|
45
|
-
|
|
46
|
-
notices: notices
|
|
46
|
+
report: report
|
|
47
47
|
)
|
|
48
48
|
@pipeline.call
|
|
49
49
|
end
|
|
@@ -54,24 +54,34 @@ module Rails
|
|
|
54
54
|
|
|
55
55
|
private
|
|
56
56
|
|
|
57
|
+
# Raised from install, so it stops the run before any content write —
|
|
58
|
+
# a dry run included.
|
|
59
|
+
def verify_lock_schema!
|
|
60
|
+
return unless lock.schema_ahead?
|
|
61
|
+
|
|
62
|
+
message = lock.schema_ahead_message(::Rails::Hyperdrive::InstallLayout::LOCK_PATH)
|
|
63
|
+
@shell.say_status :error, message, :red
|
|
64
|
+
raise Thor::Error, "hyperdrive: #{message}"
|
|
65
|
+
end
|
|
66
|
+
|
|
57
67
|
# Resolved lazily so verify_environment! can report a missing Rails app
|
|
58
68
|
# before anything dereferences ::Rails.root.
|
|
59
69
|
def root
|
|
60
70
|
@root ||= ::Rails.root.to_s
|
|
61
71
|
end
|
|
62
72
|
|
|
63
|
-
def
|
|
64
|
-
@
|
|
73
|
+
def report
|
|
74
|
+
@report ||= ::Rails::Hyperdrive::BundlerArtifactDiscovery::Report.new
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
def
|
|
68
|
-
@
|
|
77
|
+
def lock
|
|
78
|
+
@lock ||= ::Rails::Hyperdrive::LockFile.load(
|
|
79
|
+
File.join(root, ::Rails::Hyperdrive::InstallLayout::LOCK_PATH)
|
|
80
|
+
)
|
|
69
81
|
end
|
|
70
82
|
|
|
71
83
|
def enabled_gems
|
|
72
|
-
|
|
73
|
-
File.join(root, ::Rails::Hyperdrive::InstallLayout::LOCK_PATH)
|
|
74
|
-
).enabled_gems
|
|
84
|
+
lock.enabled_gems
|
|
75
85
|
end
|
|
76
86
|
|
|
77
87
|
def lock_entries
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Rake tasks for companion gem repos: add `require "hyperdrive/skill_tasks"`
|
|
2
|
+
# to the Rakefile. Every task takes 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
|
+
require "rails/hyperdrive/manifest_lint"
|
|
7
|
+
|
|
8
|
+
namespace :hyperdrive do
|
|
9
|
+
namespace :skills do
|
|
10
|
+
desc "Render each SKILL.md.erb template to its canonical static SKILL.md"
|
|
11
|
+
task :render, [:gemspec] do |_t, args|
|
|
12
|
+
written = Rails::Hyperdrive::CanonicalSkillRender.write(gemspec: args[:gemspec])
|
|
13
|
+
written.each { |r| puts "render #{r.dest}" }
|
|
14
|
+
puts "no skill templates found" if written.empty?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "Fail when a canonical skill file is stale, or raw ERB sits under a public skills root"
|
|
18
|
+
task :check, [:gemspec] do |_t, args|
|
|
19
|
+
roots = Rails::Hyperdrive::CanonicalSkillRender.resolve_roots(gemspec: args[:gemspec])
|
|
20
|
+
stale = Rails::Hyperdrive::CanonicalSkillRender.stale(roots: roots)
|
|
21
|
+
raw = Rails::Hyperdrive::CanonicalSkillRender.public_erb_templates(roots: roots)
|
|
22
|
+
stale.each { |r| warn "stale: #{r.dest}" }
|
|
23
|
+
raw.each { |p| warn "raw ERB: #{p}" }
|
|
24
|
+
abort "#{stale.size} canonical skill file(s) stale; run `rake hyperdrive:skills:render`" unless stale.empty?
|
|
25
|
+
abort "#{raw.size} ERB template(s) under a public skills root; move them to the template directory" unless raw.empty?
|
|
26
|
+
puts "canonical skill files up to date"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
namespace :manifest do
|
|
31
|
+
desc "Fail on unknown keys, unparsable gating, or entries naming nothing shipped in hyperdrive.yml"
|
|
32
|
+
task :check, [:gemspec] do |_t, args|
|
|
33
|
+
result = Rails::Hyperdrive::ManifestLint.check(gemspec: args[:gemspec])
|
|
34
|
+
result.problems.each { |p| warn "#{result.manifest}: #{p}" }
|
|
35
|
+
abort "#{result.problems.size} problem(s) in #{result.manifest}" unless result.problems.empty?
|
|
36
|
+
puts result.manifest ? "#{result.manifest} is clean" : "no hyperdrive.yml; nothing to lint"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require "rails/command"
|
|
2
|
+
require "rails/generators"
|
|
3
|
+
require "generators/hyperdrive/install/install_generator"
|
|
4
|
+
require "generators/hyperdrive/sync/sync_generator"
|
|
5
|
+
require "generators/hyperdrive/discover/discover_generator"
|
|
6
|
+
|
|
7
|
+
module Rails
|
|
8
|
+
module Command
|
|
9
|
+
# The generators are the sole authority on option parsing, defaults, and
|
|
10
|
+
# validation; this class only forwards argv and renders `--help`.
|
|
11
|
+
class HyperdriveCommand < Base
|
|
12
|
+
namespace "hyperdrive"
|
|
13
|
+
|
|
14
|
+
# Mirrors a generator's own flags so the help surface cannot drift from
|
|
15
|
+
# what the generator actually accepts. Thor's runtime options are
|
|
16
|
+
# excluded: they are inherited plumbing, not part of this command.
|
|
17
|
+
def self.mirror_generator_options(klass)
|
|
18
|
+
klass.class_options.each do |name, option|
|
|
19
|
+
next if ::Rails::Generators::Base.class_options.key?(name)
|
|
20
|
+
method_options[name] = option
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Thor treats a leading `--` as an options terminator, so with `-- --merge`
|
|
25
|
+
# the flag lands in positional args and `options[:merge]` is false. Both
|
|
26
|
+
# spellings must reach the generator identically.
|
|
27
|
+
def initialize(args = [], local_options = {}, config = {})
|
|
28
|
+
super
|
|
29
|
+
@argv = args + (local_options.is_a?(Array) ? local_options : [])
|
|
30
|
+
@argv = @argv.drop(1) if @argv.first == "--"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "init", "Install Rails Hyperdrive into this app (writes .mcp.json, CLAUDE.md, companion content, mounts engine)"
|
|
34
|
+
mirror_generator_options ::Rails::Generators::Hyperdrive::InstallGenerator
|
|
35
|
+
def init(*)
|
|
36
|
+
start_generator("install", "InstallGenerator")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc "sync", "Sync Rails Hyperdrive content (skills, guidelines, agents, commands, index.md, lockfile); locally-edited files are preserved"
|
|
40
|
+
mirror_generator_options ::Rails::Generators::Hyperdrive::SyncGenerator
|
|
41
|
+
def sync(*)
|
|
42
|
+
start_generator("sync", "SyncGenerator")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "discover", "Suggest uninstalled rails-hyperdrive companion gems for this app's stack (networked, cached)"
|
|
46
|
+
mirror_generator_options ::Rails::Generators::Hyperdrive::DiscoverGenerator
|
|
47
|
+
def discover(*)
|
|
48
|
+
start_generator("discover", "DiscoverGenerator")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
no_commands do
|
|
52
|
+
def start_generator(name, generator)
|
|
53
|
+
require_application!
|
|
54
|
+
::Rails::Generators::Hyperdrive.const_get(generator).start(@argv)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "bundler"
|
|
2
2
|
require "rails/hyperdrive/bundler_artifact_discovery"
|
|
3
3
|
require "rails/hyperdrive/drift_verdict"
|
|
4
|
+
require "rails/hyperdrive/install_layout"
|
|
4
5
|
require "rails/hyperdrive/skill_template"
|
|
5
6
|
|
|
6
7
|
module Rails
|
|
@@ -49,19 +50,15 @@ module Rails
|
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
def install_ready(body, kind:, final_name:)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
else
|
|
63
|
-
body
|
|
64
|
-
end
|
|
53
|
+
type = InstallLayout::ARTIFACT_TYPES[kind.to_s]
|
|
54
|
+
descriptor = type && InstallLayout.kind(type)
|
|
55
|
+
return body unless descriptor
|
|
56
|
+
|
|
57
|
+
ready = BundlerArtifactDiscovery.install_ready_body(
|
|
58
|
+
BundlerArtifactDiscovery::Artifact.new(artifact_type: type, body: body)
|
|
59
|
+
)
|
|
60
|
+
return ready unless final_name && descriptor.collision_rewrites_name
|
|
61
|
+
ready.sub(/^name:\s*.+$/, "name: #{final_name}")
|
|
65
62
|
end
|
|
66
63
|
|
|
67
64
|
def resolved_bundle
|
|
@@ -10,26 +10,34 @@ module Rails
|
|
|
10
10
|
class ArtifactStatus
|
|
11
11
|
STATES = %i[installed missing outdated orphaned].freeze
|
|
12
12
|
|
|
13
|
-
Entry = Struct.new(:path, :state, :artifact, :locked_source, :bundle_source,
|
|
13
|
+
Entry = Struct.new(:path, :state, :artifact, :locked_source, :bundle_source, :source_gem, :source_bundled,
|
|
14
|
+
keyword_init: true) do
|
|
14
15
|
def to_s
|
|
15
16
|
case state
|
|
16
17
|
when :missing then "#{path} (from #{bundle_source})"
|
|
17
18
|
when :outdated then "#{path} (#{locked_source} → #{bundle_source})"
|
|
18
|
-
when :orphaned then "#{path} (
|
|
19
|
+
when :orphaned then "#{path} (#{orphan_reason})"
|
|
19
20
|
else path
|
|
20
21
|
end
|
|
21
22
|
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def orphan_reason
|
|
27
|
+
LockFile.orphan_reason(source_label: locked_source, source_gem: source_gem, bundled: source_bundled)
|
|
28
|
+
end
|
|
22
29
|
end
|
|
23
30
|
|
|
24
|
-
def self.compare(root:, artifacts:)
|
|
25
|
-
new(root: root, artifacts: artifacts).tap(&:compare)
|
|
31
|
+
def self.compare(root:, artifacts:, bundled_gems: [])
|
|
32
|
+
new(root: root, artifacts: artifacts, bundled_gems: bundled_gems).tap(&:compare)
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
attr_reader :entries
|
|
29
36
|
|
|
30
|
-
def initialize(root:, artifacts:)
|
|
37
|
+
def initialize(root:, artifacts:, bundled_gems: [])
|
|
31
38
|
@root = File.expand_path(root.to_s)
|
|
32
39
|
@artifacts = artifacts
|
|
40
|
+
@bundled_gems = Array(bundled_gems).map(&:to_s)
|
|
33
41
|
@entries = []
|
|
34
42
|
end
|
|
35
43
|
|
|
@@ -63,11 +71,12 @@ module Rails
|
|
|
63
71
|
# A disabled artifact left on disk was reported at install time; the
|
|
64
72
|
# bundle still ships it, so it is not an orphan.
|
|
65
73
|
type = InstallLayout::ARTIFACT_TYPES[locked.kind]
|
|
66
|
-
next if type && InstallPlan.disabled_dest?(lock, type, locked.path)
|
|
74
|
+
next if type && InstallPlan.disabled_dest?(lock, type, locked.path, source_gem: locked.source_gem)
|
|
67
75
|
|
|
68
76
|
@entries << Entry.new(
|
|
69
77
|
path: locked.path, state: :orphaned, artifact: locked.kind&.to_sym,
|
|
70
|
-
locked_source: locked.source_label, bundle_source: nil
|
|
78
|
+
locked_source: locked.source_label, bundle_source: nil,
|
|
79
|
+
source_gem: locked.source_gem, source_bundled: @bundled_gems.include?(locked.source_gem.to_s)
|
|
71
80
|
)
|
|
72
81
|
end
|
|
73
82
|
|
|
@@ -13,7 +13,8 @@ module Rails
|
|
|
13
13
|
module AutoInstall
|
|
14
14
|
DEVELOPMENT = "development".freeze
|
|
15
15
|
|
|
16
|
-
Result = Struct.new(:installed, :outdated, :orphaned, :unwired, :
|
|
16
|
+
Result = Struct.new(:installed, :outdated, :orphaned, :unwired, :advisories, :fence_warnings,
|
|
17
|
+
:halted, :skipped, :error, keyword_init: true) do
|
|
17
18
|
def ran?
|
|
18
19
|
skipped.nil?
|
|
19
20
|
end
|
|
@@ -24,6 +25,8 @@ module Rails
|
|
|
24
25
|
|
|
25
26
|
def messages
|
|
26
27
|
return [] unless ran?
|
|
28
|
+
return [halted] if halted
|
|
29
|
+
|
|
27
30
|
lines = []
|
|
28
31
|
unless Array(installed).empty?
|
|
29
32
|
lines << "installed #{installed.size} artifact(s):"
|
|
@@ -35,6 +38,10 @@ module Rails
|
|
|
35
38
|
lines << "#{stale.size} artifact(s) need attention — run bin/rails hyperdrive:sync"
|
|
36
39
|
stale.each { |entry| lines << " #{entry}" }
|
|
37
40
|
end
|
|
41
|
+
# An advisory names content that installed but not as the manifest
|
|
42
|
+
# asked; ordinary skips are init/sync's to report, not a bundle
|
|
43
|
+
# install's.
|
|
44
|
+
(Array(advisories) | Array(fence_warnings)).each { |warning| lines << warning }
|
|
38
45
|
lines
|
|
39
46
|
end
|
|
40
47
|
end
|
|
@@ -47,9 +54,12 @@ module Rails
|
|
|
47
54
|
return skip(:not_development) unless development?(env)
|
|
48
55
|
return skip(:not_initialized) unless File.exist?(File.join(root, InstallLayout::LOCK_PATH))
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
lock = LockFile.load(File.join(root, InstallLayout::LOCK_PATH))
|
|
58
|
+
return halted(lock.schema_ahead_message(InstallLayout::LOCK_PATH)) if lock.schema_ahead?
|
|
59
|
+
|
|
60
|
+
report = BundlerArtifactDiscovery::Report.new
|
|
61
|
+
artifacts = BundlerArtifactDiscovery.discover(enabled_gems: lock.enabled_gems, report: report)
|
|
62
|
+
status = ArtifactStatus.compare(root: root, artifacts: artifacts, bundled_gems: report.bundled_gems)
|
|
53
63
|
|
|
54
64
|
installed = []
|
|
55
65
|
unwired = false
|
|
@@ -59,7 +69,8 @@ module Rails
|
|
|
59
69
|
root: root,
|
|
60
70
|
shell: InstallShell.new(root: root, io: io),
|
|
61
71
|
artifacts: artifacts,
|
|
62
|
-
mode: :additive
|
|
72
|
+
mode: :additive,
|
|
73
|
+
report: report
|
|
63
74
|
)
|
|
64
75
|
installed = pipeline.call.installed
|
|
65
76
|
# This runs from a bundle install, which must not edit CLAUDE.md, so a
|
|
@@ -68,7 +79,8 @@ module Rails
|
|
|
68
79
|
unwired = pipeline.lock.claude_md_state.nil? && (installed & pipeline.lock.guideline_paths).any?
|
|
69
80
|
end
|
|
70
81
|
|
|
71
|
-
Result.new(installed: installed, outdated: status.outdated, orphaned: status.orphaned,
|
|
82
|
+
Result.new(installed: installed, outdated: status.outdated, orphaned: status.orphaned,
|
|
83
|
+
unwired: unwired, advisories: report.advisories, fence_warnings: report.fence_warnings)
|
|
72
84
|
rescue StandardError => e
|
|
73
85
|
skip(:error, e)
|
|
74
86
|
end
|
|
@@ -93,7 +105,13 @@ module Rails
|
|
|
93
105
|
end
|
|
94
106
|
|
|
95
107
|
def skip(reason, error = nil)
|
|
96
|
-
Result.new(installed: [], outdated: [], orphaned: [],
|
|
108
|
+
Result.new(installed: [], outdated: [], orphaned: [], advisories: [], fence_warnings: [],
|
|
109
|
+
skipped: reason, error: error)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# A completed run that installed nothing and has a reason to print.
|
|
113
|
+
def halted(reason)
|
|
114
|
+
Result.new(installed: [], outdated: [], orphaned: [], advisories: [], fence_warnings: [], halted: reason)
|
|
97
115
|
end
|
|
98
116
|
end
|
|
99
117
|
end
|