neocities-red 1.1.0 → 1.1.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +32 -32
  3. data/.github/PULL_REQUEST_TEMPLATE.md +18 -18
  4. data/.github/dependabot.yml +6 -6
  5. data/.github/workflows/ci.yml +139 -139
  6. data/.gitignore +38 -38
  7. data/.rspec +3 -3
  8. data/.rubocop.yml +43 -43
  9. data/CODE_OF_CONDUCT.md +128 -128
  10. data/CONTRIBUTING.md +83 -83
  11. data/Gemfile +12 -12
  12. data/Gemfile.lock +194 -193
  13. data/LICENSE +21 -21
  14. data/README.md +153 -153
  15. data/SECURITY.md +7 -7
  16. data/bin/neocities-red +9 -9
  17. data/ext/mkrf_conf.rb +21 -21
  18. data/lib/neocities_red/cli.rb +340 -335
  19. data/lib/neocities_red/cli_display.rb +281 -271
  20. data/lib/neocities_red/client.rb +200 -200
  21. data/lib/neocities_red/services/common/pizza.rb +107 -107
  22. data/lib/neocities_red/services/file/folder_uploader.rb +65 -65
  23. data/lib/neocities_red/services/file/list.rb +65 -65
  24. data/lib/neocities_red/services/file/remover.rb +35 -35
  25. data/lib/neocities_red/services/file/uploader.rb +45 -45
  26. data/lib/neocities_red/services/site/differencer.rb +76 -76
  27. data/lib/neocities_red/services/site/exporter.rb +43 -43
  28. data/lib/neocities_red/services/site/informer.rb +43 -43
  29. data/lib/neocities_red/services/site/pusher.rb +146 -146
  30. data/lib/neocities_red/version.rb +5 -5
  31. data/lib/neocities_red.rb +17 -17
  32. data/neocities-red.gemspec +54 -54
  33. data/spec/neocities_red/cli_display_spec.rb +139 -139
  34. data/spec/neocities_red/cli_spec.rb +68 -68
  35. data/spec/neocities_red/client_spec.rb +306 -306
  36. data/spec/neocities_red/services/common/pizza_spec.rb +26 -26
  37. data/spec/neocities_red/services/file/folder_uploader_spec.rb +64 -64
  38. data/spec/neocities_red/services/file/list_spec.rb +177 -177
  39. data/spec/neocities_red/services/file/remover_spec.rb +125 -125
  40. data/spec/neocities_red/services/file/uploader_spec.rb +158 -158
  41. data/spec/neocities_red/services/site/differencer_spec.rb +162 -162
  42. data/spec/neocities_red/services/site/exporter_spec.rb +116 -116
  43. data/spec/neocities_red/services/site/informer_spec.rb +180 -180
  44. data/spec/neocities_red/services/site/pusher_spec.rb +167 -167
  45. data/spec/spec_helper.rb +20 -20
  46. metadata +4 -10
@@ -1,335 +1,340 @@
1
- # frozen_string_literal: true
2
-
3
- require "pathname"
4
- require "tty/table"
5
- require "tty/prompt"
6
- require "fileutils"
7
- require "json"
8
- require "thor"
9
- require_relative "cli_display"
10
-
11
- module NeocitiesRed
12
- class CLI < Thor
13
- package_name "neocities-red"
14
- default_task :help
15
- map %w[-h --help] => :help
16
- map %w[-v --version] => :version
17
-
18
- class_option :api_key,
19
- type: :string,
20
- desc: "Use a specific API key instead of reading from env/config"
21
-
22
- desc "diff [PATH]", "Compare local files with remote and show differences"
23
- method_option :help, aliases: "-h", type: :boolean
24
- method_option :ignore_dotfiles, type: :boolean, default: false
25
- method_option :exclude, aliases: "-e", type: :string, repeatable: true, default: []
26
- def diff(path = ".")
27
- return display_help_for("diff") if help_requested?(options[:help], path)
28
-
29
- client = ensure_client!
30
- exclude = build_diff_exclusions(path, Array(options[:exclude]))
31
-
32
- added, modified, removed = Services::Site::Differencer.new(
33
- client,
34
- path: path,
35
- detail: false,
36
- ignore_dotfiles: options[:ignore_dotfiles],
37
- exclude: exclude
38
- ).show
39
-
40
- display.display_diff_results(added: added, modified: modified, removed: removed)
41
- end
42
-
43
- desc "delete PATH [PATH ...]", "Delete files on your Neocities site"
44
- method_option :help, aliases: "-h", type: :boolean
45
- def delete(*paths)
46
- return display_help_for("delete") if paths.empty? || help_requested?(options[:help], paths)
47
-
48
- client = ensure_client!
49
- paths.each { |path| Services::File::Remover.new(client, path).remove }
50
- end
51
-
52
- desc "logout", "Remove the site api key from the config"
53
- method_option :help, aliases: "-h", type: :boolean
54
- method_option :yes, aliases: "-y", type: :boolean, default: false
55
- def logout
56
- return display_help_for("logout") if help_requested?(options[:help]) || !options[:yes]
57
-
58
- FileUtils.rm_f(app_config_path)
59
- display.display_logout_success
60
- end
61
-
62
- desc "info [SITENAME]", "Get site info"
63
- method_option :help, aliases: "-h", type: :boolean
64
- def info(sitename = nil)
65
- return display_help_for("info") if help_requested?(options[:help], sitename)
66
-
67
- client = ensure_client!
68
- profile_info = Services::Site::Informer.new(client, [sitename].compact, @sitename).pretty_print
69
- display.say TTY::Table.new(profile_info)
70
- rescue StandardError => e
71
- display.display_response(e)
72
- end
73
-
74
- desc "list [PATH]", "List files on your Neocities site"
75
- method_option :help, aliases: "-h", type: :boolean
76
- method_option :detail, aliases: "-d", type: :boolean, default: false
77
- method_option :all, aliases: "-a", type: :boolean, default: false
78
- def list(path = nil)
79
- if help_requested?(options[:help], path) || (path.nil? && options[:all].nil? && options[:detail].nil?)
80
- display_help_for("list")
81
- return
82
- end
83
-
84
- client = ensure_client!
85
- path = nil if options[:all]
86
- display.say Services::File::List.new(client, path, options[:detail]).show
87
- end
88
-
89
- desc "push PATH", "Recursively upload a local directory to your Neocities site"
90
- method_option :help, aliases: "-h", type: :boolean
91
- method_option :no_gitignore, type: :boolean, default: false
92
- method_option :ignore_dotfiles, type: :boolean, default: false
93
- method_option :exclude, aliases: "-e", type: :string, repeatable: true, default: []
94
- method_option :dry_run, type: :boolean, default: false
95
- method_option :prune, type: :boolean, default: false
96
- method_option :optimized, type: :boolean, default: false
97
- def push(root = nil)
98
- return display_help_for("push") if help_requested?(options[:help], root)
99
-
100
- client = ensure_client!
101
- return display_help_for("push") if root.nil?
102
-
103
- Services::Site::Pusher.new(
104
- client,
105
- display,
106
- root: root,
107
- no_gitignore: options[:no_gitignore],
108
- ignore_dotfiles: options[:ignore_dotfiles],
109
- exclude: Array(options[:exclude]),
110
- dry_run: options[:dry_run],
111
- prune: options[:prune],
112
- optimized: options[:optimized]
113
- ).push
114
- rescue ArgumentError => e
115
- display.display_response(result: "error", message: e.message)
116
- display_help_for("push")
117
- end
118
-
119
- desc "upload LOCAL_PATH REMOTE_PATH", "Upload a file/folder to your Neocities site"
120
- method_option :help, aliases: "-h", type: :boolean
121
- def upload(local_path = nil, remote_path = nil)
122
- return display_help_for("upload") if help_requested?(options[:help], [local_path, remote_path])
123
- return display_help_for("upload") if local_path.nil? || remote_path.nil?
124
-
125
- client = ensure_client!
126
- if File.file?(local_path)
127
- Services::File::Uploader.new(client, local_path, remote_path).upload
128
- elsif File.directory?(local_path)
129
- folder_uploader = Services::File::FolderUploader.new(client, local_path, remote_path)
130
- files_list = folder_uploader.files
131
- folder_uploader.upload(files_list)
132
- end
133
- end
134
-
135
- desc "pull", "Get the most recent version of files from your site"
136
- method_option :help, aliases: "-h", type: :boolean
137
- method_option :quiet, aliases: "-q", type: :boolean, default: false
138
- def pull
139
- return display_help_for("pull") if help_requested?(options[:help])
140
-
141
- client = ensure_client!
142
- data = read_config || {}
143
-
144
- last_pull_time = data.dig("LAST_PULL", "time")
145
- last_pull_loc = data.dig("LAST_PULL", "loc")
146
-
147
- Services::Site::Exporter.new(client, @sitename, data, app_config_path)
148
- .export(quiet: options[:quiet], last_pull_time: last_pull_time, last_pull_loc: last_pull_loc)
149
- end
150
-
151
- desc "purge", "Delete everything from your site (development only)"
152
- method_option :yes, aliases: "-y", type: :boolean, default: false
153
- def purge
154
- return display_help_for("purge") unless options[:yes]
155
-
156
- client = ensure_client!
157
- resp = client.list
158
- resp[:files].each do |file|
159
- display.display_delete_progress(file[:path])
160
- delete_resp = client.delete_wrapper_with_dry_run(file[:path], dry_run: options[:dry_run])
161
-
162
- if delete_resp[:result] == "success"
163
- display.display_delete_success
164
- else
165
- display.display_delete_error(delete_resp)
166
- end
167
- end
168
- end
169
-
170
- desc "pizza", "Order a free pizza"
171
- def pizza
172
- display_help_for(__method__)
173
- end
174
-
175
- def self.app_config_path(name)
176
- platform = case RUBY_PLATFORM
177
- when /cygwin|mswin|mingw|bccwin|wince|emx|win32/
178
- :windows
179
- when /darwin/
180
- :darwin
181
- when /linux/
182
- :linux
183
- when /freebsd/
184
- :freebsd
185
- else
186
- :unknown
187
- end
188
-
189
- case platform
190
- when :linux
191
- return File.join(ENV["XDG_CONFIG_HOME"], name) if ENV["XDG_CONFIG_HOME"]
192
- return File.join(Dir.home, ".config", name) if Dir.home
193
- when :darwin
194
- return File.join(Dir.home, "Library", "Application Support", name) if Dir.home
195
- when :windows
196
- return File.join(ENV["LOCALAPPDATA"], name) if ENV["LOCALAPPDATA"]
197
-
198
- if ENV["USERPROFILE"]
199
- return File.join(
200
- ENV["USERPROFILE"],
201
- "AppData",
202
- "Local",
203
- name
204
- )
205
- end
206
- else
207
- # FreeBSD and other unknown UNIX-like systems use dotfile directly in home directory
208
- return File.join(Dir.home, ".#{name}") if Dir.home
209
- end
210
-
211
- nil
212
- end
213
-
214
- desc "help [COMMAND]", "Show help for a command"
215
- def help(command = nil)
216
- return display.display_help_and_exit if command.nil?
217
-
218
- custom_help_method = "display_#{command}_help_and_exit"
219
- return display.public_send(custom_help_method) if display.respond_to?(custom_help_method)
220
-
221
- super
222
- end
223
-
224
- desc "version", "Display neocities-red version"
225
- def version
226
- display.say NeocitiesRed::VERSION
227
- end
228
-
229
- no_commands do
230
- alias_method :display_help_for, :help
231
-
232
- def display
233
- @display ||= NeocitiesRed::CliDisplay.new
234
- end
235
-
236
- def prompt
237
- @prompt ||= TTY::Prompt.new
238
- end
239
-
240
- def app_config_path
241
- @app_config_path ||= File.join(self.class.app_config_path("neocities"), "config.json")
242
- end
243
-
244
- def read_config
245
- file = File.read(app_config_path)
246
- JSON.parse(file)
247
- rescue Errno::ENOENT
248
- nil
249
- end
250
-
251
- def ensure_client!
252
- return @client if @client
253
-
254
- config = read_config
255
- @sitename = config && config["SITENAME"]
256
- @last_pull = config && config["LAST_PULL"]
257
-
258
- @api_key = options[:api_key] || ENV.fetch("NEOCITIES_API_KEY", nil)
259
- @api_key ||= config && config["API_KEY"]&.strip
260
-
261
- if @api_key.nil? || @api_key.empty?
262
- authenticate_and_persist_key!
263
- else
264
- @client = NeocitiesRed::Client.new(api_key: @api_key)
265
- end
266
-
267
- @client
268
- end
269
-
270
- def authenticate_and_persist_key!
271
- display.display_login_prompt
272
-
273
- @sitename ||= prompt.ask("sitename/username:", default: ENV.fetch("NEOCITIES_SITENAME", nil))
274
- password = prompt.mask("password:", default: ENV.fetch("NEOCITIES_PASSWORD", nil))
275
-
276
- temp_client = NeocitiesRed::Client.new(sitename: @sitename, password: password)
277
- resp = temp_client.key
278
-
279
- unless resp[:api_key]
280
- display.display_response(resp)
281
- raise Thor::Error, "failed to obtain API key"
282
- end
283
-
284
- @api_key = resp[:api_key]
285
- conf = {
286
- API_KEY: @api_key,
287
- SITENAME: @sitename
288
- }
289
-
290
- FileUtils.mkdir_p(Pathname(app_config_path).dirname)
291
- File.write(app_config_path, conf.to_json)
292
- display.display_api_key_saved(@sitename, app_config_path)
293
- @client = NeocitiesRed::Client.new(api_key: @api_key)
294
- end
295
-
296
- def build_diff_exclusions(base_path, excluded_entries)
297
- base = Pathname.new(base_path).expand_path
298
- excludes = []
299
-
300
- excluded_entries.each do |entry|
301
- target = Pathname.new(entry).expand_path
302
- next unless target.exist?
303
-
304
- filepath = target.relative_path_from(base).to_s
305
-
306
- if File.file?(target)
307
- excludes << filepath
308
- elsif File.directory?(target)
309
- excludes.concat(
310
- Dir.glob(File.join(target, "**", "*"), File::FNM_DOTMATCH).map do |path|
311
- Pathname.new(path).expand_path.relative_path_from(base).to_s
312
- end
313
- )
314
- excludes << filepath
315
- end
316
- end
317
-
318
- excludes
319
- end
320
-
321
- def help_requested_for?(value)
322
- case value
323
- when Array
324
- value.any? { |item| ["-h", "--help", "help"].include?(item) }
325
- else
326
- ["-h", "--help", "help"].include?(value)
327
- end
328
- end
329
-
330
- def help_requested?(help_option, value = nil)
331
- help_option || (value && help_requested_for?(value))
332
- end
333
- end
334
- end
335
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "tty/table"
5
+ require "tty/prompt"
6
+ require "fileutils"
7
+ require "json"
8
+ require "thor"
9
+ require_relative "cli_display"
10
+
11
+ module NeocitiesRed
12
+ class CLI < Thor
13
+ package_name "neocities-red"
14
+ default_task :help
15
+ map %w[-h --help] => :help
16
+ map %w[-v --version] => :version
17
+
18
+ class_option :api_key,
19
+ type: :string,
20
+ desc: "Use a specific API key instead of reading from env/config"
21
+
22
+ desc "diff [PATH]", "Compare local files with remote and show differences"
23
+ method_option :help, aliases: "-h", type: :boolean
24
+ method_option :ignore_dotfiles, type: :boolean, default: false
25
+ method_option :exclude, aliases: "-e", type: :string, repeatable: true, default: []
26
+ def diff(path = ".")
27
+ return display_help_for("diff") if help_requested?(options[:help], path)
28
+
29
+ client = ensure_client!
30
+ exclude = build_diff_exclusions(path, Array(options[:exclude]))
31
+
32
+ added, modified, removed = Services::Site::Differencer.new(
33
+ client,
34
+ path: path,
35
+ detail: false,
36
+ ignore_dotfiles: options[:ignore_dotfiles],
37
+ exclude: exclude
38
+ ).show
39
+
40
+ display.display_diff_results(added: added, modified: modified, removed: removed)
41
+ end
42
+
43
+ desc "delete PATH [PATH ...]", "Delete files on your Neocities site"
44
+ method_option :help, aliases: "-h", type: :boolean
45
+ def delete(*paths)
46
+ return display_help_for("delete") if paths.empty? || help_requested?(options[:help], paths)
47
+
48
+ client = ensure_client!
49
+ paths.each { |path| Services::File::Remover.new(client, path).remove }
50
+ end
51
+
52
+ desc "logout", "Remove the site api key from the config"
53
+ method_option :help, aliases: "-h", type: :boolean
54
+ method_option :yes, aliases: "-y", type: :boolean, default: false
55
+ def logout
56
+ return display_help_for("logout") if help_requested?(options[:help]) || !options[:yes]
57
+
58
+ FileUtils.rm_f(app_config_path)
59
+ display.display_logout_success
60
+ end
61
+
62
+ desc "info [SITENAME]", "Get site info"
63
+ method_option :help, aliases: "-h", type: :boolean
64
+ def info(sitename = nil)
65
+ return display_help_for("info") if help_requested?(options[:help], sitename)
66
+
67
+ client = ensure_client!
68
+ profile_info = Services::Site::Informer.new(client, [sitename].compact, @sitename).pretty_print
69
+ display.say TTY::Table.new(profile_info)
70
+ rescue StandardError => e
71
+ display.display_response(e)
72
+ end
73
+
74
+ desc "list [PATH]", "List files on your Neocities site"
75
+ method_option :help, aliases: "-h", type: :boolean
76
+ method_option :detail, aliases: "-d", type: :boolean, default: false
77
+ method_option :all, aliases: "-a", type: :boolean, default: false
78
+ def list(path = nil)
79
+ if help_requested?(options[:help], path) || (path.nil? && options[:all].nil? && options[:detail].nil?)
80
+ display_help_for("list")
81
+ return
82
+ end
83
+
84
+ client = ensure_client!
85
+ path = nil if options[:all]
86
+ display.say Services::File::List.new(client, path, options[:detail]).show
87
+ end
88
+
89
+ desc "push PATH", "Recursively upload a local directory to your Neocities site"
90
+ method_option :help, aliases: "-h", type: :boolean
91
+ method_option :no_gitignore, type: :boolean, default: false
92
+ method_option :ignore_dotfiles, type: :boolean, default: false
93
+ method_option :exclude, aliases: "-e", type: :string, repeatable: true, default: []
94
+ method_option :dry_run, type: :boolean, default: false
95
+ method_option :prune, type: :boolean, default: false
96
+ method_option :optimized, type: :boolean, default: false
97
+ def push(root = nil)
98
+ return display_help_for("push") if help_requested?(options[:help], root)
99
+
100
+ client = ensure_client!
101
+ return display_help_for("push") if root.nil?
102
+
103
+ Services::Site::Pusher.new(
104
+ client,
105
+ display,
106
+ root: root,
107
+ no_gitignore: options[:no_gitignore],
108
+ ignore_dotfiles: options[:ignore_dotfiles],
109
+ exclude: Array(options[:exclude]),
110
+ dry_run: options[:dry_run],
111
+ prune: options[:prune],
112
+ optimized: options[:optimized]
113
+ ).push
114
+ rescue ArgumentError => e
115
+ display.display_response(result: "error", message: e.message)
116
+ display_help_for("push")
117
+ end
118
+
119
+ desc "upload LOCAL_PATH [REMOTE_PATH]", "Upload a file/folder to your Neocities site"
120
+ method_option :help, aliases: "-h", type: :boolean
121
+ def upload(local_path = nil, remote_path = nil)
122
+ return display_help_for("upload") if help_requested?(options[:help], [local_path, remote_path])
123
+ return display_help_for("upload") if local_path.nil?
124
+
125
+ client = ensure_client!
126
+ dest = remote_path || File.basename(local_path)
127
+ if File.file?(local_path)
128
+ Services::File::Uploader.new(client, local_path, dest).upload
129
+ elsif File.directory?(local_path)
130
+ folder_uploader = Services::File::FolderUploader.new(client, local_path, dest)
131
+ files_list = folder_uploader.files
132
+ folder_uploader.upload(files_list)
133
+ end
134
+ end
135
+
136
+ desc "pull", "Get the most recent version of files from your site"
137
+ method_option :help, aliases: "-h", type: :boolean
138
+ method_option :quiet, aliases: "-q", type: :boolean, default: false
139
+ def pull
140
+ return display_help_for("pull") if help_requested?(options[:help])
141
+
142
+ client = ensure_client!
143
+ data = read_config || {}
144
+
145
+ last_pull_time = data.dig("LAST_PULL", "time")
146
+ last_pull_loc = data.dig("LAST_PULL", "loc")
147
+
148
+ Services::Site::Exporter.new(client, @sitename, data, app_config_path)
149
+ .export(quiet: options[:quiet], last_pull_time: last_pull_time, last_pull_loc: last_pull_loc)
150
+ end
151
+
152
+ desc "purge", "Delete everything from your site (development only)"
153
+ method_option :yes, aliases: "-y", type: :boolean, default: false
154
+ def purge
155
+ return display_help_for("purge") unless options[:yes]
156
+
157
+ client = ensure_client!
158
+ resp = client.list
159
+ deleted_dirs = []
160
+ resp[:files].sort_by { |f| f[:is_directory] ? 0 : 1 }.each do |file|
161
+ next if deleted_dirs.any? { |dir| file[:path].start_with?(dir) }
162
+
163
+ display.display_delete_progress(file[:path])
164
+ delete_resp = client.delete_wrapper_with_dry_run(file[:path], dry_run: options[:dry_run])
165
+
166
+ if delete_resp[:result] == "success"
167
+ deleted_dirs << file[:path] if file[:is_directory]
168
+ display.display_delete_success
169
+ else
170
+ display.display_delete_error(delete_resp)
171
+ end
172
+ end
173
+ end
174
+
175
+ desc "pizza", "Order a free pizza"
176
+ def pizza
177
+ display_help_for(__method__)
178
+ end
179
+
180
+ def self.app_config_path(name)
181
+ platform = case RUBY_PLATFORM
182
+ when /cygwin|mswin|mingw|bccwin|wince|emx|win32/
183
+ :windows
184
+ when /darwin/
185
+ :darwin
186
+ when /linux/
187
+ :linux
188
+ when /freebsd/
189
+ :freebsd
190
+ else
191
+ :unknown
192
+ end
193
+
194
+ case platform
195
+ when :linux
196
+ return File.join(ENV["XDG_CONFIG_HOME"], name) if ENV["XDG_CONFIG_HOME"]
197
+ return File.join(Dir.home, ".config", name) if Dir.home
198
+ when :darwin
199
+ return File.join(Dir.home, "Library", "Application Support", name) if Dir.home
200
+ when :windows
201
+ return File.join(ENV["LOCALAPPDATA"], name) if ENV["LOCALAPPDATA"]
202
+
203
+ if ENV["USERPROFILE"]
204
+ return File.join(
205
+ ENV["USERPROFILE"],
206
+ "AppData",
207
+ "Local",
208
+ name
209
+ )
210
+ end
211
+ else
212
+ # FreeBSD and other unknown UNIX-like systems use dotfile directly in home directory
213
+ return File.join(Dir.home, ".#{name}") if Dir.home
214
+ end
215
+
216
+ nil
217
+ end
218
+
219
+ desc "help [COMMAND]", "Show help for a command"
220
+ def help(command = nil)
221
+ return display.display_help_and_exit if command.nil?
222
+
223
+ custom_help_method = "display_#{command}_help_and_exit"
224
+ return display.public_send(custom_help_method) if display.respond_to?(custom_help_method)
225
+
226
+ super
227
+ end
228
+
229
+ desc "version", "Display neocities-red version"
230
+ def version
231
+ display.say NeocitiesRed::VERSION
232
+ end
233
+
234
+ no_commands do
235
+ alias_method :display_help_for, :help
236
+
237
+ def display
238
+ @display ||= NeocitiesRed::CliDisplay.new
239
+ end
240
+
241
+ def prompt
242
+ @prompt ||= TTY::Prompt.new
243
+ end
244
+
245
+ def app_config_path
246
+ @app_config_path ||= File.join(self.class.app_config_path("neocities"), "config.json")
247
+ end
248
+
249
+ def read_config
250
+ file = File.read(app_config_path)
251
+ JSON.parse(file)
252
+ rescue Errno::ENOENT
253
+ nil
254
+ end
255
+
256
+ def ensure_client!
257
+ return @client if @client
258
+
259
+ config = read_config
260
+ @sitename = config && config["SITENAME"]
261
+ @last_pull = config && config["LAST_PULL"]
262
+
263
+ @api_key = options[:api_key] || ENV.fetch("NEOCITIES_API_KEY", nil)
264
+ @api_key ||= config && config["API_KEY"]&.strip
265
+
266
+ if @api_key.nil? || @api_key.empty?
267
+ authenticate_and_persist_key!
268
+ else
269
+ @client = NeocitiesRed::Client.new(api_key: @api_key)
270
+ end
271
+
272
+ @client
273
+ end
274
+
275
+ def authenticate_and_persist_key!
276
+ display.display_login_prompt
277
+
278
+ @sitename ||= prompt.ask("sitename/username:", default: ENV.fetch("NEOCITIES_SITENAME", nil))
279
+ password = prompt.mask("password:", default: ENV.fetch("NEOCITIES_PASSWORD", nil))
280
+
281
+ temp_client = NeocitiesRed::Client.new(sitename: @sitename, password: password)
282
+ resp = temp_client.key
283
+
284
+ unless resp[:api_key]
285
+ display.display_response(resp)
286
+ raise Thor::Error, "failed to obtain API key"
287
+ end
288
+
289
+ @api_key = resp[:api_key]
290
+ conf = {
291
+ API_KEY: @api_key,
292
+ SITENAME: @sitename
293
+ }
294
+
295
+ FileUtils.mkdir_p(Pathname(app_config_path).dirname)
296
+ File.write(app_config_path, conf.to_json)
297
+ display.display_api_key_saved(@sitename, app_config_path)
298
+ @client = NeocitiesRed::Client.new(api_key: @api_key)
299
+ end
300
+
301
+ def build_diff_exclusions(base_path, excluded_entries)
302
+ base = Pathname.new(base_path).expand_path
303
+ excludes = []
304
+
305
+ excluded_entries.each do |entry|
306
+ target = Pathname.new(entry).expand_path
307
+ next unless target.exist?
308
+
309
+ filepath = target.relative_path_from(base).to_s
310
+
311
+ if File.file?(target)
312
+ excludes << filepath
313
+ elsif File.directory?(target)
314
+ excludes.concat(
315
+ Dir.glob(File.join(target, "**", "*"), File::FNM_DOTMATCH).map do |path|
316
+ Pathname.new(path).expand_path.relative_path_from(base).to_s
317
+ end
318
+ )
319
+ excludes << filepath
320
+ end
321
+ end
322
+
323
+ excludes
324
+ end
325
+
326
+ def help_requested_for?(value)
327
+ case value
328
+ when Array
329
+ value.intersect?(["-h", "--help", "help"])
330
+ else
331
+ ["-h", "--help", "help"].include?(value)
332
+ end
333
+ end
334
+
335
+ def help_requested?(help_option, value = nil)
336
+ help_option || (value && help_requested_for?(value))
337
+ end
338
+ end
339
+ end
340
+ end