neocities-red 1.1.2 → 1.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +29 -15
  3. data/.gitignore +4 -0
  4. data/CHANGELOG.md +95 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +9 -6
  7. data/README.md +6 -0
  8. data/lib/neocities_red/cli.rb +156 -31
  9. data/lib/neocities_red/cli_display.rb +200 -5
  10. data/lib/neocities_red/client.rb +84 -77
  11. data/lib/neocities_red/errors.rb +20 -0
  12. data/lib/neocities_red/services/common/exclusions.rb +69 -0
  13. data/lib/neocities_red/services/common/pizza.rb +34 -21
  14. data/lib/neocities_red/services/common/worker_pool.rb +54 -0
  15. data/lib/neocities_red/services/file/folder_uploader.rb +39 -25
  16. data/lib/neocities_red/services/file/list.rb +60 -30
  17. data/lib/neocities_red/services/file/remover.rb +27 -12
  18. data/lib/neocities_red/services/file/uploader.rb +33 -13
  19. data/lib/neocities_red/services/site/differencer.rb +42 -12
  20. data/lib/neocities_red/services/site/exporter.rb +120 -7
  21. data/lib/neocities_red/services/site/informer.rb +29 -4
  22. data/lib/neocities_red/services/site/pusher.rb +74 -38
  23. data/lib/neocities_red/version.rb +2 -1
  24. data/lib/neocities_red.rb +32 -13
  25. data/neocities-red.gemspec +11 -6
  26. data/spec/neocities_red/cli_display_spec.rb +0 -6
  27. data/spec/neocities_red/cli_spec.rb +85 -0
  28. data/spec/neocities_red/client_spec.rb +7 -50
  29. data/spec/neocities_red/services/common/exclusions_spec.rb +47 -0
  30. data/spec/neocities_red/services/common/worker_pool_spec.rb +48 -0
  31. data/spec/neocities_red/services/file/folder_uploader_spec.rb +8 -6
  32. data/spec/neocities_red/services/file/list_spec.rb +13 -10
  33. data/spec/neocities_red/services/file/remover_spec.rb +15 -21
  34. data/spec/neocities_red/services/file/uploader_spec.rb +25 -20
  35. data/spec/neocities_red/services/site/differencer_spec.rb +32 -6
  36. data/spec/neocities_red/services/site/exporter_spec.rb +105 -38
  37. data/spec/neocities_red/services/site/informer_spec.rb +3 -3
  38. data/spec/neocities_red/services/site/pusher_spec.rb +1 -1
  39. data/spec/spec_helper.rb +1 -1
  40. metadata +11 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 923e790fe28d24ed2aa82bb797def9013c02ec6589d164e19eb279745dca708c
4
- data.tar.gz: f14fde703e682d58a9ad1239300371830cea5d55ed17c0e669a91cee72342477
3
+ metadata.gz: bf561591a6a553947115437ac9ed12e84f72fc4d14ea9532fe3d3465584db764
4
+ data.tar.gz: 9b6b0f645578532ee3d647a8b5f87d38aca66ebd0580f8347c2e39ff4cabee18
5
5
  SHA512:
6
- metadata.gz: df338f92b78a165e3d605c5db8ca08473aaca1732c4a359e40c593f3da852041c9fa5f715f0f5e55fe577d176e6e79eaa4d4dc1a19cb8fb65dafeb2c216df379
7
- data.tar.gz: dfb0104e424c14d3706cb63e6c158f9cd7d646660bddf197e89024ce7142a6c538126afb9e18c3531d23989f34b132b023621ffb6c90cf0f3665db6d9bdff94b
6
+ metadata.gz: d2c2245f54b0afeb60684c05f5d9d7cb67c0f4d1f0a479472a4a75db1fc95587521d7e7ea3e98f9372296e9ec007dcaba2e970eb3234006f082e054dbcc16ab9
7
+ data.tar.gz: 3d01aee0ffd3d92dd8128dfa6e5cfdfd113df1564b983951c01f350f8d62f4115cc788fe95629d55c5f3aa16f9cb9b4a8f642b47129d9695b82f36610128217a
@@ -8,17 +8,21 @@ on:
8
8
 
9
9
  jobs:
10
10
  lint:
11
- name: Lint (ubuntu-latest)
11
+ name: Lint (Ruby ${{ matrix.ruby-version }})
12
12
  runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby-version: ['3.4', '3.5', '4.0']
13
17
 
14
18
  steps:
15
19
  - name: Checkout repository
16
20
  uses: actions/checkout@v4
17
21
 
18
- - name: Set up Ruby 4.0.0
22
+ - name: Set up Ruby ${{ matrix.ruby-version }}
19
23
  uses: ruby/setup-ruby@v1
20
24
  with:
21
- ruby-version: 4.0.0
25
+ ruby-version: ${{ matrix.ruby-version }}
22
26
  bundler-cache: true
23
27
 
24
28
  - name: Debug versions
@@ -31,59 +35,65 @@ jobs:
31
35
  run: bundle exec rubocop
32
36
 
33
37
  test_linux:
34
- name: Test (ubuntu-latest)
38
+ name: Test (Ruby ${{ matrix.ruby-version }}, ubuntu-latest)
35
39
  runs-on: ubuntu-latest
40
+ strategy:
41
+ fail-fast: false
42
+ matrix:
43
+ ruby-version: ['3.4', '3.5', '4.0']
36
44
 
37
45
  steps:
38
46
  - name: Checkout repository
39
47
  uses: actions/checkout@v4
40
48
 
41
- - name: Set up Ruby 4.0.0
49
+ - name: Set up Ruby ${{ matrix.ruby-version }}
42
50
  uses: ruby/setup-ruby@v1
43
51
  with:
44
- ruby-version: 4.0.0
52
+ ruby-version: ${{ matrix.ruby-version }}
45
53
  bundler-cache: true
46
54
 
47
55
  - name: Run tests
48
56
  run: bundle exec rspec
49
57
 
50
58
  test_non_linux:
51
- name: Test (${{ matrix.os }})
59
+ name: Test (Ruby ${{ matrix.ruby-version }}, ${{ matrix.os }})
52
60
  runs-on: ${{ matrix.os }}
53
61
  strategy:
54
62
  fail-fast: false
55
63
  matrix:
56
64
  os: [macos-latest, windows-latest]
65
+ ruby-version: ['3.4', '4.0']
57
66
 
58
67
  steps:
59
68
  - name: Checkout repository
60
69
  uses: actions/checkout@v4
61
70
 
62
- - name: Set up Ruby 4.0.0
71
+ - name: Set up Ruby ${{ matrix.ruby-version }}
63
72
  uses: ruby/setup-ruby@v1
64
73
  with:
65
- ruby-version: 4.0.0
74
+ ruby-version: ${{ matrix.ruby-version }}
66
75
  bundler-cache: true
67
76
 
68
77
  - name: Run tests
69
78
  run: bundle exec rspec
70
79
 
71
80
  smoke_unix:
72
- name: Start CLI (${{ matrix.os }})
81
+ name: Start CLI (Ruby ${{ matrix.ruby-version }}, ${{ matrix.os }})
73
82
  runs-on: ${{ matrix.os }}
74
83
  strategy:
75
84
  fail-fast: false
76
85
  matrix:
77
86
  os: [ubuntu-latest, macos-latest]
87
+ ruby-version: ['3.4', '4.0']
78
88
 
79
89
  steps:
80
90
  - name: Checkout repository
81
91
  uses: actions/checkout@v4
82
92
 
83
- - name: Set up Ruby 4.0.0
93
+ - name: Set up Ruby ${{ matrix.ruby-version }}
84
94
  uses: ruby/setup-ruby@v1
85
95
  with:
86
- ruby-version: 4.0.0
96
+ ruby-version: ${{ matrix.ruby-version }}
87
97
  bundler-cache: true
88
98
 
89
99
  - name: Start CLI
@@ -92,17 +102,21 @@ jobs:
92
102
  bundle exec bin/neocities-red
93
103
 
94
104
  smoke_windows:
95
- name: Start CLI (windows-latest)
105
+ name: Start CLI (Ruby ${{ matrix.ruby-version }}, windows-latest)
96
106
  runs-on: windows-latest
107
+ strategy:
108
+ fail-fast: false
109
+ matrix:
110
+ ruby-version: ['3.4', '4.0']
97
111
 
98
112
  steps:
99
113
  - name: Checkout repository
100
114
  uses: actions/checkout@v4
101
115
 
102
- - name: Set up Ruby 4.0.0
116
+ - name: Set up Ruby ${{ matrix.ruby-version }}
103
117
  uses: ruby/setup-ruby@v1
104
118
  with:
105
- ruby-version: 4.0.0
119
+ ruby-version: ${{ matrix.ruby-version }}
106
120
  bundler-cache: true
107
121
 
108
122
  - name: Start CLI
data/.gitignore CHANGED
@@ -33,6 +33,10 @@ Thumbs.db
33
33
  coverage/
34
34
  *.lcov
35
35
 
36
+ # YARD documentation
37
+ doc/
38
+ .yardoc
39
+
36
40
  # Temporary files
37
41
  tmp/
38
42
  *.log
data/CHANGELOG.md ADDED
@@ -0,0 +1,95 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.2.0] — 2026-08-04
11
+
12
+ ### Changed
13
+
14
+ - Major architecture refactor with namespaced services (`file/`, `site/`, `common/`)
15
+ - Renamed gem to `neocities-red`
16
+
17
+ ### Added
18
+
19
+ - YARD documentation
20
+
21
+ ### Removed
22
+
23
+ - Unused legacy code
24
+
25
+ ## [1.1.2] — 2026-08-04
26
+
27
+ ### Changed
28
+
29
+ - Bumped rubocop to 1.88.2, rubocop-rspec to 3.10.2, faraday to 2.14.3
30
+
31
+ ## [1.1.1] — 2026-06-22
32
+
33
+ ### Fixed
34
+
35
+ - Config for Windows/FreeBSD platforms
36
+ - Purge no longer removes already-removed files
37
+
38
+ ### Changed
39
+
40
+ - `upload` accepts a single parameter
41
+ - Bumped rake to 13.4.2
42
+
43
+ ## [1.1.0] — 2026-04-25
44
+
45
+ ### Changed
46
+
47
+ - Added Rails/Thor CLI framework, refactored `cli.rb`
48
+
49
+ ## [1.0.6] — 2026-04-14
50
+
51
+ ### Fixed
52
+
53
+ - Tests and lint fixes
54
+ - Added `faraday-retry` for flaky API/SSL handling
55
+
56
+ ## [1.0.4] — 2026-03-29
57
+
58
+ ### Changed
59
+
60
+ - CLI version display updated
61
+
62
+ ## [1.0.2] — 2026-02-11
63
+
64
+ ### Added
65
+
66
+ - Multithreaded parallel uploads
67
+ - Fixed recursive uploading in `upload` method
68
+
69
+ ## [1.0.1] — 2026-02-07
70
+
71
+ ### Added
72
+
73
+ - Folder uploading support
74
+
75
+ ## [1.0.0] — 2026-02-11
76
+
77
+ ### Added
78
+
79
+ - Initial release of `neocities-red` (fork of `neocities-ruby`)
80
+ - Recursive uploads with smart diffing
81
+ - `push`, `upload`, `delete`, `diff`, `list`, `info`, `pull` commands
82
+ - Parallel upload workers
83
+ - Automatic retries
84
+ - Config file with `NEOCITIES_API_KEY` support
85
+
86
+ [Unreleased]: https://github.com/o-200/neocities-red/compare/v1.2.0...HEAD
87
+ [1.2.0]: https://github.com/o-200/neocities-red/compare/v1.1.2...v1.2.0
88
+ [1.1.2]: https://github.com/o-200/neocities-red/compare/v1.1.1...v1.1.2
89
+ [1.1.1]: https://github.com/o-200/neocities-red/compare/v1.1.0...v1.1.1
90
+ [1.1.0]: https://github.com/o-200/neocities-red/compare/v1.0.6...v1.1.0
91
+ [1.0.6]: https://github.com/o-200/neocities-red/compare/v1.0.4...v1.0.6
92
+ [1.0.4]: https://github.com/o-200/neocities-red/compare/v1.0.2...v1.0.4
93
+ [1.0.2]: https://github.com/o-200/neocities-red/compare/v1.0.1...v1.0.2
94
+ [1.0.1]: https://github.com/o-200/neocities-red/compare/v1.0.0...v1.0.1
95
+ [1.0.0]: https://github.com/o-200/neocities-red/releases/tag/v1.0.0
data/Gemfile CHANGED
@@ -9,4 +9,5 @@ group :test, :development do
9
9
  gem "rubocop", "~> 1.88.2"
10
10
  gem "rubocop-rspec", "~> 3.10.2"
11
11
  gem "webmock"
12
+ gem "yard"
12
13
  end
data/Gemfile.lock CHANGED
@@ -1,17 +1,17 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- neocities-red (1.1.2)
4
+ neocities-red (1.2.0)
5
5
  faraday (~> 2.14.3)
6
6
  faraday-follow_redirects
7
7
  faraday-multipart
8
8
  faraday-retry
9
9
  fiddle
10
- pastel (~> 0.8, = 0.8.0)
11
- rake (~> 13, >= 13.3.0)
10
+ pastel (= 0.8.0)
11
+ rake (~> 13)
12
12
  thor (~> 1.5.0, >= 1.5.0)
13
- tty-prompt (~> 0.23, = 0.23.1)
14
- tty-table (~> 0.12, = 0.12.0)
13
+ tty-prompt (= 0.23.1)
14
+ tty-table (= 0.12.0)
15
15
  whirly (~> 0.3, >= 0.3.0)
16
16
 
17
17
  GEM
@@ -122,6 +122,7 @@ GEM
122
122
  json
123
123
  unicode-display_width (>= 1.1)
124
124
  wisper (2.0.1)
125
+ yard (0.9.45)
125
126
 
126
127
  PLATFORMS
127
128
  ruby
@@ -133,6 +134,7 @@ DEPENDENCIES
133
134
  rubocop (~> 1.88.2)
134
135
  rubocop-rspec (~> 3.10.2)
135
136
  webmock
137
+ yard
136
138
 
137
139
  CHECKSUMS
138
140
  addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
@@ -153,7 +155,7 @@ CHECKSUMS
153
155
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
154
156
  logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
155
157
  multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8
156
- neocities-red (1.1.2)
158
+ neocities-red (1.2.0)
157
159
  net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996
158
160
  parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
159
161
  parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828
@@ -189,6 +191,7 @@ CHECKSUMS
189
191
  webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
190
192
  whirly (0.4.0) sha256=3ffdf9097e711097442f6d83c91b8fc431d73224863f75b48f06fb850b3e596e
191
193
  wisper (2.0.1) sha256=ce17bc5c3a166f241a2e6613848b025c8146fce2defba505920c1d1f3f88fae6
194
+ yard (0.9.45) sha256=52e211493f7cb8a3ebf7e104a25a1e73937a3103092545d34cb88fafebb3dc51
192
195
 
193
196
  BUNDLED WITH
194
197
  4.0.17
data/README.md CHANGED
@@ -136,6 +136,12 @@ Current service namespaces:
136
136
 
137
137
  ---
138
138
 
139
+ ## Changelog
140
+
141
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
142
+
143
+ ---
144
+
139
145
  ## Contributing
140
146
 
141
147
  see CONTRIBUTING.md
@@ -9,6 +9,21 @@ require "thor"
9
9
  require_relative "cli_display"
10
10
 
11
11
  module NeocitiesRed
12
+ # Thor-based command-line interface for the NeocitiesRed gem.
13
+ #
14
+ # Provides subcommands for managing a Neocities site: push, upload,
15
+ # delete, diff, list, info, pull, purge, logout, and pizza.
16
+ #
17
+ # Authentication is handled lazily — the first command that requires
18
+ # an API connection will prompt for credentials (or read from config/env).
19
+ #
20
+ # @example Running from shell
21
+ # $ neocities-red push .
22
+ # $ neocities-red list -a
23
+ # $ neocities-red diff --ignore-dotfiles
24
+ #
25
+ # @see NeocitiesRed::Client Underlying API client
26
+ # @see NeocitiesRed::CliDisplay Terminal output helper
12
27
  class CLI < Thor
13
28
  package_name "neocities-red"
14
29
  default_task :help
@@ -23,11 +38,17 @@ module NeocitiesRed
23
38
  method_option :help, aliases: "-h", type: :boolean
24
39
  method_option :ignore_dotfiles, type: :boolean, default: false
25
40
  method_option :exclude, aliases: "-e", type: :string, repeatable: true, default: []
41
+
42
+ # Compares local files with the remote Neocities site and displays
43
+ # added, modified, and removed files.
44
+ #
45
+ # @param path [String] local directory path to compare (defaults to current directory)
46
+ # @return [void]
26
47
  def diff(path = ".")
27
48
  return display_help_for("diff") if help_requested?(options[:help], path)
28
49
 
29
50
  client = ensure_client!
30
- exclude = build_diff_exclusions(path, Array(options[:exclude]))
51
+ exclude = Services::Common::Exclusions.build(Array(options[:exclude]), base_path: path)
31
52
 
32
53
  added, modified, removed = Services::Site::Differencer.new(
33
54
  client,
@@ -42,16 +63,27 @@ module NeocitiesRed
42
63
 
43
64
  desc "delete PATH [PATH ...]", "Delete files on your Neocities site"
44
65
  method_option :help, aliases: "-h", type: :boolean
66
+
67
+ # Deletes one or more files from the remote Neocities site.
68
+ #
69
+ # @param paths [Array<String>] remote file paths to delete
70
+ # @return [void]
45
71
  def delete(*paths)
46
72
  return display_help_for("delete") if paths.empty? || help_requested?(options[:help], paths)
47
73
 
48
74
  client = ensure_client!
49
- paths.each { |path| Services::File::Remover.new(client, path).remove }
75
+ paths.each { |path| Services::File::Remover.new(client, path, display: display).remove }
50
76
  end
51
77
 
52
78
  desc "logout", "Remove the site api key from the config"
53
79
  method_option :help, aliases: "-h", type: :boolean
54
80
  method_option :yes, aliases: "-y", type: :boolean, default: false
81
+
82
+ # Removes the stored API key from the local config file.
83
+ #
84
+ # Requires the +--yes+ / +-y+ flag to confirm the action.
85
+ #
86
+ # @return [void]
55
87
  def logout
56
88
  return display_help_for("logout") if help_requested?(options[:help]) || !options[:yes]
57
89
 
@@ -61,6 +93,13 @@ module NeocitiesRed
61
93
 
62
94
  desc "info [SITENAME]", "Get site info"
63
95
  method_option :help, aliases: "-h", type: :boolean
96
+
97
+ # Displays information and statistics for a Neocities site.
98
+ #
99
+ # @param sitename [String, nil] site name to query; defaults to the
100
+ # currently authenticated site when omitted
101
+ # @return [void]
102
+ # @raise [NeocitiesRed::APIError] if the API request fails
64
103
  def info(sitename = nil)
65
104
  return display_help_for("info") if help_requested?(options[:help], sitename)
66
105
 
@@ -75,6 +114,13 @@ module NeocitiesRed
75
114
  method_option :help, aliases: "-h", type: :boolean
76
115
  method_option :detail, aliases: "-d", type: :boolean, default: false
77
116
  method_option :all, aliases: "-a", type: :boolean, default: false
117
+
118
+ # Lists files on the remote Neocities site.
119
+ #
120
+ # @param path [String, nil] remote directory path to list (nil for root,
121
+ # or when +--all+ is used)
122
+ # @return [void]
123
+ # @raise [NeocitiesRed::APIError] if the API request fails
78
124
  def list(path = nil)
79
125
  if help_requested?(options[:help], path) || (path.nil? && options[:all].nil? && options[:detail].nil?)
80
126
  display_help_for("list")
@@ -83,7 +129,9 @@ module NeocitiesRed
83
129
 
84
130
  client = ensure_client!
85
131
  path = nil if options[:all]
86
- display.say Services::File::List.new(client, path, options[:detail]).show
132
+ display.say Services::File::List.new(client, path, options[:detail], display: display).show
133
+ rescue NeocitiesRed::APIError => e
134
+ display.display_response(e)
87
135
  end
88
136
 
89
137
  desc "push PATH", "Recursively upload a local directory to your Neocities site"
@@ -94,6 +142,18 @@ module NeocitiesRed
94
142
  method_option :dry_run, type: :boolean, default: false
95
143
  method_option :prune, type: :boolean, default: false
96
144
  method_option :optimized, type: :boolean, default: false
145
+
146
+ # Recursively uploads a local directory to the Neocities site.
147
+ #
148
+ # Supports +--no-gitignore+ to ignore .gitignore rules,
149
+ # +--ignore-dotfiles+ to skip dot-prefixed files,
150
+ # +--exclude+ to skip specific paths, +--dry-run+ to preview changes,
151
+ # +--prune+ to delete remote files not present locally, and
152
+ # +--optimized+ to skip files whose SHA1 hash matches the server.
153
+ #
154
+ # @param root [String, nil] local directory path to upload
155
+ # @return [void]
156
+ # @raise [ArgumentError] if the path does not exist or is not a directory
97
157
  def push(root = nil)
98
158
  return display_help_for("push") if help_requested?(options[:help], root)
99
159
 
@@ -118,6 +178,15 @@ module NeocitiesRed
118
178
 
119
179
  desc "upload LOCAL_PATH [REMOTE_PATH]", "Upload a file/folder to your Neocities site"
120
180
  method_option :help, aliases: "-h", type: :boolean
181
+
182
+ # Uploads a single file or an entire folder to the Neocities site.
183
+ #
184
+ # When +LOCAL_PATH+ is a file, uploads it directly.
185
+ # When it is a directory, uploads all files within it in parallel.
186
+ #
187
+ # @param local_path [String, nil] local file or directory path
188
+ # @param remote_path [String, nil] remote destination; defaults to the basename of +local_path+
189
+ # @return [void]
121
190
  def upload(local_path = nil, remote_path = nil)
122
191
  return display_help_for("upload") if help_requested?(options[:help], [local_path, remote_path])
123
192
  return display_help_for("upload") if local_path.nil?
@@ -125,9 +194,9 @@ module NeocitiesRed
125
194
  client = ensure_client!
126
195
  dest = remote_path || File.basename(local_path)
127
196
  if File.file?(local_path)
128
- Services::File::Uploader.new(client, local_path, dest).upload
197
+ Services::File::Uploader.new(client, local_path, dest, display: display).upload
129
198
  elsif File.directory?(local_path)
130
- folder_uploader = Services::File::FolderUploader.new(client, local_path, dest)
199
+ folder_uploader = Services::File::FolderUploader.new(client, local_path, dest, display: display)
131
200
  files_list = folder_uploader.files
132
201
  folder_uploader.upload(files_list)
133
202
  end
@@ -136,6 +205,15 @@ module NeocitiesRed
136
205
  desc "pull", "Get the most recent version of files from your site"
137
206
  method_option :help, aliases: "-h", type: :boolean
138
207
  method_option :quiet, aliases: "-q", type: :boolean, default: false
208
+
209
+ # Downloads the latest version of site files from the remote Neocities site.
210
+ #
211
+ # Skips files that haven't changed since the last pull (based on stored
212
+ # timestamp and working directory). Use +--quiet+ to suppress per-file
213
+ # output and show a spinner instead.
214
+ #
215
+ # @return [void]
216
+ # @raise [StandardError] on network or API errors
139
217
  def pull
140
218
  return display_help_for("pull") if help_requested?(options[:help])
141
219
 
@@ -145,16 +223,27 @@ module NeocitiesRed
145
223
  last_pull_time = data.dig("LAST_PULL", "time")
146
224
  last_pull_loc = data.dig("LAST_PULL", "loc")
147
225
 
148
- Services::Site::Exporter.new(client, @sitename, data, app_config_path)
226
+ Services::Site::Exporter.new(client, @sitename, data, app_config_path, display: display)
149
227
  .export(quiet: options[:quiet], last_pull_time: last_pull_time, last_pull_loc: last_pull_loc)
228
+ rescue StandardError => e
229
+ display.display_response(e)
150
230
  end
151
231
 
152
232
  desc "purge", "Delete everything from your site (development only)"
153
233
  method_option :yes, aliases: "-y", type: :boolean, default: false
234
+ method_option :dry_run, type: :boolean, default: false
235
+
236
+ # Deletes all files from the Neocities site.
237
+ #
238
+ # Requires the +--yes+ / +-y+ flag to confirm the destructive action.
239
+ # Use +--dry-run+ to preview what would be deleted without changes.
240
+ #
241
+ # @return [void]
154
242
  def purge
155
243
  return display_help_for("purge") unless options[:yes]
156
244
 
157
245
  client = ensure_client!
246
+ display.display_dry_run_notice if options[:dry_run]
158
247
  resp = client.list
159
248
  deleted_dirs = []
160
249
  resp[:files].sort_by { |f| f[:is_directory] ? 0 : 1 }.each do |file|
@@ -173,10 +262,19 @@ module NeocitiesRed
173
262
  end
174
263
 
175
264
  desc "pizza", "Order a free pizza"
265
+
266
+ # Easter egg — displays a humorous pizza-related excuse.
267
+ #
268
+ # @return [void]
176
269
  def pizza
177
270
  display_help_for(__method__)
178
271
  end
179
272
 
273
+ # Returns the platform-specific application config directory path.
274
+ #
275
+ # @param name [String] application name (e.g. "neocities")
276
+ # @return [String, nil] full path to the config directory, or nil if
277
+ # the platform cannot be determined
180
278
  def self.app_config_path(name)
181
279
  platform = case RUBY_PLATFORM
182
280
  when /cygwin|mswin|mingw|bccwin|wince|emx|win32/
@@ -217,6 +315,12 @@ module NeocitiesRed
217
315
  end
218
316
 
219
317
  desc "help [COMMAND]", "Show help for a command"
318
+
319
+ # Displays help for a specific command or the general help screen.
320
+ #
321
+ # @param command [String, nil] command name to show help for;
322
+ # nil displays the main help screen
323
+ # @return [void]
220
324
  def help(command = nil)
221
325
  return display.display_help_and_exit if command.nil?
222
326
 
@@ -227,6 +331,10 @@ module NeocitiesRed
227
331
  end
228
332
 
229
333
  desc "version", "Display neocities-red version"
334
+
335
+ # Prints the current gem version to stdout.
336
+ #
337
+ # @return [void]
230
338
  def version
231
339
  display.say NeocitiesRed::VERSION
232
340
  end
@@ -234,18 +342,30 @@ module NeocitiesRed
234
342
  no_commands do
235
343
  alias_method :display_help_for, :help
236
344
 
345
+ # Returns the initialized {CliDisplay} instance.
346
+ #
347
+ # @return [NeocitiesRed::CliDisplay]
237
348
  def display
238
349
  @display ||= NeocitiesRed::CliDisplay.new
239
350
  end
240
351
 
352
+ # Returns the initialized TTY::Prompt instance for interactive input.
353
+ #
354
+ # @return [TTY::Prompt]
241
355
  def prompt
242
356
  @prompt ||= TTY::Prompt.new
243
357
  end
244
358
 
359
+ # Returns the full path to the application config file.
360
+ #
361
+ # @return [String] path to +config.json+ inside the platform config directory
245
362
  def app_config_path
246
363
  @app_config_path ||= File.join(self.class.app_config_path("neocities"), "config.json")
247
364
  end
248
365
 
366
+ # Reads and parses the JSON config file from disk.
367
+ #
368
+ # @return [Hash, nil] parsed config hash, or nil if the file does not exist
249
369
  def read_config
250
370
  file = File.read(app_config_path)
251
371
  JSON.parse(file)
@@ -253,12 +373,17 @@ module NeocitiesRed
253
373
  nil
254
374
  end
255
375
 
376
+ # Lazily initializes and returns an authenticated {Client} instance.
377
+ #
378
+ # Reads the API key from (in order): CLI option, environment variable,
379
+ # or stored config. If no key is found, triggers interactive login.
380
+ #
381
+ # @return [NeocitiesRed::Client]
256
382
  def ensure_client!
257
383
  return @client if @client
258
384
 
259
385
  config = read_config
260
386
  @sitename = config && config["SITENAME"]
261
- @last_pull = config && config["LAST_PULL"]
262
387
 
263
388
  @api_key = options[:api_key] || ENV.fetch("NEOCITIES_API_KEY", nil)
264
389
  @api_key ||= config && config["API_KEY"]&.strip
@@ -272,6 +397,13 @@ module NeocitiesRed
272
397
  @client
273
398
  end
274
399
 
400
+ # Prompts the user for credentials, obtains an API key, and stores it.
401
+ #
402
+ # Saves the API key and sitename to the local config file with
403
+ # restricted permissions (0600).
404
+ #
405
+ # @return [void]
406
+ # @raise [Thor::Error] if the API key cannot be obtained
275
407
  def authenticate_and_persist_key!
276
408
  display.display_login_prompt
277
409
 
@@ -293,36 +425,24 @@ module NeocitiesRed
293
425
  }
294
426
 
295
427
  FileUtils.mkdir_p(Pathname(app_config_path).dirname)
296
- File.write(app_config_path, conf.to_json)
428
+ persist_config(conf)
297
429
  display.display_api_key_saved(@sitename, app_config_path)
298
430
  @client = NeocitiesRed::Client.new(api_key: @api_key)
299
431
  end
300
432
 
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
433
+ # Writes the config hash to disk as JSON and restricts file permissions.
434
+ #
435
+ # @param conf [Hash] configuration data to persist
436
+ # @return [void]
437
+ def persist_config(conf)
438
+ File.write(app_config_path, conf.to_json)
439
+ FileUtils.chmod(0o600, app_config_path)
324
440
  end
325
441
 
442
+ # Checks if the given value contains a help flag.
443
+ #
444
+ # @param value [String, Array<String>, nil] value to inspect
445
+ # @return [Boolean] true if the value is or contains "-h", "--help", or "help"
326
446
  def help_requested_for?(value)
327
447
  case value
328
448
  when Array
@@ -332,6 +452,11 @@ module NeocitiesRed
332
452
  end
333
453
  end
334
454
 
455
+ # Determines if help was requested via the +--help+ option or the value.
456
+ #
457
+ # @param help_option [Boolean, nil] the Thor +--help+ option value
458
+ # @param value [String, Array<String>, nil] the positional argument to check
459
+ # @return [Boolean]
335
460
  def help_requested?(help_option, value = nil)
336
461
  help_option || (value && help_requested_for?(value))
337
462
  end