discourse_theme 0.4.0 → 0.5.2
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/.github/workflows/ci.yml +34 -5
- data/.gitignore +0 -10
- data/.rubocop.yml +7 -1
- data/Gemfile +1 -0
- data/Guardfile +1 -0
- data/README.md +9 -3
- data/Rakefile +1 -0
- data/bin/discourse_theme +1 -0
- data/discourse_theme.gemspec +11 -9
- data/lib/discourse_theme.rb +10 -8
- data/lib/discourse_theme/cli.rb +48 -63
- data/lib/discourse_theme/client.rb +12 -11
- data/lib/discourse_theme/config.rb +1 -0
- data/lib/discourse_theme/downloader.rb +1 -0
- data/lib/discourse_theme/scaffold.rb +110 -41
- data/lib/discourse_theme/ui.rb +35 -0
- data/lib/discourse_theme/uploader.rb +6 -5
- data/lib/discourse_theme/version.rb +1 -1
- data/lib/discourse_theme/watcher.rb +7 -6
- metadata +76 -48
- data/.travis.yml +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20a594e3cf6429ad7b721ca546710a1f57706a74bb0c8346301dea8b76a58cea
|
|
4
|
+
data.tar.gz: 477808a79184cc9251865a86a77ccfa33a64318a90a774207a5441b683499546
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d5ad9c41ca00b3172167b1985cdb5da36ef6d8073ad6b4882f6760da05006f550a12447007d64fb8486e078245eab618979c80e18e78eaa4cc1487269dd780a
|
|
7
|
+
data.tar.gz: 7e40d9ec1727422c4875307b35b38a3d178330598c4a0b1c207779564fe8cf4dbbd07864e964e8e7eee1d5ffec47ad9cdc2821f28fd24ebe1bdb44677cd659cf
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
name: CI
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
+
pull_request:
|
|
4
5
|
push:
|
|
5
6
|
branches:
|
|
6
7
|
- master
|
|
7
|
-
|
|
8
|
-
- v*
|
|
8
|
+
- main
|
|
9
9
|
|
|
10
10
|
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
ruby:
|
|
17
|
+
- 2.5
|
|
18
|
+
- 2.6
|
|
19
|
+
- 2.7
|
|
20
|
+
- 3.0
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v2
|
|
24
|
+
|
|
25
|
+
- name: Setup ruby
|
|
26
|
+
uses: ruby/setup-ruby@v1
|
|
27
|
+
with:
|
|
28
|
+
ruby-version: ${{ matrix.ruby }}
|
|
29
|
+
bundler-cache: true
|
|
30
|
+
|
|
31
|
+
- name: Lint
|
|
32
|
+
run: bundle exec rubocop
|
|
33
|
+
|
|
34
|
+
- name: Tests
|
|
35
|
+
run: bundle exec rake test
|
|
36
|
+
|
|
11
37
|
publish:
|
|
12
|
-
if:
|
|
38
|
+
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
|
|
39
|
+
needs: build
|
|
13
40
|
runs-on: ubuntu-latest
|
|
14
41
|
|
|
15
42
|
steps:
|
|
16
43
|
- uses: actions/checkout@v2
|
|
17
44
|
|
|
18
45
|
- name: Release Gem
|
|
19
|
-
uses:
|
|
46
|
+
uses: discourse/publish-rubygems-action@v2-beta
|
|
20
47
|
env:
|
|
21
|
-
RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
|
|
48
|
+
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
|
|
49
|
+
GIT_EMAIL: team@discourse.org
|
|
50
|
+
GIT_NAME: discoursebot
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/Guardfile
CHANGED
data/README.md
CHANGED
|
@@ -20,11 +20,17 @@ For help run:
|
|
|
20
20
|
discourse_theme
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
### `discourse_theme new PATH`
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Creates a new blank theme. The CLI will guide you through the process.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
### `discourse_theme download PATH`
|
|
28
|
+
|
|
29
|
+
Downloads a theme from the server and stores in the designated directory.
|
|
30
|
+
|
|
31
|
+
### `discourse_theme watch PATH`
|
|
32
|
+
|
|
33
|
+
Monitors a theme or component for changes. When changed the program will synchronize the theme or component to your Discourse of choice.
|
|
28
34
|
|
|
29
35
|
## Contributing
|
|
30
36
|
|
data/Rakefile
CHANGED
data/bin/discourse_theme
CHANGED
data/discourse_theme.gemspec
CHANGED
|
@@ -23,18 +23,20 @@ Gem::Specification.new do |spec|
|
|
|
23
23
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
24
24
|
spec.require_paths = ["lib"]
|
|
25
25
|
|
|
26
|
+
spec.required_ruby_version = '>= 2.5.0'
|
|
27
|
+
|
|
28
|
+
spec.add_runtime_dependency "minitar", "~> 0.6"
|
|
29
|
+
spec.add_runtime_dependency "listen", "~> 3.1"
|
|
30
|
+
spec.add_runtime_dependency "multipart-post", "~> 2.0"
|
|
31
|
+
spec.add_runtime_dependency "tty-prompt", "~> 0.18"
|
|
32
|
+
spec.add_runtime_dependency "rubyzip", "~> 1.2"
|
|
33
|
+
|
|
26
34
|
spec.add_development_dependency "bundler", "~> 2.0"
|
|
27
|
-
spec.add_development_dependency "rake", "~>
|
|
35
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
28
36
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
29
37
|
spec.add_development_dependency "guard", "~> 2.14"
|
|
30
38
|
spec.add_development_dependency "guard-minitest", "~> 2.4"
|
|
31
39
|
spec.add_development_dependency "webmock", "~> 3.5"
|
|
32
|
-
|
|
33
|
-
spec.
|
|
34
|
-
spec.add_dependency "listen", "~> 3.1"
|
|
35
|
-
spec.add_dependency "multipart-post", "~> 2.0"
|
|
36
|
-
spec.add_dependency "tty-prompt", "~> 0.18"
|
|
37
|
-
spec.add_dependency "rubyzip", "~> 1.2"
|
|
38
|
-
|
|
39
|
-
spec.required_ruby_version = '>= 2.6.0'
|
|
40
|
+
spec.add_development_dependency "rubocop"
|
|
41
|
+
spec.add_development_dependency "rubocop-discourse"
|
|
40
42
|
end
|
data/lib/discourse_theme.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'fileutils'
|
|
2
3
|
require 'pathname'
|
|
3
4
|
require 'tempfile'
|
|
@@ -13,14 +14,15 @@ require 'json'
|
|
|
13
14
|
require 'yaml'
|
|
14
15
|
require 'tty/prompt'
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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'
|
|
24
26
|
|
|
25
27
|
module DiscourseTheme
|
|
26
28
|
class ThemeError < StandardError; end
|
data/lib/discourse_theme/cli.rb
CHANGED
|
@@ -1,45 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module DiscourseTheme
|
|
2
3
|
class Cli
|
|
3
|
-
|
|
4
|
-
@@prompt = ::TTY::Prompt.new(help_color: :cyan)
|
|
5
|
-
@@pastel = Pastel.new
|
|
6
|
-
|
|
7
|
-
def self.yes?(message)
|
|
8
|
-
@@prompt.yes?(@@pastel.cyan("? ") + message)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def self.ask(message, default: nil)
|
|
12
|
-
@@prompt.ask(@@pastel.cyan("? ") + message, default: default)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.select(message, options)
|
|
16
|
-
@@prompt.select(@@pastel.cyan("? ") + message, options)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.info(message)
|
|
20
|
-
puts @@pastel.blue("i ") + message
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.progress(message)
|
|
24
|
-
puts @@pastel.yellow("» ") + message
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def self.error(message)
|
|
28
|
-
puts @@pastel.red("✘ #{message}")
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def self.success(message)
|
|
32
|
-
puts @@pastel.green("✔ #{message}")
|
|
33
|
-
end
|
|
34
|
-
|
|
35
4
|
SETTINGS_FILE = File.expand_path("~/.discourse_theme")
|
|
36
5
|
|
|
37
6
|
def usage
|
|
38
7
|
puts "Usage: discourse_theme COMMAND [--reset]"
|
|
39
8
|
puts
|
|
40
|
-
puts "discourse_theme new DIR
|
|
41
|
-
puts "discourse_theme download DIR
|
|
42
|
-
puts "discourse_theme watch DIR
|
|
9
|
+
puts "discourse_theme new DIR - Creates a new theme in the designated directory"
|
|
10
|
+
puts "discourse_theme download DIR - Downloads a theme from the server and stores in the designated directory"
|
|
11
|
+
puts "discourse_theme watch DIR - Watches the theme directory and synchronizes with Discourse"
|
|
43
12
|
puts
|
|
44
13
|
puts "Use --reset to change the configuration for a directory"
|
|
45
14
|
exit 1
|
|
@@ -60,13 +29,12 @@ module DiscourseTheme
|
|
|
60
29
|
components = settings.components
|
|
61
30
|
|
|
62
31
|
if command == "new"
|
|
63
|
-
raise DiscourseTheme::ThemeError.new "'#{dir} is not empty" if Dir.exists?(dir) && !Dir.empty?(dir)
|
|
32
|
+
raise DiscourseTheme::ThemeError.new "'#{dir}' is not empty" if Dir.exists?(dir) && !Dir.empty?(dir)
|
|
33
|
+
raise DiscourseTheme::ThemeError.new "git is not installed" if !command?("git")
|
|
34
|
+
raise DiscourseTheme::ThemeError.new "yarn is not installed" if !command?("yarn")
|
|
35
|
+
|
|
64
36
|
DiscourseTheme::Scaffold.generate(dir)
|
|
65
|
-
|
|
66
|
-
args[0] = "watch"
|
|
67
|
-
Cli.progress "Running discourse_theme #{args.join(' ')}"
|
|
68
|
-
run(args)
|
|
69
|
-
end
|
|
37
|
+
watch_theme?(args)
|
|
70
38
|
elsif command == "watch"
|
|
71
39
|
raise DiscourseTheme::ThemeError.new "'#{dir} does not exist" unless Dir.exists?(dir)
|
|
72
40
|
client = DiscourseTheme::Client.new(dir, settings, reset: reset)
|
|
@@ -80,13 +48,13 @@ module DiscourseTheme
|
|
|
80
48
|
options["Create and sync with a new theme"] = :create
|
|
81
49
|
options["Select a different theme"] = :select
|
|
82
50
|
|
|
83
|
-
choice =
|
|
51
|
+
choice = UI.select('How would you like to sync this theme?', options.keys)
|
|
84
52
|
|
|
85
53
|
if options[choice] == :create
|
|
86
54
|
theme_id = nil
|
|
87
55
|
elsif options[choice] == :select
|
|
88
56
|
themes = render_theme_list(theme_list)
|
|
89
|
-
choice =
|
|
57
|
+
choice = UI.select('Which theme would you like to sync with?', themes)
|
|
90
58
|
theme_id = extract_theme_id(choice)
|
|
91
59
|
theme = theme_list.find { |t| t["id"] == theme_id }
|
|
92
60
|
end
|
|
@@ -101,27 +69,26 @@ module DiscourseTheme
|
|
|
101
69
|
options["Yes"] = :sync
|
|
102
70
|
options["No"] = :none
|
|
103
71
|
options = options.sort_by { |_, b| b == components.to_sym ? 0 : 1 }.to_h if components
|
|
104
|
-
choice =
|
|
72
|
+
choice = UI.select('Would you like to update child theme components?', options.keys)
|
|
105
73
|
settings.components = components = options[choice].to_s
|
|
106
74
|
end
|
|
107
75
|
|
|
108
76
|
uploader = DiscourseTheme::Uploader.new(dir: dir, client: client, theme_id: theme_id, components: components)
|
|
109
77
|
|
|
110
|
-
|
|
78
|
+
UI.progress "Uploading theme from #{dir}"
|
|
111
79
|
settings.theme_id = theme_id = uploader.upload_full_theme
|
|
112
80
|
|
|
113
|
-
|
|
114
|
-
|
|
81
|
+
UI.success "Theme uploaded (id:#{theme_id})"
|
|
82
|
+
UI.info "Preview: #{client.root}/?preview_theme_id=#{theme_id}"
|
|
115
83
|
if client.is_theme_creator
|
|
116
|
-
|
|
84
|
+
UI.info "Manage: #{client.root}/my/themes"
|
|
117
85
|
else
|
|
118
|
-
|
|
86
|
+
UI.info "Manage: #{client.root}/admin/customize/themes/#{theme_id}"
|
|
119
87
|
end
|
|
120
88
|
watcher = DiscourseTheme::Watcher.new(dir: dir, uploader: uploader)
|
|
121
89
|
|
|
122
|
-
|
|
90
|
+
UI.progress "Watching for changes in #{dir}..."
|
|
123
91
|
watcher.watch
|
|
124
|
-
|
|
125
92
|
elsif command == "download"
|
|
126
93
|
client = DiscourseTheme::Client.new(dir, settings, reset: reset)
|
|
127
94
|
downloader = DiscourseTheme::Downloader.new(dir: dir, client: client)
|
|
@@ -129,33 +96,51 @@ module DiscourseTheme
|
|
|
129
96
|
FileUtils.mkdir_p dir unless Dir.exists?(dir)
|
|
130
97
|
raise DiscourseTheme::ThemeError.new "'#{dir} is not empty" unless Dir.empty?(dir)
|
|
131
98
|
|
|
132
|
-
|
|
99
|
+
UI.progress "Loading theme list..."
|
|
133
100
|
themes = render_theme_list(client.get_themes_list)
|
|
134
101
|
|
|
135
|
-
choice =
|
|
102
|
+
choice = UI.select('Which theme would you like to download?', themes)
|
|
136
103
|
theme_id = extract_theme_id(choice)
|
|
137
104
|
|
|
138
|
-
|
|
105
|
+
UI.progress "Downloading theme into #{dir}"
|
|
139
106
|
|
|
140
107
|
downloader.download_theme(theme_id)
|
|
141
108
|
settings.theme_id = theme_id
|
|
142
109
|
|
|
143
|
-
|
|
110
|
+
UI.success "Theme downloaded"
|
|
144
111
|
|
|
145
|
-
|
|
146
|
-
args[0] = "watch"
|
|
147
|
-
Cli.progress "Running discourse_theme #{args.join(' ')}"
|
|
148
|
-
run(args)
|
|
149
|
-
end
|
|
112
|
+
watch_theme?(args)
|
|
150
113
|
else
|
|
151
114
|
usage
|
|
152
115
|
end
|
|
153
116
|
|
|
154
|
-
|
|
117
|
+
UI.progress "Exiting..."
|
|
155
118
|
rescue DiscourseTheme::ThemeError => e
|
|
156
|
-
|
|
119
|
+
UI.error "#{e.message}"
|
|
157
120
|
rescue Interrupt, TTY::Reader::InputInterrupt => e
|
|
158
|
-
|
|
121
|
+
UI.error "Interrupted"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def command?(cmd)
|
|
127
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
|
128
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
|
129
|
+
exts.each do |ext|
|
|
130
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
|
131
|
+
return true if File.executable?(exe) && !File.directory?(exe)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def watch_theme?(args)
|
|
139
|
+
if UI.yes?("Would you like to start 'watching' this theme?")
|
|
140
|
+
args[0] = "watch"
|
|
141
|
+
UI.progress "Running discourse_theme #{args.join(' ')}"
|
|
142
|
+
run(args)
|
|
143
|
+
end
|
|
159
144
|
end
|
|
160
145
|
|
|
161
146
|
def render_theme_list(themes)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module DiscourseTheme
|
|
2
3
|
class Client
|
|
3
4
|
THEME_CREATOR_REGEX = /^https:\/\/theme-creator.discourse.org$/i
|
|
@@ -13,8 +14,8 @@ module DiscourseTheme
|
|
|
13
14
|
@is_theme_creator = !!(THEME_CREATOR_REGEX =~ @url)
|
|
14
15
|
|
|
15
16
|
if !self.class.has_needed_version?(discourse_version, "2.3.0.beta1")
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
UI.info "discourse_theme is designed for Discourse 2.3.0.beta1 or above"
|
|
18
|
+
UI.info "download will not function, and syncing destination will be unpredictable"
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -141,26 +142,26 @@ module DiscourseTheme
|
|
|
141
142
|
def guess_url(settings)
|
|
142
143
|
url = ENV['DISCOURSE_URL']
|
|
143
144
|
if url
|
|
144
|
-
|
|
145
|
+
UI.progress "Using #{url} from DISCOURSE_URL"
|
|
145
146
|
end
|
|
146
147
|
|
|
147
148
|
if !url && settings.url
|
|
148
149
|
url = settings.url
|
|
149
|
-
|
|
150
|
+
UI.progress "Using #{url} from #{DiscourseTheme::Cli::SETTINGS_FILE}"
|
|
150
151
|
end
|
|
151
152
|
|
|
152
153
|
if !url || @reset
|
|
153
|
-
url =
|
|
154
|
+
url = UI.ask("What is the root URL of your Discourse site?", default: url).strip
|
|
154
155
|
url = "http://#{url}" unless url =~ /^https?:\/\//
|
|
155
156
|
|
|
156
157
|
# maybe this is an HTTPS redirect
|
|
157
158
|
uri = URI.parse(url)
|
|
158
159
|
if URI::HTTP === uri && uri.port == 80 && is_https_redirect?(url)
|
|
159
|
-
|
|
160
|
+
UI.info "Detected that #{url} should be accessed over https"
|
|
160
161
|
url = url.sub("http", "https")
|
|
161
162
|
end
|
|
162
163
|
|
|
163
|
-
if
|
|
164
|
+
if UI.yes?("Would you like this site name stored in #{DiscourseTheme::Cli::SETTINGS_FILE}?")
|
|
164
165
|
settings.url = url
|
|
165
166
|
else
|
|
166
167
|
settings.url = nil
|
|
@@ -173,17 +174,17 @@ module DiscourseTheme
|
|
|
173
174
|
def guess_api_key(settings)
|
|
174
175
|
api_key = ENV['DISCOURSE_API_KEY']
|
|
175
176
|
if api_key
|
|
176
|
-
|
|
177
|
+
UI.progress "Using api key from DISCOURSE_API_KEY"
|
|
177
178
|
end
|
|
178
179
|
|
|
179
180
|
if !api_key && settings.api_key
|
|
180
181
|
api_key = settings.api_key
|
|
181
|
-
|
|
182
|
+
UI.progress "Using api key from #{DiscourseTheme::Cli::SETTINGS_FILE}"
|
|
182
183
|
end
|
|
183
184
|
|
|
184
185
|
if !api_key || @reset
|
|
185
|
-
api_key =
|
|
186
|
-
if
|
|
186
|
+
api_key = UI.ask("What is your API key?", default: api_key).strip
|
|
187
|
+
if UI.yes?("Would you like this API key stored in #{DiscourseTheme::Cli::SETTINGS_FILE}?")
|
|
187
188
|
settings.api_key = api_key
|
|
188
189
|
else
|
|
189
190
|
settings.api_key = nil
|
|
@@ -1,74 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
1
5
|
module DiscourseTheme
|
|
2
6
|
class Scaffold
|
|
3
7
|
|
|
4
8
|
BLANK_FILES = %w{
|
|
5
9
|
common/common.scss
|
|
6
|
-
common/header.html
|
|
7
|
-
common/after_header.html
|
|
8
|
-
common/footer.html
|
|
9
|
-
common/head_tag.html
|
|
10
|
-
common/body_tag.html
|
|
11
|
-
common/embedded.scss
|
|
12
|
-
|
|
13
|
-
desktop/desktop.scss
|
|
14
|
-
desktop/header.html
|
|
15
|
-
desktop/after_header.html
|
|
16
|
-
desktop/footer.html
|
|
17
|
-
desktop/head_tag.html
|
|
18
|
-
desktop/body_tag.html
|
|
19
|
-
|
|
20
|
-
mobile/mobile.scss
|
|
21
|
-
mobile/header.html
|
|
22
|
-
mobile/after_header.html
|
|
23
|
-
mobile/footer.html
|
|
24
|
-
mobile/head_tag.html
|
|
25
|
-
mobile/body_tag.html
|
|
26
|
-
|
|
27
|
-
locales/en.yml
|
|
28
10
|
|
|
29
11
|
settings.yml
|
|
30
12
|
}
|
|
31
13
|
|
|
32
|
-
ABOUT_JSON =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"license_url": null,
|
|
37
|
-
"assets": {
|
|
38
|
-
},
|
|
39
|
-
"color_schemes": {
|
|
40
|
-
}
|
|
14
|
+
ABOUT_JSON = {
|
|
15
|
+
about_url: nil,
|
|
16
|
+
license_url: nil,
|
|
17
|
+
assets: {}
|
|
41
18
|
}
|
|
42
|
-
STR
|
|
43
19
|
|
|
44
20
|
HELP = <<~STR
|
|
45
21
|
Are you a bit lost? Be sure to read https://meta.discourse.org/t/how-to-develop-custom-themes/60848
|
|
46
22
|
STR
|
|
47
23
|
|
|
48
24
|
GIT_IGNORE = <<~STR
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
25
|
+
.discourse-site
|
|
26
|
+
node_modules
|
|
27
|
+
HELP
|
|
28
|
+
STR
|
|
29
|
+
|
|
30
|
+
API_INITIALIZER = <<~STR
|
|
31
|
+
import { apiInitializer } from "discourse/lib/api";
|
|
32
|
+
|
|
33
|
+
export default apiInitializer("0.11.1", api => {
|
|
34
|
+
console.log("hello world from api initializer!");
|
|
35
|
+
});
|
|
36
|
+
STR
|
|
37
|
+
|
|
38
|
+
PACKAGE_JSON = <<~STR
|
|
39
|
+
{
|
|
40
|
+
"author": "#AUTHOR",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"eslint-config-discourse": "latest"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
STR
|
|
47
|
+
|
|
48
|
+
ESLINT_RC = <<~STR
|
|
49
|
+
{
|
|
50
|
+
"extends": "eslint-config-discourse"
|
|
51
|
+
}
|
|
52
|
+
STR
|
|
53
|
+
|
|
54
|
+
TEMPLATE_LINT_RC = <<~STR
|
|
55
|
+
module.exports = {
|
|
56
|
+
plugins: ["ember-template-lint-plugin-discourse"],
|
|
57
|
+
extends: "discourse:recommended",
|
|
58
|
+
};
|
|
59
|
+
STR
|
|
60
|
+
|
|
61
|
+
EN_YML = <<~YAML
|
|
62
|
+
en:
|
|
63
|
+
theme_metadata:
|
|
64
|
+
description: "#DESCRIPTION"
|
|
65
|
+
YAML
|
|
52
66
|
|
|
53
67
|
def self.generate(dir)
|
|
54
|
-
|
|
68
|
+
UI.progress "Generating a scaffold theme at #{dir}"
|
|
55
69
|
|
|
56
|
-
name =
|
|
70
|
+
name = loop do
|
|
71
|
+
input = UI.ask("What would you like to call your theme?").to_s.strip
|
|
72
|
+
if input.empty?
|
|
73
|
+
UI.error("Theme name cannot be empty")
|
|
74
|
+
else
|
|
75
|
+
break input
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
is_component = UI.yes?("Is this a component?")
|
|
57
80
|
|
|
58
81
|
FileUtils.mkdir_p dir
|
|
59
82
|
Dir.chdir dir do
|
|
60
|
-
|
|
83
|
+
author = loop do
|
|
84
|
+
input = UI.ask("Who is authoring the theme?", default: ENV['USER']).to_s.strip
|
|
85
|
+
if input.empty?
|
|
86
|
+
UI.error("Author cannot be empty")
|
|
87
|
+
else
|
|
88
|
+
break input
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
description = UI.ask("How would you describe this theme?").to_s.strip
|
|
93
|
+
|
|
94
|
+
UI.info "Creating about.json"
|
|
95
|
+
about_template = ABOUT_JSON.dup
|
|
96
|
+
about_template[:name] = name
|
|
97
|
+
if is_component
|
|
98
|
+
about_template[:component] = true
|
|
99
|
+
else
|
|
100
|
+
about_template[:color_schemes] = {}
|
|
101
|
+
end
|
|
102
|
+
File.write('about.json', JSON.pretty_generate(about_template))
|
|
103
|
+
|
|
104
|
+
UI.info "Creating HELP"
|
|
61
105
|
File.write('HELP', HELP)
|
|
106
|
+
|
|
107
|
+
UI.info "Creating package.json"
|
|
108
|
+
File.write('package.json', PACKAGE_JSON.sub("#AUTHOR", author))
|
|
109
|
+
|
|
110
|
+
UI.info "Creating .template-lintrc.js"
|
|
111
|
+
File.write('.template-lintrc.js', TEMPLATE_LINT_RC)
|
|
112
|
+
|
|
113
|
+
UI.info "Creating .eslintrc"
|
|
114
|
+
File.write('.eslintrc', ESLINT_RC)
|
|
115
|
+
|
|
116
|
+
UI.info "Creating .gitignore"
|
|
62
117
|
File.write('.gitignore', GIT_IGNORE)
|
|
63
118
|
|
|
119
|
+
locale = "locales/en.yml"
|
|
120
|
+
UI.info "Creating #{locale}"
|
|
121
|
+
FileUtils.mkdir_p(File.dirname(locale))
|
|
122
|
+
File.write(locale, EN_YML.sub("#DESCRIPTION", description))
|
|
123
|
+
|
|
124
|
+
encoded_name = name.downcase.gsub(/[^\w_-]+/, '_')
|
|
125
|
+
initializer = "javascripts/discourse/api-initializers/#{encoded_name}.js"
|
|
126
|
+
UI.info "Creating #{initializer}"
|
|
127
|
+
FileUtils.mkdir_p(File.dirname(initializer))
|
|
128
|
+
File.write(initializer, API_INITIALIZER)
|
|
129
|
+
|
|
64
130
|
BLANK_FILES.each do |f|
|
|
65
|
-
|
|
131
|
+
UI.info "Creating #{f}"
|
|
66
132
|
FileUtils.mkdir_p File.dirname(f)
|
|
67
133
|
FileUtils.touch f
|
|
68
134
|
end
|
|
69
135
|
|
|
70
|
-
|
|
71
|
-
puts `git init
|
|
136
|
+
UI.info "Initializing git repo"
|
|
137
|
+
puts `git init . --initial-branch=main`
|
|
138
|
+
|
|
139
|
+
UI.info "Installing dependencies"
|
|
140
|
+
puts `yarn`
|
|
72
141
|
end
|
|
73
142
|
end
|
|
74
143
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module DiscourseTheme
|
|
3
|
+
class UI
|
|
4
|
+
@@prompt = ::TTY::Prompt.new(help_color: :cyan)
|
|
5
|
+
@@pastel = Pastel.new
|
|
6
|
+
|
|
7
|
+
def self.yes?(message)
|
|
8
|
+
@@prompt.yes?(@@pastel.cyan("? ") + message)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.ask(message, default: nil)
|
|
12
|
+
@@prompt.ask(@@pastel.cyan("? ") + message, default: default)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.select(message, options)
|
|
16
|
+
@@prompt.select(@@pastel.cyan("? ") + message, options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.info(message)
|
|
20
|
+
puts @@pastel.blue("i ") + message
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.progress(message)
|
|
24
|
+
puts @@pastel.yellow("» ") + message
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.error(message)
|
|
28
|
+
puts @@pastel.red("✘ #{message}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.success(message)
|
|
32
|
+
puts @@pastel.green("✔ #{message}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module DiscourseTheme
|
|
2
3
|
class Uploader
|
|
3
4
|
|
|
@@ -33,9 +34,9 @@ module DiscourseTheme
|
|
|
33
34
|
json["theme"]["theme_fields"].each do |row|
|
|
34
35
|
if (error = row["error"]) && error.length > 0
|
|
35
36
|
count += 1
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
UI.error ""
|
|
38
|
+
UI.error "Error in #{row["target"]} #{row["name"]}: #{row["error"]}"
|
|
39
|
+
UI.error ""
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
count
|
|
@@ -58,7 +59,7 @@ module DiscourseTheme
|
|
|
58
59
|
response = @client.update_theme(@theme_id, args)
|
|
59
60
|
json = JSON.parse(response.body)
|
|
60
61
|
if diagnose_errors(json) != 0
|
|
61
|
-
|
|
62
|
+
UI.error "(end of errors)"
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
|
|
@@ -72,7 +73,7 @@ module DiscourseTheme
|
|
|
72
73
|
json = JSON.parse(response.body)
|
|
73
74
|
@theme_id = json["theme"]["id"]
|
|
74
75
|
if diagnose_errors(json) != 0
|
|
75
|
-
|
|
76
|
+
UI.error "(end of errors)"
|
|
76
77
|
end
|
|
77
78
|
@theme_id
|
|
78
79
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
module DiscourseTheme
|
|
2
3
|
class Watcher
|
|
3
4
|
def self.return_immediately!
|
|
@@ -22,7 +23,7 @@ module DiscourseTheme
|
|
|
22
23
|
(resolved = resolve_file(modified[0]))
|
|
23
24
|
|
|
24
25
|
target, name, type_id = resolved
|
|
25
|
-
|
|
26
|
+
UI.progress "Fast updating #{target}.scss"
|
|
26
27
|
|
|
27
28
|
@uploader.upload_theme_field(
|
|
28
29
|
target: target,
|
|
@@ -33,17 +34,17 @@ module DiscourseTheme
|
|
|
33
34
|
else
|
|
34
35
|
count = modified.length + added.length + removed.length
|
|
35
36
|
if count > 1
|
|
36
|
-
|
|
37
|
+
UI.progress "Detected changes in #{count} files, uploading theme"
|
|
37
38
|
else
|
|
38
39
|
filename = modified[0] || added[0] || removed[0]
|
|
39
|
-
|
|
40
|
+
UI.progress "Detected changes in #{filename.gsub(@dir, '')}, uploading theme"
|
|
40
41
|
end
|
|
41
42
|
@uploader.upload_full_theme
|
|
42
43
|
end
|
|
43
|
-
|
|
44
|
+
UI.success "Done! Watching for changes..."
|
|
44
45
|
rescue DiscourseTheme::ThemeError => e
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
UI.error "#{e.message}"
|
|
47
|
+
UI.progress "Watching for changes..."
|
|
47
48
|
end
|
|
48
49
|
end
|
|
49
50
|
|
metadata
CHANGED
|
@@ -1,169 +1,197 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: discourse_theme
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Saffron
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-07-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: minitar
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
20
|
-
type: :
|
|
19
|
+
version: '0.6'
|
|
20
|
+
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
26
|
+
version: '0.6'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
28
|
+
name: listen
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
34
|
-
type: :
|
|
33
|
+
version: '3.1'
|
|
34
|
+
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
40
|
+
version: '3.1'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
42
|
+
name: multipart-post
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
45
|
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
48
|
-
type: :
|
|
47
|
+
version: '2.0'
|
|
48
|
+
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
54
|
+
version: '2.0'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
56
|
+
name: tty-prompt
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
62
|
-
type: :
|
|
61
|
+
version: '0.18'
|
|
62
|
+
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
68
|
+
version: '0.18'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
70
|
+
name: rubyzip
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
73
|
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '2
|
|
76
|
-
type: :
|
|
75
|
+
version: '1.2'
|
|
76
|
+
type: :runtime
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
80
|
- - "~>"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '2
|
|
82
|
+
version: '1.2'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
|
-
name:
|
|
84
|
+
name: bundler
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '
|
|
89
|
+
version: '2.0'
|
|
90
90
|
type: :development
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '
|
|
96
|
+
version: '2.0'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
98
|
+
name: rake
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
100
100
|
requirements:
|
|
101
101
|
- - "~>"
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '0
|
|
104
|
-
type: :
|
|
103
|
+
version: '13.0'
|
|
104
|
+
type: :development
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '0
|
|
110
|
+
version: '13.0'
|
|
111
111
|
- !ruby/object:Gem::Dependency
|
|
112
|
-
name:
|
|
112
|
+
name: minitest
|
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
|
114
114
|
requirements:
|
|
115
115
|
- - "~>"
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '
|
|
118
|
-
type: :
|
|
117
|
+
version: '5.0'
|
|
118
|
+
type: :development
|
|
119
119
|
prerelease: false
|
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
121
|
requirements:
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '
|
|
124
|
+
version: '5.0'
|
|
125
125
|
- !ruby/object:Gem::Dependency
|
|
126
|
-
name:
|
|
126
|
+
name: guard
|
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
|
128
128
|
requirements:
|
|
129
129
|
- - "~>"
|
|
130
130
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: '2.
|
|
132
|
-
type: :
|
|
131
|
+
version: '2.14'
|
|
132
|
+
type: :development
|
|
133
133
|
prerelease: false
|
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
135
|
requirements:
|
|
136
136
|
- - "~>"
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: '2.
|
|
138
|
+
version: '2.14'
|
|
139
139
|
- !ruby/object:Gem::Dependency
|
|
140
|
-
name:
|
|
140
|
+
name: guard-minitest
|
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
|
142
142
|
requirements:
|
|
143
143
|
- - "~>"
|
|
144
144
|
- !ruby/object:Gem::Version
|
|
145
|
-
version: '
|
|
146
|
-
type: :
|
|
145
|
+
version: '2.4'
|
|
146
|
+
type: :development
|
|
147
147
|
prerelease: false
|
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
|
149
149
|
requirements:
|
|
150
150
|
- - "~>"
|
|
151
151
|
- !ruby/object:Gem::Version
|
|
152
|
-
version: '
|
|
152
|
+
version: '2.4'
|
|
153
153
|
- !ruby/object:Gem::Dependency
|
|
154
|
-
name:
|
|
154
|
+
name: webmock
|
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
|
156
156
|
requirements:
|
|
157
157
|
- - "~>"
|
|
158
158
|
- !ruby/object:Gem::Version
|
|
159
|
-
version: '
|
|
160
|
-
type: :
|
|
159
|
+
version: '3.5'
|
|
160
|
+
type: :development
|
|
161
161
|
prerelease: false
|
|
162
162
|
version_requirements: !ruby/object:Gem::Requirement
|
|
163
163
|
requirements:
|
|
164
164
|
- - "~>"
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
|
-
version: '
|
|
166
|
+
version: '3.5'
|
|
167
|
+
- !ruby/object:Gem::Dependency
|
|
168
|
+
name: rubocop
|
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
|
170
|
+
requirements:
|
|
171
|
+
- - ">="
|
|
172
|
+
- !ruby/object:Gem::Version
|
|
173
|
+
version: '0'
|
|
174
|
+
type: :development
|
|
175
|
+
prerelease: false
|
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
177
|
+
requirements:
|
|
178
|
+
- - ">="
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '0'
|
|
181
|
+
- !ruby/object:Gem::Dependency
|
|
182
|
+
name: rubocop-discourse
|
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
|
184
|
+
requirements:
|
|
185
|
+
- - ">="
|
|
186
|
+
- !ruby/object:Gem::Version
|
|
187
|
+
version: '0'
|
|
188
|
+
type: :development
|
|
189
|
+
prerelease: false
|
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
191
|
+
requirements:
|
|
192
|
+
- - ">="
|
|
193
|
+
- !ruby/object:Gem::Version
|
|
194
|
+
version: '0'
|
|
167
195
|
description: CLI helper for creating Discourse themes
|
|
168
196
|
email:
|
|
169
197
|
- sam.saffron@gmail.com
|
|
@@ -175,7 +203,6 @@ files:
|
|
|
175
203
|
- ".github/workflows/ci.yml"
|
|
176
204
|
- ".gitignore"
|
|
177
205
|
- ".rubocop.yml"
|
|
178
|
-
- ".travis.yml"
|
|
179
206
|
- CODE_OF_CONDUCT.md
|
|
180
207
|
- Gemfile
|
|
181
208
|
- Guardfile
|
|
@@ -190,6 +217,7 @@ files:
|
|
|
190
217
|
- lib/discourse_theme/config.rb
|
|
191
218
|
- lib/discourse_theme/downloader.rb
|
|
192
219
|
- lib/discourse_theme/scaffold.rb
|
|
220
|
+
- lib/discourse_theme/ui.rb
|
|
193
221
|
- lib/discourse_theme/uploader.rb
|
|
194
222
|
- lib/discourse_theme/version.rb
|
|
195
223
|
- lib/discourse_theme/watcher.rb
|
|
@@ -205,14 +233,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
205
233
|
requirements:
|
|
206
234
|
- - ">="
|
|
207
235
|
- !ruby/object:Gem::Version
|
|
208
|
-
version: 2.
|
|
236
|
+
version: 2.5.0
|
|
209
237
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
210
238
|
requirements:
|
|
211
239
|
- - ">="
|
|
212
240
|
- !ruby/object:Gem::Version
|
|
213
241
|
version: '0'
|
|
214
242
|
requirements: []
|
|
215
|
-
rubygems_version: 3.
|
|
243
|
+
rubygems_version: 3.1.6
|
|
216
244
|
signing_key:
|
|
217
245
|
specification_version: 4
|
|
218
246
|
summary: CLI helper for creating Discourse themes
|