discourse_theme 0.8.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8115db0a38be583770d879d2c54bf167e27d1f05582ec38144c8598d1b3111d8
4
- data.tar.gz: 1963461e7ea2b640514b2ef049ee968d619fc6163c67e0e039969fbe04db23d1
3
+ metadata.gz: ea250fe17a63ff7c35463db99c6e18a8d19d64445f38a3e572b0897547f903d7
4
+ data.tar.gz: b3e938ab61da639be2d4e2fdb7928925c78374f9840548ec6ba1aaa1eedcc3da
5
5
  SHA512:
6
- metadata.gz: c4e0de4798c6667049fbcd3ad97e50f17d3677972d269916f7fa11e0e4a3d310373a6eb58242dad20d3b7fec4f1de4c0b3c255fed30fe0b15e89dce9cac723d8
7
- data.tar.gz: e70dad03335fb5dcd6c2f3a074d4e0aca04aba798fc31bc52c9a6137b8422f46da12a1497543d104767907473b6e5e411cd592e41e68a218112873bb5c619107
6
+ metadata.gz: 8d5cc920192553d7502846f375c6042e613c425eac2f9f20f578d99cbec9d75aa118944965a43a9710b861e3277d2becdbbaafc93e431de4bdbf34b55f95df0c
7
+ data.tar.gz: e2642e70c461e25d4c4343f0b2865b390db2424918d3339800b0226e8657f55ec17a6e083c72ba843058d9b3e9893d2cf8ae74b52f64672c02f8dcfc63e316ca
@@ -13,7 +13,6 @@ jobs:
13
13
  strategy:
14
14
  matrix:
15
15
  ruby:
16
- - "2.7"
17
16
  - "3.0"
18
17
  - "3.1"
19
18
  - "3.2"
@@ -30,6 +29,11 @@ jobs:
30
29
  - name: Lint
31
30
  run: bundle exec rubocop
32
31
 
32
+ - name: syntax_tree
33
+ run: |
34
+ bundle exec stree check Gemfile $(git ls-files '*.rb') $(git ls-files '*.rake') $(git ls-files '*.thor')
35
+
36
+
33
37
  - name: Tests
34
38
  run: bundle exec rake test
35
39
 
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  inherit_gem:
2
- rubocop-discourse: default.yml
2
+ rubocop-discourse: stree-compat.yml
3
3
  inherit_mode:
4
4
  merge:
5
5
  - Exclude
data/.streerc ADDED
@@ -0,0 +1,2 @@
1
+ --print-width=100
2
+ --plugins=plugin/trailing_comma,disable_ternary
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.9.1] - 2023-10-06
9
+
10
+ ### Fixed
11
+
12
+ - `rspec` command saving settings using wrong dir
13
+
14
+ ## [0.9.0] - 2023-09-27
15
+
16
+ ### Added
17
+
18
+ - Added the `rspec` command to the CLI to support running RSpec system tests for a theme using either a Docker container
19
+ running the `discourse/discourse_test` image or a local Discourse development environment. See 100f320847a22e11c145886588fac04479c143bb and
20
+ c0c920280bef7869f0515f5e4220cf5cd3e408ef for more details.
21
+
8
22
  ## [0.7.6] - 2023-09-16
9
23
 
10
24
  ### Fixed
data/README.md CHANGED
@@ -36,6 +36,21 @@ Monitors a theme or component for changes. When changed the program will synchro
36
36
 
37
37
  Uploads a theme to the server. Requires the theme to have been previously synchronized via `watch`.
38
38
 
39
+ ### `discourse_theme rspec PATH`
40
+
41
+ Runs the [RSpec](https://rspec.info/) system tests under the `spec` folder in the designated theme directory.
42
+
43
+ On the first run for the given directory, you will be asked if you'll like to use a local Discourse repository to run the tests.
44
+
45
+ If you select 'Y' and proceeds to configure the path to the local Discourse repository, the tests will be ran using the local Discourse development environment provided by the local Discourse repository. Note that you'll have to set up the local test environment before
46
+ the tests can be ran successfully.
47
+
48
+ If the 'n' option is selected, the tests will run in a Docker container created using the [`discourse/discours_test:release`](https://hub.docker.com/r/discourse/discourse_test) Docker image. Note that this requires [Docker](https://docs.docker.com/engine/install/) to be installed.
49
+
50
+ When the `--headless` option is used, a local installation of the [Google Chrome browser](https://www.google.com/chrome/) is required.
51
+
52
+ Run `discourse_theme --help` for more usage details.
53
+
39
54
  ## Contributing
40
55
 
41
56
  Bug reports and pull requests are welcome at [Meta Discourse](https://meta.discourse.org). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
@@ -5,39 +5,41 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require "discourse_theme/version"
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- spec.name = "discourse_theme"
9
- spec.version = DiscourseTheme::VERSION
10
- spec.authors = ["Sam Saffron"]
11
- spec.email = ["sam.saffron@gmail.com"]
12
-
13
- spec.summary = %q{CLI helper for creating Discourse themes}
14
- spec.description = %q{CLI helper for creating Discourse themes}
15
- spec.homepage = "https://github.com/discourse/discourse_theme"
16
- spec.license = "MIT"
17
-
18
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
- f.match(%r{^(test|spec|features)/})
20
- end
21
-
22
- spec.bindir = "bin"
23
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
8
+ spec.name = "discourse_theme"
9
+ spec.version = DiscourseTheme::VERSION
10
+ spec.authors = ["Sam Saffron"]
11
+ spec.email = ["sam.saffron@gmail.com"]
12
+
13
+ spec.summary = "CLI helper for creating Discourse themes"
14
+ spec.description = "CLI helper for creating Discourse themes"
15
+ spec.homepage = "https://github.com/discourse/discourse_theme"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+
20
+ spec.bindir = "bin"
21
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
24
22
  spec.require_paths = ["lib"]
25
23
 
26
- spec.required_ruby_version = '>= 2.5.0'
24
+ spec.required_ruby_version = ">= 3.0.0"
27
25
 
28
26
  spec.add_runtime_dependency "minitar", "~> 0.6"
29
27
  spec.add_runtime_dependency "listen", "~> 3.1"
30
28
  spec.add_runtime_dependency "multipart-post", "~> 2.0"
31
29
  spec.add_runtime_dependency "tty-prompt", "~> 0.18"
32
30
  spec.add_runtime_dependency "rubyzip", "~> 1.2"
33
-
34
- spec.add_development_dependency "bundler", "~> 2.0"
35
- spec.add_development_dependency "rake", "~> 13.0"
36
- spec.add_development_dependency "minitest", "~> 5.0"
37
- spec.add_development_dependency "guard", "~> 2.14"
38
- spec.add_development_dependency "guard-minitest", "~> 2.4"
39
- spec.add_development_dependency "webmock", "~> 3.17"
31
+ spec.add_runtime_dependency "selenium-webdriver", "> 4.11"
32
+
33
+ spec.add_development_dependency "bundler"
34
+ spec.add_development_dependency "rake"
35
+ spec.add_development_dependency "minitest"
36
+ spec.add_development_dependency "guard"
37
+ spec.add_development_dependency "guard-minitest"
38
+ spec.add_development_dependency "webmock"
40
39
  spec.add_development_dependency "rubocop"
41
40
  spec.add_development_dependency "rubocop-discourse"
42
41
  spec.add_development_dependency "m"
42
+ spec.add_development_dependency "syntax_tree"
43
+ spec.add_development_dependency "syntax_tree-disable_ternary"
44
+ spec.add_development_dependency "mocha"
43
45
  end
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require_relative "cli_commands/rspec"
2
4
  module DiscourseTheme
3
5
  class Cli
4
6
  @@cli_settings_filename = File.expand_path("~/.discourse_theme")
@@ -12,14 +14,25 @@ module DiscourseTheme
12
14
  end
13
15
 
14
16
  def usage
15
- puts "Usage: discourse_theme COMMAND [--reset]"
16
- puts
17
- puts "discourse_theme new DIR - Creates a new theme in the designated directory"
18
- puts "discourse_theme download DIR - Downloads a theme from the server and stores in the designated directory"
19
- puts "discourse_theme upload DIR - Uploads the theme directory to Discourse"
20
- puts "discourse_theme watch DIR - Watches the theme directory and synchronizes with Discourse"
21
- puts
22
- puts "Use --reset to change the configuration for a directory"
17
+ puts <<~USAGE
18
+ Usage: discourse_theme COMMAND [DIR] [OPTIONS]
19
+
20
+ Commands:
21
+ new DIR - Creates a new theme in the specified directory.
22
+ download DIR - Downloads a theme from the server and stores it in the specified directory.
23
+ upload DIR - Uploads the theme from the specified directory to Discourse.
24
+ watch DIR - Watches the theme in the specified directory and synchronizes any changes with Discourse.
25
+ rspec DIR [OPTIONS] - Runs the RSpec tests in the specified directory. The tests can be run using a local Discourse repository or a Docker container.
26
+ --headless - Runs the RSpec system type tests in headless mode. Applies to both modes.
27
+
28
+ If specified directory has been configured to run in a Docker container, the additional options are supported.
29
+ --rebuild - Forces a rebuilds of Docker container.
30
+ --verbose - Runs the command to prepare the Docker container in verbose mode.
31
+
32
+ Global Options:
33
+ --reset - Resets the configuration for the specified directory.
34
+ USAGE
35
+
23
36
  exit 1
24
37
  end
25
38
 
@@ -38,7 +51,9 @@ module DiscourseTheme
38
51
  components = settings.components
39
52
 
40
53
  if command == "new"
41
- raise DiscourseTheme::ThemeError.new "'#{dir}' is not empty" if Dir.exist?(dir) && !Dir.empty?(dir)
54
+ if Dir.exist?(dir) && !Dir.empty?(dir)
55
+ raise DiscourseTheme::ThemeError.new "'#{dir}' is not empty"
56
+ end
42
57
  raise DiscourseTheme::ThemeError.new "git is not installed" if !command?("git")
43
58
  raise DiscourseTheme::ThemeError.new "yarn is not installed" if !command?("yarn")
44
59
 
@@ -57,18 +72,23 @@ module DiscourseTheme
57
72
  options["Create and sync with a new theme"] = :create
58
73
  options["Select a different theme"] = :select
59
74
 
60
- choice = UI.select('How would you like to sync this theme?', options.keys)
75
+ choice = UI.select("How would you like to sync this theme?", options.keys)
61
76
 
62
77
  if options[choice] == :create
63
78
  theme_id = nil
64
79
  elsif options[choice] == :select
65
80
  themes = render_theme_list(theme_list)
66
- choice = UI.select('Which theme would you like to sync with?', themes)
81
+ choice = UI.select("Which theme would you like to sync with?", themes)
67
82
  theme_id = extract_theme_id(choice)
68
83
  theme = theme_list.find { |t| t["id"] == theme_id }
69
84
  end
70
85
 
71
- about_json = JSON.parse(File.read(File.join(dir, 'about.json'))) rescue nil
86
+ about_json =
87
+ begin
88
+ JSON.parse(File.read(File.join(dir, "about.json")))
89
+ rescue StandardError
90
+ nil
91
+ end
72
92
  already_uploaded = !!theme
73
93
  is_component = theme&.[]("component")
74
94
  component_count = about_json&.[]("components")&.length || 0
@@ -78,11 +98,17 @@ module DiscourseTheme
78
98
  options["Yes"] = :sync
79
99
  options["No"] = :none
80
100
  options = options.sort_by { |_, b| b == components.to_sym ? 0 : 1 }.to_h if components
81
- choice = UI.select('Would you like to update child theme components?', options.keys)
101
+ choice = UI.select("Would you like to update child theme components?", options.keys)
82
102
  settings.components = components = options[choice].to_s
83
103
  end
84
104
 
85
- uploader = DiscourseTheme::Uploader.new(dir: dir, client: client, theme_id: theme_id, components: components)
105
+ uploader =
106
+ DiscourseTheme::Uploader.new(
107
+ dir: dir,
108
+ client: client,
109
+ theme_id: theme_id,
110
+ components: components,
111
+ )
86
112
 
87
113
  UI.progress "Uploading theme from #{dir}"
88
114
  settings.theme_id = theme_id = uploader.upload_full_theme
@@ -111,7 +137,7 @@ module DiscourseTheme
111
137
  UI.progress "Loading theme list..."
112
138
  themes = render_theme_list(client.get_themes_list)
113
139
 
114
- choice = UI.select('Which theme would you like to download?', themes)
140
+ choice = UI.select("Which theme would you like to download?", themes)
115
141
  theme_id = extract_theme_id(choice)
116
142
 
117
143
  UI.progress "Downloading theme into #{dir}"
@@ -124,15 +150,25 @@ module DiscourseTheme
124
150
  watch_theme?(args)
125
151
  elsif command == "upload"
126
152
  raise DiscourseTheme::ThemeError.new "'#{dir} does not exist" unless Dir.exist?(dir)
127
- raise DiscourseTheme::ThemeError.new "No theme_id is set, please sync via the 'watch' command initially" if theme_id == 0
153
+ if theme_id == 0
154
+ raise DiscourseTheme::ThemeError.new "No theme_id is set, please sync via the 'watch' command initially"
155
+ end
128
156
  client = DiscourseTheme::Client.new(dir, settings, reset: reset)
129
157
 
130
158
  theme_list = client.get_themes_list
131
159
 
132
160
  theme = theme_list.find { |t| t["id"] == theme_id }
133
- raise DiscourseTheme::ThemeError.new "theme_id is set, but the theme does not exist in Discourse" unless theme
161
+ unless theme
162
+ raise DiscourseTheme::ThemeError.new "theme_id is set, but the theme does not exist in Discourse"
163
+ end
134
164
 
135
- uploader = DiscourseTheme::Uploader.new(dir: dir, client: client, theme_id: theme_id, components: components)
165
+ uploader =
166
+ DiscourseTheme::Uploader.new(
167
+ dir: dir,
168
+ client: client,
169
+ theme_id: theme_id,
170
+ components: components,
171
+ )
136
172
 
137
173
  UI.progress "Uploading theme (id:#{theme_id}) from #{dir} "
138
174
  settings.theme_id = theme_id = uploader.upload_full_theme
@@ -144,6 +180,13 @@ module DiscourseTheme
144
180
  else
145
181
  UI.info "Manage: #{client.root}/admin/customize/themes/#{theme_id}"
146
182
  end
183
+ elsif command == "rspec"
184
+ DiscourseTheme::CliCommands::Rspec.run(
185
+ settings: config[dir.split("/spec")[0]],
186
+ dir: dir,
187
+ args: args,
188
+ reset: reset,
189
+ )
147
190
  else
148
191
  usage
149
192
  end
@@ -158,13 +201,15 @@ module DiscourseTheme
158
201
  private
159
202
 
160
203
  def command?(cmd)
161
- exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
162
- ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
163
- exts.each do |ext|
164
- exe = File.join(path, "#{cmd}#{ext}")
165
- return true if File.executable?(exe) && !File.directory?(exe)
204
+ exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
205
+ ENV["PATH"]
206
+ .split(File::PATH_SEPARATOR)
207
+ .each do |path|
208
+ exts.each do |ext|
209
+ exe = File.join(path, "#{cmd}#{ext}")
210
+ return true if File.executable?(exe) && !File.directory?(exe)
211
+ end
166
212
  end
167
- end
168
213
 
169
214
  false
170
215
  end
@@ -172,14 +217,16 @@ module DiscourseTheme
172
217
  def watch_theme?(args)
173
218
  if UI.yes?("Would you like to start 'watching' this theme?")
174
219
  args[0] = "watch"
175
- UI.progress "Running discourse_theme #{args.join(' ')}"
220
+ UI.progress "Running discourse_theme #{args.join(" ")}"
176
221
  run(args)
177
222
  end
178
223
  end
179
224
 
180
225
  def render_theme_list(themes)
181
- themes.sort_by { |t| t["updated_at"] }
182
- .reverse.map { |theme| "#{theme["name"]} (id:#{theme["id"]})" }
226
+ themes
227
+ .sort_by { |t| t["updated_at"] }
228
+ .reverse
229
+ .map { |theme| "#{theme["name"]} (id:#{theme["id"]})" }
183
230
  end
184
231
 
185
232
  def extract_theme_id(rendered_name)
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "selenium-webdriver"
4
+
5
+ module DiscourseTheme
6
+ module CliCommands
7
+ class Rspec
8
+ DISCOURSE_TEST_DOCKER_CONTAINER_NAME_PREFIX = "discourse_theme_test"
9
+ DISCOURSE_THEME_TEST_TMP_DIR = "/tmp/.discourse_theme_test"
10
+ SELENIUM_HEADLESS_ENV = "SELENIUM_HEADLESS=0"
11
+
12
+ class << self
13
+ def discourse_test_docker_container_name
14
+ "#{DISCOURSE_TEST_DOCKER_CONTAINER_NAME_PREFIX}_#{DiscourseTheme::VERSION}"
15
+ end
16
+
17
+ def run(settings:, dir:, args:, reset: false)
18
+ settings.local_discourse_directory = nil if reset
19
+
20
+ spec_path = "/spec"
21
+ index = dir.index(spec_path)
22
+
23
+ if index
24
+ spec_path = dir[index..-1]
25
+ dir = dir[0..index - 1]
26
+ end
27
+
28
+ spec_directory = File.join(dir, "/spec")
29
+
30
+ unless Dir.exist?(spec_directory)
31
+ raise DiscourseTheme::ThemeError.new "'#{spec_directory} does not exist"
32
+ end
33
+
34
+ configure_local_directory(settings)
35
+
36
+ headless = !!args.delete("--headless")
37
+
38
+ if settings.local_discourse_directory.empty?
39
+ run_tests_with_docker(
40
+ File.basename(dir),
41
+ spec_directory,
42
+ spec_path,
43
+ headless: headless,
44
+ verbose: !!args.delete("--verbose"),
45
+ rebuild: !!args.delete("--rebuild"),
46
+ )
47
+ else
48
+ run_tests_locally(
49
+ settings.local_discourse_directory,
50
+ spec_directory,
51
+ headless: headless,
52
+ )
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def execute(command:, message: nil, exit_on_error: true, stream: false)
59
+ UI.progress(message) if message
60
+
61
+ success = false
62
+ output = +""
63
+
64
+ Open3.popen2e(command) do |stdin, stdout_and_stderr, wait_thr|
65
+ Thread.new do
66
+ stdout_and_stderr.each do |line|
67
+ puts line if stream
68
+ output << line
69
+ end
70
+ end
71
+
72
+ exit_status = wait_thr.value
73
+ success = exit_status.success?
74
+
75
+ unless success
76
+ UI.error "Error occured while running: `#{command}`:\n\n#{output}" unless stream
77
+ exit 1 if exit_on_error
78
+ end
79
+ end
80
+
81
+ output
82
+ end
83
+
84
+ def run_tests_locally(local_directory, spec_path, headless: false)
85
+ UI.progress(
86
+ "Running RSpec tests using local Discourse repository located at '#{local_directory}'...",
87
+ )
88
+
89
+ Kernel.exec(
90
+ ENV,
91
+ "cd #{local_directory} && #{headless ? SELENIUM_HEADLESS_ENV : ""} bundle exec rspec #{spec_path}",
92
+ )
93
+ end
94
+
95
+ def run_tests_with_docker(
96
+ theme_directory_name,
97
+ spec_directory,
98
+ spec_path,
99
+ headless: false,
100
+ verbose: false,
101
+ rebuild: false
102
+ )
103
+ image = "discourse/discourse_test:release"
104
+ UI.progress("Running RSpec tests using '#{image}' Docker image...")
105
+
106
+ unless Dir.exist?(DISCOURSE_THEME_TEST_TMP_DIR)
107
+ FileUtils.mkdir_p DISCOURSE_THEME_TEST_TMP_DIR
108
+ end
109
+
110
+ # Checks if the container is running
111
+ container_name = discourse_test_docker_container_name
112
+ is_running = false
113
+
114
+ if !(
115
+ output =
116
+ execute(
117
+ command: "docker ps -a --filter name=#{container_name} --format '{{json .}}'",
118
+ )
119
+ ).empty?
120
+ is_running = JSON.parse(output)["State"] == "running"
121
+ end
122
+
123
+ if !is_running || rebuild
124
+ # Stop older versions of Docker container
125
+ existing_docker_container_ids =
126
+ execute(
127
+ command:
128
+ "docker ps -a -q --filter name=#{DISCOURSE_TEST_DOCKER_CONTAINER_NAME_PREFIX}",
129
+ ).split("\n").join(" ")
130
+
131
+ if !existing_docker_container_ids.empty?
132
+ execute(command: "docker stop #{existing_docker_container_ids}")
133
+ execute(command: "docker rm -f #{existing_docker_container_ids}")
134
+ end
135
+
136
+ execute(
137
+ command: <<~CMD.squeeze(" "),
138
+ docker run -d \
139
+ -p 31337:31337 \
140
+ --add-host host.docker.internal:host-gateway \
141
+ --entrypoint=/sbin/boot \
142
+ --name=#{container_name} \
143
+ --pull=always \
144
+ -v #{DISCOURSE_THEME_TEST_TMP_DIR}:/tmp \
145
+ #{image}
146
+ CMD
147
+ message: "Creating #{image} Docker container...",
148
+ stream: verbose,
149
+ )
150
+
151
+ execute(
152
+ command:
153
+ "docker exec -u discourse:discourse #{container_name} ruby script/docker_test.rb --no-tests --checkout-ref origin/tests-passed",
154
+ message: "Checking out latest Discourse source code...",
155
+ stream: verbose,
156
+ )
157
+
158
+ execute(
159
+ command:
160
+ "docker exec -e SKIP_MULTISITE=1 -u discourse:discourse #{container_name} bundle exec rake docker:test:setup",
161
+ message: "Setting up Discourse test environment...",
162
+ stream: verbose,
163
+ )
164
+
165
+ execute(
166
+ command: "docker exec -u discourse:discourse #{container_name} bin/ember-cli --build",
167
+ message: "Building Ember CLI assets...",
168
+ stream: verbose,
169
+ )
170
+ end
171
+
172
+ rspec_envs = []
173
+
174
+ if headless
175
+ container_ip =
176
+ execute(
177
+ command:
178
+ "docker inspect #{container_name} --format '{{.NetworkSettings.IPAddress}}'",
179
+ ).chomp("\n")
180
+
181
+ service =
182
+ start_chromedriver(allowed_origin: "host.docker.internal", allowed_ip: container_ip)
183
+
184
+ rspec_envs.push(SELENIUM_HEADLESS_ENV)
185
+ rspec_envs.push("CAPYBARA_SERVER_HOST=0.0.0.0")
186
+ rspec_envs.push(
187
+ "CAPYBARA_REMOTE_DRIVER_URL=http://host.docker.internal:#{service.uri.port}",
188
+ )
189
+ end
190
+
191
+ rspec_envs = rspec_envs.map { |env| "-e #{env}" }.join(" ")
192
+
193
+ begin
194
+ tmp_theme_directory = File.join(DISCOURSE_THEME_TEST_TMP_DIR, theme_directory_name)
195
+ FileUtils.mkdir_p(tmp_theme_directory) if !Dir.exist?(tmp_theme_directory)
196
+ FileUtils.cp_r(spec_directory, File.join(tmp_theme_directory))
197
+
198
+ execute(
199
+ command:
200
+ "docker exec #{rspec_envs} -t -u discourse:discourse #{container_name} bundle exec rspec #{File.join("/tmp", theme_directory_name, spec_path)}".squeeze(
201
+ " ",
202
+ ),
203
+ stream: true,
204
+ )
205
+ ensure
206
+ FileUtils.rm_rf(File.join(tmp_theme_directory, "/spec"))
207
+ end
208
+ end
209
+
210
+ def configure_local_directory(settings)
211
+ return if settings.local_discourse_directory_configured?
212
+
213
+ should_configure_local_directory =
214
+ UI.yes?(
215
+ "Would you like to configure a local Discourse repository used to run the RSpec tests? If you select 'n', the tests will be run using a Docker container.",
216
+ )
217
+
218
+ if should_configure_local_directory
219
+ local_discourse_directory =
220
+ UI.ask("Please enter the path to the local Discourse directory:")
221
+
222
+ unless Dir.exist?(local_discourse_directory)
223
+ raise DiscourseTheme::ThemeError.new "'#{local_discourse_directory} does not exist"
224
+ end
225
+
226
+ unless File.exist?("#{local_discourse_directory}/lib/discourse.rb")
227
+ raise DiscourseTheme::ThemeError.new "'#{local_discourse_directory} is not a Discourse repository"
228
+ end
229
+
230
+ settings.local_discourse_directory = local_discourse_directory
231
+ else
232
+ settings.local_discourse_directory = ""
233
+ end
234
+ end
235
+
236
+ def start_chromedriver(allowed_ip:, allowed_origin:)
237
+ service = Selenium::WebDriver::Service.chrome
238
+ options = Selenium::WebDriver::Options.chrome
239
+ service.executable_path = Selenium::WebDriver::DriverFinder.path(options, service.class)
240
+ service.args = ["--allowed-ips=#{allowed_ip}", "--allowed-origins=#{allowed_origin}"]
241
+ service.launch
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -44,7 +44,15 @@ module DiscourseTheme
44
44
  end
45
45
 
46
46
  def get_themes_list
47
- endpoint = root + (@is_theme_creator ? "/user_themes.json" : "/admin/customize/themes.json")
47
+ endpoint =
48
+ root +
49
+ (
50
+ if @is_theme_creator
51
+ "/user_themes.json"
52
+ else
53
+ "/admin/customize/themes.json"
54
+ end
55
+ )
48
56
 
49
57
  response = request(Net::HTTP::Get.new(endpoint), never_404: true)
50
58
  json = JSON.parse(response.body)
@@ -54,7 +62,13 @@ module DiscourseTheme
54
62
  def get_raw_theme_export(id)
55
63
  endpoint =
56
64
  root +
57
- (@is_theme_creator ? "/user_themes/#{id}/export" : "/admin/customize/themes/#{id}/export")
65
+ (
66
+ if @is_theme_creator
67
+ "/user_themes/#{id}/export"
68
+ else
69
+ "/admin/customize/themes/#{id}/export"
70
+ end
71
+ )
58
72
 
59
73
  response = request(Net::HTTP::Get.new endpoint)
60
74
  raise "Error downloading theme: #{response.code}" unless response.code.to_i == 200
@@ -72,7 +86,14 @@ module DiscourseTheme
72
86
 
73
87
  def upload_full_theme(tgz, theme_id:, components:)
74
88
  endpoint =
75
- root + (@is_theme_creator ? "/user_themes/import.json" : "/admin/themes/import.json")
89
+ root +
90
+ (
91
+ if @is_theme_creator
92
+ "/user_themes/import.json"
93
+ else
94
+ "/admin/themes/import.json"
95
+ end
96
+ )
76
97
 
77
98
  post =
78
99
  Net::HTTP::Post::Multipart.new(
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  class DiscourseTheme::Config
3
-
4
3
  class PathSetting
5
4
  def initialize(config, path)
6
5
  @config = config
@@ -39,6 +38,18 @@ class DiscourseTheme::Config
39
38
  set("components", val)
40
39
  end
41
40
 
41
+ def local_discourse_directory_configured?
42
+ !safe_config["local_discourse_directory"].nil?
43
+ end
44
+
45
+ def local_discourse_directory
46
+ safe_config["local_discourse_directory"]
47
+ end
48
+
49
+ def local_discourse_directory=(dir)
50
+ set("local_discourse_directory", dir)
51
+ end
52
+
42
53
  protected
43
54
 
44
55
  def set(name, val)
@@ -79,7 +90,7 @@ class DiscourseTheme::Config
79
90
  begin
80
91
  @raw_config = YAML.load_file(@filename)
81
92
  raise unless Hash === @raw_config
82
- rescue
93
+ rescue StandardError
83
94
  @raw_config = {}
84
95
  $stderr.puts "ERROR: #{@filename} contains invalid config, resetting"
85
96
  end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require 'zip'
2
+ require "zip"
3
3
 
4
4
  class DiscourseTheme::Downloader
5
-
6
5
  def initialize(dir:, client:)
7
6
  @dir = dir
8
7
  @client = client
@@ -26,7 +25,7 @@ class DiscourseTheme::Downloader
26
25
 
27
26
  # Minitar extracts into a sub directory, move all the files up one dir
28
27
  Dir.chdir(@dir) do
29
- folders = Dir.glob('*/')
28
+ folders = Dir.glob("*/")
30
29
  raise "Extraction failed" unless folders.length == 1
31
30
  FileUtils.mv(Dir.glob("#{folders[0]}*"), "./")
32
31
  FileUtils.remove_dir(folders[0])
@@ -37,8 +36,6 @@ class DiscourseTheme::Downloader
37
36
  private
38
37
 
39
38
  def add_headers(request)
40
- if @is_theme_creator
41
- request["User-Api-Key"] = @api_key
42
- end
39
+ request["User-Api-Key"] = @api_key if @is_theme_creator
43
40
  end
44
41
  end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'date'
4
- require 'json'
3
+ require "date"
4
+ require "json"
5
5
 
6
6
  module DiscourseTheme
7
7
  class Scaffold
8
-
9
8
  ABOUT_JSON = {
10
9
  about_url: "TODO: Put your theme's public repo or Meta topic URL here",
11
10
  license_url: "TODO: Put your theme's LICENSE URL here",
12
- assets: {}
11
+ assets: {
12
+ },
13
13
  }
14
14
 
15
15
  HELP = <<~STR
@@ -93,27 +93,29 @@ module DiscourseTheme
93
93
  def self.generate(dir)
94
94
  UI.progress "Generating a scaffold theme at #{dir}"
95
95
 
96
- name = loop do
97
- input = UI.ask("What would you like to call your theme?").to_s.strip
98
- if input.empty?
99
- UI.error("Theme name cannot be empty")
100
- else
101
- break input
96
+ name =
97
+ loop do
98
+ input = UI.ask("What would you like to call your theme?").to_s.strip
99
+ if input.empty?
100
+ UI.error("Theme name cannot be empty")
101
+ else
102
+ break input
103
+ end
102
104
  end
103
- end
104
105
 
105
106
  is_component = UI.yes?("Is this a component?")
106
107
 
107
108
  FileUtils.mkdir_p dir
108
109
  Dir.chdir dir do
109
- author = loop do
110
- input = UI.ask("Who is authoring the theme?", default: ENV['USER']).to_s.strip
111
- if input.empty?
112
- UI.error("Author cannot be empty")
113
- else
114
- break input
110
+ author =
111
+ loop do
112
+ input = UI.ask("Who is authoring the theme?", default: ENV["USER"]).to_s.strip
113
+ if input.empty?
114
+ UI.error("Author cannot be empty")
115
+ else
116
+ break input
117
+ end
115
118
  end
116
- end
117
119
 
118
120
  description = UI.ask("How would you describe this theme?").to_s.strip
119
121
 
@@ -125,19 +127,19 @@ module DiscourseTheme
125
127
  about_template[:color_schemes] = {}
126
128
  end
127
129
 
128
- encoded_name = name.downcase.gsub(/[^a-zA-Z0-9_-]+/, '_')
129
-
130
- write('about.json', JSON.pretty_generate(about_template))
131
- write('HELP', HELP)
132
- write('LICENSE', LICENSE.sub("#YEAR", "#{Date.today.year}").sub("#AUTHOR", author))
133
- write('.eslintrc', ESLINT_RC)
134
- write('.gitignore', GIT_IGNORE)
135
- write('.template-lintrc.js', TEMPLATE_LINT_RC)
136
- write('package.json', PACKAGE_JSON.sub("#AUTHOR", author))
137
- write('settings.yml', SETTINGS_YML)
138
- write('common/common.scss', '')
130
+ encoded_name = name.downcase.gsub(/[^a-zA-Z0-9_-]+/, "_")
131
+
132
+ write("about.json", JSON.pretty_generate(about_template))
133
+ write("HELP", HELP)
134
+ write("LICENSE", LICENSE.sub("#YEAR", "#{Date.today.year}").sub("#AUTHOR", author))
135
+ write(".eslintrc", ESLINT_RC)
136
+ write(".gitignore", GIT_IGNORE)
137
+ write(".template-lintrc.js", TEMPLATE_LINT_RC)
138
+ write("package.json", PACKAGE_JSON.sub("#AUTHOR", author))
139
+ write("settings.yml", SETTINGS_YML)
140
+ write("common/common.scss", "")
139
141
  write("javascripts/discourse/api-initializers/#{encoded_name}.js", API_INITIALIZER)
140
- write('locales/en.yml', EN_YML.sub("#DESCRIPTION", description))
142
+ write("locales/en.yml", EN_YML.sub("#DESCRIPTION", description))
141
143
 
142
144
  UI.info "Initializing git repo"
143
145
  puts `git init && git symbolic-ref HEAD refs/heads/main`
@@ -9,13 +9,13 @@ module DiscourseTheme
9
9
  end
10
10
 
11
11
  def compress_dir(gzip, dir)
12
- sgz = Zlib::GzipWriter.new(File.open(gzip, 'wb'))
12
+ sgz = Zlib::GzipWriter.new(File.open(gzip, "wb"))
13
13
  tar = Archive::Tar::Minitar::Output.new(sgz)
14
14
 
15
15
  Dir.chdir(dir + "/../") do
16
16
  Find.find(File.basename(dir)) do |x|
17
17
  bn = File.basename(x)
18
- Find.prune if bn == "node_modules" || bn == "src" || bn[0] == ?.
18
+ Find.prune if bn == "node_modules" || bn == "src" || bn[0] == "."
19
19
  next if File.directory?(x)
20
20
 
21
21
  Minitar.pack_file(x, tar)
@@ -39,25 +39,18 @@ module DiscourseTheme
39
39
  count
40
40
  end
41
41
 
42
- def upload_theme_field(target: , name: , type_id: , value:)
42
+ def upload_theme_field(target:, name:, type_id:, value:)
43
43
  raise "expecting theme_id to be set!" unless @theme_id
44
44
 
45
45
  args = {
46
46
  theme: {
47
- theme_fields: [{
48
- name: name,
49
- target: target,
50
- type_id: type_id,
51
- value: value
52
- }]
53
- }
47
+ theme_fields: [{ name: name, target: target, type_id: type_id, value: value }],
48
+ },
54
49
  }
55
50
 
56
51
  response = @client.update_theme(@theme_id, args)
57
52
  json = JSON.parse(response.body)
58
- if diagnose_errors(json) != 0
59
- UI.error "(end of errors)"
60
- end
53
+ UI.error "(end of errors)" if diagnose_errors(json) != 0
61
54
  end
62
55
 
63
56
  def upload_full_theme
@@ -69,14 +62,11 @@ module DiscourseTheme
69
62
 
70
63
  json = JSON.parse(response.body)
71
64
  @theme_id = json["theme"]["id"]
72
- if diagnose_errors(json) != 0
73
- UI.error "(end of errors)"
74
- end
65
+ UI.error "(end of errors)" if diagnose_errors(json) != 0
75
66
  @theme_id
76
67
  end
77
68
  ensure
78
69
  FileUtils.rm_f filename
79
70
  end
80
-
81
71
  end
82
72
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module DiscourseTheme
3
- VERSION = "0.8.0"
3
+ VERSION = "0.9.1"
4
4
  end
@@ -15,38 +15,36 @@ module DiscourseTheme
15
15
  end
16
16
 
17
17
  def watch
18
- listener = Listen.to(@dir) do |modified, added, removed|
19
- begin
20
- if modified.length == 1 &&
21
- added.length == 0 &&
22
- removed.length == 0 &&
23
- (resolved = resolve_file(modified[0]))
18
+ listener =
19
+ Listen.to(@dir) do |modified, added, removed|
20
+ begin
21
+ if modified.length == 1 && added.length == 0 && removed.length == 0 &&
22
+ (resolved = resolve_file(modified[0]))
23
+ target, name, type_id = resolved
24
+ UI.progress "Fast updating #{target}.scss"
24
25
 
25
- target, name, type_id = resolved
26
- UI.progress "Fast updating #{target}.scss"
27
-
28
- @uploader.upload_theme_field(
29
- target: target,
30
- name: name,
31
- value: File.read(modified[0]),
32
- type_id: type_id
33
- )
34
- else
35
- count = modified.length + added.length + removed.length
36
- if count > 1
37
- UI.progress "Detected changes in #{count} files, uploading theme"
26
+ @uploader.upload_theme_field(
27
+ target: target,
28
+ name: name,
29
+ value: File.read(modified[0]),
30
+ type_id: type_id,
31
+ )
38
32
  else
39
- filename = modified[0] || added[0] || removed[0]
40
- UI.progress "Detected changes in #{filename.gsub(@dir, '')}, uploading theme"
33
+ count = modified.length + added.length + removed.length
34
+ if count > 1
35
+ UI.progress "Detected changes in #{count} files, uploading theme"
36
+ else
37
+ filename = modified[0] || added[0] || removed[0]
38
+ UI.progress "Detected changes in #{filename.gsub(@dir, "")}, uploading theme"
39
+ end
40
+ @uploader.upload_full_theme
41
41
  end
42
- @uploader.upload_full_theme
42
+ UI.success "Done! Watching for changes..."
43
+ rescue DiscourseTheme::ThemeError => e
44
+ UI.error "#{e.message}"
45
+ UI.progress "Watching for changes..."
43
46
  end
44
- UI.success "Done! Watching for changes..."
45
- rescue DiscourseTheme::ThemeError => e
46
- UI.error "#{e.message}"
47
- UI.progress "Watching for changes..."
48
47
  end
49
- end
50
48
 
51
49
  listener.start
52
50
  sleep unless self.class.return_immediately?
@@ -60,10 +58,10 @@ module DiscourseTheme
60
58
 
61
59
  target, file = name.split("/")
62
60
 
63
- if ["common", "desktop", "mobile"].include?(target)
61
+ if %w[common desktop mobile].include?(target)
64
62
  if file == "#{target}.scss"
65
63
  # a CSS file
66
- return [target, "scss", 1]
64
+ return target, "scss", 1
67
65
  end
68
66
  end
69
67
 
@@ -1,29 +1,30 @@
1
1
  # frozen_string_literal: true
2
- require 'fileutils'
3
- require 'pathname'
4
- require 'tempfile'
5
- require 'securerandom'
6
- require 'minitar'
7
- require 'zlib'
8
- require 'find'
9
- require 'net/http'
10
- require 'net/http/post/multipart'
11
- require 'uri'
12
- require 'listen'
13
- require 'json'
14
- require 'yaml'
15
- require 'tty/prompt'
2
+ require "fileutils"
3
+ require "pathname"
4
+ require "tempfile"
5
+ require "securerandom"
6
+ require "minitar"
7
+ require "zlib"
8
+ require "find"
9
+ require "net/http"
10
+ require "net/http/post/multipart"
11
+ require "uri"
12
+ require "listen"
13
+ require "json"
14
+ require "yaml"
15
+ require "tty/prompt"
16
16
 
17
- require_relative 'discourse_theme/version'
18
- require_relative 'discourse_theme/config'
19
- require_relative 'discourse_theme/ui'
20
- require_relative 'discourse_theme/cli'
21
- require_relative 'discourse_theme/client'
22
- require_relative 'discourse_theme/downloader'
23
- require_relative 'discourse_theme/uploader'
24
- require_relative 'discourse_theme/watcher'
25
- require_relative 'discourse_theme/scaffold'
17
+ require_relative "discourse_theme/version"
18
+ require_relative "discourse_theme/config"
19
+ require_relative "discourse_theme/ui"
20
+ require_relative "discourse_theme/cli"
21
+ require_relative "discourse_theme/client"
22
+ require_relative "discourse_theme/downloader"
23
+ require_relative "discourse_theme/uploader"
24
+ require_relative "discourse_theme/watcher"
25
+ require_relative "discourse_theme/scaffold"
26
26
 
27
27
  module DiscourseTheme
28
- class ThemeError < StandardError; end
28
+ class ThemeError < StandardError
29
+ end
29
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: discourse_theme
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-06 00:00:00.000000000 Z
11
+ date: 2023-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitar
@@ -80,90 +80,104 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '1.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: selenium-webdriver
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.11'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">"
95
+ - !ruby/object:Gem::Version
96
+ version: '4.11'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: bundler
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
- - - "~>"
101
+ - - ">="
88
102
  - !ruby/object:Gem::Version
89
- version: '2.0'
103
+ version: '0'
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
- - - "~>"
108
+ - - ">="
95
109
  - !ruby/object:Gem::Version
96
- version: '2.0'
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rake
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
- - - "~>"
115
+ - - ">="
102
116
  - !ruby/object:Gem::Version
103
- version: '13.0'
117
+ version: '0'
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
- - - "~>"
122
+ - - ">="
109
123
  - !ruby/object:Gem::Version
110
- version: '13.0'
124
+ version: '0'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: minitest
113
127
  requirement: !ruby/object:Gem::Requirement
114
128
  requirements:
115
- - - "~>"
129
+ - - ">="
116
130
  - !ruby/object:Gem::Version
117
- version: '5.0'
131
+ version: '0'
118
132
  type: :development
119
133
  prerelease: false
120
134
  version_requirements: !ruby/object:Gem::Requirement
121
135
  requirements:
122
- - - "~>"
136
+ - - ">="
123
137
  - !ruby/object:Gem::Version
124
- version: '5.0'
138
+ version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: guard
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
- - - "~>"
143
+ - - ">="
130
144
  - !ruby/object:Gem::Version
131
- version: '2.14'
145
+ version: '0'
132
146
  type: :development
133
147
  prerelease: false
134
148
  version_requirements: !ruby/object:Gem::Requirement
135
149
  requirements:
136
- - - "~>"
150
+ - - ">="
137
151
  - !ruby/object:Gem::Version
138
- version: '2.14'
152
+ version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
154
  name: guard-minitest
141
155
  requirement: !ruby/object:Gem::Requirement
142
156
  requirements:
143
- - - "~>"
157
+ - - ">="
144
158
  - !ruby/object:Gem::Version
145
- version: '2.4'
159
+ version: '0'
146
160
  type: :development
147
161
  prerelease: false
148
162
  version_requirements: !ruby/object:Gem::Requirement
149
163
  requirements:
150
- - - "~>"
164
+ - - ">="
151
165
  - !ruby/object:Gem::Version
152
- version: '2.4'
166
+ version: '0'
153
167
  - !ruby/object:Gem::Dependency
154
168
  name: webmock
155
169
  requirement: !ruby/object:Gem::Requirement
156
170
  requirements:
157
- - - "~>"
171
+ - - ">="
158
172
  - !ruby/object:Gem::Version
159
- version: '3.17'
173
+ version: '0'
160
174
  type: :development
161
175
  prerelease: false
162
176
  version_requirements: !ruby/object:Gem::Requirement
163
177
  requirements:
164
- - - "~>"
178
+ - - ">="
165
179
  - !ruby/object:Gem::Version
166
- version: '3.17'
180
+ version: '0'
167
181
  - !ruby/object:Gem::Dependency
168
182
  name: rubocop
169
183
  requirement: !ruby/object:Gem::Requirement
@@ -206,6 +220,48 @@ dependencies:
206
220
  - - ">="
207
221
  - !ruby/object:Gem::Version
208
222
  version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: syntax_tree
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ - !ruby/object:Gem::Dependency
238
+ name: syntax_tree-disable_ternary
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - ">="
242
+ - !ruby/object:Gem::Version
243
+ version: '0'
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '0'
251
+ - !ruby/object:Gem::Dependency
252
+ name: mocha
253
+ requirement: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - ">="
256
+ - !ruby/object:Gem::Version
257
+ version: '0'
258
+ type: :development
259
+ prerelease: false
260
+ version_requirements: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - ">="
263
+ - !ruby/object:Gem::Version
264
+ version: '0'
209
265
  description: CLI helper for creating Discourse themes
210
266
  email:
211
267
  - sam.saffron@gmail.com
@@ -217,6 +273,7 @@ files:
217
273
  - ".github/workflows/ci.yml"
218
274
  - ".gitignore"
219
275
  - ".rubocop.yml"
276
+ - ".streerc"
220
277
  - CHANGELOG.md
221
278
  - CODE_OF_CONDUCT.md
222
279
  - Gemfile
@@ -228,6 +285,7 @@ files:
228
285
  - discourse_theme.gemspec
229
286
  - lib/discourse_theme.rb
230
287
  - lib/discourse_theme/cli.rb
288
+ - lib/discourse_theme/cli_commands/rspec.rb
231
289
  - lib/discourse_theme/client.rb
232
290
  - lib/discourse_theme/config.rb
233
291
  - lib/discourse_theme/downloader.rb
@@ -248,7 +306,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
248
306
  requirements:
249
307
  - - ">="
250
308
  - !ruby/object:Gem::Version
251
- version: 2.5.0
309
+ version: 3.0.0
252
310
  required_rubygems_version: !ruby/object:Gem::Requirement
253
311
  requirements:
254
312
  - - ">="