openui-rails 0.1.0 → 0.2.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/AUDIT.md +24 -4
- data/CHANGELOG.md +28 -1
- data/CONFIGURATION.md +9 -77
- data/CONTRIBUTING.md +29 -1
- data/DEVELOPMENT.md +68 -0
- data/README.md +49 -439
- data/RELEASING.md +18 -8
- data/THIRD_PARTY_NOTICES.md +10 -0
- data/UPGRADING.md +7 -24
- data/lib/generators/openui/install/install_generator.rb +6 -5
- data/lib/generators/openui/install/templates/config/initializers/openui.rb.tt +2 -2
- data/lib/generators/openui/install/templates/config/openui/README.md +19 -4
- data/lib/openui/rails/default_library/library.prompt.txt +198 -0
- data/lib/openui/rails/default_library/library.spec.json +4390 -0
- data/lib/openui/rails/default_library/openui.LICENSE.txt +21 -0
- data/lib/openui/rails/default_library/provenance.json +15 -0
- data/lib/openui/rails/default_library.rb +85 -0
- data/lib/openui/rails/doctor.rb +143 -0
- data/lib/openui/rails/library_manifest.rb +2 -1
- data/lib/openui/rails/version.rb +1 -1
- data/lib/openui/rails.rb +2 -0
- data/lib/tasks/openui.rake +19 -0
- metadata +8 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2011-2024 Thesys Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"runtime_package": "@openuidev/browser-bundle",
|
|
3
|
+
"runtime_version": "0.2.1",
|
|
4
|
+
"runtime_integrity": "sha384-GNNBzicRNkuscleA9e7/4cz6JwE9PE4BRKJ1udKWNzBh4DTD2/zpfOz4p91n8sW0",
|
|
5
|
+
"library": "openuiChatLibrary",
|
|
6
|
+
"exports": {
|
|
7
|
+
"library.prompt.txt": "prompt()",
|
|
8
|
+
"library.spec.json": "toSpec()"
|
|
9
|
+
},
|
|
10
|
+
"sha256": {
|
|
11
|
+
"library.prompt.txt": "7a334672957298e60543d80650615102b934bc88f69115e8602770a988514c3d",
|
|
12
|
+
"library.spec.json": "2c36842674a3d1f3a06deddd90304c67da65fc83686dd45e51d1b67d4a2178ad",
|
|
13
|
+
"openui.LICENSE.txt": "b6bdd2d3d161722fb3f3afc039114cfa9f3236f036e1c98e0853bc3a85f6cd9e"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
module OpenUI
|
|
7
|
+
module Rails
|
|
8
|
+
# Task implementation for installing exports of the pinned default library.
|
|
9
|
+
class DefaultLibrary # :nodoc:
|
|
10
|
+
DIRECTORY = File.expand_path("default_library", __dir__)
|
|
11
|
+
NAMES = %w[library.spec.json library.prompt.txt openui.LICENSE.txt].freeze
|
|
12
|
+
|
|
13
|
+
def self.files(directory: DIRECTORY)
|
|
14
|
+
provenance = JSON.parse(File.read(File.join(directory, "provenance.json")))
|
|
15
|
+
integrity = RuntimePackage::FILES.fetch("package/dist/openui-bundle.min.js").last
|
|
16
|
+
unless provenance.fetch("runtime_version") == RuntimeAsset::PINNED_VERSION && provenance.fetch("runtime_integrity") == integrity
|
|
17
|
+
raise ConfigurationError, "default library exports do not match the pinned runtime"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
NAMES.to_h do |name|
|
|
21
|
+
content = File.binread(File.join(directory, name))
|
|
22
|
+
unless Digest::SHA256.hexdigest(content) == provenance.fetch("sha256").fetch(name)
|
|
23
|
+
raise ConfigurationError, "default library export checksum mismatch; reinstall the gem"
|
|
24
|
+
end
|
|
25
|
+
[ name, content ]
|
|
26
|
+
end
|
|
27
|
+
rescue JSON::ParserError, KeyError, SystemCallError
|
|
28
|
+
raise ConfigurationError, "default library exports are unavailable or invalid; reinstall the gem"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.prepare(root:, configuration: OpenUI::Rails.configuration, force: false)
|
|
32
|
+
root = File.realpath(root)
|
|
33
|
+
exports = files
|
|
34
|
+
targets = [ configuration.library_spec_path, configuration.prompt_path ].map { |path| safe_path(path, root) }
|
|
35
|
+
targets << safe_path(File.join(File.dirname(targets.last), "openui.LICENSE.txt"), root)
|
|
36
|
+
if targets.uniq.size != targets.size || targets.any? { |path| targets.any? { |other| path.start_with?("#{other}/") } }
|
|
37
|
+
raise ConfigurationError, "prompt and spec need distinct file paths"
|
|
38
|
+
end
|
|
39
|
+
contents = targets.zip(NAMES.map { |name| exports.fetch(name) }).to_h
|
|
40
|
+
if !force && contents.any? { |path, content| File.exist?(path) && File.binread(path) != content }
|
|
41
|
+
raise ConfigurationError, "existing library files differ; preserve your custom library or explicitly use openui:library:prepare[true]"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
contents.each do |path, content|
|
|
45
|
+
next if File.file?(path) && File.binread(path) == content
|
|
46
|
+
|
|
47
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
48
|
+
Tempfile.create(".openui-library-", File.dirname(path)) do |temporary|
|
|
49
|
+
temporary.chmod(0o644)
|
|
50
|
+
temporary.binmode
|
|
51
|
+
temporary.write(content)
|
|
52
|
+
temporary.flush
|
|
53
|
+
if force
|
|
54
|
+
File.rename(temporary.path, path)
|
|
55
|
+
else
|
|
56
|
+
File.link(temporary.path, path)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
configuration.reset!
|
|
61
|
+
targets
|
|
62
|
+
rescue Errno::EEXIST
|
|
63
|
+
raise ConfigurationError, "library file appeared during preparation; rerun without overwriting it"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.safe_path(path, root)
|
|
67
|
+
raise ConfigurationError, "configure both library_spec_path and prompt_path before preparing the library" if path.nil?
|
|
68
|
+
|
|
69
|
+
target = File.expand_path(path, root)
|
|
70
|
+
unless !File.symlink?(target) && (!File.exist?(target) || File.file?(target))
|
|
71
|
+
raise ConfigurationError, "library outputs must be regular files inside the application root"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
ancestor = File.dirname(target)
|
|
75
|
+
ancestor = File.dirname(ancestor) until File.exist?(ancestor) || File.symlink?(ancestor)
|
|
76
|
+
real = File.realpath(ancestor)
|
|
77
|
+
unless File.directory?(ancestor) && (real == root || real.start_with?("#{root}/"))
|
|
78
|
+
raise ConfigurationError, "library output directory resolves outside the application root"
|
|
79
|
+
end
|
|
80
|
+
File.join(real, target.delete_prefix(ancestor).delete_prefix("/"))
|
|
81
|
+
end
|
|
82
|
+
private_class_method :safe_path
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenUI
|
|
4
|
+
module Rails
|
|
5
|
+
# Read-only task diagnostics. Never executes application authorization,
|
|
6
|
+
# makes HTTP requests, or prints file contents and exception messages.
|
|
7
|
+
class Doctor # :nodoc:
|
|
8
|
+
Check = Struct.new(:name, :status, :message, keyword_init: true)
|
|
9
|
+
Report = Struct.new(:checks, keyword_init: true) do
|
|
10
|
+
def ok? = checks.none? { |check| check.status == :fail }
|
|
11
|
+
|
|
12
|
+
def to_s
|
|
13
|
+
lines = checks.map { |check| "[#{check.status.to_s.upcase}] #{check.name}: #{check.message}" }
|
|
14
|
+
lines << (ok? ? "Required local checks passed. Review all UNVERIFIED checks." : "Setup has required failures. Fix them and rerun openui:doctor.")
|
|
15
|
+
lines.join("\n")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(root:, configuration: OpenUI::Rails.configuration, routes:)
|
|
20
|
+
@root = File.expand_path(root)
|
|
21
|
+
@configuration = configuration
|
|
22
|
+
@routes = routes
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run
|
|
26
|
+
@checks = []
|
|
27
|
+
valid = check_configuration
|
|
28
|
+
check_route
|
|
29
|
+
check_assets
|
|
30
|
+
check_library_identity if valid
|
|
31
|
+
record("browser", :unverified, "Verify Stimulus registration, rendering, stylesheet loading, and event handling in a browser.")
|
|
32
|
+
record("authorization", :unverified, "Review actor resolution, tenant-scoped surfaces, and global or per-action authorizers. Callbacks were not executed; missing authorization denies actions.")
|
|
33
|
+
record("idempotency", :unverified, "Verify atomic shared storage for multiple workers, TTL limits, and business constraints.")
|
|
34
|
+
record("recovery", :unverified, "Verify authorized subscriptions, canonical snapshots, and host-owned fallback UI.")
|
|
35
|
+
Report.new(checks: @checks.freeze)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def record(name, status, message)
|
|
41
|
+
@checks << Check.new(name: name, status: status, message: message).freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def check_configuration
|
|
45
|
+
@configuration.validate!
|
|
46
|
+
prompt = @configuration.prompt_path
|
|
47
|
+
raise ConfigurationError if prompt.nil? || !File.file?(prompt) || File.zero?(prompt)
|
|
48
|
+
|
|
49
|
+
record("configuration", :pass, "Library files and numeric limits are valid.")
|
|
50
|
+
true
|
|
51
|
+
rescue OpenUI::Rails::Error, SystemCallError, ArgumentError, TypeError
|
|
52
|
+
record("configuration", :fail, "Review library paths, nonempty prompt, manifest JSON, asset settings, and numeric limits. For the default library, run openui:library:prepare.")
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def check_route
|
|
57
|
+
endpoint = @configuration.actions_endpoint
|
|
58
|
+
raise ConfigurationError unless endpoint.is_a?(String) && endpoint.start_with?("/") && !endpoint.start_with?("//") && !endpoint.match?(/[?#]/)
|
|
59
|
+
|
|
60
|
+
raise ConfigurationError unless endpoint.end_with?("/actions")
|
|
61
|
+
|
|
62
|
+
mounts = @routes.routes.select { |route| route.app.respond_to?(:app) && route.app.app == Engine }
|
|
63
|
+
mount_path = endpoint.delete_suffix("/actions")
|
|
64
|
+
mount_path = "/" if mount_path.empty?
|
|
65
|
+
matching = mounts.any? do |route|
|
|
66
|
+
route.path.spec.to_s == mount_path && (route.verb.empty? || route.verb.include?("POST"))
|
|
67
|
+
end
|
|
68
|
+
if matching
|
|
69
|
+
record("actions route", :pass, "Static engine mount matches actions_endpoint. Request constraints were not executed.")
|
|
70
|
+
elsif mounts.any? { |route| route.path.spec.to_s.match?(/[:*]/) }
|
|
71
|
+
record("actions route", :unverified, "The engine has a dynamic mount. Verify actions_endpoint and request constraints in the host.")
|
|
72
|
+
else
|
|
73
|
+
raise ConfigurationError
|
|
74
|
+
end
|
|
75
|
+
rescue OpenUI::Rails::Error, ArgumentError
|
|
76
|
+
record("actions route", :fail, "Mount OpenUI::Rails::Engine and match actions_endpoint to that mount's /actions path.")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def default_assets?
|
|
80
|
+
@configuration.runtime_assets == :self_hosted && @configuration.runtime_url.nil?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def check_assets
|
|
84
|
+
unless default_assets? && @configuration.runtime_stylesheet_url.nil?
|
|
85
|
+
urls = [ RuntimeAsset.url(@configuration), RuntimeAsset.stylesheet_url(@configuration) ]
|
|
86
|
+
hashes = [ RuntimeAsset.integrity(@configuration), RuntimeAsset.stylesheet_integrity(@configuration) ]
|
|
87
|
+
raise ConfigurationError unless urls.all? { |url| url.is_a?(String) && !url.empty? } &&
|
|
88
|
+
hashes.all? { |value| value.is_a?(String) && value.match?(/\Asha(?:256|384|512)-[A-Za-z0-9+\/=]+\z/) }
|
|
89
|
+
|
|
90
|
+
record("runtime assets", :unverified, "Custom or external URLs and integrity settings exist. No network request or content verification was performed.")
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
raise ConfigurationError unless RuntimeAsset.version(@configuration) == RuntimeAsset::PINNED_VERSION
|
|
95
|
+
|
|
96
|
+
public_root = File.join(@root, "public")
|
|
97
|
+
url = @configuration.local_runtime_path
|
|
98
|
+
raise ConfigurationError unless url.is_a?(String) && url.start_with?("/") && !url.match?(/[?#]/)
|
|
99
|
+
|
|
100
|
+
directory = File.expand_path(url.delete_prefix("/"), public_root)
|
|
101
|
+
raise ConfigurationError unless directory.start_with?("#{public_root}/")
|
|
102
|
+
|
|
103
|
+
RuntimePackage::FILES.each_value do |suffix, expected|
|
|
104
|
+
path = File.join(directory, "#{RuntimeAsset::FILE_PREFIX}-#{RuntimeAsset::PINNED_VERSION}.#{suffix}")
|
|
105
|
+
inside = File.file?(path) && File.realpath(path).start_with?("#{File.realpath(@root)}/public/")
|
|
106
|
+
verified = inside && "sha384-#{Digest::SHA384.file(path).base64digest}" == expected
|
|
107
|
+
sri = case suffix
|
|
108
|
+
when "js" then RuntimeAsset.integrity(@configuration)
|
|
109
|
+
when "css" then RuntimeAsset.stylesheet_integrity(@configuration)
|
|
110
|
+
else expected
|
|
111
|
+
end
|
|
112
|
+
verified &&= valid_integrity?(path, sri)
|
|
113
|
+
record("runtime #{suffix}", verified ? :pass : :fail,
|
|
114
|
+
verified ? "Matches the pinned checksum and configured integrity." : "Missing asset, checksum mismatch, or incorrect integrity override. Run openui:runtime:fetch and review integrity settings.")
|
|
115
|
+
end
|
|
116
|
+
rescue OpenUI::Rails::Error, SystemCallError, ArgumentError, TypeError
|
|
117
|
+
record("runtime assets", :fail, "Review runtime version, paths, URLs, stylesheet, and integrity hashes. Keep default assets inside public/ and run openui:runtime:fetch.")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def valid_integrity?(path, value)
|
|
121
|
+
return false unless value.is_a?(String) && value.match?(/\Asha(?:256|384|512)-[A-Za-z0-9+\/=]+\z/)
|
|
122
|
+
|
|
123
|
+
algorithm = value.split("-", 2).first
|
|
124
|
+
"#{algorithm}-#{Digest.const_get(algorithm.upcase).file(path).base64digest}" == value
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def check_library_identity
|
|
128
|
+
unless default_assets?
|
|
129
|
+
record("library identity", :unverified, "Verify the configured prompt/spec against the actual custom runtime library.")
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
files = DefaultLibrary.files
|
|
134
|
+
matching = @configuration.library_fingerprint == LibraryManifest.parse(files.fetch("library.spec.json")).fingerprint &&
|
|
135
|
+
File.binread(@configuration.prompt_path) == files.fetch("library.prompt.txt")
|
|
136
|
+
record("library identity", matching ? :pass : :fail,
|
|
137
|
+
matching ? "Prompt and spec match the pinned default chat library." : "The default runtime needs its matching prompt/spec. Preserve custom files or use openui:library:prepare; custom libraries need a matching custom runtime.")
|
|
138
|
+
rescue OpenUI::Rails::Error, SystemCallError, ArgumentError, TypeError
|
|
139
|
+
record("library identity", :fail, "Cannot verify the pinned default library exports. Review the gem installation and configured files.")
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -123,7 +123,7 @@ module OpenUI
|
|
|
123
123
|
# Human-readable identifier, e.g. "support@1.2.0+1a2b3c4d5e6f".
|
|
124
124
|
attr_reader :identifier
|
|
125
125
|
|
|
126
|
-
# Return
|
|
126
|
+
# Return names from an array or name-keyed components collection.
|
|
127
127
|
#
|
|
128
128
|
# @return [Array<String>]
|
|
129
129
|
def component_names = names_for(:components)
|
|
@@ -178,6 +178,7 @@ module OpenUI
|
|
|
178
178
|
keys = COLLECTION_KEYS.fetch(collection)
|
|
179
179
|
keys.each do |key|
|
|
180
180
|
value = document[key]
|
|
181
|
+
return value.keys if value.is_a?(Hash)
|
|
181
182
|
next unless value.is_a?(Array)
|
|
182
183
|
|
|
183
184
|
return value.filter_map { |entry| name_of(entry) }
|
data/lib/openui/rails/version.rb
CHANGED
data/lib/openui/rails.rb
CHANGED
|
@@ -27,7 +27,9 @@ require "openui/rails/actions"
|
|
|
27
27
|
require "openui/rails/adapters/ruby_llm"
|
|
28
28
|
require "openui/rails/runtime_asset"
|
|
29
29
|
require "openui/rails/runtime_package"
|
|
30
|
+
require "openui/rails/default_library"
|
|
30
31
|
require "openui/rails/surface"
|
|
31
32
|
require "openui/rails/view_helper"
|
|
32
33
|
require "openui/rails/configuration"
|
|
34
|
+
require "openui/rails/doctor"
|
|
33
35
|
require "openui/rails/engine"
|
data/lib/tasks/openui.rake
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
namespace :openui do
|
|
4
|
+
desc "Check local OpenUI setup without writes or network requests"
|
|
5
|
+
task doctor: :environment do
|
|
6
|
+
report = OpenUI::Rails::Doctor.new(root: ::Rails.root, routes: ::Rails.application.routes).run
|
|
7
|
+
puts report
|
|
8
|
+
exit 1 unless report.ok?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
namespace :library do
|
|
12
|
+
desc "Prepare the pinned default chat prompt and spec (pass true to overwrite)"
|
|
13
|
+
task :prepare, [ :force ] => :environment do |_task, args|
|
|
14
|
+
unless [ nil, "true", "false" ].include?(args[:force])
|
|
15
|
+
raise OpenUI::Rails::ConfigurationError, "force must be true or false"
|
|
16
|
+
end
|
|
17
|
+
paths = OpenUI::Rails::DefaultLibrary.prepare(root: ::Rails.root, force: args[:force] == "true")
|
|
18
|
+
paths.each { |path| puts "Prepared #{path}" }
|
|
19
|
+
puts "Default library ready; commit the prompt and spec together."
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
4
23
|
namespace :runtime do
|
|
5
24
|
desc "Vendor the checksum-locked OpenUI JS, CSS and license (requires npm)"
|
|
6
25
|
task :fetch, [ :destination ] => :environment do |_task, args|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openui-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Wang
|
|
@@ -49,6 +49,7 @@ files:
|
|
|
49
49
|
- CODE_OF_CONDUCT.md
|
|
50
50
|
- CONFIGURATION.md
|
|
51
51
|
- CONTRIBUTING.md
|
|
52
|
+
- DEVELOPMENT.md
|
|
52
53
|
- LICENSE
|
|
53
54
|
- README.md
|
|
54
55
|
- RELEASING.md
|
|
@@ -73,6 +74,12 @@ files:
|
|
|
73
74
|
- lib/openui/rails/actions/submission.rb
|
|
74
75
|
- lib/openui/rails/adapters/ruby_llm.rb
|
|
75
76
|
- lib/openui/rails/configuration.rb
|
|
77
|
+
- lib/openui/rails/default_library.rb
|
|
78
|
+
- lib/openui/rails/default_library/library.prompt.txt
|
|
79
|
+
- lib/openui/rails/default_library/library.spec.json
|
|
80
|
+
- lib/openui/rails/default_library/openui.LICENSE.txt
|
|
81
|
+
- lib/openui/rails/default_library/provenance.json
|
|
82
|
+
- lib/openui/rails/doctor.rb
|
|
76
83
|
- lib/openui/rails/engine.rb
|
|
77
84
|
- lib/openui/rails/errors.rb
|
|
78
85
|
- lib/openui/rails/json_value.rb
|