enola 0.0.0 → 0.4.4
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 +47 -0
- data/README.md +98 -8
- data/exe/enola +6 -0
- data/lib/enola/channel.rb +36 -0
- data/lib/enola/cli.rb +29 -0
- data/lib/enola/config.rb +40 -0
- data/lib/enola/error.rb +11 -0
- data/lib/enola/fetcher.rb +112 -0
- data/lib/enola/platform.rb +22 -0
- data/lib/enola/probe.rb +37 -0
- data/lib/enola/providers/prism/enola_prism_provider.rb +188 -0
- data/lib/enola/providers.rb +53 -0
- data/lib/enola/resolver.rb +51 -0
- data/lib/enola/runner.rb +27 -0
- data/lib/enola/version.rb +5 -0
- data/lib/enola.rb +30 -2
- metadata +44 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b1c9e53fd33fd57e5764c1e4e3e2e6b71ac74145a5fd15aeac99d0efe896e21e
|
|
4
|
+
data.tar.gz: ab03a293ad4cb939351e4dcbed5680c84959e8797834bde3db9e45eb6c500746
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 252640692ed6d525918737d1ff1d74c3849321f3eac480fe1455aa92823eafba7c4555924f71ac08830c104732af687eebc17a92192b32edc446d5516f154b85
|
|
7
|
+
data.tar.gz: 4018811a6cdd5ef38fc5e8703444fba327f761135214ced01f8847f22487edf32dfe3c0c204a5018c250ead313484ae1350059f6e7834a8acc97661a668bd798
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
## munola 0.4.4.1 (2026-08-23)
|
|
2
|
+
|
|
3
|
+
The wrapper over the munola channel, first release with an implementation.
|
|
4
|
+
`munola init` writes the starter laws and upstream's bindings the way
|
|
5
|
+
`enola:install` does, then the recipe catalogue from enola-guides, binds the
|
|
6
|
+
recipes the tree shows a need for, fills the tenant foreign-key template
|
|
7
|
+
from the schema, writes `mcp-arch.yaml` with Prism and Rubydex on and fetches
|
|
8
|
+
the Rubydex library; `munola --version` names the munola version, its
|
|
9
|
+
upstream base and the binary that answered; `MUNOLA_BINARY` names a binary
|
|
10
|
+
to drive until the first munola release is cut. The channel's tag shape is
|
|
11
|
+
`munola-v<version>` on the fork's releases.
|
|
12
|
+
|
|
13
|
+
## enola 0.4.4 (2026-08-23)
|
|
14
|
+
|
|
15
|
+
The wrapper, first release with an implementation: a pure-Ruby `enola`
|
|
16
|
+
executable that drives upstream's v0.4.4. The binary is fetched on first use
|
|
17
|
+
from the release for the running platform, verified against the sha256 the
|
|
18
|
+
release publishes, and kept under `~/.cache/enola/upstream/0.4.4/`; a binary on
|
|
19
|
+
PATH is used only when it answers the pinned version; offline with an empty
|
|
20
|
+
cache is a refusal that names the cache and the release. Every argument and
|
|
21
|
+
exit code is forwarded; `--verbose` adds one stderr line naming channel,
|
|
22
|
+
version and where the binary came from; `enola --wrapper-probe` prints the
|
|
23
|
+
channel, pin, found version and which surfaces the binary answers.
|
|
24
|
+
|
|
25
|
+
Both providers by default: the gem vendors upstream's Prism provider script
|
|
26
|
+
at the pinned version and depends on the `prism` gem; `enola init` writes
|
|
27
|
+
`mcp-arch.yaml` with Prism (run by this Ruby) and Rubydex on, then runs the
|
|
28
|
+
binary's `constraints init`; the Rubydex engine library is fetched once per
|
|
29
|
+
binary version after the binary itself, a failure printed once as a named
|
|
30
|
+
skip. `Enola::Config.write_default` and `Enola::Providers` are the seams the
|
|
31
|
+
Rails layer and munola call.
|
|
32
|
+
|
|
33
|
+
## enola-rb 0.4.4 (2026-08-23)
|
|
34
|
+
|
|
35
|
+
The Rails layer, first release with an implementation. `rails generate
|
|
36
|
+
enola:install` writes `enola/constraints/` from the enola-guides starter laws,
|
|
37
|
+
binds the recipes the binary's own `constraints init` picks, writes the rest
|
|
38
|
+
as commented bindings and ignores `.enola/`; the `enola:init`,
|
|
39
|
+
`enola:snapshot` and `enola:check` rake tasks drive the binary the `enola` gem
|
|
40
|
+
fetches. A surface the pinned binary lacks is refused by name with the remedy;
|
|
41
|
+
an absent binary still leaves the laws written. Depends on `enola` at the same
|
|
42
|
+
version and on `enola-guides`. The generator writes the provider config first,
|
|
43
|
+
so a fresh app's graph carries Prism and Rubydex facts from its first snapshot.
|
|
44
|
+
|
|
45
|
+
## munola 0.0.0
|
|
46
|
+
|
|
47
|
+
Placeholder. `munola` is the same wrapper over another channel. Follows.
|
data/README.md
CHANGED
|
@@ -1,13 +1,103 @@
|
|
|
1
1
|
# enola-rb
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Three pure-Ruby gems, one repository, for running
|
|
4
|
+
[enola](https://github.com/enola-labs/enola), the architecture-graph tool,
|
|
5
|
+
from Ruby. None of them carries a binary or compiles anything.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
enola binary is on your path. Adding the `munola` gem switches it to the build
|
|
8
|
-
this project ships, which carries capabilities not yet in an upstream release.
|
|
7
|
+
## `enola`
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
The wrapper of the released enola. Its version is the upstream release it
|
|
10
|
+
drives: `enola 0.4.4` runs enola-labs' v0.4.4.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
```ruby
|
|
13
|
+
gem "enola"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
bundle exec enola --generate .
|
|
18
|
+
bundle exec enola baseline pin .
|
|
19
|
+
bundle exec enola check .
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The first command that needs the binary downloads the release for your
|
|
23
|
+
platform (linux, darwin, windows; amd64, arm64), verifies it against the
|
|
24
|
+
sha256 file the same release publishes, and keeps it under
|
|
25
|
+
`~/.cache/enola/upstream/0.4.4/` (`ENOLA_CACHE_DIR` moves the root). An
|
|
26
|
+
`enola` already on your PATH is used only when it answers the pinned version;
|
|
27
|
+
offline with an empty cache is a refusal that names the cache and the release
|
|
28
|
+
rather than a fallback to whatever is installed. Nothing downloads at
|
|
29
|
+
`bundle install`.
|
|
30
|
+
|
|
31
|
+
Every argument and exit code is forwarded unchanged; the wrapper adds no flag
|
|
32
|
+
of its own to enola's surface. `--verbose` on any command writes one line to
|
|
33
|
+
stderr first, naming the channel, the version and where the binary came from.
|
|
34
|
+
`enola --wrapper-probe` prints the channel, the pin, the version the binary
|
|
35
|
+
answers and which surfaces it has (`constraints`, `providers`, `check`,
|
|
36
|
+
`hook`), read by running them, never by comparing version strings.
|
|
37
|
+
|
|
38
|
+
This gem is not an enola-labs release; it runs theirs.
|
|
39
|
+
|
|
40
|
+
## `enola-rb`
|
|
41
|
+
|
|
42
|
+
The Rails layer over `enola`. It depends on `enola` at the same version and on
|
|
43
|
+
[enola-guides](https://github.com/misabegovic/enola-guides), and adds nothing
|
|
44
|
+
to the binary's surface.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
gem "enola-rb"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
bin/rails generate enola:install
|
|
52
|
+
bin/rake enola:snapshot
|
|
53
|
+
bin/rake enola:check
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The generator writes `enola/constraints/` from the guides' starter laws (four
|
|
57
|
+
laws a Rails team keeps, each with its reason), asks the binary's own
|
|
58
|
+
`constraints init` to bind the shipped recipes whose roles resolve in the app,
|
|
59
|
+
writes every other shipped recipe as a commented binding to uncomment once the
|
|
60
|
+
directories exist, and ignores `.enola/`. It reads what `init` wrote rather
|
|
61
|
+
than its exit code, and when the binary cannot be fetched it still writes the
|
|
62
|
+
laws and says so. `enola:snapshot` generates and pins the baseline;
|
|
63
|
+
`enola:check` grades the working tree against it and fails on a new breach of
|
|
64
|
+
a declared law. A surface the pinned binary lacks is refused by name with the
|
|
65
|
+
remedy.
|
|
66
|
+
|
|
67
|
+
## `munola`
|
|
68
|
+
|
|
69
|
+
One person's taste on top of enola: the same wrapper over another channel,
|
|
70
|
+
the builds cut from [a fork of enola](https://github.com/misabegovic/enola),
|
|
71
|
+
versioned as the upstream they are built on plus a fourth segment
|
|
72
|
+
(`0.4.4.1` drives a build on v0.4.4), each release naming what differs from
|
|
73
|
+
that upstream. It is offered upstream where it fits and is not positioned
|
|
74
|
+
against it. What it adds is the channel and the catalogue.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
gem "munola"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
bin/rails generate munola:install --tenant-column company_id
|
|
82
|
+
bundle exec munola init .
|
|
83
|
+
bundle exec munola --version
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`munola init` does what `enola:install` does, then what only munola carries:
|
|
87
|
+
it writes the recipe catalogue the `enola-guides` gem ships (Ember
|
|
88
|
+
conventions, data ownership, API boundaries, background work, a tenant
|
|
89
|
+
foreign key) into `enola/recipes/`, binds the recipes the tree justifies
|
|
90
|
+
(`ember-cli-build.js`, a schema with `app/models`, `config/routes.rb` with
|
|
91
|
+
`app/policies`, `app/tasks`, a column most tables share confirmed against
|
|
92
|
+
`db/schema.rb`) and writes every other one as a commented binding; it fills
|
|
93
|
+
the tenant template from the schema's own table names, never from an
|
|
94
|
+
inflection table; it writes `mcp-arch.yaml` with both Ruby providers on by
|
|
95
|
+
default, Prism through the script the `enola` gem carries and Rubydex built
|
|
96
|
+
into the binary, and fetches the Rubydex library, a failed fetch reported as a
|
|
97
|
+
named skip. It never asks a question. `munola --version` names the munola
|
|
98
|
+
version, the upstream it is built on and which binary answered.
|
|
99
|
+
|
|
100
|
+
The first munola release is what makes the channel real; until it is cut,
|
|
101
|
+
`MUNOLA_BINARY=/path/to/enola` names the binary to drive, and every command
|
|
102
|
+
says so. `munola` depends on `enola` and `enola-rb` at its upstream version
|
|
103
|
+
and on `enola-guides` 0.3.1 or later.
|
data/exe/enola
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enola
|
|
4
|
+
Channel = Struct.new(:name, :release_base, :version, :asset_prefix, :tag_prefix, keyword_init: true) do
|
|
5
|
+
def asset(platform)
|
|
6
|
+
"#{asset_prefix}-#{version}-#{platform}.tar.gz"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def checksum_asset(platform)
|
|
10
|
+
"#{asset_prefix}-#{version}-#{platform}.sha256"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def tag
|
|
14
|
+
"#{tag_prefix || 'v'}#{version}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def url(file)
|
|
18
|
+
"#{release_base.chomp('/')}/#{tag}/#{file}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cache_dir(root)
|
|
22
|
+
File.join(root, name, version)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
"#{name} #{version}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Channel::UPSTREAM = Channel.new(
|
|
31
|
+
name: "upstream",
|
|
32
|
+
release_base: "https://github.com/enola-labs/enola/releases/download",
|
|
33
|
+
version: VERSION,
|
|
34
|
+
asset_prefix: "enola"
|
|
35
|
+
).freeze
|
|
36
|
+
end
|
data/lib/enola/cli.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Enola
|
|
6
|
+
module CLI
|
|
7
|
+
def self.start(argv, stderr: $stderr, stdout: $stdout)
|
|
8
|
+
case argv.first
|
|
9
|
+
when "--wrapper-probe"
|
|
10
|
+
found = Enola.resolver.resolve
|
|
11
|
+
stdout.puts JSON.pretty_generate(Probe.new(found.path).report.merge(source: found.source))
|
|
12
|
+
0
|
|
13
|
+
when "--wrapper-fetch"
|
|
14
|
+
stdout.puts Fetcher.new.fetch
|
|
15
|
+
0
|
|
16
|
+
when "init"
|
|
17
|
+
root = argv[1] || "."
|
|
18
|
+
written = Config.write_default(root)
|
|
19
|
+
stdout.puts(written ? "wrote #{written}" : "#{Config.path(root)} already exists, left as is")
|
|
20
|
+
Runner.new(stderr: stderr).run(["constraints", "init", root])
|
|
21
|
+
else
|
|
22
|
+
Runner.new.exec(argv)
|
|
23
|
+
end
|
|
24
|
+
rescue Error => e
|
|
25
|
+
stderr.puts "enola: #{e.message}"
|
|
26
|
+
127
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/enola/config.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enola
|
|
4
|
+
module Config
|
|
5
|
+
FILE = "mcp-arch.yaml"
|
|
6
|
+
|
|
7
|
+
IGNORE = %w[**/.git/** **/node_modules/** **/vendor/** **/tmp/** **/log/** **/coverage/** **/public/assets/**].freeze
|
|
8
|
+
|
|
9
|
+
def self.path(root)
|
|
10
|
+
File.join(File.expand_path(root), FILE)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.write_default(root, ruby: Providers.ruby)
|
|
14
|
+
target = path(root)
|
|
15
|
+
return nil if File.exist?(target)
|
|
16
|
+
|
|
17
|
+
File.write(target, render(ruby: ruby))
|
|
18
|
+
target
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.render(ruby: Providers.ruby)
|
|
22
|
+
<<~YAML
|
|
23
|
+
# Written by the enola gem; edit freely. Both providers are on by default:
|
|
24
|
+
# Prism runs under the Ruby that runs the gem, Rubydex is built into the
|
|
25
|
+
# binary and fetched once. Remove one to leave it out of the graph.
|
|
26
|
+
repo: .
|
|
27
|
+
repos:
|
|
28
|
+
- .
|
|
29
|
+
ignore:
|
|
30
|
+
#{IGNORE.map { |glob| " - \"#{glob}\"" }.join("\n")}
|
|
31
|
+
providers:
|
|
32
|
+
- name: prism
|
|
33
|
+
command: [#{ruby.inspect}, #{Providers.prism_script.inspect}]
|
|
34
|
+
expected_version: "#{Providers.prism_version || '0.1.0'}"
|
|
35
|
+
- name: rubydex
|
|
36
|
+
expected_version: "#{Providers::RUBYDEX_EXPECTED_VERSION}"
|
|
37
|
+
YAML
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/enola/error.rb
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module Enola
|
|
11
|
+
class Fetcher
|
|
12
|
+
REDIRECTS = 5
|
|
13
|
+
|
|
14
|
+
def initialize(channel: Enola.channel, cache_root: Enola.cache_root, platform: Platform.current)
|
|
15
|
+
@channel = channel
|
|
16
|
+
@cache_root = cache_root
|
|
17
|
+
@platform = platform
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def binary_path
|
|
21
|
+
File.join(@channel.cache_dir(@cache_root), Platform.windows?(@platform) ? "enola.exe" : "enola")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def cached?
|
|
25
|
+
File.executable?(binary_path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def fetch
|
|
29
|
+
return binary_path if cached?
|
|
30
|
+
|
|
31
|
+
Dir.mktmpdir("enola-fetch") do |work|
|
|
32
|
+
asset = @channel.asset(@platform)
|
|
33
|
+
tarball = File.join(work, asset)
|
|
34
|
+
write(@channel.url(asset), tarball)
|
|
35
|
+
verify(tarball, read(@channel.url(@channel.checksum_asset(@platform))), asset)
|
|
36
|
+
unpack(tarball, work)
|
|
37
|
+
end
|
|
38
|
+
binary_path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def verify(tarball, checksum_text, asset)
|
|
44
|
+
expected = checksum_text.split.first.to_s.downcase
|
|
45
|
+
actual = Digest::SHA256.file(tarball).hexdigest
|
|
46
|
+
return if !expected.empty? && expected == actual
|
|
47
|
+
|
|
48
|
+
raise Unverified, "#{asset} did not match the checksum #{@channel.name} published for #{@channel.version} " \
|
|
49
|
+
"(expected #{expected.empty? ? 'nothing readable' : expected[0, 12]}, got #{actual[0, 12]}); nothing was installed"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def unpack(tarball, work)
|
|
53
|
+
unpacked = File.join(work, "unpacked")
|
|
54
|
+
FileUtils.mkdir_p(unpacked)
|
|
55
|
+
_, err, status = Open3.capture3("tar", "xzf", tarball, "-C", unpacked)
|
|
56
|
+
raise Unavailable, "could not unpack #{File.basename(tarball)}: #{err.strip}" unless status.success?
|
|
57
|
+
|
|
58
|
+
inner = Dir.children(unpacked).find { |name| name.start_with?("#{@channel.asset_prefix}-") && !name.end_with?(".tar.gz") }
|
|
59
|
+
raise Unavailable, "#{File.basename(tarball)} holds no #{@channel.asset_prefix} binary" unless inner
|
|
60
|
+
|
|
61
|
+
dir = @channel.cache_dir(@cache_root)
|
|
62
|
+
begin
|
|
63
|
+
FileUtils.mkdir_p(dir)
|
|
64
|
+
rescue SystemCallError => e
|
|
65
|
+
raise Unavailable, "cannot write the cache at #{dir} (#{e.message}); set ENOLA_CACHE_DIR to a writable directory"
|
|
66
|
+
end
|
|
67
|
+
FileUtils.mv(File.join(unpacked, inner), binary_path, force: true)
|
|
68
|
+
FileUtils.chmod(0o755, binary_path)
|
|
69
|
+
%w[LICENSE NOTICE].each do |extra|
|
|
70
|
+
source = File.join(unpacked, extra)
|
|
71
|
+
FileUtils.mv(source, File.join(dir, extra), force: true) if File.exist?(source)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def read(url)
|
|
76
|
+
body = +""
|
|
77
|
+
stream(url) { |chunk| body << chunk }
|
|
78
|
+
body
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def write(url, path)
|
|
82
|
+
File.open(path, "wb") { |file| stream(url) { |chunk| file.write(chunk) } }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def stream(url, hops = 0, &block)
|
|
86
|
+
uri = URI(url)
|
|
87
|
+
return File.open(uri.path, "rb") { |file| file.each(nil, 1 << 16, &block) } if uri.scheme == "file"
|
|
88
|
+
|
|
89
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 15, read_timeout: 60) do |http|
|
|
90
|
+
http.request(Net::HTTP::Get.new(uri)) do |response|
|
|
91
|
+
case response
|
|
92
|
+
when Net::HTTPRedirection
|
|
93
|
+
raise Unavailable, "too many redirects fetching #{url}" if hops >= REDIRECTS
|
|
94
|
+
|
|
95
|
+
return stream(response["location"], hops + 1, &block)
|
|
96
|
+
when Net::HTTPSuccess
|
|
97
|
+
response.read_body(&block)
|
|
98
|
+
else
|
|
99
|
+
raise Unavailable, "#{url} answered #{response.code}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
rescue SocketError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout, IOError => e
|
|
104
|
+
raise Unavailable, offline_message(e)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def offline_message(error)
|
|
108
|
+
"enola #{@channel} is not in the cache at #{@channel.cache_dir(@cache_root)} and could not be fetched " \
|
|
109
|
+
"from #{@channel.release_base} (#{error.class}: #{error.message}); connect once, or place the verified binary there"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module Enola
|
|
6
|
+
module Platform
|
|
7
|
+
OS = { "linux" => "linux", "darwin" => "darwin", "mingw" => "windows", "mswin" => "windows" }.freeze
|
|
8
|
+
ARCH = { "x86_64" => "amd64", "amd64" => "amd64", "aarch64" => "arm64", "arm64" => "arm64" }.freeze
|
|
9
|
+
|
|
10
|
+
def self.current(config = RbConfig::CONFIG)
|
|
11
|
+
os = OS.find { |key, _| config["host_os"].include?(key) }&.last
|
|
12
|
+
arch = ARCH[config["host_cpu"]]
|
|
13
|
+
raise UnsupportedPlatform, "no enola release for #{config['host_os']} on #{config['host_cpu']}" unless os && arch
|
|
14
|
+
|
|
15
|
+
"#{os}-#{arch}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.windows?(platform)
|
|
19
|
+
platform.start_with?("windows")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/enola/probe.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Enola
|
|
6
|
+
class Probe
|
|
7
|
+
SURFACES = %w[constraints providers check plan hook].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(binary)
|
|
10
|
+
@binary = binary
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def version
|
|
14
|
+
out, status = Open3.capture2e(@binary, "--version")
|
|
15
|
+
return nil unless status.success?
|
|
16
|
+
|
|
17
|
+
out[/\d+\.\d+\.\d+(?:[.-][\w.]+)?/]
|
|
18
|
+
rescue SystemCallError
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A surface the binary has prints its usage (exit code 2 is its convention
|
|
23
|
+
# for --help); one it lacks is refused by the top-level dispatch by name.
|
|
24
|
+
def capabilities
|
|
25
|
+
SURFACES.to_h do |surface|
|
|
26
|
+
out, = Open3.capture2e(@binary, surface, "--help")
|
|
27
|
+
[surface.to_sym, !out.include?("unknown command \"#{surface}\"")]
|
|
28
|
+
rescue SystemCallError
|
|
29
|
+
[surface.to_sym, false]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def report(channel: Enola.channel)
|
|
34
|
+
{ channel: channel.name, pinned: channel.version, binary: @binary, version: version, capabilities: capabilities }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# Vendored from enola-labs/enola examples/providers/ruby/prism/enola_prism_provider.rb at v0.4.4 (1e35dfd88e7b293bb2bffec6949989bd6105b796), Apache-2.0.
|
|
4
|
+
|
|
5
|
+
# enola Prism reference provider.
|
|
6
|
+
#
|
|
7
|
+
# Parses every **/*.rb under the path given as ARGV[0] with Prism (the Ruby
|
|
8
|
+
# parser that ships in the standard library from Ruby 3.3) and emits JSONL
|
|
9
|
+
# call facts on stdout in enola's fact schema — one fact per method call,
|
|
10
|
+
# typed by what the receiver resolves to LEXICALLY. No runtime, no gems, no
|
|
11
|
+
# guessing:
|
|
12
|
+
#
|
|
13
|
+
# receiver is a constant -> callee "Const#method", resolution_level "constant-receiver"
|
|
14
|
+
# self or no receiver, inside a def
|
|
15
|
+
# nested in a nameable class/module -> callee "Enclosing#method", resolution_level "lexical-self"
|
|
16
|
+
# anything else -> callee "method", resolution_level "name-only"
|
|
17
|
+
#
|
|
18
|
+
# Fact naming is designed to ADD typed call edges without colliding with the
|
|
19
|
+
# identities enola's own Ruby extractor emits — the extractor already owns the
|
|
20
|
+
# symbol facts, and the seam skips any provider fact sharing a name+kind
|
|
21
|
+
# identity with an extractor fact. Every fact here is therefore kind
|
|
22
|
+
# "dependency" with the distinctive name "prism-call: <caller> -> <callee>",
|
|
23
|
+
# carrying one calls relation targeting the callee. The seam stamps
|
|
24
|
+
# provider/provider_version onto accepted facts; this script declares only
|
|
25
|
+
# resolution_level, which the seam requires on every provider fact.
|
|
26
|
+
#
|
|
27
|
+
# Determinism: files are enumerated in sorted order, output lines are sorted
|
|
28
|
+
# before printing, and nothing time- or environment-dependent is emitted.
|
|
29
|
+
# vendor/, node_modules/ and tmp/ subtrees are skipped at any depth; a file
|
|
30
|
+
# Prism cannot parse cleanly is skipped whole — fail closed, never guess.
|
|
31
|
+
# --version prints a fixed semver on stdout and exits, which is how the seam
|
|
32
|
+
# learns what build it is talking to.
|
|
33
|
+
|
|
34
|
+
require "json"
|
|
35
|
+
require "prism"
|
|
36
|
+
|
|
37
|
+
PROVIDER_VERSION = "0.1.0"
|
|
38
|
+
|
|
39
|
+
if ARGV.include?("--version")
|
|
40
|
+
puts PROVIDER_VERSION
|
|
41
|
+
exit 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
root = ARGV[0]
|
|
45
|
+
abort "usage: enola_prism_provider.rb <repo-path>" unless root && File.directory?(root)
|
|
46
|
+
|
|
47
|
+
SKIP_SEGMENTS = %w[vendor node_modules tmp].freeze
|
|
48
|
+
|
|
49
|
+
class CallCollector < Prism::Visitor
|
|
50
|
+
attr_reader :facts
|
|
51
|
+
|
|
52
|
+
def initialize(file)
|
|
53
|
+
@file = file
|
|
54
|
+
@scopes = []
|
|
55
|
+
@methods = []
|
|
56
|
+
@facts = []
|
|
57
|
+
@chained = []
|
|
58
|
+
super()
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def visit_class_node(node)
|
|
62
|
+
with_scope(constant_name(node.constant_path)) { super }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def visit_module_node(node)
|
|
66
|
+
with_scope(constant_name(node.constant_path)) { super }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# An eigenclass body (class << self) pushes an unnameable scope: calls inside
|
|
70
|
+
# it degrade to name-only rather than being attributed to a name this script
|
|
71
|
+
# cannot lexically prove.
|
|
72
|
+
def visit_singleton_class_node(node)
|
|
73
|
+
with_scope(nil) { super }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def visit_def_node(node)
|
|
77
|
+
@methods.push(node.name.to_s)
|
|
78
|
+
super
|
|
79
|
+
@methods.pop
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def visit_call_node(node)
|
|
83
|
+
record(node)
|
|
84
|
+
receiver = node.receiver
|
|
85
|
+
chained = receiver.is_a?(Prism::CallNode) && receiver.name.to_s == "new" ? node.name.to_s : nil
|
|
86
|
+
@chained.push(chained)
|
|
87
|
+
super
|
|
88
|
+
@chained.pop
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def with_scope(name)
|
|
94
|
+
@scopes.push(name)
|
|
95
|
+
yield
|
|
96
|
+
@scopes.pop
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def enclosing
|
|
100
|
+
return nil if @scopes.empty? || @scopes.any?(&:nil?)
|
|
101
|
+
|
|
102
|
+
@scopes.join("::")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def caller_name
|
|
106
|
+
enc = enclosing
|
|
107
|
+
return @file unless enc
|
|
108
|
+
return enc if @methods.empty?
|
|
109
|
+
|
|
110
|
+
"#{enc}##{@methods.last}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def constant_name(node)
|
|
114
|
+
case node
|
|
115
|
+
when Prism::ConstantReadNode
|
|
116
|
+
node.name.to_s
|
|
117
|
+
when Prism::ConstantPathNode
|
|
118
|
+
begin
|
|
119
|
+
node.full_name
|
|
120
|
+
rescue StandardError
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def record(node)
|
|
127
|
+
method = node.name.to_s
|
|
128
|
+
receiver = node.receiver
|
|
129
|
+
return record_instantiation(node, receiver) if method == "new" && receiver && constant_name(receiver)
|
|
130
|
+
|
|
131
|
+
callee, level =
|
|
132
|
+
if receiver && (const = constant_name(receiver))
|
|
133
|
+
["#{const}##{method}", "constant-receiver"]
|
|
134
|
+
elsif (receiver.nil? || receiver.is_a?(Prism::SelfNode)) && !@methods.empty? && (enc = enclosing)
|
|
135
|
+
["#{enc}##{method}", "lexical-self"]
|
|
136
|
+
else
|
|
137
|
+
[method, "name-only"]
|
|
138
|
+
end
|
|
139
|
+
@facts << {
|
|
140
|
+
"kind" => "dependency",
|
|
141
|
+
"name" => "prism-call: #{caller_name} -> #{callee}",
|
|
142
|
+
"file" => @file,
|
|
143
|
+
"line" => node.location.start_line,
|
|
144
|
+
"props" => { "resolution_level" => level },
|
|
145
|
+
"relations" => [{ "kind" => "calls", "target" => callee }]
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A `new` on a literal constant is the one call whose result has a knowable
|
|
150
|
+
# type, so it is recorded as an instantiation rather than a call. When the
|
|
151
|
+
# instantiation is itself the receiver of a further call the ceremony
|
|
152
|
+
# `Foo.new(...).bar` is named on the fact, which is what a rule about
|
|
153
|
+
# one-shot objects reads.
|
|
154
|
+
def record_instantiation(node, receiver)
|
|
155
|
+
const = constant_name(receiver)
|
|
156
|
+
props = { "resolution_level" => "constant-receiver" }
|
|
157
|
+
props["one_shot_call"] = @chained.last unless @chained.empty? || @chained.last.nil?
|
|
158
|
+
@facts << {
|
|
159
|
+
"kind" => "dependency",
|
|
160
|
+
"name" => "prism-new: #{caller_name} -> #{const}",
|
|
161
|
+
"file" => @file,
|
|
162
|
+
"line" => node.location.start_line,
|
|
163
|
+
"props" => props,
|
|
164
|
+
"relations" => [{ "kind" => "instantiates", "target" => const }]
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
lines = []
|
|
170
|
+
files = Dir.glob(File.join("**", "*.rb"), base: root).reject do |rel|
|
|
171
|
+
rel.split("/").any? { |segment| SKIP_SEGMENTS.include?(segment) }
|
|
172
|
+
end.sort
|
|
173
|
+
|
|
174
|
+
files.each do |rel|
|
|
175
|
+
source = begin
|
|
176
|
+
File.read(File.join(root, rel))
|
|
177
|
+
rescue StandardError
|
|
178
|
+
next
|
|
179
|
+
end
|
|
180
|
+
result = Prism.parse(source)
|
|
181
|
+
next unless result.success?
|
|
182
|
+
|
|
183
|
+
collector = CallCollector.new(rel)
|
|
184
|
+
result.value.accept(collector)
|
|
185
|
+
collector.facts.each { |fact| lines << JSON.generate(fact) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
puts lines.sort
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "rbconfig"
|
|
6
|
+
|
|
7
|
+
module Enola
|
|
8
|
+
# Prism and Rubydex are on by default. Prism runs as the vendored upstream
|
|
9
|
+
# script under the Ruby running this gem; Rubydex is built into the binary
|
|
10
|
+
# and needs its engine library fetched once per binary version.
|
|
11
|
+
module Providers
|
|
12
|
+
RUBYDEX_EXPECTED_VERSION = "0.4.0"
|
|
13
|
+
|
|
14
|
+
def self.prism_script
|
|
15
|
+
File.expand_path("providers/prism/enola_prism_provider.rb", __dir__)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.ruby
|
|
19
|
+
RbConfig.ruby
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.prism_version
|
|
23
|
+
out, status = Open3.capture2(ruby, prism_script, "--version")
|
|
24
|
+
status.success? ? out.strip : nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.rubydex_expected_version(binary = nil)
|
|
28
|
+
return RUBYDEX_EXPECTED_VERSION unless binary
|
|
29
|
+
|
|
30
|
+
out, status = Open3.capture2(binary, "providers", "list")
|
|
31
|
+
return RUBYDEX_EXPECTED_VERSION unless status.success?
|
|
32
|
+
|
|
33
|
+
out[/rubydex\s+(\d+\.\d+\.\d+)/, 1] || RUBYDEX_EXPECTED_VERSION
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Runs `providers fetch rubydex` once per binary version; the outcome is
|
|
37
|
+
# remembered beside the binary so a session never asks twice, and a
|
|
38
|
+
# failure is a named skip the graph will also report, never silence.
|
|
39
|
+
def self.ensure_rubydex(binary, channel: Enola.channel, cache_root: Enola.cache_root, stderr: $stderr)
|
|
40
|
+
marker = File.join(channel.cache_dir(cache_root), "providers-rubydex")
|
|
41
|
+
return File.read(marker).strip if File.exist?(marker)
|
|
42
|
+
|
|
43
|
+
out, status = Open3.capture2e(binary, "providers", "fetch", "rubydex")
|
|
44
|
+
outcome = status.success? ? "fetched" : "skipped: #{out.lines.last&.strip || 'providers fetch rubydex failed'}"
|
|
45
|
+
stderr.puts "enola: rubydex #{outcome}; Rubydex facts will be a named skip in the receipt until it succeeds" unless status.success?
|
|
46
|
+
FileUtils.mkdir_p(File.dirname(marker))
|
|
47
|
+
File.write(marker, "#{outcome}\n")
|
|
48
|
+
outcome
|
|
49
|
+
rescue SystemCallError => e
|
|
50
|
+
"skipped: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enola
|
|
4
|
+
class Resolver
|
|
5
|
+
Found = Struct.new(:path, :source)
|
|
6
|
+
|
|
7
|
+
def initialize(channel: Enola.channel, cache_root: Enola.cache_root, platform: Platform.current, path: ENV.fetch("PATH", ""))
|
|
8
|
+
@channel = channel
|
|
9
|
+
@fetcher = Fetcher.new(channel: channel, cache_root: cache_root, platform: platform)
|
|
10
|
+
@path = path
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve
|
|
14
|
+
return Found.new(@fetcher.binary_path, "cache") if @fetcher.cached?
|
|
15
|
+
|
|
16
|
+
on_path, found_version = matching_path_binary
|
|
17
|
+
return Found.new(on_path, "path") if on_path
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
Found.new(@fetcher.fetch, "fetched")
|
|
21
|
+
rescue Unavailable => e
|
|
22
|
+
raise Unavailable, found_version ? "#{e.message}; the enola on PATH is #{found_version}, the Gemfile pins #{@channel.version}" : e.message
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
EXECUTABLE = File.expand_path("../../exe/enola", __dir__)
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def matching_path_binary
|
|
31
|
+
candidate = @path.split(File::PATH_SEPARATOR).map { |dir| File.join(dir, "enola") }
|
|
32
|
+
.find { |bin| File.executable?(bin) && !wrapper?(bin) }
|
|
33
|
+
return [nil, nil] unless candidate
|
|
34
|
+
|
|
35
|
+
version = Probe.new(candidate).version
|
|
36
|
+
version == @channel.version ? [candidate, version] : [nil, version]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Bundler puts this gem's own exe on PATH, and a rubygems binstub for the
|
|
40
|
+
# gem looks like a binary too; probing either would run the wrapper inside
|
|
41
|
+
# itself without end.
|
|
42
|
+
def wrapper?(bin)
|
|
43
|
+
return true if File.realpath(bin) == File.realpath(EXECUTABLE)
|
|
44
|
+
|
|
45
|
+
head = File.binread(bin, 512)
|
|
46
|
+
head.include?("Gem.activate_bin_path") || head.include?('require "enola"')
|
|
47
|
+
rescue SystemCallError
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/enola/runner.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Enola
|
|
4
|
+
class Runner
|
|
5
|
+
def initialize(resolver: Enola.resolver, channel: Enola.channel, stderr: $stderr)
|
|
6
|
+
@resolver = resolver
|
|
7
|
+
@channel = channel
|
|
8
|
+
@stderr = stderr
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def command(argv)
|
|
12
|
+
found = @resolver.resolve
|
|
13
|
+
Providers.ensure_rubydex(found.path, channel: @channel, stderr: @stderr)
|
|
14
|
+
@stderr.puts "enola: #{@channel} via #{found.source} (#{found.path})" if argv.include?("--verbose")
|
|
15
|
+
[found.path, *argv]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run(argv)
|
|
19
|
+
system(*command(argv))
|
|
20
|
+
$?.exitstatus || 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def exec(argv)
|
|
24
|
+
Kernel.exec(*command(argv))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/enola.rb
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
require_relative "enola/version"
|
|
4
|
+
require_relative "enola/error"
|
|
5
|
+
require_relative "enola/channel"
|
|
6
|
+
require_relative "enola/platform"
|
|
7
|
+
require_relative "enola/fetcher"
|
|
8
|
+
require_relative "enola/probe"
|
|
9
|
+
require_relative "enola/providers"
|
|
10
|
+
require_relative "enola/config"
|
|
11
|
+
require_relative "enola/resolver"
|
|
12
|
+
require_relative "enola/runner"
|
|
13
|
+
require_relative "enola/cli"
|
|
14
|
+
|
|
15
|
+
module Enola
|
|
16
|
+
class << self
|
|
17
|
+
attr_writer :channel, :resolver_factory
|
|
18
|
+
|
|
19
|
+
# Another channel's gem installs its own resolver here, so every rake
|
|
20
|
+
# task and wrapped command built on Runner drives that channel's binary.
|
|
21
|
+
def resolver
|
|
22
|
+
(@resolver_factory || -> { Resolver.new(channel: channel) }).call
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def channel
|
|
26
|
+
@channel ||= Channel::UPSTREAM
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def cache_root
|
|
30
|
+
ENV.fetch("ENOLA_CACHE_DIR") { File.join(Dir.home, ".cache", "enola") }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
5
33
|
end
|
metadata
CHANGED
|
@@ -1,35 +1,66 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: enola
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Muhamed Isabegovic
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
12
|
-
dependencies:
|
|
11
|
+
date: 2026-08-23 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: prism
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.3'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.3'
|
|
13
27
|
description: |
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
A pure-Ruby wrapper around enola, the architecture-graph tool by enola-labs.
|
|
29
|
+
The gem's version is the upstream release it drives. The binary is not in
|
|
30
|
+
the gem: the first command that needs it downloads the release for your
|
|
31
|
+
platform, verifies it against the checksum the release publishes, and keeps
|
|
32
|
+
it in a per-user cache. Every command and exit code is forwarded unchanged.
|
|
33
|
+
This gem is not an enola-labs release.
|
|
19
34
|
email:
|
|
20
35
|
- m.isabegovic@hotmail.com
|
|
21
|
-
executables:
|
|
36
|
+
executables:
|
|
37
|
+
- enola
|
|
22
38
|
extensions: []
|
|
23
39
|
extra_rdoc_files: []
|
|
24
40
|
files:
|
|
41
|
+
- CHANGELOG.md
|
|
25
42
|
- LICENSE
|
|
26
43
|
- README.md
|
|
44
|
+
- exe/enola
|
|
27
45
|
- lib/enola.rb
|
|
28
|
-
|
|
46
|
+
- lib/enola/channel.rb
|
|
47
|
+
- lib/enola/cli.rb
|
|
48
|
+
- lib/enola/config.rb
|
|
49
|
+
- lib/enola/error.rb
|
|
50
|
+
- lib/enola/fetcher.rb
|
|
51
|
+
- lib/enola/platform.rb
|
|
52
|
+
- lib/enola/probe.rb
|
|
53
|
+
- lib/enola/providers.rb
|
|
54
|
+
- lib/enola/providers/prism/enola_prism_provider.rb
|
|
55
|
+
- lib/enola/resolver.rb
|
|
56
|
+
- lib/enola/runner.rb
|
|
57
|
+
- lib/enola/version.rb
|
|
58
|
+
homepage: https://github.com/misabegovic/enola-rb
|
|
29
59
|
licenses:
|
|
30
60
|
- Apache-2.0
|
|
31
61
|
metadata:
|
|
32
62
|
source_code_uri: https://github.com/misabegovic/enola-rb
|
|
63
|
+
changelog_uri: https://github.com/misabegovic/enola-rb/blob/main/CHANGELOG.md
|
|
33
64
|
post_install_message:
|
|
34
65
|
rdoc_options: []
|
|
35
66
|
require_paths:
|
|
@@ -48,5 +79,6 @@ requirements: []
|
|
|
48
79
|
rubygems_version: 3.5.22
|
|
49
80
|
signing_key:
|
|
50
81
|
specification_version: 4
|
|
51
|
-
summary:
|
|
82
|
+
summary: 'Runs the released enola from Ruby: fetched on first use, verified, pinned
|
|
83
|
+
to this gem''s version.'
|
|
52
84
|
test_files: []
|