Icarus-Mod-Tools 2.3.0 → 2.5.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: f629740e09031cd9f71c655aa8872e1f9ca80db5e9ca0aaab9e2894d64190c61
4
- data.tar.gz: '08d27ce72493df462e3d028bdced86a23f5ddc78e1da7f37d0b9f637b2b720c7'
3
+ metadata.gz: 4de5e1f299177ea6e8681ae6d92fd808086ed601aa0c61346b9dc5552925bb82
4
+ data.tar.gz: e76f59e4cf4774e737240e310015a5870b99338b7c16f85599d7881f836f090f
5
5
  SHA512:
6
- metadata.gz: af55744f78e88d220f83578cd1f85ac341f4b1421b1ef44c580737effc249a053a5798b8dc040385f4e08c16e2bd3c8e5a9e038d143f6a36ed14daf68e64816d
7
- data.tar.gz: a3b2c2f18203a5472236cead6506b060010254828d8a05412863f9979b1c1c2bb4f2d0f19e250e5e320ab72ae496fc5d8c66e29779c86a86db851338b702218b
6
+ metadata.gz: d1e1f69dbf0a7ccb9558e9eac6b4ae802cd14cf78aeac3a18b54cb9c533008d98eaed562347cdf65712429173ddf614e018cf69d3366271753ba27c93c3fb80e
7
+ data.tar.gz: a95184b4d2be164e626390012630d86bdf94359e2e832396496f30dfd74911cee9d01dc03f605357e408ee912dd4dbf850cbaf8886206babd9cfbb08231facb8
@@ -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,39 @@ 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.0 - 2026-01-12
8
+
9
+ - Add automatic conversion of GitHub URLs to `raw.githubusercontent.com` format during sync
10
+ - Converts both `/blob/` and `/raw/` URLs from `github.com` domain
11
+ - Applies to all file URLs in modinfo and toolinfo files
12
+ - Also applies to imageURL and readmeURL fields
13
+ - Warnings added to alert users to update source files
14
+ - Ensures compatibility with GitHub's current raw file hosting on `raw.githubusercontent.com`
15
+
16
+ ### v2.4.1 - 2026-01-12
17
+
18
+ - Fix JSON parsing error messages during sync to show concise error instead of full stack trace
19
+ - Include URL in JSON parsing error messages to identify which repository has invalid JSON
20
+
21
+ ### v2.4.0 - 2026-01-11
22
+
23
+ - Add `imt remove mod` command to remove entries from `mods` collection
24
+ - Add `imt remove tool` command to remove entries from `tools` collection
25
+ - Add cascade delete to `imt remove repos` (enabled by default via `--cascade` flag)
26
+ - When removing a repository, also removes associated modinfo, toolinfo, mods, and tools entries
27
+ - Improvements to cascade delete functionality:
28
+ - Fix URL matching to prevent false matches (e.g., "owner/repo" no longer matches "owner/repo-fork")
29
+ - Handle multiple entities with same name and author (deletes all matches)
30
+ - Improve error handling with specific exception types and comprehensive reporting
31
+ - Track and report both fetch failures and delete failures with detailed summaries
32
+ - Enhanced dry-run output showing all entities that would be deleted
33
+ - Add Firestore cache invalidation for mod/tool deletions
34
+
35
+ ### v2.3.0 - 2025-12-18
36
+
37
+ - Add comprehensive test coverage
38
+ - Fix sync 404 errors
39
+
7
40
  ### v2.1 - 2023-02-11
8
41
 
9
42
  - Remove support for `fileType` and `fileURL` in `modinfo.json` files
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,31 @@ 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)
76
+ google-protobuf (4.33.2)
77
77
  bigdecimal
78
78
  rake (>= 13)
79
- google-protobuf (4.33.0-aarch64-linux-gnu)
79
+ google-protobuf (4.33.2-aarch64-linux-gnu)
80
80
  bigdecimal
81
81
  rake (>= 13)
82
- google-protobuf (4.33.0-aarch64-linux-musl)
82
+ google-protobuf (4.33.2-aarch64-linux-musl)
83
83
  bigdecimal
84
84
  rake (>= 13)
85
- google-protobuf (4.33.0-arm64-darwin)
85
+ google-protobuf (4.33.2-arm64-darwin)
86
86
  bigdecimal
87
87
  rake (>= 13)
88
- google-protobuf (4.33.0-x86-linux-gnu)
88
+ google-protobuf (4.33.2-x86-linux-gnu)
89
89
  bigdecimal
90
90
  rake (>= 13)
91
- google-protobuf (4.33.0-x86-linux-musl)
91
+ google-protobuf (4.33.2-x86-linux-musl)
92
92
  bigdecimal
93
93
  rake (>= 13)
94
- google-protobuf (4.33.0-x86_64-darwin)
94
+ google-protobuf (4.33.2-x86_64-darwin)
95
95
  bigdecimal
96
96
  rake (>= 13)
97
- google-protobuf (4.33.0-x86_64-linux-gnu)
97
+ google-protobuf (4.33.2-x86_64-linux-gnu)
98
98
  bigdecimal
99
99
  rake (>= 13)
100
- google-protobuf (4.33.0-x86_64-linux-musl)
100
+ google-protobuf (4.33.2-x86_64-linux-musl)
101
101
  bigdecimal
102
102
  rake (>= 13)
103
103
  googleapis-common-protos (1.9.0)
@@ -106,7 +106,7 @@ GEM
106
106
  grpc (~> 1.41)
107
107
  googleapis-common-protos-types (1.22.0)
108
108
  google-protobuf (~> 4.26)
109
- googleauth (1.15.1)
109
+ googleauth (1.16.0)
110
110
  faraday (>= 1.0, < 3.a)
111
111
  google-cloud-env (~> 2.2)
112
112
  google-logging-utils (~> 0.1)
@@ -157,8 +157,8 @@ GEM
157
157
  guard (~> 2.1)
158
158
  guard-compat (~> 1.1)
159
159
  rspec (>= 2.99.0, < 4.0)
160
- io-console (0.8.1)
161
- json (2.16.0)
160
+ io-console (0.8.2)
161
+ json (2.18.0)
162
162
  jwt (3.1.2)
163
163
  base64
164
164
  language_server-protocol (3.17.0.5)
@@ -169,10 +169,10 @@ GEM
169
169
  logger (1.7.0)
170
170
  lumberjack (1.4.2)
171
171
  method_source (1.1.0)
172
- multi_json (1.17.0)
172
+ multi_json (1.19.1)
173
173
  nenv (0.3.0)
174
- net-http (0.7.0)
175
- uri
174
+ net-http (0.9.1)
175
+ uri (>= 0.11.1)
176
176
  notiffany (0.1.3)
177
177
  nenv (~> 0.1)
178
178
  shellany (~> 0.0)
@@ -186,11 +186,11 @@ GEM
186
186
  parser (3.3.10.0)
187
187
  ast (~> 2.4.1)
188
188
  racc
189
- prism (1.6.0)
189
+ prism (1.7.0)
190
190
  pry (0.14.2)
191
191
  coderay (~> 1.1)
192
192
  method_source (~> 1.0)
193
- public_suffix (6.0.2)
193
+ public_suffix (7.0.2)
194
194
  racc (1.8.1)
195
195
  rainbow (3.1.1)
196
196
  rake (13.3.1)
@@ -214,7 +214,7 @@ GEM
214
214
  diff-lcs (>= 1.2.0, < 2.0)
215
215
  rspec-support (~> 3.13.0)
216
216
  rspec-support (3.13.6)
217
- rubocop (1.81.7)
217
+ rubocop (1.82.1)
218
218
  json (~> 2.3)
219
219
  language_server-protocol (~> 3.17.0.2)
220
220
  lint_roller (~> 1.1.0)
@@ -222,16 +222,16 @@ GEM
222
222
  parser (>= 3.3.0.2)
223
223
  rainbow (>= 2.2.2, < 4.0)
224
224
  regexp_parser (>= 2.9.3, < 3.0)
225
- rubocop-ast (>= 1.47.1, < 2.0)
225
+ rubocop-ast (>= 1.48.0, < 2.0)
226
226
  ruby-progressbar (~> 1.7)
227
227
  unicode-display_width (>= 2.4.0, < 4.0)
228
- rubocop-ast (1.48.0)
228
+ rubocop-ast (1.49.0)
229
229
  parser (>= 3.3.7.2)
230
- prism (~> 1.4)
230
+ prism (~> 1.7)
231
231
  rubocop-capybara (2.22.1)
232
232
  lint_roller (~> 1.1)
233
233
  rubocop (~> 1.72, >= 1.72.1)
234
- rubocop-factory_bot (2.27.1)
234
+ rubocop-factory_bot (2.28.0)
235
235
  lint_roller (~> 1.1)
236
236
  rubocop (~> 1.72, >= 1.72.1)
237
237
  rubocop-rspec (2.31.0)
@@ -254,7 +254,7 @@ GEM
254
254
  thor (1.4.0)
255
255
  unicode-display_width (3.2.0)
256
256
  unicode-emoji (~> 4.1)
257
- unicode-emoji (4.1.0)
257
+ unicode-emoji (4.2.0)
258
258
  uri (1.1.1)
259
259
 
260
260
  PLATFORMS
@@ -267,6 +267,7 @@ PLATFORMS
267
267
  x86-linux-gnu
268
268
  x86-linux-musl
269
269
  x86_64-darwin
270
+ x86_64-linux
270
271
  x86_64-linux-gnu
271
272
  x86_64-linux-musl
272
273
 
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,36 @@ 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
20
  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
- )
21
+
22
+ # Check if repository exists
23
+ unless firestore.repositories.include?(repo_name)
24
+ warn "Repository not found: #{repo_name}"
25
+ exit 1
26
+ end
27
+
28
+ puts Paint["Removing repository: #{repo_name}", :black] if verbose?
29
+
30
+ if options[:cascade]
31
+ cascade_delete_repo(repo_name)
32
+ else
33
+ # Just remove from repositories list
34
+ remove_item(
35
+ :repositories,
36
+ repo_name,
37
+ "Repository",
38
+ "Repository not found: #{repo_name}",
39
+ "Successfully removed repository: #{repo_name}",
40
+ "Failed to remove repository: #{repo_name}"
41
+ )
42
+ end
25
43
  end
26
44
 
27
45
  desc "modinfo ITEM", "Removes an entry from 'meta/modinfo/list'"
@@ -48,6 +66,30 @@ module Icarus
48
66
  )
49
67
  end
50
68
 
69
+ desc "mod MOD_ID", "Removes a mod from the 'mods' collection"
70
+ def mod(mod_id)
71
+ remove_entity(
72
+ :mod,
73
+ mod_id,
74
+ "Mod",
75
+ "Mod not found with ID: #{mod_id}",
76
+ "Successfully removed mod: #{mod_id}",
77
+ "Failed to remove mod: #{mod_id}"
78
+ )
79
+ end
80
+
81
+ desc "tool TOOL_ID", "Removes a tool from the 'tools' collection"
82
+ def tool(tool_id)
83
+ remove_entity(
84
+ :tool,
85
+ tool_id,
86
+ "Tool",
87
+ "Tool not found with ID: #{tool_id}",
88
+ "Successfully removed tool: #{tool_id}",
89
+ "Failed to remove tool: #{tool_id}"
90
+ )
91
+ end
92
+
51
93
  private
52
94
 
53
95
  def remove_item(type, item, display_name, not_found_msg, success_msg, failure_msg)
@@ -78,6 +120,205 @@ module Icarus
78
120
  end
79
121
  end
80
122
 
123
+ def remove_entity(type, entity_id, display_name, not_found_msg, success_msg, failure_msg)
124
+ # Find the entity by ID
125
+ collection = type == :mod ? firestore.mods : firestore.tools
126
+ entity = collection.find { |e| e.id == entity_id }
127
+
128
+ unless entity
129
+ warn not_found_msg
130
+ exit 1
131
+ end
132
+
133
+ puts Paint["Removing #{display_name.downcase}: #{entity.name} (ID: #{entity_id})", :black] if verbose?
134
+
135
+ if options[:dry_run]
136
+ puts Paint["Dry run; no changes will be made", :yellow]
137
+ return
138
+ end
139
+
140
+ if firestore.delete(type, entity)
141
+ puts Paint[success_msg, :green]
142
+ else
143
+ warn Paint[failure_msg, :red]
144
+ exit 1
145
+ end
146
+ end
147
+
148
+ def cascade_delete_repo(repo_name)
149
+ @delete_failures = []
150
+
151
+ # Find all modinfo URLs belonging to this repository
152
+ # Match full "owner/repo" path component, not substring
153
+ repo_pattern = %r{/#{Regexp.escape(repo_name)}(?=/|$)}
154
+ modinfo_urls = firestore.modinfo.select { |url| url.match?(repo_pattern) }
155
+ toolinfo_urls = firestore.toolinfo.select { |url| url.match?(repo_pattern) }
156
+
157
+ puts Paint["Found #{modinfo_urls.size} modinfo entries and #{toolinfo_urls.size} toolinfo entries", :cyan] if verbose?
158
+
159
+ if options[:dry_run]
160
+ puts Paint["Dry run; no changes will be made", :yellow]
161
+ preview = preview_cascade_deletions(repo_name, modinfo_urls, toolinfo_urls)
162
+ display_cascade_preview(repo_name, preview)
163
+ return
164
+ end
165
+
166
+ # Delete modinfo URLs and their associated mods
167
+ modinfo_urls.each do |url|
168
+ puts Paint[" Removing modinfo: #{url}", :black] if verbose?
169
+ track_delete(:modinfo, url) { firestore.delete(:modinfo, url) }
170
+
171
+ # Find and delete associated mods
172
+ delete_entities_from_url(url, :mod)
173
+ end
174
+
175
+ # Delete toolinfo URLs and their associated tools
176
+ toolinfo_urls.each do |url|
177
+ puts Paint[" Removing toolinfo: #{url}", :black] if verbose?
178
+ track_delete(:toolinfo, url) { firestore.delete(:toolinfo, url) }
179
+
180
+ # Find and delete associated tools
181
+ delete_entities_from_url(url, :tool)
182
+ end
183
+
184
+ # Report any failures
185
+ report_delete_failures
186
+ report_fetch_failures
187
+
188
+ # Finally, remove the repository
189
+ if firestore.delete(:repositories, repo_name)
190
+ puts Paint["Successfully removed repository and all associated entries: #{repo_name}", :green]
191
+ else
192
+ warn Paint["Failed to remove repository: #{repo_name}", :red]
193
+ exit 1
194
+ end
195
+ end
196
+
197
+ def delete_entities_from_url(url, type)
198
+ # Fetch the modinfo/toolinfo JSON
199
+ begin
200
+ data = retrieve_from_url(url)
201
+ entities = data[type == :mod ? :mods : :tools] || []
202
+
203
+ entities.each do |entity_data|
204
+ # Find ALL matching entities in Firestore by name and author
205
+ collection = type == :mod ? firestore.mods : firestore.tools
206
+ matching_entities = collection.select do |e|
207
+ e.name == entity_data[:name] && e.author == entity_data[:author]
208
+ end
209
+
210
+ next if matching_entities.empty?
211
+
212
+ if matching_entities.size > 1 && verbose?
213
+ warn Paint[" Note: Found #{matching_entities.size} entities matching '#{entity_data[:name]}' by #{entity_data[:author]}", :yellow]
214
+ end
215
+
216
+ matching_entities.each do |entity|
217
+ puts Paint[" Removing #{type}: #{entity.name} (ID: #{entity.id})", :black] if verbose?
218
+ track_delete(type, "#{entity.name} (#{entity.id})") { firestore.delete(type, entity) }
219
+ end
220
+ end
221
+ rescue SocketError, IOError, SystemCallError, Timeout::Error, JSON::ParserError => e
222
+ @failed_entity_fetches ||= []
223
+ @failed_entity_fetches << { url: url, error: e.class.name, message: e.message }
224
+ warn Paint["Warning: Could not fetch #{url} to remove entities: #{e.message}", :yellow]
225
+ end
226
+ end
227
+
228
+ def preview_cascade_deletions(repo_name, modinfo_urls, toolinfo_urls)
229
+ mods = []
230
+ tools = []
231
+
232
+ modinfo_urls.each do |url|
233
+ entities = fetch_entities_from_url(url, :mod)
234
+ mods.concat(entities) if entities
235
+ end
236
+
237
+ toolinfo_urls.each do |url|
238
+ entities = fetch_entities_from_url(url, :tool)
239
+ tools.concat(entities) if entities
240
+ end
241
+
242
+ {
243
+ modinfo_urls: modinfo_urls,
244
+ toolinfo_urls: toolinfo_urls,
245
+ mods: mods,
246
+ tools: tools
247
+ }
248
+ end
249
+
250
+ def fetch_entities_from_url(url, type)
251
+ data = retrieve_from_url(url)
252
+ entity_data_list = data[type == :mod ? :mods : :tools] || []
253
+
254
+ collection = type == :mod ? firestore.mods : firestore.tools
255
+ entities = []
256
+
257
+ entity_data_list.each do |entity_data|
258
+ matching_entities = collection.select do |e|
259
+ e.name == entity_data[:name] && e.author == entity_data[:author]
260
+ end
261
+ entities.concat(matching_entities)
262
+ end
263
+
264
+ entities
265
+ rescue SocketError, IOError, SystemCallError, Timeout::Error, JSON::ParserError => e
266
+ warn Paint["Warning: Could not fetch #{url}: #{e.message}", :yellow] if verbose?
267
+ nil
268
+ end
269
+
270
+ def display_cascade_preview(repo_name, preview)
271
+ puts "Would remove:"
272
+ puts " - Repository: #{repo_name}"
273
+
274
+ if preview[:modinfo_urls].any?
275
+ puts " - Modinfo URLs: #{preview[:modinfo_urls].size}"
276
+ preview[:modinfo_urls].each { |url| puts " • #{url}" }
277
+ end
278
+
279
+ if preview[:mods].any?
280
+ puts " - Mods: #{preview[:mods].size}"
281
+ preview[:mods].each { |mod| puts " • #{mod.name} by #{mod.author} (ID: #{mod.id})" }
282
+ end
283
+
284
+ if preview[:toolinfo_urls].any?
285
+ puts " - Toolinfo URLs: #{preview[:toolinfo_urls].size}"
286
+ preview[:toolinfo_urls].each { |url| puts " • #{url}" }
287
+ end
288
+
289
+ if preview[:tools].any?
290
+ puts " - Tools: #{preview[:tools].size}"
291
+ preview[:tools].each { |tool| puts " • #{tool.name} by #{tool.author} (ID: #{tool.id})" }
292
+ end
293
+ end
294
+
295
+ def track_delete(type, identifier)
296
+ success = yield
297
+ unless success
298
+ @delete_failures ||= []
299
+ @delete_failures << { type: type, identifier: identifier.to_s }
300
+ end
301
+ success
302
+ end
303
+
304
+ def report_delete_failures
305
+ return unless @delete_failures&.any?
306
+
307
+ warn Paint["\nWarning: #{@delete_failures.size} delete operation(s) failed:", :red]
308
+ @delete_failures.each do |failure|
309
+ warn Paint[" • #{failure[:type]}: #{failure[:identifier]}", :red]
310
+ end
311
+ end
312
+
313
+ def report_fetch_failures
314
+ return unless @failed_entity_fetches&.any?
315
+
316
+ warn Paint["\nWarning: Failed to fetch #{@failed_entity_fetches.size} URL(s) - some entities may not have been deleted:", :yellow]
317
+ @failed_entity_fetches.each do |failure|
318
+ warn Paint[" • #{failure[:url]} (#{failure[:error]})", :yellow]
319
+ end
320
+ end
321
+
81
322
  def firestore
82
323
  $firestore ||= Firestore.new
83
324
  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.0"
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.0
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
@@ -1,9 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(bundle exec rspec:*)"
5
- ],
6
- "deny": [],
7
- "ask": []
8
- }
9
- }