fluid_cli 0.1.8 → 0.1.9
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/lib/fluid_cli/commands/theme/dev.rb +1 -0
- data/lib/fluid_cli/commands/theme/navigate.rb +57 -0
- data/lib/fluid_cli/commands/theme/push.rb +2 -2
- data/lib/fluid_cli/commands/theme.rb +5 -4
- data/lib/fluid_cli/theme/dev_server.rb +19 -4
- data/lib/fluid_cli/theme/file.rb +26 -1
- data/lib/fluid_cli/theme/navigation/resource_fetcher.rb +25 -0
- data/lib/fluid_cli/theme/navigation/route_navigator.rb +87 -0
- data/lib/fluid_cli/theme/schema_validator.rb +274 -0
- data/lib/fluid_cli/theme/syncer.rb +14 -6
- data/lib/fluid_cli/version.rb +1 -1
- data/lib/fluid_cli.rb +5 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 77caec2dd9193b287504a5ebb36eb91746f4a4f5f13e37403e0af2ac89f8dcf2
|
|
4
|
+
data.tar.gz: 1de9dc376d3f1c151bd47ff897305f55fc3c13d6409d3202676e394fd7a41e98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0da7f1602d15d1ba32f8995039a2d4d40cb2292f2546fbf941f8dcedbded6b5b73df918df068045d815f10bbb2a2377d6b2600beeed87b3cf952d7236a7970a6
|
|
7
|
+
data.tar.gz: 46ac939cb3df539f6b447e127487d80aa59c39ac995f17e1f1b858bd3c68e255ec2c28d922c117720c8da520efa77787d3c9b292a5157907f41b821217762282
|
|
@@ -24,6 +24,7 @@ module FluidCLI
|
|
|
24
24
|
parser.on("-t", "--theme=NAME_OR_ID") { |theme| flags[:theme] = theme }
|
|
25
25
|
parser.on("-f", "--force") { flags[:force] = true }
|
|
26
26
|
parser.on("--overwrite-json") { flags[:overwrite_json] = true }
|
|
27
|
+
parser.on("--navigate") { flags[:navigate] = true }
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def call(_args, name)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fluid_cli"
|
|
4
|
+
|
|
5
|
+
module FluidCLI
|
|
6
|
+
module Commands
|
|
7
|
+
class Theme
|
|
8
|
+
class Navigate < FluidCLI::Command
|
|
9
|
+
DEFAULT_HTTP_HOST = "127.0.0.1"
|
|
10
|
+
DEFAULT_PORT = 9292
|
|
11
|
+
|
|
12
|
+
options do |parser, flags|
|
|
13
|
+
parser.on("--host=HOST") { |host| flags[:host] = host.to_s }
|
|
14
|
+
parser.on("--port=PORT") { |port| flags[:port] = port.to_i }
|
|
15
|
+
parser.on("-t", "--theme=NAME_OR_ID") { |theme| flags[:theme] = theme }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(_args, _name)
|
|
19
|
+
valid_authentication_method!
|
|
20
|
+
|
|
21
|
+
flags = options.flags.dup
|
|
22
|
+
host = flags[:host] || DEFAULT_HTTP_HOST
|
|
23
|
+
port = flags[:port] || DEFAULT_PORT
|
|
24
|
+
address = "http://#{host}:#{port}"
|
|
25
|
+
|
|
26
|
+
theme_id = resolve_theme_id(flags[:theme])
|
|
27
|
+
|
|
28
|
+
FluidCLI::Theme::Navigation::RouteNavigator.new(@ctx, address, theme_id).navigate
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.help
|
|
32
|
+
"Interactively select a route and resource to navigate to in the theme dev server."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def valid_authentication_method!
|
|
38
|
+
if FluidCLI::Environment.auth_token.nil? && FluidCLI::DB.get(:jwt).nil?
|
|
39
|
+
FluidCLI::Context.abort("Auth token is missing", "Please login using 'fluid login' command.")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resolve_theme_id(theme_identifier)
|
|
44
|
+
if theme_identifier
|
|
45
|
+
theme = FluidCLI::Theme::Theme.find_by_identifier(@ctx, root: ".", identifier: theme_identifier)
|
|
46
|
+
@ctx.abort("Theme not found: #{theme_identifier}") unless theme
|
|
47
|
+
theme.id
|
|
48
|
+
else
|
|
49
|
+
id = FluidCLI::DB.get(:development_theme_id)
|
|
50
|
+
@ctx.abort("No active development theme found. Run 'fluid theme dev' first, or specify a theme with --theme.") unless id
|
|
51
|
+
id
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -22,7 +22,7 @@ module FluidCLI
|
|
|
22
22
|
parser.on("-j", "--json") { flags[:json] = true }
|
|
23
23
|
parser.on("-a", "--allow-live") { flags[:allow_live] = true }
|
|
24
24
|
parser.on("-p", "--publish") { flags[:publish] = true }
|
|
25
|
-
|
|
25
|
+
parser.on("-f", "--force") { flags[:force] = true }
|
|
26
26
|
# parser.on("--development-theme-id=DEVELOPMENT_THEME_ID") do |development_theme_id|
|
|
27
27
|
# flags[:development_theme_id] = development_theme_id.to_i
|
|
28
28
|
# end
|
|
@@ -45,7 +45,7 @@ module FluidCLI
|
|
|
45
45
|
# return unless CLI::UI::Prompt.confirm(question)
|
|
46
46
|
# end
|
|
47
47
|
|
|
48
|
-
syncer = FluidCLI::Theme::Syncer.new(@ctx, theme: theme)
|
|
48
|
+
syncer = FluidCLI::Theme::Syncer.new(@ctx, theme: theme, force: options.flags[:force])
|
|
49
49
|
begin
|
|
50
50
|
syncer.start_threads
|
|
51
51
|
if options.flags[:json]
|
|
@@ -3,10 +3,11 @@ require "fluid_cli"
|
|
|
3
3
|
module FluidCLI
|
|
4
4
|
module Commands
|
|
5
5
|
class Theme < FluidCLI::Command
|
|
6
|
-
subcommand :Dev,
|
|
7
|
-
subcommand :Init,
|
|
8
|
-
subcommand :
|
|
9
|
-
subcommand :
|
|
6
|
+
subcommand :Dev, 'dev', 'fluid_cli/commands/theme/dev'
|
|
7
|
+
subcommand :Init, 'init', 'fluid_cli/commands/theme/init'
|
|
8
|
+
subcommand :Navigate, 'navigate', 'fluid_cli/commands/theme/navigate'
|
|
9
|
+
subcommand :Pull, 'pull', 'fluid_cli/commands/theme/pull'
|
|
10
|
+
subcommand :Push, 'push', 'fluid_cli/commands/theme/push'
|
|
10
11
|
|
|
11
12
|
def call(*args)
|
|
12
13
|
self.class.help(*args)
|
|
@@ -28,8 +28,8 @@ module FluidCLI
|
|
|
28
28
|
class DevServer
|
|
29
29
|
include Singleton
|
|
30
30
|
|
|
31
|
-
attr_reader :app, :stopped, :ctx, :root, :host, :theme_identifier, :port, :poll, :editor_sync, :mode,
|
|
32
|
-
:block, :includes, :ignores
|
|
31
|
+
attr_reader :app, :stopped, :ctx, :root, :host, :theme_identifier, :port, :poll, :editor_sync, :force, :mode,
|
|
32
|
+
:block, :includes, :ignores, :navigate
|
|
33
33
|
|
|
34
34
|
class << self
|
|
35
35
|
def start(
|
|
@@ -40,7 +40,9 @@ module FluidCLI
|
|
|
40
40
|
port: 9292,
|
|
41
41
|
poll: false,
|
|
42
42
|
editor_sync: false,
|
|
43
|
+
force: false,
|
|
43
44
|
overwrite_json: false,
|
|
45
|
+
navigate: false,
|
|
44
46
|
mode: ReloadMode.default,
|
|
45
47
|
includes: nil,
|
|
46
48
|
ignores: nil,
|
|
@@ -54,7 +56,9 @@ module FluidCLI
|
|
|
54
56
|
port,
|
|
55
57
|
poll,
|
|
56
58
|
editor_sync,
|
|
59
|
+
force,
|
|
57
60
|
overwrite_json,
|
|
61
|
+
navigate,
|
|
58
62
|
mode,
|
|
59
63
|
includes,
|
|
60
64
|
ignores,
|
|
@@ -77,7 +81,9 @@ module FluidCLI
|
|
|
77
81
|
port,
|
|
78
82
|
poll,
|
|
79
83
|
editor_sync,
|
|
84
|
+
force,
|
|
80
85
|
overwrite_json,
|
|
86
|
+
navigate,
|
|
81
87
|
mode,
|
|
82
88
|
includes,
|
|
83
89
|
ignores,
|
|
@@ -90,7 +96,9 @@ module FluidCLI
|
|
|
90
96
|
@port = port
|
|
91
97
|
@poll = poll
|
|
92
98
|
@editor_sync = editor_sync
|
|
99
|
+
@force = force
|
|
93
100
|
@overwrite_json = overwrite_json
|
|
101
|
+
@navigate = navigate
|
|
94
102
|
@mode = mode
|
|
95
103
|
@includes = includes
|
|
96
104
|
@ignores = ignores
|
|
@@ -175,10 +183,12 @@ module FluidCLI
|
|
|
175
183
|
return if stopped
|
|
176
184
|
|
|
177
185
|
ctx.puts(serving_theme_message)
|
|
178
|
-
|
|
186
|
+
|
|
179
187
|
ctx.puts(browser_open_message)
|
|
180
188
|
|
|
181
189
|
ctx.puts(preview_message)
|
|
190
|
+
|
|
191
|
+
navigate_to_resource if @navigate
|
|
182
192
|
end
|
|
183
193
|
end
|
|
184
194
|
|
|
@@ -195,7 +205,8 @@ module FluidCLI
|
|
|
195
205
|
@syncer ||= Syncer.new(
|
|
196
206
|
ctx,
|
|
197
207
|
theme: theme,
|
|
198
|
-
overwrite_json: !editor_sync || @overwrite_json
|
|
208
|
+
overwrite_json: !editor_sync || @overwrite_json,
|
|
209
|
+
force: @force
|
|
199
210
|
)
|
|
200
211
|
end
|
|
201
212
|
|
|
@@ -238,6 +249,10 @@ module FluidCLI
|
|
|
238
249
|
@address ||= "http://#{host}:#{port}"
|
|
239
250
|
end
|
|
240
251
|
|
|
252
|
+
def navigate_to_resource
|
|
253
|
+
FluidCLI::Theme::Navigation::RouteNavigator.new(ctx, address, theme.id).navigate
|
|
254
|
+
end
|
|
255
|
+
|
|
241
256
|
# Messages
|
|
242
257
|
|
|
243
258
|
def ensure_user_message
|
data/lib/fluid_cli/theme/file.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require_relative "mime_type"
|
|
3
|
+
require_relative "schema_validator"
|
|
3
4
|
|
|
4
5
|
module FluidCLI
|
|
5
6
|
module Theme
|
|
@@ -7,6 +8,11 @@ module FluidCLI
|
|
|
7
8
|
attr_accessor :remote_checksum
|
|
8
9
|
attr_writer :warnings
|
|
9
10
|
|
|
11
|
+
TEMPLATE_TYPES = %w[
|
|
12
|
+
product medium enrollment_pack shop_page library category_page collection_page cart_page
|
|
13
|
+
home_page join_page page collection post category post_page navbar footer
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
10
16
|
def initialize(path, root)
|
|
11
17
|
super(Pathname.new(path))
|
|
12
18
|
|
|
@@ -71,7 +77,7 @@ module FluidCLI
|
|
|
71
77
|
end
|
|
72
78
|
|
|
73
79
|
def template?
|
|
74
|
-
relative_path.start_with?(
|
|
80
|
+
TEMPLATE_TYPES.any? { |type| relative_path.start_with?(type) }
|
|
75
81
|
end
|
|
76
82
|
|
|
77
83
|
def checksum
|
|
@@ -100,6 +106,25 @@ module FluidCLI
|
|
|
100
106
|
def warnings
|
|
101
107
|
@warnings || []
|
|
102
108
|
end
|
|
109
|
+
|
|
110
|
+
def validate_schema
|
|
111
|
+
return [] if !liquid?
|
|
112
|
+
|
|
113
|
+
FluidCLI::Theme::SchemaValidator
|
|
114
|
+
.new(self, blocks_schema_type: template? ? "object" : "array")
|
|
115
|
+
.validate
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def validate_schema_and_raise
|
|
119
|
+
diags = validate_schema
|
|
120
|
+
return if diags.empty?
|
|
121
|
+
|
|
122
|
+
message = "Schema validation errors in #{relative_path}:\n"
|
|
123
|
+
diags.each do |diag|
|
|
124
|
+
message += "- [#{diag[:severity]}] #{diag[:message]}\n"
|
|
125
|
+
end
|
|
126
|
+
raise FluidCLI::Theme::SchemaValidationError, message
|
|
127
|
+
end
|
|
103
128
|
end
|
|
104
129
|
end
|
|
105
130
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FluidCLI
|
|
4
|
+
module Theme
|
|
5
|
+
module Navigation
|
|
6
|
+
class ResourceFetcher
|
|
7
|
+
def initialize(ctx, theme_id)
|
|
8
|
+
@ctx = ctx
|
|
9
|
+
@theme_id = theme_id
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def fetch(themeable_type)
|
|
13
|
+
_status, body = FluidCLI::API.new(@ctx).get(
|
|
14
|
+
path: "application_themes/#{@theme_id}/available_themeables",
|
|
15
|
+
query: URI.encode_www_form(themeable: themeable_type, per_page: 50),
|
|
16
|
+
)
|
|
17
|
+
body["available_themeables"] || []
|
|
18
|
+
rescue FluidCLI::API::APIRequestError => e
|
|
19
|
+
@ctx.debug("Failed to fetch #{themeable_type} themeables: #{e.message}")
|
|
20
|
+
[]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FluidCLI
|
|
4
|
+
module Theme
|
|
5
|
+
module Navigation
|
|
6
|
+
class RouteNavigator
|
|
7
|
+
STATIC_ROUTES = [
|
|
8
|
+
{ label: "Home", path: "/home" },
|
|
9
|
+
{ label: "Shop", path: "/home/shop" },
|
|
10
|
+
{ label: "Join / Sign Up", path: "/home/join" },
|
|
11
|
+
{ label: "Cart", path: "/cart" },
|
|
12
|
+
{ label: "Blog", path: "/home/blog" },
|
|
13
|
+
{ label: "Categories (all)", path: "/home/categories" },
|
|
14
|
+
{ label: "Collections (all)", path: "/home/collections" },
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
RESOURCE_ROUTES = [
|
|
18
|
+
{ label: "Category", themeable_type: "category", path_template: "/home/categories/%<id>s", fallback_path: "/home/categories" },
|
|
19
|
+
{ label: "Collection", themeable_type: "collection", path_template: "/home/collections/%<id>s", fallback_path: "/home/collections" },
|
|
20
|
+
{ label: "Product", themeable_type: "product", path_template: "/home/products/%<id>s", fallback_path: "/home/shop" },
|
|
21
|
+
{ label: "Library", themeable_type: "library", path_template: "/home/libraries/%<id>s", fallback_path: "/home/libraries" },
|
|
22
|
+
{ label: "Post", themeable_type: "post", path_template: "/home/posts/%<id>s", fallback_path: "/home/blog" },
|
|
23
|
+
{ label: "Media", themeable_type: "medium", path_template: "/home/media/%<id>s", fallback_path: "/home/media" },
|
|
24
|
+
{ label: "Enrollment Pack", themeable_type: "enrollment_pack", path_template: "/home/enrollments/%<id>s", fallback_path: "/home/join" },
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def initialize(ctx, address, theme_id)
|
|
28
|
+
@ctx = ctx
|
|
29
|
+
@address = address
|
|
30
|
+
@theme_id = theme_id
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def navigate
|
|
34
|
+
path = select_destination
|
|
35
|
+
return unless path
|
|
36
|
+
|
|
37
|
+
url = "#{@address}#{path}"
|
|
38
|
+
@ctx.puts("\n{{bold:Navigate to: {{green:#{url}}}}}\n")
|
|
39
|
+
open_browser(url)
|
|
40
|
+
rescue => e
|
|
41
|
+
@ctx.debug("Navigation error: #{e.message}")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def select_destination
|
|
47
|
+
all_options = STATIC_ROUTES.map { |r| [:static, r] } + RESOURCE_ROUTES.map { |r| [:resource, r] }
|
|
48
|
+
type, data = CLI::UI::Prompt.ask("Select a route to navigate to") do |handler|
|
|
49
|
+
all_options.each { |t, r| handler.option(option_label(t, r)) { [t, r] } }
|
|
50
|
+
end
|
|
51
|
+
type == :static ? data[:path] : resolve_resource_path(data)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def option_label(type, data)
|
|
55
|
+
type == :static ? data[:label] : "#{data[:label]} (select specific)"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resolve_resource_path(route)
|
|
59
|
+
resources = ResourceFetcher.new(@ctx, @theme_id).fetch(route[:themeable_type])
|
|
60
|
+
|
|
61
|
+
if resources.empty?
|
|
62
|
+
@ctx.puts("{{yellow:No #{route[:label].downcase} resources found, navigating to listing page.}}")
|
|
63
|
+
return route[:fallback_path]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
resource = CLI::UI::Prompt.ask("Select a #{route[:label].downcase}") do |handler|
|
|
67
|
+
resources.each { |r| handler.option(display_name(r)) { r } }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
format(route[:path_template], id: resource["slug"])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def display_name(resource)
|
|
74
|
+
resource["title"] || resource["id"].to_s
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def open_browser(url)
|
|
78
|
+
case RbConfig::CONFIG["host_os"]
|
|
79
|
+
when /darwin/ then system("open", url)
|
|
80
|
+
when /linux/ then system("xdg-open", url)
|
|
81
|
+
when /mswin|mingw|cygwin/ then system("start", "", url)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module FluidCLI
|
|
6
|
+
module Theme
|
|
7
|
+
|
|
8
|
+
class SchemaValidator
|
|
9
|
+
Diagnostic = Struct.new(:severity, :message, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
VALID_SETTING_TYPES = %w[
|
|
12
|
+
text rich_text richtext textarea range select checkbox color text_alignment
|
|
13
|
+
color_background radio url image image_picker video_picker font_picker html
|
|
14
|
+
html_textarea link_list header blog posts posts_list products products_list
|
|
15
|
+
product product_list forms collections collections_list collection
|
|
16
|
+
collection_list categories categories_list category category_list enrollments
|
|
17
|
+
enrollments_list enrollment_pack enrollment enrollment_list
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(file, blocks_schema_type: "unknown")
|
|
21
|
+
@file = file
|
|
22
|
+
@blocks_schema_type = blocks_schema_type
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate
|
|
26
|
+
text = @file.read.to_s
|
|
27
|
+
diagnostics = []
|
|
28
|
+
|
|
29
|
+
match = text.match(/{% schema %}([\s\S]*?){% endschema %}/)
|
|
30
|
+
|
|
31
|
+
return diagnostics.map(&:to_h) unless match
|
|
32
|
+
|
|
33
|
+
json_text = match[1] || ""
|
|
34
|
+
|
|
35
|
+
schema = nil
|
|
36
|
+
begin
|
|
37
|
+
schema = JSON.parse(json_text)
|
|
38
|
+
rescue JSON::ParserError => e
|
|
39
|
+
diagnostics << Diagnostic.new(severity: "error", message: "Invalid JSON:\n #{e.message}")
|
|
40
|
+
return diagnostics.map(&:to_h)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
scan = JsonTokenScan.scan(json_text)
|
|
44
|
+
|
|
45
|
+
scan.duplicate_blocks_key_tokens.each do
|
|
46
|
+
diagnostics << Diagnostic.new(
|
|
47
|
+
severity: "error",
|
|
48
|
+
message: "Error in blocks: duplicate 'blocks' key in the same object"
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if schema.is_a?(Hash) && schema["settings"].is_a?(Array)
|
|
53
|
+
diagnostics.concat(validate_settings(schema["settings"]))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if @blocks_schema_type == "object" && !schema["blocks"].is_a?(Hash)
|
|
57
|
+
scan.blocks_array_value_tokens.each do
|
|
58
|
+
diagnostics << Diagnostic.new(
|
|
59
|
+
severity: "error",
|
|
60
|
+
message: "Error in blocks: expected an object ({})"
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if schema.is_a?(Hash) && schema.key?("blocks")
|
|
66
|
+
blocks = schema["blocks"]
|
|
67
|
+
|
|
68
|
+
case @blocks_schema_type
|
|
69
|
+
when "array"
|
|
70
|
+
if !blocks.is_a?(Array)
|
|
71
|
+
diagnostics << Diagnostic.new(
|
|
72
|
+
severity: "error",
|
|
73
|
+
message: "Error in blocks: expected an array ([])"
|
|
74
|
+
)
|
|
75
|
+
else
|
|
76
|
+
diagnostics.concat(validate_blocks(blocks))
|
|
77
|
+
end
|
|
78
|
+
when "object"
|
|
79
|
+
unless blocks.is_a?(Hash)
|
|
80
|
+
diagnostics << Diagnostic.new(
|
|
81
|
+
severity: "error",
|
|
82
|
+
message: "Error in blocks: expected an object ({})"
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
else # "unknown"
|
|
86
|
+
diagnostics.concat(validate_blocks(blocks)) if blocks.is_a?(Array)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
diagnostics.map(&:to_h)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def validate_settings(settings)
|
|
96
|
+
diagnostics = []
|
|
97
|
+
ids = {}
|
|
98
|
+
|
|
99
|
+
settings.each_with_index do |setting, index|
|
|
100
|
+
setting ||= {}
|
|
101
|
+
setting = {} unless setting.is_a?(Hash)
|
|
102
|
+
|
|
103
|
+
id = setting["id"]
|
|
104
|
+
type = setting["type"]
|
|
105
|
+
|
|
106
|
+
if id.is_a?(String) && id.strip.empty?
|
|
107
|
+
diagnostics << Diagnostic.new(
|
|
108
|
+
severity: "error",
|
|
109
|
+
message: "Error in settings: id cannot be empty"
|
|
110
|
+
)
|
|
111
|
+
elsif id && ids.key?(id)
|
|
112
|
+
diagnostics << Diagnostic.new(
|
|
113
|
+
severity: "error",
|
|
114
|
+
message: "Error in settings: duplicate id '#{id}' found"
|
|
115
|
+
)
|
|
116
|
+
elsif id
|
|
117
|
+
ids[id] = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if !type
|
|
121
|
+
diagnostics << Diagnostic.new(
|
|
122
|
+
severity: "error",
|
|
123
|
+
message: "Error in setting '#{id || index}': missing required field 'type'"
|
|
124
|
+
)
|
|
125
|
+
elsif !VALID_SETTING_TYPES.include?(type)
|
|
126
|
+
diagnostics << Diagnostic.new(
|
|
127
|
+
severity: "error",
|
|
128
|
+
message: "Invalid settings type: '#{type}'"
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
diagnostics
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def validate_blocks(blocks)
|
|
137
|
+
diagnostics = []
|
|
138
|
+
types = {}
|
|
139
|
+
|
|
140
|
+
blocks.each_with_index do |block, index|
|
|
141
|
+
block ||= {}
|
|
142
|
+
block = {} unless block.is_a?(Hash)
|
|
143
|
+
|
|
144
|
+
type = block["type"]
|
|
145
|
+
name = block["name"]
|
|
146
|
+
|
|
147
|
+
if !type
|
|
148
|
+
diagnostics << Diagnostic.new(
|
|
149
|
+
severity: "error",
|
|
150
|
+
message: "Error in blocks at index #{index}: missing required field 'type'"
|
|
151
|
+
)
|
|
152
|
+
elsif types.key?(type)
|
|
153
|
+
diagnostics << Diagnostic.new(
|
|
154
|
+
severity: "warning",
|
|
155
|
+
message: "Warning in blocks: duplicate type '#{type}' found"
|
|
156
|
+
)
|
|
157
|
+
else
|
|
158
|
+
types[type] = true
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if !name
|
|
162
|
+
diagnostics << Diagnostic.new(
|
|
163
|
+
severity: "error",
|
|
164
|
+
message: "Error in block '#{type || index}': missing required field 'name'"
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if block["settings"].is_a?(Array)
|
|
169
|
+
diagnostics.concat(validate_settings(block["settings"]))
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
diagnostics
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Minimal token scanner:
|
|
177
|
+
# - detects duplicate "blocks" keys inside the same object
|
|
178
|
+
# - detects when "blocks" value starts with an array ([)
|
|
179
|
+
class JsonTokenScan
|
|
180
|
+
Token = Struct.new(:value)
|
|
181
|
+
|
|
182
|
+
attr_reader :duplicate_blocks_key_tokens, :blocks_array_value_tokens
|
|
183
|
+
|
|
184
|
+
def initialize
|
|
185
|
+
@duplicate_blocks_key_tokens = []
|
|
186
|
+
@blocks_array_value_tokens = []
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.scan(json_text)
|
|
190
|
+
new.tap { |s| s.scan!(json_text) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def scan!(s)
|
|
194
|
+
stack = []
|
|
195
|
+
pending_key = nil
|
|
196
|
+
i = 0
|
|
197
|
+
|
|
198
|
+
while i < s.length
|
|
199
|
+
ch = s.getbyte(i)
|
|
200
|
+
|
|
201
|
+
if whitespace?(ch)
|
|
202
|
+
i += 1
|
|
203
|
+
next
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
case ch
|
|
207
|
+
when 0x7B # {
|
|
208
|
+
stack << { type: :object, keys: {}, expecting_key: true }
|
|
209
|
+
pending_key = nil
|
|
210
|
+
i += 1
|
|
211
|
+
when 0x7D # }
|
|
212
|
+
stack.pop
|
|
213
|
+
pending_key = nil
|
|
214
|
+
i += 1
|
|
215
|
+
when 0x5B # [
|
|
216
|
+
if pending_key == "blocks"
|
|
217
|
+
@blocks_array_value_tokens << Token.new("blocks-array")
|
|
218
|
+
end
|
|
219
|
+
pending_key = nil
|
|
220
|
+
stack << { type: :array }
|
|
221
|
+
i += 1
|
|
222
|
+
when 0x5D # ]
|
|
223
|
+
stack.pop
|
|
224
|
+
pending_key = nil
|
|
225
|
+
i += 1
|
|
226
|
+
when 0x3A # :
|
|
227
|
+
# after colon, we are reading a value next
|
|
228
|
+
i += 1
|
|
229
|
+
when 0x2C # ,
|
|
230
|
+
if stack.last&.dig(:type) == :object
|
|
231
|
+
stack.last[:expecting_key] = true
|
|
232
|
+
end
|
|
233
|
+
pending_key = nil
|
|
234
|
+
i += 1
|
|
235
|
+
when 0x22 # "
|
|
236
|
+
str, i = read_string(s, i)
|
|
237
|
+
|
|
238
|
+
if stack.last&.dig(:type) == :object && stack.last[:expecting_key]
|
|
239
|
+
# this string is a key
|
|
240
|
+
if str == "blocks" && stack.last[:keys][str]
|
|
241
|
+
@duplicate_blocks_key_tokens << Token.new(str)
|
|
242
|
+
end
|
|
243
|
+
stack.last[:keys][str] = true
|
|
244
|
+
stack.last[:expecting_key] = false
|
|
245
|
+
pending_key = str
|
|
246
|
+
else
|
|
247
|
+
pending_key = nil
|
|
248
|
+
end
|
|
249
|
+
else
|
|
250
|
+
# literals / numbers / etc.
|
|
251
|
+
pending_key = nil
|
|
252
|
+
i += 1
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def read_string(s, start)
|
|
258
|
+
i = start + 1
|
|
259
|
+
while i < s.length
|
|
260
|
+
break if s.getbyte(i) == 0x22 && s.getbyte(i - 1) != 0x5C
|
|
261
|
+
i += 1
|
|
262
|
+
end
|
|
263
|
+
[s[start + 1...i], i + 1]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def whitespace?(ch)
|
|
267
|
+
ch == 0x20 || ch == 0x0A || ch == 0x0D || ch == 0x09
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
class SchemaValidationError < StandardError; end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
@@ -31,10 +31,11 @@ module FluidCLI
|
|
|
31
31
|
|
|
32
32
|
def_delegators :@error_reporter, :has_any_error?
|
|
33
33
|
|
|
34
|
-
def initialize(ctx, theme:, overwrite_json: true)
|
|
34
|
+
def initialize(ctx, theme:, overwrite_json: true, force: false)
|
|
35
35
|
@ctx = ctx
|
|
36
36
|
@theme = theme
|
|
37
37
|
@overwrite_json = overwrite_json
|
|
38
|
+
@force = force
|
|
38
39
|
@error_reporter = ErrorReporter.new(ctx)
|
|
39
40
|
@standard_reporter = StandardReporter.new(ctx)
|
|
40
41
|
@reporters = [@error_reporter, @standard_reporter]
|
|
@@ -230,12 +231,15 @@ module FluidCLI
|
|
|
230
231
|
else
|
|
231
232
|
JSON.parse(response&.body)
|
|
232
233
|
end
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
errors = parsed_body.dig("errors") # either nil or another type
|
|
236
|
-
errors = errors.dig("asset") if errors&.is_a?(Hash)
|
|
234
|
+
errors = parsed_body.dig("errors") # either nil or another type
|
|
235
|
+
errors = errors.dig("asset") if errors&.is_a?(Hash)
|
|
237
236
|
|
|
238
|
-
|
|
237
|
+
["#{parsed_body['error_message']}\n#{errors}"]
|
|
238
|
+
elsif exception.respond_to?(:message)
|
|
239
|
+
[exception.message]
|
|
240
|
+
else
|
|
241
|
+
["An unknown error occurred while syncing the asset #{file}"]
|
|
242
|
+
end
|
|
239
243
|
rescue JSON::ParserError
|
|
240
244
|
[exception.message]
|
|
241
245
|
rescue StandardError => e
|
|
@@ -310,6 +314,10 @@ module FluidCLI
|
|
|
310
314
|
path = "application_themes/#{@theme.id}/resources"
|
|
311
315
|
|
|
312
316
|
if file.text?
|
|
317
|
+
if file.liquid? && !@force
|
|
318
|
+
file.validate_schema_and_raise
|
|
319
|
+
end
|
|
320
|
+
|
|
313
321
|
application_theme_resource[:content] = file.read
|
|
314
322
|
_status, body, response = api_client.put(
|
|
315
323
|
path: path,
|
data/lib/fluid_cli/version.rb
CHANGED
data/lib/fluid_cli.rb
CHANGED
|
@@ -32,6 +32,11 @@ module FluidCLI
|
|
|
32
32
|
module UI
|
|
33
33
|
autoload(:SyncProgressBar, 'fluid_cli/theme/ui/sync_progress_bar')
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
module Navigation
|
|
37
|
+
autoload(:RouteNavigator, 'fluid_cli/theme/navigation/route_navigator')
|
|
38
|
+
autoload(:ResourceFetcher, 'fluid_cli/theme/navigation/resource_fetcher')
|
|
39
|
+
end
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def self.cache_dir
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fluid_cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fluid
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-02-24 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bugsnag
|
|
@@ -105,6 +105,7 @@ files:
|
|
|
105
105
|
- lib/fluid_cli/commands/theme/dev.rb
|
|
106
106
|
- lib/fluid_cli/commands/theme/help.rb
|
|
107
107
|
- lib/fluid_cli/commands/theme/init.rb
|
|
108
|
+
- lib/fluid_cli/commands/theme/navigate.rb
|
|
108
109
|
- lib/fluid_cli/commands/theme/pull.rb
|
|
109
110
|
- lib/fluid_cli/commands/theme/push.rb
|
|
110
111
|
- lib/fluid_cli/commands/whoami.rb
|
|
@@ -143,9 +144,12 @@ files:
|
|
|
143
144
|
- lib/fluid_cli/theme/file.rb
|
|
144
145
|
- lib/fluid_cli/theme/forms/select.rb
|
|
145
146
|
- lib/fluid_cli/theme/mime_type.rb
|
|
147
|
+
- lib/fluid_cli/theme/navigation/resource_fetcher.rb
|
|
148
|
+
- lib/fluid_cli/theme/navigation/route_navigator.rb
|
|
146
149
|
- lib/fluid_cli/theme/presenters/theme_presenter.rb
|
|
147
150
|
- lib/fluid_cli/theme/presenters/themes_presenter.rb
|
|
148
151
|
- lib/fluid_cli/theme/root.rb
|
|
152
|
+
- lib/fluid_cli/theme/schema_validator.rb
|
|
149
153
|
- lib/fluid_cli/theme/syncer.rb
|
|
150
154
|
- lib/fluid_cli/theme/syncer/checksums.rb
|
|
151
155
|
- lib/fluid_cli/theme/syncer/downloader.rb
|