11#!/usr/bin/env bun
22
3- import { program } from '@commander-js/extra-typings' ;
4- import { CurseForgeRepository , LocalSolutionFinder , LoggerConfig , ModLoader , ModQueryService , ModrinthRepository , LogLevel , Constraints , Solution } from 'mclib' ;
3+ import { program , Option } from '@commander-js/extra-typings' ;
4+ import { CurseForgeRepository , LocalSolutionFinder , LoggerConfig , ModLoader , ModQueryService , ModrinthRepository , LogLevel , Constraints , Solution , ModMetadata , RepositoryUtil , ModRepositoryName , ModRepoMetadata } from 'mclib' ;
55import { readFileSync } from 'fs' ;
66import pino from 'pino' ;
77
@@ -33,17 +33,31 @@ async function fetchWrapper(input: RequestInfo | URL, options?: RequestInit): Pr
3333 return response ;
3434}
3535
36- function getModQueryService ( ) {
37- const repositories = [
38- new ModrinthRepository ( fetchWrapper ) ,
39- new CurseForgeRepository ( fetchWrapper ) ,
40- ] ;
36+ function getModQueryService ( selectedRepos ?: string [ ] ) {
37+ const repoMap = {
38+ modrinth : ( ) => new ModrinthRepository ( fetchWrapper ) ,
39+ curseforge : ( ) => new CurseForgeRepository ( fetchWrapper ) ,
40+ } ;
41+ let repositories : any [ ] = [ ] ;
42+ if ( selectedRepos && selectedRepos . length > 0 ) {
43+ for ( const repo of selectedRepos ) {
44+ const factory = repoMap [ repo . toLowerCase ( ) as keyof typeof repoMap ] ;
45+ if ( factory ) repositories . push ( factory ( ) ) ;
46+ else {
47+ logger . error ( `Unknown repository: ${ repo } ` ) ;
48+ process . exit ( 1 ) ;
49+ }
50+ }
51+ } else {
52+ repositories = [ new ModrinthRepository ( fetchWrapper ) , new CurseForgeRepository ( fetchWrapper ) ] ;
53+ }
4154 return new ModQueryService ( repositories ) ;
4255}
4356
4457interface CliOptions {
4558 modId ?: string [ ] ;
4659 modFile ?: string [ ] ;
60+ exactVersion ?: string ;
4761 minVersion ?: string ;
4862 maxVersion ?: string ;
4963 loader ?: string [ ] ;
@@ -58,15 +72,35 @@ function validateCliOptions(options: CliOptions) {
5872 }
5973}
6074
61- async function getModIds ( modQueryService : ModQueryService , options : CliOptions ) : Promise < string [ ] > {
62- const modIdSet = new Set < string > ( ) ;
75+ async function getMods ( modQueryService : ModQueryService , options : CliOptions ) : Promise < ModMetadata [ ] > {
76+ const modsMap = new Map < string , ModMetadata > ( ) ;
6377
6478 if ( options . modId ) {
6579 for ( const id of options . modId ) {
66- if ( modIdSet . has ( id ) ) {
80+ let [ repoName , modId ] = id . split ( '/' ) ;
81+ if ( ! modId ) {
82+ logger . error ( `Invalid mod ID format: ${ id } . Expected format is 'repository/modId'.` ) ;
83+ continue ;
84+ }
85+
86+ let repo = RepositoryUtil . from ( repoName as ModRepositoryName , fetch ) ;
87+ if ( ! repo ) {
88+ logger . error ( `Unknown repository: ${ repoName } ` ) ;
89+ continue ;
90+ }
91+
92+ let fullId = `${ repo . getRepositoryName ( ) } /${ modId } ` . toLowerCase ( ) ;
93+ if ( modsMap . has ( fullId ) ) {
6794 logger . warn ( `Duplicate mod ID from --mod-id: ${ id } ` ) ;
6895 } else {
69- modIdSet . add ( id ) ;
96+ modsMap . set ( fullId , [ {
97+ id,
98+ repository : repo . getRepositoryName ( ) ,
99+ name : id ,
100+ homepageURL : "" ,
101+ imageURL : "" ,
102+ downloadCount : 0
103+ } ] ) ;
70104 }
71105 }
72106 }
@@ -81,50 +115,54 @@ async function getModIds(modQueryService: ModQueryService, options: CliOptions):
81115 logger . debug ( `readFileSync(${ file } ): ${ duration } ms` ) ;
82116
83117 // Get metadata
84- let modMetadata = await modQueryService . getModByDataHash ( new Uint8Array ( modData ) ) ;
85- if ( ! modMetadata ) {
118+ let modMetadatas = await modQueryService . getModByDataHash ( new Uint8Array ( modData ) ) ;
119+ if ( modMetadatas . length === 0 ) {
86120 logger . warn ( `Could not extract mod ID from file: ${ file } ` ) ;
87121 continue ;
88122 }
89-
90- // add to set
91- if ( modIdSet . has ( modMetadata . id ) ) {
92- logger . warn ( `Duplicate mod ID from file: ${ modMetadata . id } (file: ${ file } )` ) ;
93- } else {
94- modIdSet . add ( modMetadata . id ) ;
95- logger . info ( `Found mod ID ${ modMetadata . id } from file: ${ file } ` ) ;
123+ logger . debug ( `Extracted ${ modMetadatas . length } match in repositories from file: ${ file } ` ) ;
124+
125+ // verify if there are duplicates
126+ for ( const modMetadata of modMetadatas ) {
127+ let fullId = `${ modMetadata . repository } |${ modMetadata . id } ` . toLowerCase ( ) ;
128+ if ( modsMap . has ( fullId ) ) {
129+ logger . warn ( `Duplicate mod ID from file: ${ modMetadata . id } (file: ${ file } )` ) ;
130+ }
96131 }
132+
133+ // add to set (with random key, we don't care anyway)
134+ modsMap . set ( `|${ modMetadatas [ 0 ] . id } ` , modMetadatas ) ;
97135 } catch ( error ) {
98136 logger . error ( `Error processing mod file ${ file } : ${ error } ` ) ;
99137 continue ;
100138 }
101139 }
102140 }
103141
104- return Array . from ( modIdSet ) ;
142+ return Array . from ( modsMap . values ( ) ) ;
105143}
106144
107145async function findSolutions (
108146 modQueryService : ModQueryService ,
109- requestedModIds : string [ ] ,
147+ requestedMods : ModMetadata [ ] ,
110148 constraints : Constraints ,
111149 nbSolutions : number ,
112150 sinytra : boolean
113151) : Promise < Solution [ ] > {
114152 let solutionFinder = new LocalSolutionFinder ( modQueryService ) ;
115153
116154 // Resolve mods
117- const mods = await solutionFinder . resolveMods ( requestedModIds ) ;
155+ const mods = await solutionFinder . resolveMods ( requestedMods ) ;
118156
119157 // Sinytra loader injection
120158 if ( sinytra ) {
121159 logger . info ( 'Sinytra mode: Injecting Forge and NeoForge into Fabric-compatible releases...' ) ;
122160 for ( const mod of mods ) {
123- for ( const release of mod . releases ) {
161+ for ( const release of mod ) {
124162 if ( release . loaders . has ( ModLoader . FABRIC ) ) {
125163 release . loaders . add ( ModLoader . FORGE ) ;
126164 release . loaders . add ( ModLoader . NEOFORGE ) ;
127- logger . trace ( `Injected forge and neoforge into fabric-compatible release: ${ mod . id } ${ release . modVersion } ` ) ;
165+ logger . trace ( `Injected forge and neoforge into fabric-compatible release: ${ release . modMetadata . id } ${ release . modVersion } ` ) ;
128166 }
129167 }
130168 }
@@ -145,29 +183,31 @@ program
145183 . description ( 'Find mod versions that match constraints' )
146184 . option ( '--mod-id <modID...>' , 'Mod IDs to include in the modpack' )
147185 . option ( '--mod-file <path...>' , 'Mod IDs to include in the modpack' )
186+ . addOption ( new Option ( '--exact-version <version>' , 'Exact Minecraft version to consider' ) . conflicts ( [ 'minVersion' , 'maxVersion' ] ) )
148187 . option ( '--min-version <version>' , 'Minimum Minecraft version to consider' )
149188 . option ( '--max-version <version>' , 'Maximum Minecraft version to consider' )
150189 . option ( '--loader <loader...>' , 'Loaders to consider (e.g., forge, fabric)' , [ ] )
151190 . option ( '-d, --details' , 'Include details (e.g. unsupported mods in solutions found)' , false )
152191 . option ( '-n, --nb-solutions <number>' , 'Number of solutions to output' , ( value ) => parseInt ( value , 10 ) , 3 )
153192 . option ( '--sinytra' , 'Inject forge and neoforge into fabric-compatible releases' , false )
154- . action ( async ( cliOptions : CliOptions & { sinytra ?: boolean } ) => {
155- let modQueryService = getModQueryService ( ) ;
193+ . option ( '-r, --repository <repo...>' , 'Repositories to use (modrinth, curseforge)' )
194+ . action ( async ( cliOptions : CliOptions & { repository ?: string [ ] } ) => {
195+ let modQueryService = getModQueryService ( cliOptions . repository ) ;
156196 validateCliOptions ( cliOptions ) ;
157197
158- const requestedModIds = await getModIds ( modQueryService , cliOptions ) ;
159- if ( requestedModIds . length === 0 ) {
198+ const requestedMods = await getMods ( modQueryService , cliOptions ) ;
199+ if ( requestedMods . length === 0 ) {
160200 logger . error ( 'No valid mod IDs found from provided options.' ) ;
161201 return ;
162202 }
163203
164- logger . info ( `Searching for solutions with ${ requestedModIds . length } mod(s)...` ) ;
204+ logger . info ( `Searching for solutions with ${ requestedMods . length } mod(s)...` ) ;
165205 const solutions = await findSolutions (
166206 modQueryService ,
167- requestedModIds ,
207+ requestedMods ,
168208 {
169- minVersion : cliOptions . minVersion ,
170- maxVersion : cliOptions . maxVersion ,
209+ minVersion : cliOptions . exactVersion || cliOptions . minVersion ,
210+ maxVersion : cliOptions . exactVersion || cliOptions . maxVersion ,
171211 loaders : new Set ( cliOptions . loader as ModLoader [ ] ) ,
172212 } ,
173213 cliOptions . nbSolutions ,
@@ -181,13 +221,28 @@ program
181221
182222 logger . info ( `Found ${ solutions . length } solution(s):` ) ;
183223 for ( const solution of solutions ) {
184- logger . info ( `- Version: ${ solution . mcConfig . mcVersion } , Loader: ${ solution . mcConfig . loader } , Mods: ${ solution . mods . length } /${ requestedModIds . length } ` ) ;
185- if ( cliOptions . details && solution . mods . length != requestedModIds . length ) {
186- let unsupportedMods = requestedModIds . filter ( modId => ! solution . mods . some ( mod => mod . id === modId ) ) ;
187- logger . info ( ` Unsupported mods (${ unsupportedMods . length } ):` ) ;
188- for ( const modId of unsupportedMods ) {
189- logger . info ( ` - ${ modId } ` ) ;
224+ logger . info ( `- Version: ${ solution . mcConfig . mcVersion } , Loader: ${ solution . mcConfig . loader } , Mods: ${ solution . mods . length } /${ requestedMods . length } ` ) ;
225+ if ( cliOptions . details && solution . mods . length != requestedMods . length ) {
226+
227+ // Get unsupported mods
228+ let unsupportedModsMeta : ModMetadata [ ] = [ ] ;
229+ for ( const requestedMod of requestedMods ) {
230+ let hasIncludedRelease = false ;
231+ for ( const release of solution . mods ) {
232+ // check if we have repo-specific metadata of this mod in our solution
233+ if ( requestedMod . includes ( release . modMetadata ) ) {
234+ hasIncludedRelease = true ;
235+ break ;
236+ }
237+ }
238+ if ( ! hasIncludedRelease ) unsupportedModsMeta . push ( requestedMod ) ;
190239 }
240+
241+ logger . info ( ` Unsupported mods (${ unsupportedModsMeta . length } ):` ) ;
242+ for ( const modMeta of unsupportedModsMeta ) {
243+ logger . info ( ` - ${ modMeta [ 0 ] . id } (${ modMeta [ 0 ] . name } )` ) ;
244+ }
245+
191246 }
192247 }
193248 if ( ! cliOptions . details ) {
0 commit comments