pagecord-cli 0.1.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1e156f74ff6f6b5860f2a21cae283aaccf1d3bec27ef02ed5bfc02002a1bdb3
4
- data.tar.gz: d05ee18c97ee613d5903e76bf3508c20ed6536b8120a3a3f60dc279539048203
3
+ metadata.gz: 5e79c8864b73579755f1699da00835d474cae8871b64c5ff21e091d4540e95be
4
+ data.tar.gz: bbdc8963f7317ec56f55de326898021d1bf5712f15c8833c83f6ccbac533f06f
5
5
  SHA512:
6
- metadata.gz: f02588d28a65bfd183cd5830f96d88395523960ef7a0ab4584be5e7e386b0f15530a1b5fa8b2c169b629a5cbabac138d77d428cffaeabaada09c2ffbe7a4d105
7
- data.tar.gz: 5057321b2a894a091ab7f7e76cb8fb0e9724f3576b9c435f83dc06dac38e59cb78f0e240f942297f34fd4479b2caf8a9de3887b4bd1e7593c2f0f89b796b8e65
6
+ metadata.gz: b6dab32f1f4884be02388a220d441fbe21041940d5f3c4a6e607ce4c9ff4c60d12582ae9f60435cbcf87b3e3645f9fd58cc144a55b4b453bd2a9f628cde3c0a3
7
+ data.tar.gz: 208af262450558105da20a6006cf70340e804ad6c4fadf4dbab05aada82a572efd0f5af01cfd12cdec2d1b20511473407d00d5f87c51d1ae36e6b45bebbde3ef
data/README.md CHANGED
@@ -52,7 +52,7 @@ pagecord publish hello.md myblog
52
52
  See configured blogs:
53
53
 
54
54
  ```bash
55
- pagecord list
55
+ pagecord blog list
56
56
  ```
57
57
 
58
58
  Remove a saved blog:
@@ -61,7 +61,60 @@ Remove a saved blog:
61
61
  pagecord logout myblog
62
62
  ```
63
63
 
64
- ## Options
64
+ ## Choosing a blog
65
+
66
+ With several blogs configured, pick a default once:
67
+
68
+ ```bash
69
+ pagecord blog use myblog
70
+ ```
71
+
72
+ Every command works out which blog to use in this order: the subdomain passed
73
+ as a positional argument, then `--blog myblog`, then the `PAGECORD_BLOG`
74
+ environment variable, then the default set by `pagecord blog use`, and finally
75
+ the only configured blog if there is just one.
76
+
77
+ ## Appearance
78
+
79
+ ```bash
80
+ pagecord appearance show
81
+ pagecord appearance update --theme sand --font serif
82
+ ```
83
+
84
+ Fields: `--theme`, `--font`, `--width`, `--layout`, the six
85
+ `--custom-theme-*` colours, and `--show-branding true|false`.
86
+
87
+ ## Custom code
88
+
89
+ ```bash
90
+ pagecord custom-code show --css > blog.css
91
+ pagecord custom-code update --css blog.css
92
+ ```
93
+
94
+ `--css`, `--footer-html`, `--head-html` and `--body-html` each take the path to
95
+ a file holding that content, so you can keep all of it under version control:
96
+
97
+ ```bash
98
+ pagecord custom-code update --css blog.css --footer-html footer.html
99
+ ```
100
+
101
+ There is also `--enabled true|false` to switch your head and body code off
102
+ without deleting it.
103
+
104
+ `custom-code show` prints JSON, or the raw field when you name one, so
105
+ redirecting it to a file round-trips.
106
+
107
+ ## Global options
108
+
109
+ Accepted by every command, before or after the command name:
110
+
111
+ ```bash
112
+ --blog SUBDOMAIN # which blog to act on
113
+ --json # machine-readable output
114
+ --quiet # suppress confirmation messages
115
+ ```
116
+
117
+ ## Publish options
65
118
 
66
119
  `publish` and `draft` accept:
67
120
 
@@ -1,11 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "io/console"
4
+ require "json"
4
5
  require "optparse"
5
6
 
6
7
  module PagecordCLI
7
8
  class CLI
8
- attr_reader :argv, :config, :input, :output, :error
9
+ class Error < StandardError; end
10
+
11
+ APPEARANCE_FIELDS = %w[
12
+ theme font width layout
13
+ custom_theme_bg_light custom_theme_text_light custom_theme_accent_light
14
+ custom_theme_bg_dark custom_theme_text_dark custom_theme_accent_dark
15
+ ].freeze
16
+
17
+ CUSTOM_CODE_FIELDS = {
18
+ "css" => "custom_css",
19
+ "footer-html" => "custom_footer_html",
20
+ "head-html" => "custom_head_html",
21
+ "body-html" => "custom_body_html"
22
+ }.freeze
23
+
24
+ attr_reader :argv, :config, :input, :output, :error, :options
9
25
 
10
26
  def initialize(argv, config: Config.new, input: $stdin, output: $stdout, error: $stderr)
11
27
  @argv = argv.dup
@@ -13,26 +29,30 @@ module PagecordCLI
13
29
  @input = input
14
30
  @output = output
15
31
  @error = error
32
+ @options = {}
16
33
  end
17
34
 
18
35
  def run
36
+ return help if argv.empty? || %w[help -h --help].include?(argv.first)
37
+
38
+ parser.order!(argv)
19
39
  command = argv.shift
20
40
 
21
41
  case command
22
42
  when "login" then login
23
43
  when "logout" then logout
24
44
  when "list" then list
45
+ when "blog" then blog
46
+ when "appearance" then appearance
47
+ when "custom-code" then custom_code
25
48
  when "publish" then publish("published")
26
49
  when "draft" then publish("draft")
27
- when "help", nil, "-h", "--help" then help
28
50
  else
29
51
  fail_with("Unknown command: #{command}")
30
52
  end
31
53
  rescue Client::Error => e
32
54
  fail_with(api_error_message(e))
33
- rescue Config::Error => e
34
- fail_with(e.message)
35
- rescue Errno::ENOENT => e
55
+ rescue Error, Config::Error, OptionParser::ParseError, Errno::ENOENT => e
36
56
  fail_with(e.message)
37
57
  rescue Interrupt
38
58
  error.puts
@@ -42,77 +62,165 @@ module PagecordCLI
42
62
  private
43
63
 
44
64
  def login
45
- options = { base_url: Config::DEFAULT_BASE_URL }
46
- parser = OptionParser.new do |opts|
47
- opts.on("--base-url URL") { |value| options[:base_url] = value }
48
- end
49
- parser.parse!(argv)
65
+ base_url = Config::DEFAULT_BASE_URL
66
+ parser { |opts| opts.on("--base-url URL") { |value| base_url = value } }.parse!(argv)
50
67
 
51
68
  subdomain = argv.shift
52
69
  return fail_with("Usage: pagecord login SUBDOMAIN") unless subdomain
53
70
 
54
- base_url = Config.normalize_base_url(options[:base_url])
71
+ base_url = Config.normalize_base_url(base_url)
55
72
  api_key = prompt_api_key
56
- client = Client.new(api_key: api_key, base_url: base_url)
57
- client.verify!
73
+ Client.new(api_key: api_key, base_url: base_url).verify!
58
74
  config.save_blog(subdomain, api_key: api_key, base_url: base_url)
59
75
 
60
- output.puts "Saved #{subdomain}"
76
+ say "Saved #{subdomain}"
61
77
  0
62
78
  end
63
79
 
64
80
  def logout
81
+ parser.parse!(argv)
65
82
  subdomain = resolve_blog(argv.shift)
66
- return fail_with("No blogs are configured") if config.blogs.empty?
67
- return fail_with("Please specify a subdomain") unless subdomain
68
- return fail_with("Unknown blog: #{subdomain}") unless config.blog(subdomain)
69
83
 
70
84
  config.delete_blog(subdomain)
71
- output.puts "Removed #{subdomain}"
85
+ say "Removed #{subdomain}"
72
86
  0
73
87
  end
74
88
 
89
+ def blog
90
+ case argv.shift
91
+ when "list" then list
92
+ when "use" then use
93
+ else fail_with("Usage: pagecord blog list|use SUBDOMAIN")
94
+ end
95
+ end
96
+
75
97
  def list
98
+ parser.parse!(argv)
76
99
  return fail_with("No blogs are configured") if config.blogs.empty?
77
100
 
78
- config.blogs.each do |subdomain, details|
79
- suffix = details["base_url"] == Config::DEFAULT_BASE_URL ? "" : " (#{details["base_url"]})"
80
- output.puts "#{subdomain}#{suffix}"
101
+ if options[:json]
102
+ print_json(config.blogs.map { |subdomain, details|
103
+ { subdomain: subdomain, base_url: details["base_url"], default: subdomain == config.default_blog }
104
+ })
105
+ else
106
+ config.blogs.each do |subdomain, details|
107
+ suffix = details["base_url"] == Config::DEFAULT_BASE_URL ? "" : " (#{details["base_url"]})"
108
+ suffix += " (default)" if subdomain == config.default_blog
109
+ output.puts "#{subdomain}#{suffix}"
110
+ end
81
111
  end
82
112
 
83
113
  0
84
114
  end
85
115
 
86
- def publish(status)
87
- options = publish_options
88
- parser = OptionParser.new do |opts|
89
- opts.on("--title TITLE") { |value| options[:title] = value }
90
- opts.on("--slug SLUG") { |value| options[:slug] = value }
91
- opts.on("--published-at TIME") { |value| options[:published_at] = value }
92
- opts.on("--tags TAGS") { |value| options[:tags] = value }
93
- opts.on("--canonical-url URL") { |value| options[:canonical_url] = value }
94
- opts.on("--hidden") { options[:hidden] = true }
95
- opts.on("--locale LOCALE") { |value| options[:locale] = value }
116
+ def use
117
+ parser.parse!(argv)
118
+ subdomain = argv.shift
119
+ return fail_with("Usage: pagecord blog use SUBDOMAIN") unless subdomain
120
+ return fail_with("Unknown blog: #{subdomain}") unless config.blog(subdomain)
121
+
122
+ config.save_default_blog(subdomain)
123
+ say "Using #{subdomain}"
124
+ 0
125
+ end
126
+
127
+ def appearance
128
+ case argv.shift
129
+ when "show" then show_appearance
130
+ when "update" then update_appearance
131
+ else fail_with("Usage: pagecord appearance show|update [--FIELD VALUE]")
96
132
  end
133
+ end
134
+
135
+ def show_appearance
97
136
  parser.parse!(argv)
137
+ settings = client_for(resolve_blog).appearance
98
138
 
99
- file_path = argv.shift
100
- subdomain = resolve_blog(argv.shift)
139
+ if options[:json]
140
+ print_json(settings)
141
+ else
142
+ settings.each { |key, value| output.puts "#{key}: #{value}" }
143
+ end
144
+
145
+ 0
146
+ end
147
+
148
+ def update_appearance
149
+ params = {}
150
+ parser do |opts|
151
+ APPEARANCE_FIELDS.each do |field|
152
+ opts.on("--#{field.tr("_", "-")} VALUE") { |value| params[field] = value }
153
+ end
154
+ opts.on("--show-branding BOOL", TrueClass) { |value| params["show_branding"] = value }
155
+ end.parse!(argv)
156
+
157
+ return fail_with("Nothing to update") if params.empty?
158
+
159
+ settings = client_for(resolve_blog).update_appearance(params)
160
+ options[:json] ? print_json(settings) : say("Updated appearance")
161
+ 0
162
+ end
163
+
164
+ def custom_code
165
+ case argv.shift
166
+ when "show" then show_custom_code
167
+ when "update" then update_custom_code
168
+ else fail_with("Usage: pagecord custom-code show|update [--FIELD VALUE]")
169
+ end
170
+ end
171
+
172
+ def show_custom_code
173
+ field = nil
174
+ parser do |opts|
175
+ CUSTOM_CODE_FIELDS.each { |flag, name| opts.on("--#{flag}") { field = name } }
176
+ end.parse!(argv)
177
+
178
+ settings = client_for(resolve_blog).custom_code
179
+ field ? output.print(settings[field].to_s) : print_json(settings)
180
+ 0
181
+ end
182
+
183
+ def update_custom_code
184
+ params = {}
185
+ parser do |opts|
186
+ CUSTOM_CODE_FIELDS.each do |flag, name|
187
+ opts.on("--#{flag} PATH") { |path| params[name] = read_file(flag, path) }
188
+ end
189
+ opts.on("--enabled BOOL", TrueClass) { |value| params["custom_code_enabled"] = value }
190
+ end.parse!(argv)
101
191
 
192
+ return fail_with("Nothing to update") if params.empty?
193
+
194
+ settings = client_for(resolve_blog).update_custom_code(params)
195
+ options[:json] ? print_json(settings) : say("Updated custom code")
196
+ 0
197
+ end
198
+
199
+ def publish(status)
200
+ overrides = {}
201
+ parser do |opts|
202
+ opts.on("--title TITLE") { |value| overrides[:title] = value }
203
+ opts.on("--slug SLUG") { |value| overrides[:slug] = value }
204
+ opts.on("--published-at TIME") { |value| overrides[:published_at] = value }
205
+ opts.on("--tags TAGS") { |value| overrides[:tags] = value }
206
+ opts.on("--canonical-url URL") { |value| overrides[:canonical_url] = value }
207
+ opts.on("--hidden") { overrides[:hidden] = true }
208
+ opts.on("--locale LOCALE") { |value| overrides[:locale] = value }
209
+ end.parse!(argv)
210
+
211
+ file_path = argv.shift
102
212
  return fail_with("Usage: pagecord #{status == "draft" ? "draft" : "publish"} FILE [SUBDOMAIN]") unless file_path
103
- return fail_with("No blogs are configured. Run pagecord login SUBDOMAIN first.") if config.blogs.empty?
104
- return fail_with("Please specify a subdomain") unless subdomain
105
- return fail_with("Unknown blog: #{subdomain}") unless config.blog(subdomain)
213
+
214
+ subdomain = resolve_blog(argv.shift)
106
215
 
107
216
  post_file = PostFile.new(file_path)
108
217
  return fail_with("Unsupported file type: #{file_path}") unless post_file.supported?
109
218
 
110
- blog_config = config.blog(subdomain)
111
- api_key = blog_config.fetch("api_key")
219
+ api_key = config.blog(subdomain).fetch("api_key")
112
220
  return fail_with("This file is linked to another configured blog.") if post_file.wrong_blog?(subdomain, api_key)
113
221
 
114
- client = Client.new(api_key: blog_config.fetch("api_key"), base_url: blog_config.fetch("base_url", Config::DEFAULT_BASE_URL))
115
- params = post_file.params.merge(options.compact).merge(
222
+ client = client_for(subdomain)
223
+ params = post_file.params.merge(overrides).merge(
116
224
  content: post_file.content_for(subdomain, client: client),
117
225
  status: status
118
226
  )
@@ -125,27 +233,51 @@ module PagecordCLI
125
233
  end
126
234
 
127
235
  post_file.write_token(subdomain, result.fetch("token"), api_key: api_key, status: status)
128
- output.puts "#{status == "draft" ? "Saved draft" : "Published"} #{file_path} to #{subdomain}"
236
+ say "#{status == "draft" ? "Saved draft" : "Published"} #{file_path} to #{subdomain}"
129
237
  0
130
238
  end
131
239
 
132
- def publish_options
133
- {
134
- title: nil,
135
- slug: nil,
136
- published_at: nil,
137
- tags: nil,
138
- canonical_url: nil,
139
- hidden: nil,
140
- locale: nil
141
- }
240
+ def parser
241
+ OptionParser.new do |opts|
242
+ opts.on("--blog SUBDOMAIN") { |value| options[:blog] = value }
243
+ opts.on("--json") { options[:json] = true }
244
+ opts.on("--quiet") { options[:quiet] = true }
245
+ yield opts if block_given?
246
+ end
247
+ end
248
+
249
+ def resolve_blog(name = nil)
250
+ raise Error, "No blogs are configured. Run pagecord login SUBDOMAIN first." if config.blogs.empty?
251
+
252
+ subdomain = name || options[:blog] || ENV["PAGECORD_BLOG"] || config.default_blog ||
253
+ (config.blogs.keys.first if config.blogs.size == 1)
254
+
255
+ raise Error, "Please specify a subdomain" unless subdomain
256
+ raise Error, "Unknown blog: #{subdomain}" unless config.blog(subdomain)
257
+
258
+ subdomain
259
+ end
260
+
261
+ def client_for(subdomain)
262
+ blog_config = config.blog(subdomain)
263
+ Client.new(
264
+ api_key: blog_config.fetch("api_key"),
265
+ base_url: blog_config.fetch("base_url", Config::DEFAULT_BASE_URL)
266
+ )
142
267
  end
143
268
 
144
- def resolve_blog(name)
145
- return name if name
146
- return config.blogs.keys.first if config.blogs.size == 1
269
+ def read_file(flag, path)
270
+ File.read(path)
271
+ rescue SystemCallError
272
+ raise Error, path.match?(/[{<]/) ? "--#{flag} takes a file path, not the content itself" : "Could not read #{path}"
273
+ end
147
274
 
148
- nil
275
+ def say(message)
276
+ output.puts message unless options[:quiet]
277
+ end
278
+
279
+ def print_json(value)
280
+ output.puts JSON.pretty_generate(value)
149
281
  end
150
282
 
151
283
  def prompt_api_key
@@ -173,10 +305,21 @@ module PagecordCLI
173
305
  Usage:
174
306
  pagecord login SUBDOMAIN
175
307
  pagecord logout [SUBDOMAIN]
176
- pagecord list
308
+ pagecord blog list
309
+ pagecord blog use SUBDOMAIN
310
+ pagecord appearance show
311
+ pagecord appearance update [options]
312
+ pagecord custom-code show [--css|--footer-html|--head-html|--body-html]
313
+ pagecord custom-code update --css blog.css
314
+ pagecord custom-code update [options]
177
315
  pagecord publish FILE [SUBDOMAIN] [options]
178
316
  pagecord draft FILE [SUBDOMAIN] [options]
179
317
 
318
+ Global options:
319
+ --blog SUBDOMAIN
320
+ --json
321
+ --quiet
322
+
180
323
  Publish options:
181
324
  --title TITLE
182
325
  --slug SLUG
@@ -185,6 +328,16 @@ module PagecordCLI
185
328
  --canonical-url URL
186
329
  --hidden
187
330
  --locale LOCALE
331
+
332
+ Appearance options:
333
+ --theme, --font, --width, --layout
334
+ --custom-theme-bg-light, --custom-theme-text-light, --custom-theme-accent-light
335
+ --custom-theme-bg-dark, --custom-theme-text-dark, --custom-theme-accent-dark
336
+ --show-branding true|false
337
+
338
+ Custom code options:
339
+ --css, --footer-html, --head-html, --body-html (each takes a file path)
340
+ --enabled true|false
188
341
  HELP
189
342
  0
190
343
  end
@@ -38,6 +38,22 @@ module PagecordCLI
38
38
  request(json_request(Net::HTTP::Patch, "/posts/#{token}", params))
39
39
  end
40
40
 
41
+ def appearance
42
+ request(Net::HTTP::Get.new(uri_for("/settings/appearance")))
43
+ end
44
+
45
+ def update_appearance(params)
46
+ request(json_request(Net::HTTP::Patch, "/settings/appearance", params))
47
+ end
48
+
49
+ def custom_code
50
+ request(Net::HTTP::Get.new(uri_for("/settings/custom_code")))
51
+ end
52
+
53
+ def update_custom_code(params)
54
+ request(json_request(Net::HTTP::Patch, "/settings/custom_code", params))
55
+ end
56
+
41
57
  def upload_attachment(path)
42
58
  uri = uri_for("/attachments")
43
59
  http_request = Net::HTTP::Post.new(uri)
@@ -47,16 +47,16 @@ module PagecordCLI
47
47
  def delete_blog(name)
48
48
  new_data = data
49
49
  new_data.fetch("blogs", {}).delete(name)
50
+ new_data.delete("default_blog") if new_data["default_blog"] == name
50
51
  write(new_data)
51
52
  end
52
53
 
53
- def resolve_blog(name = nil)
54
- return name if name && blog(name)
55
- return name if name
56
-
57
- return blogs.keys.first if blogs.size == 1
54
+ def default_blog
55
+ data["default_blog"]
56
+ end
58
57
 
59
- nil
58
+ def save_default_blog(name)
59
+ write(data.merge("default_blog" => name))
60
60
  end
61
61
 
62
62
  def data
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PagecordCLI
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pagecord-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Olly Headey