pagecord-cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1e156f74ff6f6b5860f2a21cae283aaccf1d3bec27ef02ed5bfc02002a1bdb3
4
- data.tar.gz: d05ee18c97ee613d5903e76bf3508c20ed6536b8120a3a3f60dc279539048203
3
+ metadata.gz: 128d888cf41e5516fb1266253ff9bd277ac4975aabe4d703d55377b487742a9b
4
+ data.tar.gz: f1eb62f81e6382bf02babcac836c3ea7f84e855235697950c58e91a3437b4bdb
5
5
  SHA512:
6
- metadata.gz: f02588d28a65bfd183cd5830f96d88395523960ef7a0ab4584be5e7e386b0f15530a1b5fa8b2c169b629a5cbabac138d77d428cffaeabaada09c2ffbe7a4d105
7
- data.tar.gz: 5057321b2a894a091ab7f7e76cb8fb0e9724f3576b9c435f83dc06dac38e59cb78f0e240f942297f34fd4479b2caf8a9de3887b4bd1e7593c2f0f89b796b8e65
6
+ metadata.gz: f392720b150de97d42b8e5d74bf231575e4c90a644653d3d1fb1748c09e1c5896a355be6afc51dbe001c6b95558729728d7387d9b6f66501a24b75b734cdc1f6
7
+ data.tar.gz: 2c0b4fc00acfb25fbfdf5ef16f7c20a31f7dfb22c1a4886967561558555279fcf2b553706eb30df0cdac378b08fa8d355f9339639a87cb4491f81330a934e0bc
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,58 @@ 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
+ Fields: `--css`, `--footer-html`, `--head-html`, `--body-html`, and
95
+ `--enabled true|false`. A value can be given literally, as `@path` to read a
96
+ file, or as `@-` to read standard input:
97
+
98
+ ```bash
99
+ cat blog.css | pagecord custom-code update --css @-
100
+ ```
101
+
102
+ `custom-code show` prints JSON, or the raw field when you name one, so
103
+ redirecting it to a file round-trips.
104
+
105
+ ## Global options
106
+
107
+ Accepted by every command, before or after the command name:
108
+
109
+ ```bash
110
+ --blog SUBDOMAIN # which blog to act on
111
+ --json # machine-readable output
112
+ --quiet # suppress confirmation messages
113
+ ```
114
+
115
+ ## Publish options
65
116
 
66
117
  `publish` and `draft` accept:
67
118
 
@@ -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
101
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} VALUE") { |value| params[name] = read_value(value) }
188
+ end
189
+ opts.on("--enabled BOOL", TrueClass) { |value| params["custom_code_enabled"] = value }
190
+ end.parse!(argv)
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,55 @@ 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_value(value)
270
+ case value
271
+ when "@-" then input.read
272
+ when /\A@/ then File.read(value[1..])
273
+ else value
274
+ end
275
+ rescue SystemCallError
276
+ raise Error, "Could not read #{value[1..]}"
277
+ end
278
+
279
+ def say(message)
280
+ output.puts message unless options[:quiet]
281
+ end
147
282
 
148
- nil
283
+ def print_json(value)
284
+ output.puts JSON.pretty_generate(value)
149
285
  end
150
286
 
151
287
  def prompt_api_key
@@ -173,10 +309,20 @@ module PagecordCLI
173
309
  Usage:
174
310
  pagecord login SUBDOMAIN
175
311
  pagecord logout [SUBDOMAIN]
176
- pagecord list
312
+ pagecord blog list
313
+ pagecord blog use SUBDOMAIN
314
+ pagecord appearance show
315
+ pagecord appearance update [options]
316
+ pagecord custom-code show [--css|--footer-html|--head-html|--body-html]
317
+ pagecord custom-code update [options]
177
318
  pagecord publish FILE [SUBDOMAIN] [options]
178
319
  pagecord draft FILE [SUBDOMAIN] [options]
179
320
 
321
+ Global options:
322
+ --blog SUBDOMAIN
323
+ --json
324
+ --quiet
325
+
180
326
  Publish options:
181
327
  --title TITLE
182
328
  --slug SLUG
@@ -185,6 +331,18 @@ module PagecordCLI
185
331
  --canonical-url URL
186
332
  --hidden
187
333
  --locale LOCALE
334
+
335
+ Appearance options:
336
+ --theme, --font, --width, --layout
337
+ --custom-theme-bg-light, --custom-theme-text-light, --custom-theme-accent-light
338
+ --custom-theme-bg-dark, --custom-theme-text-dark, --custom-theme-accent-dark
339
+ --show-branding true|false
340
+
341
+ Custom code options:
342
+ --css, --footer-html, --head-html, --body-html
343
+ --enabled true|false
344
+
345
+ Custom code values can be a literal, @path to read a file, or @- to read stdin.
188
346
  HELP
189
347
  0
190
348
  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.0"
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Olly Headey