Icarus-Mod-Tools 2.3.0 → 2.5.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: f629740e09031cd9f71c655aa8872e1f9ca80db5e9ca0aaab9e2894d64190c61
4
- data.tar.gz: '08d27ce72493df462e3d028bdced86a23f5ddc78e1da7f37d0b9f637b2b720c7'
3
+ metadata.gz: e7011bea56d03dd0edd74138d193a5ad4e5628c4c8d59e94abf6e8793b397051
4
+ data.tar.gz: 4e9c054fe489ec2abb4aa36f7b8ea9b6d5621723a8ce0cf50563166b7ef002ee
5
5
  SHA512:
6
- metadata.gz: af55744f78e88d220f83578cd1f85ac341f4b1421b1ef44c580737effc249a053a5798b8dc040385f4e08c16e2bd3c8e5a9e038d143f6a36ed14daf68e64816d
7
- data.tar.gz: a3b2c2f18203a5472236cead6506b060010254828d8a05412863f9979b1c1c2bb4f2d0f19e250e5e320ab72ae496fc5d8c66e29779c86a86db851338b702218b
6
+ metadata.gz: 15d5e90ecb8c7f91accfb3ded406374278ab53e8b3d3d6b997e44904f4d9425acd0bd4461cf5def41dfe502ead8df5f0c54b5002380c78e7da928a39da45db8c
7
+ data.tar.gz: 0f9723a7821f213c70612c6510a2bc1c9508353cdf4e10ebf1568291f97e918325c12911e3621aaa4c82f7dd471c2236584d305532b40be32a84dcb0cafd90bf
@@ -0,0 +1,178 @@
1
+ # Claude Code Session Context - 2026-01-11
2
+
3
+ ## Status: COMPLETE - PR Open for Review
4
+
5
+ **Date:** 2026-01-11
6
+ **Pull Request:** #13 - https://github.com/DonovanMods/icarus-mod-tools/pull/13
7
+ **Branch:** feature/remove-mods-tools
8
+ **Issues Resolved:** #12, #5
9
+
10
+ ---
11
+
12
+ ## What Was Implemented
13
+
14
+ Added three new removal commands to the CLI:
15
+
16
+ 1. **`imt remove mod <MOD_ID>`** - Removes entries from the `mods` collection
17
+ 2. **`imt remove tool <TOOL_ID>`** - Removes entries from the `tools` collection
18
+ 3. **Enhanced `imt remove repos`** - Added cascade delete functionality (enabled by default via `--cascade` flag)
19
+
20
+ ### Cascade Delete Feature
21
+
22
+ When removing a repository with `--cascade` (default: true):
23
+ - Removes the repository from `meta/repos/list`
24
+ - Finds and removes all associated modinfo URLs from `meta/modinfo/list`
25
+ - Finds and removes all associated toolinfo URLs from `meta/toolinfo/list`
26
+ - Fetches each modinfo/toolinfo JSON and removes associated mods/tools from collections
27
+ - Gracefully handles fetch errors with warnings
28
+
29
+ Users can disable cascade with `--no-cascade` to only remove the repository entry.
30
+
31
+ ---
32
+
33
+ ## Files Modified
34
+
35
+ 1. **lib/icarus/mod/cli/remove.rb** (+151 lines)
36
+ - Added `mod(mod_id)` method
37
+ - Added `tool(tool_id)` method
38
+ - Enhanced `repos(repo)` with cascade logic
39
+ - Added private methods: `remove_entity`, `cascade_delete_repo`, `delete_entities_from_url`
40
+ - Includes `Tools::Sync::Helpers` for `retrieve_from_url` functionality
41
+
42
+ 2. **spec/icarus/mod/cli/remove_spec.rb** (+192 lines)
43
+ - Comprehensive test coverage for all new commands
44
+ - Tests for cascade delete functionality
45
+ - Tests for `--dry-run` compatibility
46
+ - Tests for error handling when URL fetch fails
47
+
48
+ 3. **lib/icarus/mod/version.rb**
49
+ - Bumped from 2.3.0 → 2.4.0
50
+
51
+ 4. **CHANGELOG.md**
52
+ - Added v2.4.0 entry documenting all changes
53
+
54
+ 5. **README.md**
55
+ - Updated `imt remove` command documentation
56
+ - Added `--cascade` and `--dry-run` options
57
+
58
+ 6. **Gemfile.lock**
59
+ - Updated version reference
60
+
61
+ ---
62
+
63
+ ## Test Results
64
+
65
+ ✅ All 290 tests passing
66
+ - 16 tests for remove command (including new mod/tool/cascade tests)
67
+ - Full test suite verified
68
+
69
+ ---
70
+
71
+ ## Key Implementation Details
72
+
73
+ ### Finding Entities by ID
74
+ Both `mod` and `tool` commands find entities by their Firestore document ID:
75
+ ```ruby
76
+ entity = collection.find { |e| e.id == entity_id }
77
+ ```
78
+
79
+ ### Cascade Delete Logic
80
+ 1. Filters modinfo/toolinfo URLs by repository name
81
+ 2. Deletes each URL from meta collections
82
+ 3. Fetches the JSON from each URL to get entity names
83
+ 4. Finds and deletes matching entities from mods/tools collections by name+author
84
+ 5. Finally removes the repository
85
+
86
+ ### Error Handling
87
+ - Warnings (not failures) when URLs can't be fetched during cascade
88
+ - Proper exit codes for not found scenarios
89
+ - Dry-run support for all operations
90
+
91
+ ---
92
+
93
+ ## PR Details
94
+
95
+ **Title:** feat: add remove commands for mods and tools collections (#12)
96
+ **URL:** https://github.com/DonovanMods/icarus-mod-tools/pull/13
97
+ **Status:** Open, mergeable
98
+ **Reviewer:** GitHub Copilot (auto-requested)
99
+ **Changes:** +349 lines, -19 lines across 6 files
100
+
101
+ **PR Body:**
102
+ ```markdown
103
+ ## Summary
104
+
105
+ Resolves #12
106
+ Resolves #5
107
+
108
+ Adds direct removal commands for the `mods` and `tools` collections, along with cascade delete functionality for repository removal.
109
+
110
+ ## Changes
111
+
112
+ - Add `imt remove mod <MOD_ID>` to delete entries from mods collection
113
+ - Add `imt remove tool <TOOL_ID>` to delete entries from tools collection
114
+ - Enhance `imt remove repos` with `--cascade` flag (enabled by default) to automatically remove associated modinfo, toolinfo, mods, and tools
115
+ - Include comprehensive test coverage for all new functionality
116
+ - Update documentation (README, CHANGELOG) for new commands
117
+ - Bump version to 2.4.0
118
+
119
+ ## Test Plan
120
+
121
+ - [x] All 290 tests passing
122
+ - [x] Added comprehensive test coverage for new commands
123
+ - [x] Tested cascade delete functionality
124
+ - [x] Tested --dry-run flag compatibility
125
+ - [x] Updated documentation
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Git State
131
+
132
+ **Current Branch:** main (reset to match origin/main after creating PR)
133
+ **Main HEAD:** de2146d - "Add comprehensive test coverage and fix sync 404 errors (#9)"
134
+
135
+ **Feature Branch:** feature/remove-mods-tools
136
+ **Feature HEAD:** 5109a1a - "feat: add remove commands for mods and tools collections (#12)"
137
+ **Pushed to:** origin/feature/remove-mods-tools
138
+
139
+ ---
140
+
141
+ ## Next Steps
142
+
143
+ 1. ✅ PR created and open for review
144
+ 2. ⏳ Wait for PR review/approval
145
+ 3. ⏳ Merge PR when approved
146
+ 4. ⏳ Issues #12 and #5 will auto-close on merge
147
+ 5. ⏳ Consider tagging v2.4.0 release after merge
148
+
149
+ ---
150
+
151
+ ## How to Resume
152
+
153
+ When you move the repository and restart Claude Code:
154
+
155
+ 1. The PR is already created: https://github.com/DonovanMods/icarus-mod-tools/pull/13
156
+ 2. All code is committed to `feature/remove-mods-tools` branch
157
+ 3. The main branch is clean
158
+ 4. If you need to make changes to the PR, checkout the feature branch:
159
+ ```bash
160
+ git checkout feature/remove-mods-tools
161
+ # make changes
162
+ git add -A && git commit -m "update: ..."
163
+ git push
164
+ ```
165
+
166
+ 5. To merge the PR (when ready):
167
+ ```bash
168
+ gh pr merge 13 --squash # or --merge or --rebase
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Related Issues
174
+
175
+ - **#12** - "Add remove commands for mods and tools collections" (main issue)
176
+ - **#5** - "When a repo is deleted, the associated entry is not removed from the modinfo db" (cascade delete)
177
+
178
+ Both issues are linked in the PR and will auto-close when merged.
data/CHANGELOG.md CHANGED
@@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## History (reverse chronological order)
6
6
 
7
+ ### v2.5.1 - 2026-01-31
8
+
9
+ - Fix `imt remove repo` failing to find repositories stored as full URLs
10
+ - Now checks for exact match first (handles full URLs), then falls back to stripped format
11
+
12
+ ### v2.5.0 - 2026-01-12
13
+
14
+ - Add automatic conversion of GitHub URLs to `raw.githubusercontent.com` format during sync
15
+ - Converts both `/blob/` and `/raw/` URLs from `github.com` domain
16
+ - Applies to all file URLs in modinfo and toolinfo files
17
+ - Also applies to imageURL and readmeURL fields
18
+ - Warnings added to alert users to update source files
19
+ - Ensures compatibility with GitHub's current raw file hosting on `raw.githubusercontent.com`
20
+
21
+ ### v2.4.1 - 2026-01-12
22
+
23
+ - Fix JSON parsing error messages during sync to show concise error instead of full stack trace
24
+ - Include URL in JSON parsing error messages to identify which repository has invalid JSON
25
+
26
+ ### v2.4.0 - 2026-01-11
27
+
28
+ - Add `imt remove mod` command to remove entries from `mods` collection
29
+ - Add `imt remove tool` command to remove entries from `tools` collection
30
+ - Add cascade delete to `imt remove repos` (enabled by default via `--cascade` flag)
31
+ - When removing a repository, also removes associated modinfo, toolinfo, mods, and tools entries
32
+ - Improvements to cascade delete functionality:
33
+ - Fix URL matching to prevent false matches (e.g., "owner/repo" no longer matches "owner/repo-fork")
34
+ - Handle multiple entities with same name and author (deletes all matches)
35
+ - Improve error handling with specific exception types and comprehensive reporting
36
+ - Track and report both fetch failures and delete failures with detailed summaries
37
+ - Enhanced dry-run output showing all entities that would be deleted
38
+ - Add Firestore cache invalidation for mod/tool deletions
39
+
40
+ ### v2.3.0 - 2025-12-18
41
+
42
+ - Add comprehensive test coverage
43
+ - Fix sync 404 errors
44
+
7
45
  ### v2.1 - 2023-02-11
8
46
 
9
47
  - Remove support for `fileType` and `fileURL` in `modinfo.json` files
data/Gemfile CHANGED
@@ -16,3 +16,5 @@ group :develop do
16
16
  gem "rubocop", "~> 1.41"
17
17
  gem "rubocop-rspec", "~> 2.16", require: false
18
18
  end
19
+
20
+ gem "ostruct", "~> 0.6.3"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- Icarus-Mod-Tools (2.3.0)
4
+ Icarus-Mod-Tools (2.5.0)
5
5
  google-cloud-firestore (~> 2.7)
6
6
  octokit (~> 6.0)
7
7
  paint (~> 2.3)
@@ -10,13 +10,13 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- addressable (2.8.7)
14
- public_suffix (>= 2.0.2, < 7.0)
13
+ addressable (2.8.8)
14
+ public_suffix (>= 2.0.2, < 8.0)
15
15
  ast (2.4.3)
16
16
  base64 (0.3.0)
17
17
  bigdecimal (3.3.1)
18
18
  coderay (1.1.3)
19
- concurrent-ruby (1.3.5)
19
+ concurrent-ruby (1.3.6)
20
20
  diff-lcs (1.6.2)
21
21
  faraday (2.14.0)
22
22
  faraday-net_http (>= 2.0, < 3.5)
@@ -24,20 +24,20 @@ GEM
24
24
  logger
25
25
  faraday-net_http (3.4.2)
26
26
  net-http (~> 0.5)
27
- faraday-retry (2.3.2)
27
+ faraday-retry (2.4.0)
28
28
  faraday (~> 2.0)
29
- ffi (1.17.2)
30
- ffi (1.17.2-aarch64-linux-gnu)
31
- ffi (1.17.2-aarch64-linux-musl)
32
- ffi (1.17.2-arm-linux-gnu)
33
- ffi (1.17.2-arm-linux-musl)
34
- ffi (1.17.2-arm64-darwin)
35
- ffi (1.17.2-x86-linux-gnu)
36
- ffi (1.17.2-x86-linux-musl)
37
- ffi (1.17.2-x86_64-darwin)
38
- ffi (1.17.2-x86_64-linux-gnu)
39
- ffi (1.17.2-x86_64-linux-musl)
40
- formatador (1.2.2)
29
+ ffi (1.17.3)
30
+ ffi (1.17.3-aarch64-linux-gnu)
31
+ ffi (1.17.3-aarch64-linux-musl)
32
+ ffi (1.17.3-arm-linux-gnu)
33
+ ffi (1.17.3-arm-linux-musl)
34
+ ffi (1.17.3-arm64-darwin)
35
+ ffi (1.17.3-x86-linux-gnu)
36
+ ffi (1.17.3-x86-linux-musl)
37
+ ffi (1.17.3-x86_64-darwin)
38
+ ffi (1.17.3-x86_64-linux-gnu)
39
+ ffi (1.17.3-x86_64-linux-musl)
40
+ formatador (1.2.3)
41
41
  reline
42
42
  fuubar (2.5.1)
43
43
  rspec-core (~> 3.0)
@@ -73,31 +73,7 @@ GEM
73
73
  gapic-common (~> 1.2)
74
74
  google-cloud-errors (~> 1.0)
75
75
  google-logging-utils (0.2.0)
76
- google-protobuf (4.33.0)
77
- bigdecimal
78
- rake (>= 13)
79
- google-protobuf (4.33.0-aarch64-linux-gnu)
80
- bigdecimal
81
- rake (>= 13)
82
- google-protobuf (4.33.0-aarch64-linux-musl)
83
- bigdecimal
84
- rake (>= 13)
85
- google-protobuf (4.33.0-arm64-darwin)
86
- bigdecimal
87
- rake (>= 13)
88
- google-protobuf (4.33.0-x86-linux-gnu)
89
- bigdecimal
90
- rake (>= 13)
91
- google-protobuf (4.33.0-x86-linux-musl)
92
- bigdecimal
93
- rake (>= 13)
94
- google-protobuf (4.33.0-x86_64-darwin)
95
- bigdecimal
96
- rake (>= 13)
97
- google-protobuf (4.33.0-x86_64-linux-gnu)
98
- bigdecimal
99
- rake (>= 13)
100
- google-protobuf (4.33.0-x86_64-linux-musl)
76
+ google-protobuf (4.33.4)
101
77
  bigdecimal
102
78
  rake (>= 13)
103
79
  googleapis-common-protos (1.9.0)
@@ -106,7 +82,7 @@ GEM
106
82
  grpc (~> 1.41)
107
83
  googleapis-common-protos-types (1.22.0)
108
84
  google-protobuf (~> 4.26)
109
- googleauth (1.15.1)
85
+ googleauth (1.16.1)
110
86
  faraday (>= 1.0, < 3.a)
111
87
  google-cloud-env (~> 2.2)
112
88
  google-logging-utils (~> 0.1)
@@ -141,14 +117,13 @@ GEM
141
117
  grpc (1.76.0-x86_64-linux-musl)
142
118
  google-protobuf (>= 3.25, < 5.0)
143
119
  googleapis-common-protos-types (~> 1.0)
144
- guard (2.19.1)
120
+ guard (2.20.0)
145
121
  formatador (>= 0.2.4)
146
122
  listen (>= 2.7, < 4.0)
147
123
  logger (~> 1.6)
148
124
  lumberjack (>= 1.0.12, < 2.0)
149
125
  nenv (~> 0.1)
150
126
  notiffany (~> 0.0)
151
- ostruct (~> 0.6)
152
127
  pry (>= 0.13.0)
153
128
  shellany (~> 0.0)
154
129
  thor (>= 0.18.1)
@@ -157,22 +132,23 @@ GEM
157
132
  guard (~> 2.1)
158
133
  guard-compat (~> 1.1)
159
134
  rspec (>= 2.99.0, < 4.0)
160
- io-console (0.8.1)
161
- json (2.16.0)
135
+ io-console (0.8.2)
136
+ json (2.18.0)
162
137
  jwt (3.1.2)
163
138
  base64
164
139
  language_server-protocol (3.17.0.5)
165
140
  lint_roller (1.1.0)
166
- listen (3.9.0)
141
+ listen (3.10.0)
142
+ logger
167
143
  rb-fsevent (~> 0.10, >= 0.10.3)
168
144
  rb-inotify (~> 0.9, >= 0.9.10)
169
145
  logger (1.7.0)
170
146
  lumberjack (1.4.2)
171
147
  method_source (1.1.0)
172
- multi_json (1.17.0)
148
+ multi_json (1.19.1)
173
149
  nenv (0.3.0)
174
- net-http (0.7.0)
175
- uri
150
+ net-http (0.9.1)
151
+ uri (>= 0.11.1)
176
152
  notiffany (0.1.3)
177
153
  nenv (~> 0.1)
178
154
  shellany (~> 0.0)
@@ -183,14 +159,14 @@ GEM
183
159
  ostruct (0.6.3)
184
160
  paint (2.3.0)
185
161
  parallel (1.27.0)
186
- parser (3.3.10.0)
162
+ parser (3.3.10.1)
187
163
  ast (~> 2.4.1)
188
164
  racc
189
- prism (1.6.0)
165
+ prism (1.9.0)
190
166
  pry (0.14.2)
191
167
  coderay (~> 1.1)
192
168
  method_source (~> 1.0)
193
- public_suffix (6.0.2)
169
+ public_suffix (7.0.2)
194
170
  racc (1.8.1)
195
171
  rainbow (3.1.1)
196
172
  rake (13.3.1)
@@ -213,8 +189,8 @@ GEM
213
189
  rspec-mocks (3.13.7)
214
190
  diff-lcs (>= 1.2.0, < 2.0)
215
191
  rspec-support (~> 3.13.0)
216
- rspec-support (3.13.6)
217
- rubocop (1.81.7)
192
+ rspec-support (3.13.7)
193
+ rubocop (1.84.0)
218
194
  json (~> 2.3)
219
195
  language_server-protocol (~> 3.17.0.2)
220
196
  lint_roller (~> 1.1.0)
@@ -222,16 +198,16 @@ GEM
222
198
  parser (>= 3.3.0.2)
223
199
  rainbow (>= 2.2.2, < 4.0)
224
200
  regexp_parser (>= 2.9.3, < 3.0)
225
- rubocop-ast (>= 1.47.1, < 2.0)
201
+ rubocop-ast (>= 1.49.0, < 2.0)
226
202
  ruby-progressbar (~> 1.7)
227
203
  unicode-display_width (>= 2.4.0, < 4.0)
228
- rubocop-ast (1.48.0)
204
+ rubocop-ast (1.49.0)
229
205
  parser (>= 3.3.7.2)
230
- prism (~> 1.4)
206
+ prism (~> 1.7)
231
207
  rubocop-capybara (2.22.1)
232
208
  lint_roller (~> 1.1)
233
209
  rubocop (~> 1.72, >= 1.72.1)
234
- rubocop-factory_bot (2.27.1)
210
+ rubocop-factory_bot (2.28.0)
235
211
  lint_roller (~> 1.1)
236
212
  rubocop (~> 1.72, >= 1.72.1)
237
213
  rubocop-rspec (2.31.0)
@@ -251,10 +227,10 @@ GEM
251
227
  faraday (>= 0.17.5, < 3.a)
252
228
  jwt (>= 1.5, < 4.0)
253
229
  multi_json (~> 1.10)
254
- thor (1.4.0)
230
+ thor (1.5.0)
255
231
  unicode-display_width (3.2.0)
256
232
  unicode-emoji (~> 4.1)
257
- unicode-emoji (4.1.0)
233
+ unicode-emoji (4.2.0)
258
234
  uri (1.1.1)
259
235
 
260
236
  PLATFORMS
@@ -267,6 +243,7 @@ PLATFORMS
267
243
  x86-linux-gnu
268
244
  x86-linux-musl
269
245
  x86_64-darwin
246
+ x86_64-linux
270
247
  x86_64-linux-gnu
271
248
  x86_64-linux-musl
272
249
 
@@ -275,6 +252,7 @@ DEPENDENCIES
275
252
  fuubar
276
253
  guard (~> 2.18)
277
254
  guard-rspec (~> 4.7)
255
+ ostruct (~> 0.6.3)
278
256
  pry (~> 0.14.1)
279
257
  rake (~> 13.0)
280
258
  rspec (~> 3.12)
@@ -282,4 +260,4 @@ DEPENDENCIES
282
260
  rubocop-rspec (~> 2.16)
283
261
 
284
262
  BUNDLED WITH
285
- 2.6.9
263
+ 4.0.5
data/README.md CHANGED
@@ -110,9 +110,11 @@ Options:
110
110
  ```sh
111
111
  Commands:
112
112
  imt remove help [COMMAND] # Describe subcommands or one specific subcommand
113
+ imt remove mod MOD_ID # Removes a mod from the 'mods' collection
113
114
  imt remove modinfo ITEM # Removes an entry from 'meta/modinfo/list'
114
- imt remove toolinfo ITEM # Removes an entry from 'meta/toolinfo/list'
115
- imt remove repos REPO # Removes an entry from 'meta/repos/list'
115
+ imt remove repos REPO # Removes an entry from 'meta/repos/list' and cascades to associated mods/tools
116
+ imt remove tool TOOL_ID # Removes a tool from the 'tools' collection
117
+ imt remove toolinfo ITEM # Removes an entry from 'meta/toolinfo/list'
116
118
 
117
119
  Options:
118
120
  -C, [--config=CONFIG] # Path to the config file
@@ -120,6 +122,9 @@ Options:
120
122
  -V, [--version], [--no-version] # Print the version and exit
121
123
  -v, [--verbose], [--no-verbose] # Increase verbosity. May be repeated for even more verbosity.
122
124
  # Default: [true]
125
+ [--dry-run], [--no-dry-run] # Dry run (no changes will be made)
126
+ [--cascade], [--no-cascade] # Also remove associated modinfo, toolinfo, mods, and tools entries (for repos only)
127
+ # Default: true
123
128
  ```
124
129
 
125
130
  #### `imt sync`
@@ -142,6 +147,8 @@ Options:
142
147
  [--dry-run], [--no-dry-run] # Dry run (no changes will be made)
143
148
  ```
144
149
 
150
+ **Note**: Sync operations automatically convert GitHub URLs to the correct `raw.githubusercontent.com` format for direct downloads. If you're using GitHub URLs in `modinfo.json` or `toolinfo.json` files, you can use either `/blob/` or `/raw/` formats and they will be automatically converted. Warnings will be displayed when URLs are auto-fixed so you can update your source files.
151
+
145
152
  ## Development
146
153
 
147
154
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "firestore"
4
4
  require "cli/subcommand_base"
5
+ require "tools/sync/helpers"
5
6
 
6
7
  module Icarus
7
8
  module Mod
@@ -9,19 +10,41 @@ module Icarus
9
10
  # Remove CLI command definitions
10
11
  # rubocop:disable Style/GlobalVars
11
12
  class Remove < SubcommandBase
13
+ include Tools::Sync::Helpers
12
14
  class_option :dry_run, type: :boolean, default: false, desc: "Dry run (no changes will be made)"
13
15
 
14
- desc "repos REPO", "Removes an entry from 'meta/repos/list'"
16
+ desc "repos REPO", "Removes an entry from 'meta/repos/list' and cascades to associated mods/tools"
17
+ method_option :cascade, type: :boolean, default: true,
18
+ desc: "Also remove associated modinfo, toolinfo, mods, and tools entries"
15
19
  def repos(repo)
16
- repo_name = repo.gsub(%r{https?://.*github\.com/}, "")
17
- remove_item(
18
- :repositories,
19
- repo_name,
20
- "Repository",
21
- "Repository not found: #{repo_name}",
22
- "Successfully removed repository: #{repo_name}",
23
- "Failed to remove repository: #{repo_name}"
24
- )
20
+ # Try exact match first (for full URLs), then stripped format
21
+ repo_name = if firestore.repositories.include?(repo)
22
+ repo
23
+ else
24
+ repo.gsub(%r{https?://.*github\.com/}, "")
25
+ end
26
+
27
+ # Check if repository exists
28
+ unless firestore.repositories.include?(repo_name)
29
+ warn "Repository not found: #{repo_name}"
30
+ exit 1
31
+ end
32
+
33
+ puts Paint["Removing repository: #{repo_name}", :black] if verbose?
34
+
35
+ if options[:cascade]
36
+ cascade_delete_repo(repo_name)
37
+ else
38
+ # Just remove from repositories list
39
+ remove_item(
40
+ :repositories,
41
+ repo_name,
42
+ "Repository",
43
+ "Repository not found: #{repo_name}",
44
+ "Successfully removed repository: #{repo_name}",
45
+ "Failed to remove repository: #{repo_name}"
46
+ )
47
+ end
25
48
  end
26
49
 
27
50
  desc "modinfo ITEM", "Removes an entry from 'meta/modinfo/list'"
@@ -48,6 +71,30 @@ module Icarus
48
71
  )
49
72
  end
50
73
 
74
+ desc "mod MOD_ID", "Removes a mod from the 'mods' collection"
75
+ def mod(mod_id)
76
+ remove_entity(
77
+ :mod,
78
+ mod_id,
79
+ "Mod",
80
+ "Mod not found with ID: #{mod_id}",
81
+ "Successfully removed mod: #{mod_id}",
82
+ "Failed to remove mod: #{mod_id}"
83
+ )
84
+ end
85
+
86
+ desc "tool TOOL_ID", "Removes a tool from the 'tools' collection"
87
+ def tool(tool_id)
88
+ remove_entity(
89
+ :tool,
90
+ tool_id,
91
+ "Tool",
92
+ "Tool not found with ID: #{tool_id}",
93
+ "Successfully removed tool: #{tool_id}",
94
+ "Failed to remove tool: #{tool_id}"
95
+ )
96
+ end
97
+
51
98
  private
52
99
 
53
100
  def remove_item(type, item, display_name, not_found_msg, success_msg, failure_msg)
@@ -78,6 +125,205 @@ module Icarus
78
125
  end
79
126
  end
80
127
 
128
+ def remove_entity(type, entity_id, display_name, not_found_msg, success_msg, failure_msg)
129
+ # Find the entity by ID
130
+ collection = type == :mod ? firestore.mods : firestore.tools
131
+ entity = collection.find { |e| e.id == entity_id }
132
+
133
+ unless entity
134
+ warn not_found_msg
135
+ exit 1
136
+ end
137
+
138
+ puts Paint["Removing #{display_name.downcase}: #{entity.name} (ID: #{entity_id})", :black] if verbose?
139
+
140
+ if options[:dry_run]
141
+ puts Paint["Dry run; no changes will be made", :yellow]
142
+ return
143
+ end
144
+
145
+ if firestore.delete(type, entity)
146
+ puts Paint[success_msg, :green]
147
+ else
148
+ warn Paint[failure_msg, :red]
149
+ exit 1
150
+ end
151
+ end
152
+
153
+ def cascade_delete_repo(repo_name)
154
+ @delete_failures = []
155
+
156
+ # Find all modinfo URLs belonging to this repository
157
+ # Match full "owner/repo" path component, not substring
158
+ repo_pattern = %r{/#{Regexp.escape(repo_name)}(?=/|$)}
159
+ modinfo_urls = firestore.modinfo.select { |url| url.match?(repo_pattern) }
160
+ toolinfo_urls = firestore.toolinfo.select { |url| url.match?(repo_pattern) }
161
+
162
+ puts Paint["Found #{modinfo_urls.size} modinfo entries and #{toolinfo_urls.size} toolinfo entries", :cyan] if verbose?
163
+
164
+ if options[:dry_run]
165
+ puts Paint["Dry run; no changes will be made", :yellow]
166
+ preview = preview_cascade_deletions(repo_name, modinfo_urls, toolinfo_urls)
167
+ display_cascade_preview(repo_name, preview)
168
+ return
169
+ end
170
+
171
+ # Delete modinfo URLs and their associated mods
172
+ modinfo_urls.each do |url|
173
+ puts Paint[" Removing modinfo: #{url}", :black] if verbose?
174
+ track_delete(:modinfo, url) { firestore.delete(:modinfo, url) }
175
+
176
+ # Find and delete associated mods
177
+ delete_entities_from_url(url, :mod)
178
+ end
179
+
180
+ # Delete toolinfo URLs and their associated tools
181
+ toolinfo_urls.each do |url|
182
+ puts Paint[" Removing toolinfo: #{url}", :black] if verbose?
183
+ track_delete(:toolinfo, url) { firestore.delete(:toolinfo, url) }
184
+
185
+ # Find and delete associated tools
186
+ delete_entities_from_url(url, :tool)
187
+ end
188
+
189
+ # Report any failures
190
+ report_delete_failures
191
+ report_fetch_failures
192
+
193
+ # Finally, remove the repository
194
+ if firestore.delete(:repositories, repo_name)
195
+ puts Paint["Successfully removed repository and all associated entries: #{repo_name}", :green]
196
+ else
197
+ warn Paint["Failed to remove repository: #{repo_name}", :red]
198
+ exit 1
199
+ end
200
+ end
201
+
202
+ def delete_entities_from_url(url, type)
203
+ # Fetch the modinfo/toolinfo JSON
204
+ begin
205
+ data = retrieve_from_url(url)
206
+ entities = data[type == :mod ? :mods : :tools] || []
207
+
208
+ entities.each do |entity_data|
209
+ # Find ALL matching entities in Firestore by name and author
210
+ collection = type == :mod ? firestore.mods : firestore.tools
211
+ matching_entities = collection.select do |e|
212
+ e.name == entity_data[:name] && e.author == entity_data[:author]
213
+ end
214
+
215
+ next if matching_entities.empty?
216
+
217
+ if matching_entities.size > 1 && verbose?
218
+ warn Paint[" Note: Found #{matching_entities.size} entities matching '#{entity_data[:name]}' by #{entity_data[:author]}", :yellow]
219
+ end
220
+
221
+ matching_entities.each do |entity|
222
+ puts Paint[" Removing #{type}: #{entity.name} (ID: #{entity.id})", :black] if verbose?
223
+ track_delete(type, "#{entity.name} (#{entity.id})") { firestore.delete(type, entity) }
224
+ end
225
+ end
226
+ rescue SocketError, IOError, SystemCallError, Timeout::Error, JSON::ParserError => e
227
+ @failed_entity_fetches ||= []
228
+ @failed_entity_fetches << { url: url, error: e.class.name, message: e.message }
229
+ warn Paint["Warning: Could not fetch #{url} to remove entities: #{e.message}", :yellow]
230
+ end
231
+ end
232
+
233
+ def preview_cascade_deletions(repo_name, modinfo_urls, toolinfo_urls)
234
+ mods = []
235
+ tools = []
236
+
237
+ modinfo_urls.each do |url|
238
+ entities = fetch_entities_from_url(url, :mod)
239
+ mods.concat(entities) if entities
240
+ end
241
+
242
+ toolinfo_urls.each do |url|
243
+ entities = fetch_entities_from_url(url, :tool)
244
+ tools.concat(entities) if entities
245
+ end
246
+
247
+ {
248
+ modinfo_urls: modinfo_urls,
249
+ toolinfo_urls: toolinfo_urls,
250
+ mods: mods,
251
+ tools: tools
252
+ }
253
+ end
254
+
255
+ def fetch_entities_from_url(url, type)
256
+ data = retrieve_from_url(url)
257
+ entity_data_list = data[type == :mod ? :mods : :tools] || []
258
+
259
+ collection = type == :mod ? firestore.mods : firestore.tools
260
+ entities = []
261
+
262
+ entity_data_list.each do |entity_data|
263
+ matching_entities = collection.select do |e|
264
+ e.name == entity_data[:name] && e.author == entity_data[:author]
265
+ end
266
+ entities.concat(matching_entities)
267
+ end
268
+
269
+ entities
270
+ rescue SocketError, IOError, SystemCallError, Timeout::Error, JSON::ParserError => e
271
+ warn Paint["Warning: Could not fetch #{url}: #{e.message}", :yellow] if verbose?
272
+ nil
273
+ end
274
+
275
+ def display_cascade_preview(repo_name, preview)
276
+ puts "Would remove:"
277
+ puts " - Repository: #{repo_name}"
278
+
279
+ if preview[:modinfo_urls].any?
280
+ puts " - Modinfo URLs: #{preview[:modinfo_urls].size}"
281
+ preview[:modinfo_urls].each { |url| puts " • #{url}" }
282
+ end
283
+
284
+ if preview[:mods].any?
285
+ puts " - Mods: #{preview[:mods].size}"
286
+ preview[:mods].each { |mod| puts " • #{mod.name} by #{mod.author} (ID: #{mod.id})" }
287
+ end
288
+
289
+ if preview[:toolinfo_urls].any?
290
+ puts " - Toolinfo URLs: #{preview[:toolinfo_urls].size}"
291
+ preview[:toolinfo_urls].each { |url| puts " • #{url}" }
292
+ end
293
+
294
+ if preview[:tools].any?
295
+ puts " - Tools: #{preview[:tools].size}"
296
+ preview[:tools].each { |tool| puts " • #{tool.name} by #{tool.author} (ID: #{tool.id})" }
297
+ end
298
+ end
299
+
300
+ def track_delete(type, identifier)
301
+ success = yield
302
+ unless success
303
+ @delete_failures ||= []
304
+ @delete_failures << { type: type, identifier: identifier.to_s }
305
+ end
306
+ success
307
+ end
308
+
309
+ def report_delete_failures
310
+ return unless @delete_failures&.any?
311
+
312
+ warn Paint["\nWarning: #{@delete_failures.size} delete operation(s) failed:", :red]
313
+ @delete_failures.each do |failure|
314
+ warn Paint[" • #{failure[:type]}: #{failure[:identifier]}", :red]
315
+ end
316
+ end
317
+
318
+ def report_fetch_failures
319
+ return unless @failed_entity_fetches&.any?
320
+
321
+ warn Paint["\nWarning: Failed to fetch #{@failed_entity_fetches.size} URL(s) - some entities may not have been deleted:", :yellow]
322
+ @failed_entity_fetches.each do |failure|
323
+ warn Paint[" • #{failure[:url]} (#{failure[:error]})", :yellow]
324
+ end
325
+ end
326
+
81
327
  def firestore
82
328
  $firestore ||= Firestore.new
83
329
  end
@@ -66,6 +66,12 @@ module Icarus
66
66
  case type.to_sym
67
67
  when :mod, :tool
68
68
  response = @client.doc("#{collections.send(pluralize(type))}/#{payload.id}").delete
69
+ # Invalidate cache to prevent stale data
70
+ if response.is_a?(Google::Cloud::Firestore::CommitResponse::WriteResult)
71
+ cache_var = type == :mod ? :@mods : :@tools
72
+ cached_collection = instance_variable_get(cache_var)
73
+ cached_collection&.delete_if { |item| item.id == payload.id }
74
+ end
69
75
  when :modinfo, :toolinfo, :repositories
70
76
  update_array = (send(type) - [payload]).flatten.uniq
71
77
 
@@ -36,14 +36,16 @@ module Icarus
36
36
  if use_cache
37
37
  @resources.each { |file| block.call(file) } if block
38
38
  else
39
- @client.contents(repository, path:).each do |entry|
40
- if entry[:type] == "dir"
41
- all_files(path: entry[:path], cache: false, recursive: true, &block) if recursive
42
- next # we don't need directories in our output
39
+ begin
40
+ @client.contents(repository, path:).each do |entry|
41
+ if entry[:type] == "dir"
42
+ all_files(path: entry[:path], cache: false, recursive: true, &block) if recursive
43
+ next # we don't need directories in our output
44
+ end
45
+
46
+ block&.call(entry)
47
+ @resources << entry # cache the file
43
48
  end
44
-
45
- block&.call(entry)
46
- @resources << entry # cache the file
47
49
  rescue Octokit::NotFound
48
50
  warn "WARNING: Could not access #{repository}: 404 - not found"
49
51
  end
@@ -9,6 +9,15 @@ module Icarus
9
9
 
10
10
  HASHKEYS = %i[name author version compatibility description files imageURL readmeURL].freeze
11
11
 
12
+ # Match github.com URLs with /blob/ or /raw/ and convert to raw.githubusercontent.com
13
+ GITHUB_BLOB_RAW_URL_PATTERN = %r{
14
+ (https?)://(?:www\.)?github\.com/ # Protocol and GitHub domain (capture group 1)
15
+ ([^/]+)/ # owner (capture group 2)
16
+ ([^/]+)/ # repo (capture group 3)
17
+ (?:blob|raw)/ # /blob/ or /raw/ to remove
18
+ (.+) # branch and file path (capture group 4)
19
+ }x
20
+
12
21
  def initialize(data, id: nil, created: nil, updated: nil)
13
22
  @id = id
14
23
  @created_at = created
@@ -26,6 +35,8 @@ module Icarus
26
35
 
27
36
  def read(data)
28
37
  @data = data.is_a?(String) ? JSON.parse(data, symbolize_names: true) : data
38
+ normalize_github_urls_in_data
39
+ @data
29
40
  end
30
41
 
31
42
  def errors
@@ -147,6 +158,26 @@ module Icarus
147
158
  @warnings << "Version should be a version string" unless /^\d+[.\d+]*/.match?(version)
148
159
  end
149
160
  end
161
+
162
+ def normalize_github_url(url)
163
+ return url if url.nil? || url.empty?
164
+
165
+ if url.match?(GITHUB_BLOB_RAW_URL_PATTERN)
166
+ normalized = url.gsub(GITHUB_BLOB_RAW_URL_PATTERN, '\1://raw.githubusercontent.com/\2/\3/\4')
167
+ @warnings << "GitHub URL converted to raw.githubusercontent.com format for direct download. Auto-fixed: #{url}"
168
+ normalized
169
+ else
170
+ url
171
+ end
172
+ end
173
+
174
+ def normalize_github_urls_in_data
175
+ # Skip normalization if data is frozen (e.g., from Firestore)
176
+ return if @data.frozen?
177
+
178
+ @data[:imageURL] = normalize_github_url(@data[:imageURL])
179
+ @data[:readmeURL] = normalize_github_url(@data[:readmeURL])
180
+ end
150
181
  end
151
182
  end
152
183
  end
@@ -21,6 +21,22 @@ module Icarus
21
21
 
22
22
  super
23
23
  end
24
+
25
+ private
26
+
27
+ def normalize_github_urls_in_data
28
+ super # Handle imageURL and readmeURL
29
+
30
+ # Skip normalization if data is frozen (e.g., from Firestore)
31
+ return if @data.frozen?
32
+
33
+ # Normalize each file URL in the files hash
34
+ return unless @data[:files].is_a?(Hash)
35
+
36
+ @data[:files].transform_values! do |url|
37
+ normalize_github_url(url)
38
+ end
39
+ end
24
40
  end
25
41
  end
26
42
  end
@@ -26,7 +26,7 @@ module Icarus
26
26
  warn "Skipped; Failed to retrieve #{url}"
27
27
  next
28
28
  rescue JSON::ParserError => e
29
- warn "Skipped; Invalid JSON: #{e.full_message}"
29
+ warn "Skipped; Invalid JSON in #{url}: #{e.message}"
30
30
  next
31
31
  end.flatten.compact
32
32
  end
@@ -28,7 +28,7 @@ module Icarus
28
28
  warn "Skipped; Failed to retrieve #{url}"
29
29
  next
30
30
  rescue JSON::ParserError => e
31
- warn "Skipped; Invalid JSON: #{e.full_message}"
31
+ warn "Skipped; Invalid JSON in #{url}: #{e.message}"
32
32
  next
33
33
  end.flatten.compact
34
34
  end
@@ -21,6 +21,16 @@ module Icarus
21
21
 
22
22
  private
23
23
 
24
+ def normalize_github_urls_in_data
25
+ super # Handle imageURL and readmeURL
26
+
27
+ # Skip normalization if data is frozen (e.g., from Firestore)
28
+ return if @data.frozen?
29
+
30
+ # Normalize fileURL
31
+ @data[:fileURL] = normalize_github_url(@data[:fileURL])
32
+ end
33
+
24
34
  def filetype_pattern
25
35
  /(zip|exe)/i
26
36
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Icarus
4
4
  module Mod
5
- VERSION = "2.3.0"
5
+ VERSION = "2.5.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: Icarus-Mod-Tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Donovan Young
@@ -73,7 +73,7 @@ executables:
73
73
  extensions: []
74
74
  extra_rdoc_files: []
75
75
  files:
76
- - ".claude/settings.local.json"
76
+ - ".claude-context.md"
77
77
  - ".rspec"
78
78
  - ".ruby-version"
79
79
  - CHANGELOG.md
@@ -132,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
132
  - !ruby/object:Gem::Version
133
133
  version: '0'
134
134
  requirements: []
135
- rubygems_version: 3.6.9
135
+ rubygems_version: 4.0.3
136
136
  specification_version: 4
137
137
  summary: Various tools for Icarus Modding
138
138
  test_files: []
@@ -1,9 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(bundle exec rspec:*)"
5
- ],
6
- "deny": [],
7
- "ask": []
8
- }
9
- }