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
|
@@ -4,11 +4,11 @@ module Rails
|
|
|
4
4
|
module Hyperdrive
|
|
5
5
|
# installed_at is volatile metadata, never an input to any comparison.
|
|
6
6
|
class LockFile
|
|
7
|
-
SCHEMA_VERSION =
|
|
7
|
+
SCHEMA_VERSION = 2
|
|
8
8
|
STATE_PRESENT = "present".freeze
|
|
9
9
|
STATE_REMOVED = "removed-by-user".freeze
|
|
10
10
|
|
|
11
|
-
DISABLED_KEYS = { skill: "skills", guideline: "guidelines" }.freeze
|
|
11
|
+
DISABLED_KEYS = { skill: "skills", guideline: "guidelines", agent: "agents", command: "commands" }.freeze
|
|
12
12
|
|
|
13
13
|
# In-memory form of one files: entry. On disk, source_gem and
|
|
14
14
|
# source_version are a single "gem@version" string; the split/join lives
|
|
@@ -19,7 +19,7 @@ module Rails
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
attr_reader :path
|
|
22
|
+
attr_reader :path, :schema_version
|
|
23
23
|
attr_accessor :claude_md_state
|
|
24
24
|
|
|
25
25
|
# Load existing lock state from disk (absent file → empty lock).
|
|
@@ -27,6 +27,13 @@ module Rails
|
|
|
27
27
|
new(path).tap(&:read)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Every surface reporting an orphan must name the same two cases, or a
|
|
31
|
+
# sync and a bundle install would disagree about why a file is stranded.
|
|
32
|
+
def self.orphan_reason(source_label:, source_gem:, bundled:)
|
|
33
|
+
return "no longer shipped by #{source_label}" unless bundled
|
|
34
|
+
"#{source_gem} is still bundled but did not offer this file"
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
def initialize(path)
|
|
31
38
|
@path = path.to_s
|
|
32
39
|
@claude_md_state = nil # nil = no lock has been written yet
|
|
@@ -34,6 +41,7 @@ module Rails
|
|
|
34
41
|
@document = {} # raw parsed YAML, kept so unknown keys survive
|
|
35
42
|
@disabled = empty_disabled
|
|
36
43
|
@enabled = []
|
|
44
|
+
@schema_version = nil
|
|
37
45
|
end
|
|
38
46
|
|
|
39
47
|
def read
|
|
@@ -43,6 +51,7 @@ module Rails
|
|
|
43
51
|
return self unless data.is_a?(Hash)
|
|
44
52
|
|
|
45
53
|
@document = data
|
|
54
|
+
@schema_version = data["version"]
|
|
46
55
|
claude_md = data["claude_md"]
|
|
47
56
|
@claude_md_state = claude_md["state"] if claude_md.is_a?(Hash)
|
|
48
57
|
@disabled = parse_disabled(data["disabled"])
|
|
@@ -57,6 +66,19 @@ module Rails
|
|
|
57
66
|
self
|
|
58
67
|
end
|
|
59
68
|
|
|
69
|
+
# A lock written by a newer installer holds state this one cannot read,
|
|
70
|
+
# so rewriting it would silently drop the user's settings. A missing or
|
|
71
|
+
# non-numeric version reads as not ahead: every lock ever written carries
|
|
72
|
+
# an integer version, and a hand-broken one must not block a sync.
|
|
73
|
+
def schema_ahead?
|
|
74
|
+
@schema_version.is_a?(Numeric) && @schema_version > SCHEMA_VERSION
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def schema_ahead_message(display_path)
|
|
78
|
+
"#{display_path} was written by a newer rails-hyperdrive (lock schema #{@schema_version}, " \
|
|
79
|
+
"this installer supports #{SCHEMA_VERSION}); upgrade rails-hyperdrive"
|
|
80
|
+
end
|
|
81
|
+
|
|
60
82
|
def entry(file_path)
|
|
61
83
|
@files[file_path.to_s]
|
|
62
84
|
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "rails/hyperdrive/bundler_artifact_discovery"
|
|
3
|
+
require "rails/hyperdrive/gem_manifest"
|
|
4
|
+
require "rails/hyperdrive/gemspec_locator"
|
|
5
|
+
require "rails/hyperdrive/install_layout"
|
|
6
|
+
|
|
7
|
+
module Rails
|
|
8
|
+
module Hyperdrive
|
|
9
|
+
# Author-side check of a companion gem's hyperdrive.yml, run in the
|
|
10
|
+
# companion's own repo. Where the installer warns and falls open — so that
|
|
11
|
+
# a manifest written for a newer schema never blocks an install — this
|
|
12
|
+
# fails, and unknown keys fail outright: the author's own rails-hyperdrive
|
|
13
|
+
# is the one that knows the whole schema. A manifest that passes draws no
|
|
14
|
+
# gating warning at install time.
|
|
15
|
+
module ManifestLint
|
|
16
|
+
Error = GemspecLocator::Error
|
|
17
|
+
|
|
18
|
+
GATE_KEYS = %w[gem gems hyperdrive_version].freeze
|
|
19
|
+
DIR_KEYS = GemManifest::DIR_KEYS
|
|
20
|
+
ROOT_KEYS = (GATE_KEYS + DIR_KEYS + InstallLayout.content_kinds.map(&:section)).freeze
|
|
21
|
+
SKILL_ENTRY_KEYS = (GATE_KEYS + %w[conditional]).freeze
|
|
22
|
+
CONDITIONAL_ENTRY_KEYS = %w[gem gems].freeze
|
|
23
|
+
RETIRED_VERSION_KEYS = %w[versions version].freeze
|
|
24
|
+
RETIRED_VERSION_HINT = "is not a gating key; put the requirement on the gem: member, " \
|
|
25
|
+
"e.g. `- name: \">= 1.0\"`".freeze
|
|
26
|
+
|
|
27
|
+
# `manifest` is nil when the gem ships none, which is legal.
|
|
28
|
+
Result = Struct.new(:manifest, :problems, keyword_init: true)
|
|
29
|
+
|
|
30
|
+
# Discovery resolves every root against an installed gem's path; in the
|
|
31
|
+
# companion's repo that is the checkout.
|
|
32
|
+
RepoSpec = Struct.new(:name, :full_gem_path, :metadata, keyword_init: true)
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def check(gemspec: nil, dir: Dir.pwd)
|
|
37
|
+
spec, gem_root = GemspecLocator.load_spec(gemspec, dir)
|
|
38
|
+
relpath = GemManifest.manifest_relpath(spec)
|
|
39
|
+
path = File.join(gem_root, relpath)
|
|
40
|
+
return missing_manifest(spec, relpath) unless File.file?(path)
|
|
41
|
+
|
|
42
|
+
repo_spec = RepoSpec.new(name: spec.name, full_gem_path: gem_root, metadata: spec.metadata || {})
|
|
43
|
+
Result.new(manifest: relpath, problems: problems_in(path, repo_spec))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Shipping no manifest is legal, but a declared path that is not a file
|
|
47
|
+
# leaves the gem opted in as a companion with every artifact ungated —
|
|
48
|
+
# the author-side slip this lint exists to catch.
|
|
49
|
+
def missing_manifest(spec, relpath)
|
|
50
|
+
declared = (spec.metadata || {})[GemManifest::METADATA_KEY].to_s
|
|
51
|
+
return Result.new(manifest: nil, problems: []) unless declared == relpath
|
|
52
|
+
|
|
53
|
+
Result.new(
|
|
54
|
+
manifest: relpath,
|
|
55
|
+
problems: ["gemspec metadata #{GemManifest::METADATA_KEY} names '#{relpath}', which is not a file"]
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def problems_in(path, repo_spec)
|
|
60
|
+
problems = []
|
|
61
|
+
root = parse_root(path, problems)
|
|
62
|
+
return problems if root.nil?
|
|
63
|
+
|
|
64
|
+
check_keys(root, ROOT_KEYS, "top level", problems)
|
|
65
|
+
check_gate(root, "top level", problems)
|
|
66
|
+
check_dirs(root, problems)
|
|
67
|
+
|
|
68
|
+
manifest = GemManifest.load(repo_spec, warnings: [])
|
|
69
|
+
skills = shipped_skills(repo_spec, manifest)
|
|
70
|
+
InstallLayout.content_kinds.each do |kind|
|
|
71
|
+
shipped = kind.dir_shaped? ? skills.keys : shipped_flat(repo_spec, kind, manifest)
|
|
72
|
+
each_entry(root, kind, shipped, problems) do |key, entry|
|
|
73
|
+
where = "#{kind.section} entry '#{key}'"
|
|
74
|
+
check_keys(entry, kind.dir_shaped? ? SKILL_ENTRY_KEYS : GATE_KEYS, where, problems)
|
|
75
|
+
check_gate(entry, where, problems)
|
|
76
|
+
next unless kind.dir_shaped? && entry.key?("conditional")
|
|
77
|
+
check_conditional(entry["conditional"], where, skills[key], problems)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
problems
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# nil means nothing further is readable; an empty manifest is legal.
|
|
85
|
+
def parse_root(path, problems)
|
|
86
|
+
root, failure = GemManifest.read_root(path)
|
|
87
|
+
problems << failure unless root
|
|
88
|
+
root
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def each_entry(root, kind, shipped, problems)
|
|
92
|
+
value = root[kind.section]
|
|
93
|
+
return if value.nil?
|
|
94
|
+
unless value.is_a?(Hash)
|
|
95
|
+
problems << "#{kind.section}: must be a map of #{kind.shipped_label} keys to gating maps"
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
entries = value.transform_keys(&:to_s)
|
|
100
|
+
check_section_settings(entries, kind, problems)
|
|
101
|
+
|
|
102
|
+
entries.each do |key, entry|
|
|
103
|
+
next if Array(kind.prefix_key).include?(key)
|
|
104
|
+
|
|
105
|
+
unless shipped.include?(key)
|
|
106
|
+
problems << "#{kind.section} entry '#{key}' names no shipped #{kind.shipped_label}"
|
|
107
|
+
end
|
|
108
|
+
unless entry.is_a?(Hash)
|
|
109
|
+
problems << "#{kind.section} entry '#{key}' must be a map of gating keys"
|
|
110
|
+
next
|
|
111
|
+
end
|
|
112
|
+
yield key, entry.transform_keys(&:to_s)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A kind's section-level scalar is a setting, not a gating entry.
|
|
117
|
+
def check_section_settings(entries, kind, problems)
|
|
118
|
+
Array(kind.prefix_key).each do |key|
|
|
119
|
+
next unless entries.key?(key)
|
|
120
|
+
|
|
121
|
+
value = entries[key]
|
|
122
|
+
next if value.is_a?(String) && !value.strip.empty? &&
|
|
123
|
+
!value.match?(%r{[/\\]}) && !value.include?("..")
|
|
124
|
+
problems << "#{kind.section}: #{key}: must be a name prefix with no path separators or '..' segments"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def check_keys(map, allowed, where, problems)
|
|
129
|
+
(map.keys - allowed).each do |key|
|
|
130
|
+
problems << if RETIRED_VERSION_KEYS.include?(key)
|
|
131
|
+
"#{where}: '#{key}:' #{RETIRED_VERSION_HINT}"
|
|
132
|
+
else
|
|
133
|
+
"#{where}: unknown key '#{key}'#{did_you_mean(key, allowed)}; " \
|
|
134
|
+
"allowed keys are #{allowed.join(", ")}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def check_dirs(root, problems)
|
|
140
|
+
DIR_KEYS.each do |key|
|
|
141
|
+
next unless root.key?(key)
|
|
142
|
+
|
|
143
|
+
dir, failure = GemManifest.read_dir(root, key)
|
|
144
|
+
next if dir
|
|
145
|
+
problems << "top level: #{failure || "#{key}: must name a directory relative to the gem root"}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def did_you_mean(key, allowed)
|
|
150
|
+
near = allowed.find { |a| a.start_with?(key) || key.start_with?(a) }
|
|
151
|
+
near ? " (did you mean '#{near}'?)" : ""
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The parse rules come from GemManifest itself, so a form it accepts is
|
|
155
|
+
# never reported here and a form it falls open on always is. A parser
|
|
156
|
+
# warning counts as a failure: it names a spelling that installs, but not
|
|
157
|
+
# as written.
|
|
158
|
+
def check_gate(map, where, problems, fence: true)
|
|
159
|
+
gem_key = GemManifest.gem_key(map)
|
|
160
|
+
problems << "#{where}: gem: and gems: are aliases; keep only one" if gem_key.conflict
|
|
161
|
+
|
|
162
|
+
if gem_key.present
|
|
163
|
+
parsed = GemManifest.parse_targets(gem_key.value)
|
|
164
|
+
if parsed.nil? || parsed.targets.empty?
|
|
165
|
+
problems << "#{where}: gem: must name #{GemManifest::GEM_FORMS}"
|
|
166
|
+
elsif parsed.warning
|
|
167
|
+
problems << "#{where}: gem: #{parsed.warning}"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
return unless fence && map.key?("hyperdrive_version")
|
|
172
|
+
return unless GemManifest.malformed_fence?(map["hyperdrive_version"])
|
|
173
|
+
problems << "#{where}: hyperdrive_version: must be a version requirement, e.g. \">= 0.8\""
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# `shipped` is nil when the owning skills entry names no shipped
|
|
177
|
+
# directory, which is already reported — every key would repeat it.
|
|
178
|
+
def check_conditional(value, where, shipped, problems)
|
|
179
|
+
unless value.is_a?(Hash)
|
|
180
|
+
problems << "#{where}: conditional: must be a map of supporting-file path to a gating map"
|
|
181
|
+
return
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
keys = value.keys.map(&:to_s)
|
|
185
|
+
value.each do |key, entry|
|
|
186
|
+
key = key.to_s
|
|
187
|
+
inner = "#{where} conditional key '#{key}'"
|
|
188
|
+
face = BundlerArtifactDiscovery.target_path(key)
|
|
189
|
+
if BundlerArtifactDiscovery::SKILL_FILE_NAMES.include?(key)
|
|
190
|
+
problems << "#{inner}: the entry's own gem: gates the whole skill"
|
|
191
|
+
elsif shipped && !shipped.include?(key)
|
|
192
|
+
problems << "#{inner}: names no shipped supporting file"
|
|
193
|
+
end
|
|
194
|
+
problems << "#{inner}: '#{face}' keys the same file; keep one spelling" if face != key && keys.include?(face)
|
|
195
|
+
|
|
196
|
+
unless entry.is_a?(Hash)
|
|
197
|
+
problems << "#{inner}: must be a map with gem:"
|
|
198
|
+
next
|
|
199
|
+
end
|
|
200
|
+
entry = entry.transform_keys(&:to_s)
|
|
201
|
+
check_keys(entry, CONDITIONAL_ENTRY_KEYS, inner, problems)
|
|
202
|
+
check_gate(entry, inner, problems, fence: false)
|
|
203
|
+
problems << "#{inner}: gem: is required" unless GemManifest.gem_key(entry).present
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Maps each skill's manifest key to the dir-relative paths a conditional:
|
|
208
|
+
# entry may name: every supporting file as shipped, plus the rendered
|
|
209
|
+
# face of each *.md.erb, since either spelling gates the same file.
|
|
210
|
+
def shipped_skills(repo_spec, manifest)
|
|
211
|
+
found = BundlerArtifactDiscovery.skill_paths(repo_spec, manifest: manifest)
|
|
212
|
+
found.each_with_object({}) do |(path, support_root, rel), h|
|
|
213
|
+
shipped = BundlerArtifactDiscovery.support_relpaths(support_root) |
|
|
214
|
+
BundlerArtifactDiscovery.support_relpaths(
|
|
215
|
+
BundlerArtifactDiscovery.template_dir_for(path, support_root), glob: "**/*.md.erb"
|
|
216
|
+
)
|
|
217
|
+
h[rel] = h.fetch(rel, []) | shipped | shipped.map { |p| BundlerArtifactDiscovery.target_path(p) }
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def shipped_flat(repo_spec, kind, manifest)
|
|
222
|
+
BundlerArtifactDiscovery.flat_paths(repo_spec, kind, manifest: manifest).map { |p| File.basename(p) }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private_class_method :missing_manifest, :problems_in, :parse_root, :each_entry, :check_section_settings,
|
|
226
|
+
:check_keys, :check_dirs, :did_you_mean, :check_gate, :check_conditional,
|
|
227
|
+
:shipped_skills, :shipped_flat
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -3,8 +3,9 @@ require "erb"
|
|
|
3
3
|
module Rails
|
|
4
4
|
module Hyperdrive
|
|
5
5
|
# Renders a skill's *.md.erb sources against the app's resolved bundle.
|
|
6
|
-
# The
|
|
7
|
-
#
|
|
6
|
+
# The three helpers are the only supported template API, but the binding is
|
|
7
|
+
# an ordinary object rather than a sandbox: a template runs arbitrary Ruby
|
|
8
|
+
# with the privileges of whoever triggered discovery.
|
|
8
9
|
module SkillTemplate
|
|
9
10
|
class Context
|
|
10
11
|
def initialize(resolved)
|
|
@@ -31,9 +32,10 @@ module Rails
|
|
|
31
32
|
end
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
35
|
+
# Canonical render binding: every gem reads as present at any requested
|
|
36
|
+
# version, so an if/elsif/else contributes only its first branch and
|
|
37
|
+
# templates meant for this render must use additive if blocks. There is
|
|
38
|
+
# no bundle to resolve against, so gem_version is nil.
|
|
37
39
|
class CanonicalContext
|
|
38
40
|
def gem?(_name, _requirement = nil)
|
|
39
41
|
true
|
|
@@ -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.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bakaface
|
|
@@ -69,8 +69,8 @@ description: |
|
|
|
69
69
|
Rails Hyperdrive mounts an MCP (Model Context Protocol) server at /_hyperdrive/mcp in development,
|
|
70
70
|
exposing introspection tools for AI coding agents (eval Ruby, query DB, tail logs,
|
|
71
71
|
list models/routes, locate source, fetch docs, snapshot stack). It also ships a
|
|
72
|
-
`hyperdrive:init` generator that discovers and installs
|
|
73
|
-
and
|
|
72
|
+
`hyperdrive:init` generator that discovers and installs four artifact kinds — skills,
|
|
73
|
+
guidelines, agents, and commands — shipped by companion gems under a documented contract.
|
|
74
74
|
rails-hyperdrive is the mechanism; companion gems (rails-hyperdrive-<library>) are the content.
|
|
75
75
|
email:
|
|
76
76
|
- afaceisnomore@gmail.com
|
|
@@ -89,12 +89,13 @@ 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
|
|
96
95
|
- lib/generators/hyperdrive/sync_runner.rb
|
|
96
|
+
- lib/hyperdrive/skill_tasks.rb
|
|
97
97
|
- lib/rails-hyperdrive.rb
|
|
98
|
+
- lib/rails/commands/hyperdrive/hyperdrive_command.rb
|
|
98
99
|
- lib/rails/hyperdrive.rb
|
|
99
100
|
- lib/rails/hyperdrive/ancestor_locator.rb
|
|
100
101
|
- lib/rails/hyperdrive/artifact_status.rb
|
|
@@ -108,17 +109,18 @@ files:
|
|
|
108
109
|
- lib/rails/hyperdrive/eager_footprint.rb
|
|
109
110
|
- lib/rails/hyperdrive/engine.rb
|
|
110
111
|
- lib/rails/hyperdrive/gem_manifest.rb
|
|
112
|
+
- lib/rails/hyperdrive/gemspec_locator.rb
|
|
111
113
|
- lib/rails/hyperdrive/index_document.rb
|
|
112
114
|
- lib/rails/hyperdrive/install_layout.rb
|
|
113
115
|
- lib/rails/hyperdrive/install_pipeline.rb
|
|
114
116
|
- lib/rails/hyperdrive/install_plan.rb
|
|
115
117
|
- lib/rails/hyperdrive/install_shell.rb
|
|
116
118
|
- lib/rails/hyperdrive/lock_file.rb
|
|
119
|
+
- lib/rails/hyperdrive/manifest_lint.rb
|
|
117
120
|
- lib/rails/hyperdrive/mcp_server.rb
|
|
118
121
|
- lib/rails/hyperdrive/resources/skill.rb
|
|
119
122
|
- lib/rails/hyperdrive/resources/stack_profile.rb
|
|
120
123
|
- lib/rails/hyperdrive/safety/rack_middleware.rb
|
|
121
|
-
- lib/rails/hyperdrive/skill_tasks.rb
|
|
122
124
|
- lib/rails/hyperdrive/skill_template.rb
|
|
123
125
|
- lib/rails/hyperdrive/sql_safety.rb
|
|
124
126
|
- lib/rails/hyperdrive/stack_profile.rb
|
|
@@ -133,7 +135,6 @@ files:
|
|
|
133
135
|
- lib/rails/hyperdrive/tools/run_sql.rb
|
|
134
136
|
- lib/rails/hyperdrive/tools/tail_logs.rb
|
|
135
137
|
- lib/rails/hyperdrive/version.rb
|
|
136
|
-
- lib/tasks/hyperdrive.rake
|
|
137
138
|
homepage: https://github.com/rails-hyperdrive/rails-hyperdrive
|
|
138
139
|
licenses:
|
|
139
140
|
- MIT
|
|
@@ -159,6 +160,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
159
160
|
requirements: []
|
|
160
161
|
rubygems_version: 3.6.9
|
|
161
162
|
specification_version: 4
|
|
162
|
-
summary: Dev-only Rails engine that bootstraps an MCP server + skills
|
|
163
|
-
AI coding agents.
|
|
163
|
+
summary: Dev-only Rails engine that bootstraps an MCP server + stack-matched skills,
|
|
164
|
+
guidelines, agents, and commands for AI coding agents.
|
|
164
165
|
test_files: []
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
data/lib/tasks/hyperdrive.rake
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
namespace :hyperdrive do
|
|
2
|
-
desc "Install Rails Hyperdrive into this app (writes .mcp.json, CLAUDE.md, skills, guidelines, mounts engine)"
|
|
3
|
-
task :init do
|
|
4
|
-
require "rails/generators"
|
|
5
|
-
require "generators/hyperdrive/install/install_generator"
|
|
6
|
-
Rails::Generators::Hyperdrive::InstallGenerator.start(ARGV.drop(1))
|
|
7
|
-
end
|
|
8
|
-
|
|
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
|
-
task :sync do
|
|
11
|
-
require "rails/generators"
|
|
12
|
-
require "generators/hyperdrive/sync/sync_generator"
|
|
13
|
-
Rails::Generators::Hyperdrive::SyncGenerator.start(ARGV.drop(1))
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
desc "Suggest uninstalled rails-hyperdrive companion gems for this app's stack (networked, cached; pass --refresh to re-query)"
|
|
17
|
-
task :discover do
|
|
18
|
-
require "rails/generators"
|
|
19
|
-
require "generators/hyperdrive/discover/discover_generator"
|
|
20
|
-
Rails::Generators::Hyperdrive::DiscoverGenerator.start(ARGV.drop(1))
|
|
21
|
-
end
|
|
22
|
-
end
|